Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/images/shared/base.tape
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion docs/images/shared/cleanup.tape
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Hide
Type "cd /tmp && rm -rf /tmp/commitizen-example"
Type `cd "${TMPDIR:-/tmp}" && rm -rf "$WORKDIR"`
Enter
Sleep 200ms
96 changes: 69 additions & 27 deletions scripts/gen_cli_interactive_gifs.py
Original file line number Diff line number Diff line change
@@ -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)
Loading