Add automated TUI screenshot capture script

Textual Pilot-based script that launches demo mode, navigates
all screens, and exports SVG + PNG via rsvg-convert.
This commit is contained in:
Ryan Malloy 2026-02-17 17:49:01 -07:00
parent f8bfd69ceb
commit b0aee4e5a6

View File

@ -0,0 +1,160 @@
"""Capture all TUI screenshots for documentation.
Runs the Birdcage TUI in demo mode using Textual's Pilot API,
navigates to each screen/sub-mode, and exports SVG + PNG screenshots.
Usage:
cd tui && uv run python scripts/capture_screenshots.py
Output goes to ../site/public/screenshots/
"""
import asyncio
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
from birdcage_tui.app import BirdcageApp
OUTPUT_DIR = (
Path(__file__).resolve().parent.parent.parent / "site" / "public" / "screenshots"
)
TERMINAL_SIZE = (120, 42)
# Demo search results for craft-search screenshot
DEMO_SEARCH_RESULTS = [
{
"name": "ISS (ZARYA)",
"target_type": "satellite",
"target_id": "25544",
"groups": ["stations"],
},
{
"name": "NOAA 19",
"target_type": "satellite",
"target_id": "33591",
"groups": ["weather"],
},
{
"name": "SO-50 (SAUDISAT 1C)",
"target_type": "satellite",
"target_id": "27607",
"groups": ["amateur"],
},
{
"name": "TEVEL-2",
"target_type": "satellite",
"target_id": "50988",
"groups": ["amateur"],
},
{
"name": "AO-91 (FOX-1B)",
"target_type": "satellite",
"target_id": "43017",
"groups": ["amateur"],
},
]
def save(name: str, svg: str) -> None:
"""Write SVG and convert to PNG via rsvg-convert."""
svg_path = OUTPUT_DIR / f"{name}.svg"
png_path = OUTPUT_DIR / f"{name}.png"
svg_path.write_text(svg)
print(f" {svg_path.name}")
try:
subprocess.run(
["rsvg-convert", "-o", str(png_path), str(svg_path)],
check=True,
capture_output=True,
)
print(f" {png_path.name}")
except FileNotFoundError:
print(" WARNING: rsvg-convert not found, skipping PNG")
async def capture_all() -> None:
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
app = BirdcageApp()
app.demo_mode = True
async with app.run_test(size=TERMINAL_SIZE) as pilot:
await pilot.pause(1.0)
# ---- F1 Dashboard ----
print("F1 Dashboard")
await pilot.press("f1")
await pilot.pause(0.5)
save("tui-dashboard", app.export_screenshot())
# ---- F2 Control (Manual mode) ----
print("F2 Control")
await pilot.press("f2")
await pilot.pause(0.5)
save("tui-control", app.export_screenshot())
# ---- F2 Control > Craft (search results) ----
print("F2 Control > Craft (search)")
craft_mode_btn = app.query_one("#mode-craft")
craft_mode_btn.press()
await pilot.pause(0.5)
# Populate search results directly via widget API
craft_panel = app.query_one("#ctrl-craft-panel")
craft_panel.set_search_results(DEMO_SEARCH_RESULTS)
await pilot.pause(0.3)
save("tui-craft-search", app.export_screenshot())
# ---- F2 Control > Craft (tracking Moon) ----
print("F2 Control > Craft (tracking)")
# Clear results and set tracking state
craft_panel.set_search_results([])
craft_panel.set_tracking_status(
state="TRACKING",
target_name="Moon",
azimuth=145.00,
elevation=32.01,
distance_km=384400,
range_rate=-0.3,
moves=47,
rate=1.0,
)
await pilot.pause(0.3)
save("tui-craft-tracking", app.export_screenshot())
# ---- F3 Signal (Monitor mode) ----
print("F3 Signal")
await pilot.press("f3")
await pilot.pause(0.5)
save("tui-signal", app.export_screenshot())
# ---- F4 System ----
print("F4 System")
await pilot.press("f4")
await pilot.pause(0.5)
save("tui-system", app.export_screenshot())
# ---- F5 Console overlay ----
print("F5 Console")
await pilot.press("f5")
await pilot.pause(0.5)
save("tui-console", app.export_screenshot())
await pilot.press("f5")
await pilot.pause(0.3)
# ---- F6 Camera overlay ----
print("F6 Camera")
await pilot.press("f6")
await pilot.pause(0.5)
save("tui-camera", app.export_screenshot())
await pilot.press("f6")
await pilot.pause(0.3)
print("\nDone!")
if __name__ == "__main__":
asyncio.run(capture_all())