diff --git a/docs/images/shared/base.tape b/docs/images/shared/base.tape index 08cf5f884..2a42cf424 100644 --- a/docs/images/shared/base.tape +++ b/docs/images/shared/base.tape @@ -40,6 +40,6 @@ Type "PS1='$ '" Enter Sleep 300ms -Type "rm -rf /tmp/commitizen-example && mkdir -p /tmp/commitizen-example && cd /tmp/commitizen-example" +Type `WORKDIR=$(mktemp -d "${TMPDIR:-/tmp}/commitizen-example.XXXXXX") && cd "$WORKDIR"` Enter Sleep 500ms diff --git a/docs/images/shared/cleanup.tape b/docs/images/shared/cleanup.tape index 1d7d3e74c..1eb8cd10e 100644 --- a/docs/images/shared/cleanup.tape +++ b/docs/images/shared/cleanup.tape @@ -1,4 +1,4 @@ Hide -Type "cd /tmp && rm -rf /tmp/commitizen-example" +Type `cd "${TMPDIR:-/tmp}" && rm -rf "$WORKDIR"` Enter Sleep 200ms diff --git a/scripts/gen_cli_interactive_gifs.py b/scripts/gen_cli_interactive_gifs.py index d60c47605..f258c3203 100644 --- a/scripts/gen_cli_interactive_gifs.py +++ b/scripts/gen_cli_interactive_gifs.py @@ -1,39 +1,81 @@ +"""Render docs/images/*.tape with VHS in parallel. + +Usage: + uv run poe doc:screenshots # default (parallel) + python scripts/gen_cli_interactive_gifs.py # default (parallel) + python scripts/gen_cli_interactive_gifs.py -j 1 # serial +""" + +from __future__ import annotations + +import argparse +import shutil import subprocess +import sys +from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path +VHS_DIR = Path(__file__).parent.parent / "docs" / "images" +OUTPUT_DIR = VHS_DIR / "cli_interactive" -def gen_cli_interactive_gifs() -> None: - """Generate GIF screenshots for interactive commands using VHS.""" - vhs_dir = Path(__file__).parent.parent / "docs" / "images" - output_dir = Path(__file__).parent.parent / "docs" / "images" / "cli_interactive" - output_dir.mkdir(parents=True, exist_ok=True) - vhs_files = list(vhs_dir.glob("*.tape")) +def gen_cli_interactive_gifs(max_workers: int | None = None) -> None: + """Render every ``docs/images/*.tape`` with VHS in parallel. - if not vhs_files: + ``max_workers`` defaults to ``min(len(tapes), 4)``; pass ``1`` for serial. + """ + if shutil.which("vhs") is None: + raise SystemExit( + "VHS is not installed. Please install it from: " + "https://github.com/charmbracelet/vhs" + ) + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + tapes = sorted(VHS_DIR.glob("*.tape")) + if not tapes: print("No VHS tape files found in docs/images/, skipping") return - for vhs_file in vhs_files: - print(f"Processing: {vhs_file.name}") - try: - subprocess.run( - ["vhs", vhs_file.name], - check=True, - cwd=vhs_dir, - ) - gif_name = vhs_file.stem + ".gif" - print(f"✓ Generated {gif_name}") - except FileNotFoundError: - print( - "✗ VHS is not installed. Please install it from: " - "https://github.com/charmbracelet/vhs" - ) - raise - except subprocess.CalledProcessError as e: - print(f"✗ Error processing {vhs_file.name}: {e}") - raise + workers = max(1, max_workers if max_workers is not None else min(len(tapes), 4)) + print(f"Rendering {len(tapes)} tape(s) with up to {workers} worker(s)") + + def _render(tape: Path) -> None: + subprocess.run( + ["vhs", tape.name], + check=True, + cwd=VHS_DIR, + capture_output=True, + text=True, + ) + + errors: list[Path] = [] + with ThreadPoolExecutor(max_workers=workers) as pool: + futures = {pool.submit(_render, t): t for t in tapes} + for fut in as_completed(futures): + tape = futures[fut] + try: + fut.result() + except subprocess.CalledProcessError as exc: + print(f"✗ {tape.name}", file=sys.stderr) + if exc.stdout: + print(exc.stdout, file=sys.stderr) + if exc.stderr: + print(exc.stderr, file=sys.stderr) + errors.append(tape) + else: + print(f"✓ {tape.stem}.gif") + + if errors: + raise SystemExit("vhs failed for: " + ", ".join(t.name for t in errors)) if __name__ == "__main__": - gen_cli_interactive_gifs() + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument( + "-j", + "--max-workers", + type=int, + default=None, + help="Max parallel vhs invocations. Default: min(len(tapes), 4). Use 1 for serial.", + ) + args = parser.parse_args() + gen_cli_interactive_gifs(args.max_workers)