Generate motor.svg and survey.svg for the new TUI screens. Fix MotorScreen bug: two Horizontal widgets shared id="motor-usals-inputs", causing MountError. Replaced with classes="usals-input-row" since both rows need the same styling. Regenerated all 13 screenshots.
248 lines
8.0 KiB
Python
248 lines
8.0 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 (13 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 RF 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_device_screen() -> None:
|
|
"""Capture F6 Device screen — show EEPROM tab with hex dump."""
|
|
app = _new_app(show_splash=False)
|
|
|
|
async with app.run_test(size=TERM_SIZE, headless=True) as pilot:
|
|
await pilot.pause(MOUNT_PAUSE)
|
|
|
|
# Switch to Device screen
|
|
await pilot.press("f6")
|
|
await pilot.pause(MODE_SWITCH_PAUSE)
|
|
|
|
# Wait for identity info to populate
|
|
await pilot.pause(1.0)
|
|
|
|
# Capture Firmware tab (default)
|
|
svg = app.export_screenshot(title="SkyWalker-1 — Device")
|
|
_save(svg, "device")
|
|
|
|
|
|
async def capture_stream_screen() -> None:
|
|
"""Capture F7 Stream screen — needs time for TS packets to accumulate."""
|
|
app = _new_app(show_splash=False)
|
|
|
|
async with app.run_test(size=TERM_SIZE, headless=True) as pilot:
|
|
await pilot.pause(MOUNT_PAUSE)
|
|
|
|
# Switch to Stream screen (auto-starts in demo mode)
|
|
await pilot.press("f7")
|
|
await pilot.pause(MODE_SWITCH_PAUSE)
|
|
|
|
# Wait for PID stats and PSI tree to populate
|
|
await pilot.pause(2.0)
|
|
|
|
svg = app.export_screenshot(title="SkyWalker-1 — Stream")
|
|
_save(svg, "stream")
|
|
|
|
|
|
async def capture_config_screen() -> None:
|
|
"""Capture F8 Config screen."""
|
|
app = _new_app(show_splash=False)
|
|
|
|
async with app.run_test(size=TERM_SIZE, headless=True) as pilot:
|
|
await pilot.pause(MOUNT_PAUSE)
|
|
|
|
# Switch to Config screen
|
|
await pilot.press("f8")
|
|
await pilot.pause(MODE_SWITCH_PAUSE)
|
|
|
|
# Wait for config status to load
|
|
await pilot.pause(0.5)
|
|
|
|
svg = app.export_screenshot(title="SkyWalker-1 — Config")
|
|
_save(svg, "config")
|
|
|
|
|
|
async def capture_motor_screen() -> None:
|
|
"""Capture F9 Motor screen — 3-column layout with signal gauge."""
|
|
app = _new_app(show_splash=False)
|
|
|
|
async with app.run_test(size=TERM_SIZE, headless=True) as pilot:
|
|
await pilot.pause(MOUNT_PAUSE)
|
|
|
|
# Switch to Motor screen
|
|
await pilot.press("f9")
|
|
await pilot.pause(MODE_SWITCH_PAUSE)
|
|
|
|
# Wait for signal gauge to populate
|
|
await pilot.pause(1.0)
|
|
|
|
svg = app.export_screenshot(title="SkyWalker-1 — Motor Control")
|
|
_save(svg, "motor")
|
|
|
|
|
|
async def capture_survey_screen() -> None:
|
|
"""Capture F10 Survey screen — Full Band tab with spectrum plot."""
|
|
app = _new_app(show_splash=False)
|
|
|
|
async with app.run_test(size=TERM_SIZE, headless=True) as pilot:
|
|
await pilot.pause(MOUNT_PAUSE)
|
|
|
|
# Switch to Survey screen (Full Band tab is default)
|
|
await pilot.press("f10")
|
|
await pilot.pause(MODE_SWITCH_PAUSE)
|
|
|
|
# Wait for demo spectrum data to render
|
|
await pilot.pause(1.5)
|
|
|
|
svg = app.export_screenshot(title="SkyWalker-1 — Carrier Survey")
|
|
_save(svg, "survey")
|
|
|
|
|
|
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),
|
|
("Device screen (F6)", capture_device_screen),
|
|
("Stream screen (F7)", capture_stream_screen),
|
|
("Config screen (F8)", capture_config_screen),
|
|
("Motor screen (F9)", capture_motor_screen),
|
|
("Survey screen (F10)", capture_survey_screen),
|
|
("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())
|