skywalker-1/tui/scripts/generate_screenshots.py
Ryan Malloy 5d9dfa7794 Add TUI documentation page with generated SVG screenshots
- New docs page (tools/tui.mdx) covering all 5 RF modes, keyboard
  shortcuts, easter eggs, and splash screen with inline SVG screenshots
- Screenshot generation script using Textual headless Pilot API to
  capture 8 screens in demo mode without hardware
- Fix dark mode toggle: migrate from removed App.dark to App.theme API
  (Textual 7.x breaking change)
- Update social link to Gitea repo, add TUI to sidebar
2026-02-14 14:49:02 -07:00

152 lines
4.9 KiB
Python

#!/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())