Skip to content

Commit 5fa00aa

Browse files
committed
Widen README progress animations
1 parent aca7842 commit 5fa00aa

6 files changed

Lines changed: 117 additions & 55 deletions

File tree

docs/_static/progressbar-ergonomics.svg

Lines changed: 3 additions & 3 deletions
Loading

docs/_static/progressbar-hero.svg

Lines changed: 3 additions & 3 deletions
Loading

docs/_static/progressbar-multibar.svg

Lines changed: 3 additions & 3 deletions
Loading

docs/_static/progressbar-unknown-length.svg

Lines changed: 3 additions & 3 deletions
Loading

scripts/render_readme_demos.py

Lines changed: 56 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
PERCENT_RE = re.compile(r'\b\d{1,3}%')
1919
POSTFIX_RE = re.compile(r'\b[A-Za-z_][\w-]*=[^\s,]+')
2020
LABEL_RE = re.compile(r'[A-Za-z][\w-]*:?')
21-
ANIMATION_FRAME_SECONDS = 0.12
22-
MAX_ANIMATION_FRAMES = 16
21+
ANIMATION_FRAME_SECONDS = 0.08
22+
MAX_ANIMATION_FRAMES = 24
23+
SVG_WIDTH = 1080
2324

2425

2526
@dataclass(frozen=True)
@@ -39,16 +40,16 @@ class Demo:
3940
import progressbar
4041
4142
with progressbar.ProgressBar(
42-
total=12,
43+
total=24,
4344
desc='Build',
4445
fd=sys.stdout,
4546
redirect_stdout=True,
4647
line_breaks=False,
4748
is_terminal=True,
48-
term_width=64,
49+
term_width=112,
4950
) as bar:
50-
for step in range(12):
51-
if step in {0, 4, 8}:
51+
for step in range(24):
52+
if step in {8, 16}:
5253
print(f'log: completed step {step}')
5354
bar.update(step + 1, force=True)
5455
time.sleep(0.005)
@@ -65,32 +66,35 @@ class Demo:
6566
fd = io.StringIO()
6667
multibar = progressbar.MultiBar(
6768
fd=fd,
68-
total=12,
69+
total=24,
6970
initial_format=None,
7071
finished_format=None,
7172
remove_finished=None,
7273
sort_reverse=False,
73-
term_width=64,
74+
term_width=112,
7475
)
7576
build = multibar['build']
7677
test = multibar['test']
77-
multibar.render(force=True, flush=True)
78-
fd.seek(0)
79-
fd.truncate(0)
8078
terminal_control_re = re.compile(r'\\x1b\\[[0-9;]*[A-Za-z]')
8179
82-
for step in range(12):
83-
build.update(step + 1, force=True)
84-
test.update(step + 1, force=True)
85-
fd.seek(0)
86-
fd.truncate(0)
87-
multibar.render(force=True, flush=True)
80+
def emit_frame():
8881
output = terminal_control_re.sub('', fd.getvalue())
8982
for line in output.split('\\r'):
9083
line = line.strip()
9184
if line:
9285
print(line)
9386
print('\f', end='')
87+
fd.seek(0)
88+
fd.truncate(0)
89+
90+
multibar.render(force=True, flush=True)
91+
emit_frame()
92+
93+
for step in range(24):
94+
build.update(step + 1, force=True)
95+
test.update(step + 1, force=True)
96+
multibar.render(force=True, flush=True)
97+
emit_frame()
9498
""",
9599
),
96100
Demo(
@@ -105,9 +109,22 @@ class Demo:
105109
fd=sys.stdout,
106110
line_breaks=False,
107111
is_terminal=True,
108-
term_width=64,
112+
term_width=112,
109113
) as bar:
110-
for value in (3, 7, 12, 18, 25, 31, 40, 52):
114+
for value in (
115+
3,
116+
7,
117+
12,
118+
18,
119+
25,
120+
31,
121+
40,
122+
52,
123+
67,
124+
83,
125+
101,
126+
124,
127+
):
111128
bar.update(value, force=True)
112129
""",
113130
),
@@ -119,26 +136,19 @@ class Demo:
119136
import progressbar
120137
121138
with progressbar.ProgressBar(
122-
total=4096,
139+
total=8192,
123140
desc='Items',
124141
unit='B',
125142
unit_scale=True,
126143
postfix={'loss': '1.0'},
127144
fd=sys.stdout,
128145
line_breaks=False,
129146
is_terminal=True,
130-
term_width=72,
147+
term_width=112,
131148
) as bar:
132-
for value, loss in (
133-
(256, '0.9'),
134-
(512, '0.8'),
135-
(768, '0.7'),
136-
(1024, '0.6'),
137-
(1536, '0.5'),
138-
(2048, '0.4'),
139-
(3072, '0.2'),
140-
(4096, '0.1'),
141-
):
149+
for step in range(24):
150+
value = int(8192 * (step + 1) / 24)
151+
loss = f'{1 - ((step + 1) / 24):.2f}'
142152
bar.update(value, postfix={'loss': loss}, force=True)
143153
""",
144154
),
@@ -186,14 +196,26 @@ def frames_from_output(output: str) -> list[list[str]]:
186196
]
187197
if lines:
188198
frames.append(lines)
189-
return frames[-MAX_ANIMATION_FRAMES:] or [['No output captured']]
199+
return limit_animation_frames(frames) or [['No output captured']]
190200

191201
for raw_frame in output.splitlines():
192202
for part in raw_frame.split('\r'):
193203
line = part.strip()
194204
if line:
195205
frames.append([normalize_terminal_line(line)])
196-
return frames[-MAX_ANIMATION_FRAMES:] or [['No output captured']]
206+
return limit_animation_frames(frames) or [['No output captured']]
207+
208+
209+
def limit_animation_frames(frames: list[list[str]]) -> list[list[str]]:
210+
if len(frames) <= MAX_ANIMATION_FRAMES:
211+
return frames
212+
213+
last_index = len(frames) - 1
214+
selected = [
215+
round(index * last_index / (MAX_ANIMATION_FRAMES - 1))
216+
for index in range(MAX_ANIMATION_FRAMES)
217+
]
218+
return [frames[index] for index in selected]
197219

198220

199221
def tspan(text: str, class_name: str | None = None) -> str:
@@ -272,7 +294,7 @@ def styled_terminal_line(line: str) -> str:
272294

273295

274296
def svg_document(title: str, frames: list[list[str]]) -> str:
275-
width = 760
297+
width = SVG_WIDTH
276298
line_height = 24
277299
max_lines = max(len(frame) for frame in frames)
278300
height = 72 + max_lines * line_height

tests/test_readme_demos.py

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@
77
import scripts.render_readme_demos as demos # noqa: E402
88

99

10+
def _bar_widths(frames: list[list[str]]) -> list[int]:
11+
return [
12+
len(match.group('inner'))
13+
for frame in frames
14+
for line in frame
15+
if (match := demos.BAR_RE.search(line))
16+
]
17+
18+
1019
def test_render_svg_escapes_terminal_text(tmp_path: Path) -> None:
1120
output = tmp_path / 'demo.svg'
1221
demos.render_svg(
@@ -104,11 +113,22 @@ def test_render_svg_uses_fast_readme_animation_pacing(
104113
demos.render_svg(output, title='Demo', frames=[['a'], ['b'], ['c']])
105114

106115
text = output.read_text(encoding='utf-8')
107-
assert 'dur="0.36s"' in text
116+
assert 'dur="0.24s"' in text
108117
assert 'dur="3.6s"' not in text
109118
assert '3.599999999' not in text
110119

111120

121+
def test_render_svg_uses_wide_canvas_for_readable_bars(
122+
tmp_path: Path,
123+
) -> None:
124+
output = tmp_path / 'demo.svg'
125+
demos.render_svg(output, title='Demo', frames=[['a']])
126+
127+
text = output.read_text(encoding='utf-8')
128+
assert 'width="1080"' in text
129+
assert 'viewBox="0 0 1080 96"' in text
130+
131+
112132
def test_multibar_demo_captures_rendered_progressbar_output() -> None:
113133
demo = next(demo for demo in demos.DEMOS if demo.name == 'multibar')
114134
frames = demos.capture_demo(demo)
@@ -121,7 +141,9 @@ def test_multibar_demo_captures_rendered_progressbar_output() -> None:
121141
assert 'test' in text
122142
assert 'Elapsed Time:' in text
123143
assert 'ETA:' in text
124-
assert '(1 of 12)' in text or '(2 of 12)' in text
144+
assert '(0 of 24)' in text
145+
assert '(1 of 24)' in text or '(2 of 24)' in text
146+
assert '(24 of 24)' in text
125147

126148

127149
def test_capture_demo_timeout_raises_clear_error(
@@ -146,23 +168,41 @@ def test_capture_frames_splits_carriage_return_output() -> None:
146168

147169
def test_capture_frames_keeps_more_animation_states() -> None:
148170
frames = demos.frames_from_output(
149-
'\n'.join(f'frame {index}' for index in range(20))
171+
'\n'.join(f'frame {index}' for index in range(30))
150172
)
151173

152-
assert len(frames) == 16
153-
assert frames[0] == ['frame 4']
154-
assert frames[-1] == ['frame 19']
174+
assert len(frames) == 24
175+
assert frames[0] == ['frame 0']
176+
assert frames[-1] == ['frame 29']
155177

156178

157179
def test_readme_demos_emit_enough_frames_to_look_responsive() -> None:
158180
frame_counts = {
159181
demo.name: len(demos.capture_demo(demo)) for demo in demos.DEMOS
160182
}
161183

162-
assert frame_counts['hero'] >= 12
163-
assert frame_counts['multibar'] >= 8
184+
assert frame_counts['hero'] == 24
185+
assert frame_counts['multibar'] == 24
164186
assert frame_counts['unknown-length'] >= 8
165-
assert frame_counts['ergonomics'] >= 8
187+
assert frame_counts['ergonomics'] == 24
188+
189+
190+
def test_readme_demos_show_wide_determinate_progress_bars() -> None:
191+
frames_by_name = {
192+
demo.name: demos.capture_demo(demo) for demo in demos.DEMOS
193+
}
194+
195+
for name in ('hero', 'multibar', 'ergonomics'):
196+
text = '\n'.join(
197+
line for frame in frames_by_name[name] for line in frame
198+
)
199+
first_frame = '\n'.join(frames_by_name[name][0])
200+
last_frame = '\n'.join(frames_by_name[name][-1])
201+
assert '0%' in first_frame
202+
assert '100%' in last_frame
203+
assert '0%' in text
204+
assert '100%' in text
205+
assert max(_bar_widths(frames_by_name[name])) >= 32
166206

167207

168208
def test_capture_frames_normalizes_variable_timing_text() -> None:

0 commit comments

Comments
 (0)