Skip to content

Commit 68fadd8

Browse files
chore(scripts): parallel + filterable cli_interactive gif rendering
`scripts/gen_cli_interactive_gifs.py` now renders tapes through a ThreadPoolExecutor (CPU-aware default capped at 4) and accepts optional tape names as CLI args, so contributors can iterate on a single tape without re-rendering all five. Make `shared/base.tape` and `shared/cleanup.tape` use a per-tape `mktemp -d` workdir instead of the hardcoded `/tmp/commitizen-example`, so concurrent vhs runs do not race on the same directory. Generated-by: GitHub Copilot CLI (claude-opus-4.7-1m-internal) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d024c22 commit 68fadd8

4 files changed

Lines changed: 100 additions & 29 deletions

File tree

docs/contributing/contributing.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ If you're a first-time contributor, please check out issues labeled [good first
7171
5. **Documentation**
7272
- Update `docs/README.md` if needed
7373
- For CLI help screenshots: `uv run poe doc:screenshots`
74+
- To re-render a single interactive tape, e.g. `commit`:
75+
`python scripts/gen_cli_interactive_gifs.py commit`
7476
- Prefer [Google style documentation](https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings), which works well with editors like VSCode and PyCharm
7577
- **DO NOT** update `CHANGELOG.md` (automatically generated)
7678
- **DO NOT** update version numbers (automatically handled)

docs/images/shared/base.tape

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,6 @@ Type "PS1='$ '"
4040
Enter
4141
Sleep 300ms
4242

43-
Type "rm -rf /tmp/commitizen-example && mkdir -p /tmp/commitizen-example && cd /tmp/commitizen-example"
43+
Type `WORKDIR=$(mktemp -d "${TMPDIR:-/tmp}/commitizen-example.XXXXXX") && cd "$WORKDIR"`
4444
Enter
4545
Sleep 500ms

docs/images/shared/cleanup.tape

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
Hide
2-
Type "cd /tmp && rm -rf /tmp/commitizen-example"
2+
Type `cd "${TMPDIR:-/tmp}" && rm -rf "$WORKDIR"`
33
Enter
44
Sleep 200ms
Lines changed: 96 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,108 @@
1+
"""Render docs/images/*.tape with VHS, optionally in parallel.
2+
3+
Usage:
4+
uv run poe doc:screenshots # all tapes
5+
python scripts/gen_cli_interactive_gifs.py # all tapes
6+
python scripts/gen_cli_interactive_gifs.py commit init # subset
7+
python scripts/gen_cli_interactive_gifs.py -j 1 # serial
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import argparse
13+
import shutil
114
import subprocess
15+
import sys
16+
from concurrent.futures import ThreadPoolExecutor, as_completed
217
from pathlib import Path
318

19+
VHS_DIR = Path(__file__).parent.parent / "docs" / "images"
20+
OUTPUT_DIR = VHS_DIR / "cli_interactive"
421

5-
def gen_cli_interactive_gifs() -> None:
6-
"""Generate GIF screenshots for interactive commands using VHS."""
7-
vhs_dir = Path(__file__).parent.parent / "docs" / "images"
8-
output_dir = Path(__file__).parent.parent / "docs" / "images" / "cli_interactive"
9-
output_dir.mkdir(parents=True, exist_ok=True)
1022

11-
vhs_files = list(vhs_dir.glob("*.tape"))
23+
def gen_cli_interactive_gifs(
24+
tape_names: list[str] | None = None,
25+
max_workers: int | None = None,
26+
) -> None:
27+
"""Render VHS tapes in parallel.
1228
13-
if not vhs_files:
29+
``tape_names`` filters by stem or filename (``None`` renders all).
30+
``max_workers`` defaults to ``min(len(tapes), 4)``; pass ``1`` for serial.
31+
"""
32+
if shutil.which("vhs") is None:
33+
raise SystemExit(
34+
"VHS is not installed. Please install it from: "
35+
"https://github.com/charmbracelet/vhs"
36+
)
37+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
38+
all_tapes = sorted(VHS_DIR.glob("*.tape"))
39+
if not all_tapes:
1440
print("No VHS tape files found in docs/images/, skipping")
1541
return
1642

17-
for vhs_file in vhs_files:
18-
print(f"Processing: {vhs_file.name}")
19-
try:
20-
subprocess.run(
21-
["vhs", vhs_file.name],
22-
check=True,
23-
cwd=vhs_dir,
24-
)
25-
gif_name = vhs_file.stem + ".gif"
26-
print(f"✓ Generated {gif_name}")
27-
except FileNotFoundError:
28-
print(
29-
"✗ VHS is not installed. Please install it from: "
30-
"https://github.com/charmbracelet/vhs"
31-
)
32-
raise
33-
except subprocess.CalledProcessError as e:
34-
print(f"✗ Error processing {vhs_file.name}: {e}")
35-
raise
43+
if tape_names:
44+
by_stem = {t.stem: t for t in all_tapes}
45+
tapes: list[Path] = []
46+
seen: set[str] = set()
47+
for name in tape_names:
48+
stem = Path(name).stem
49+
if stem in seen:
50+
continue
51+
if stem not in by_stem:
52+
raise SystemExit(
53+
f"Unknown tape: {name}. Available: {', '.join(sorted(by_stem))}"
54+
)
55+
seen.add(stem)
56+
tapes.append(by_stem[stem])
57+
else:
58+
tapes = all_tapes
59+
60+
workers = max(1, max_workers if max_workers is not None else min(len(tapes), 4))
61+
print(f"Rendering {len(tapes)} tape(s) with up to {workers} worker(s)")
62+
63+
def _render(tape: Path) -> None:
64+
subprocess.run(
65+
["vhs", tape.name],
66+
check=True,
67+
cwd=VHS_DIR,
68+
capture_output=True,
69+
text=True,
70+
)
71+
72+
errors: list[Path] = []
73+
with ThreadPoolExecutor(max_workers=workers) as pool:
74+
futures = {pool.submit(_render, t): t for t in tapes}
75+
for fut in as_completed(futures):
76+
tape = futures[fut]
77+
try:
78+
fut.result()
79+
except subprocess.CalledProcessError as exc:
80+
print(f"✗ {tape.name}", file=sys.stderr)
81+
if exc.stdout:
82+
print(exc.stdout, file=sys.stderr)
83+
if exc.stderr:
84+
print(exc.stderr, file=sys.stderr)
85+
errors.append(tape)
86+
else:
87+
print(f"✓ {tape.stem}.gif")
88+
89+
if errors:
90+
raise SystemExit("vhs failed for: " + ", ".join(t.name for t in errors))
3691

3792

3893
if __name__ == "__main__":
39-
gen_cli_interactive_gifs()
94+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
95+
parser.add_argument(
96+
"tapes",
97+
nargs="*",
98+
help="Tape stems or filenames (e.g. 'commit' or 'commit.tape'). Default: all.",
99+
)
100+
parser.add_argument(
101+
"-j",
102+
"--max-workers",
103+
type=int,
104+
default=None,
105+
help="Max parallel vhs invocations. Default: min(len(tapes), 4). Use 1 for serial.",
106+
)
107+
args = parser.parse_args()
108+
gen_cli_interactive_gifs(args.tapes or None, args.max_workers)

0 commit comments

Comments
 (0)