#!/usr/bin/env python3 """Generate SVG screenshots of every TUI screen for documentation. Uses Textual's headless run_test() + Pilot API to programmatically navigate each screen and export SVG renders. Requires no hardware — runs entirely with DemoDevice synthetic signal data. Output: ../site/src/assets/tui/*.svg (8 screenshots) Usage: cd tui && uv run python scripts/generate_screenshots.py """ import asyncio import sys from pathlib import Path # Ensure the src layout is importable when running from scripts/ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src")) from skywalker_tui.app import SkyWalkerApp from skywalker_tui.bridge import USBBridge from skywalker_tui.demo import DemoDevice OUTPUT_DIR = Path(__file__).resolve().parent.parent.parent / "site" / "src" / "assets" / "tui" # Terminal size for screenshots — wide enough for sidebar + content TERM_SIZE = (120, 36) # Pause durations for rendering MOUNT_PAUSE = 1.5 # initial mount + mode screen init MODE_SWITCH_PAUSE = 0.8 # after F-key press NOTIFY_PAUSE = 0.6 # for toast notifications STARWARS_PAUSE = 12.0 # time for offline crawl to reach Star Destroyer frame def _new_app(**kwargs) -> SkyWalkerApp: """Create a fresh app instance with demo device.""" return SkyWalkerApp(bridge=USBBridge(DemoDevice()), **kwargs) def _save(svg: str, name: str) -> None: path = OUTPUT_DIR / f"{name}.svg" path.write_text(svg) print(f" OK {name}.svg ({len(svg):,} bytes)") async def capture_mode_screens() -> None: """Capture F1-F5 mode screens.""" app = _new_app(show_splash=False) async with app.run_test(size=TERM_SIZE, headless=True) as pilot: await pilot.pause(MOUNT_PAUSE) modes = [ ("f1", "spectrum", "Spectrum"), ("f2", "scan", "Scan"), ("f3", "monitor", "Monitor"), ("f4", "lband", "L-Band"), ("f5", "track", "Track"), ] for key, filename, label in modes: await pilot.press(key) await pilot.pause(MODE_SWITCH_PAUSE) svg = app.export_screenshot(title=f"SkyWalker-1 — {label}") _save(svg, filename) async def capture_dark_mode() -> None: """Capture dark-mode toggle with Star Wars notification toast.""" app = _new_app(show_splash=False) async with app.run_test(size=TERM_SIZE, headless=True) as pilot: await pilot.pause(MOUNT_PAUSE) # Toggle to light then back to dark to trigger "Dark Side" notification await pilot.press("d") # -> light await pilot.pause(0.3) await pilot.press("d") # -> dark (shows "Dark Side" toast) await pilot.pause(NOTIFY_PAUSE) svg = app.export_screenshot(title="SkyWalker-1 — Dark Side") _save(svg, "dark-mode") async def capture_splash() -> None: """Capture splash screen — needs show_splash=True and quick capture.""" app = _new_app(show_splash=True) async with app.run_test(size=TERM_SIZE, headless=True) as pilot: # Splash auto-dismisses after 5s, capture it quickly await pilot.pause(MOUNT_PAUSE) svg = app.export_screenshot(title="SkyWalker-1 — Splash") _save(svg, "splash") async def capture_starwars() -> None: """Capture Star Wars easter egg — uses offline fallback crawl. The offline crawl plays through several frames: 1. Black pause (2s) 2. "A long time ago..." (3s) 3. STAR WARS logo (4s) 4. Episode info (3s) 5. Opening crawl (per-line at 0.18s) 6. Star Destroyer (3.5s) 7. Credits (stays) We wait long enough to capture the Star Destroyer frame. """ app = _new_app(show_splash=False) async with app.run_test(size=TERM_SIZE, headless=True) as pilot: await pilot.pause(MOUNT_PAUSE) await pilot.press("ctrl+w") await pilot.pause(STARWARS_PAUSE) svg = app.export_screenshot(title="SkyWalker-1 — Star Wars") _save(svg, "starwars") async def main() -> None: OUTPUT_DIR.mkdir(parents=True, exist_ok=True) print(f"Generating TUI screenshots -> {OUTPUT_DIR}/\n") captures = [ ("Mode screens (F1-F5)", capture_mode_screens), ("Dark mode toggle", capture_dark_mode), ("Splash screen", capture_splash), ("Star Wars easter egg", capture_starwars), ] failed = [] for label, fn in captures: print(f"── {label} ──") try: await fn() except Exception as e: print(f" FAIL {e}") failed.append(label) print() count = len(list(OUTPUT_DIR.glob("*.svg"))) print(f"Done. {count} SVG screenshots generated.") if failed: print(f"\nFailed: {', '.join(failed)}") sys.exit(1) if __name__ == "__main__": asyncio.run(main())