Normalize line endings to LF across entire repository
Apply .gitattributes normalization to convert all CRLF line endings inherited from Windows-origin source files to Unix LF. 175 files, zero content changes.
This commit is contained in:
parent
696d2dd387
commit
bbdcb243dc
175 changed files with 56794 additions and 56794 deletions
|
|
@ -1,99 +1,99 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Download splash art from 16colo.rs for SkyWalker-1 TUI.
|
||||
|
||||
Fetches pixel/teletext art from Mistigris packs on 16colo.rs and saves
|
||||
them as bundled assets for the splash screen.
|
||||
|
||||
Usage:
|
||||
python scripts/fetch_art.py
|
||||
"""
|
||||
|
||||
import urllib.request
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ASSETS_DIR = Path(__file__).resolve().parent.parent / "src" / "skywalker_tui" / "assets" / "splash"
|
||||
|
||||
ART_SOURCES = [
|
||||
{
|
||||
"url": "https://16colo.rs/pack/mist0717/raw/ILLARTERATE-SETI-100_02_SATELLITE.PNG",
|
||||
"filename": "seti-satellite.png",
|
||||
"artist": "Illarterate",
|
||||
"title": "S.E.T.I. Satellite",
|
||||
"pack": "mist0717",
|
||||
},
|
||||
{
|
||||
"url": "https://16colo.rs/pack/mist1119/raw/192.168.10.13-DIALTONE.JPG",
|
||||
"filename": "dialtone.jpg",
|
||||
"artist": "192.168.10.13",
|
||||
"title": "Dialtone",
|
||||
"pack": "mist1119",
|
||||
},
|
||||
{
|
||||
"url": "https://16colo.rs/pack/mist0523/raw/BLIPPYPIXEL-SO_FAR_AWAY.GIF",
|
||||
"filename": "so-far-away.gif",
|
||||
"artist": "Blippypixel",
|
||||
"title": "So Far Away",
|
||||
"pack": "mist0523",
|
||||
},
|
||||
{
|
||||
"url": "https://16colo.rs/pack/mist0121/raw/JELLICA_JAKE-PRODIGY.JPG",
|
||||
"filename": "prodigy-out-of-space.jpg",
|
||||
"artist": "Jellica Jake",
|
||||
"title": "Prodigy / Out of Space",
|
||||
"pack": "mist0121",
|
||||
},
|
||||
{
|
||||
"url": "https://16colo.rs/pack/mist1120/raw/BLIPPYPIXEL-SPACE_DOCKER.GIF",
|
||||
"filename": "space-docker.gif",
|
||||
"artist": "Blippypixel",
|
||||
"title": "Space Docker",
|
||||
"pack": "mist1120",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def fetch_all():
|
||||
ASSETS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
downloaded = 0
|
||||
|
||||
for art in ART_SOURCES:
|
||||
dest = ASSETS_DIR / art["filename"]
|
||||
if dest.exists():
|
||||
print(f" exists: {art['filename']}", file=sys.stderr)
|
||||
downloaded += 1
|
||||
continue
|
||||
|
||||
print(f" fetch: {art['filename']} from {art['url']}", file=sys.stderr)
|
||||
try:
|
||||
req = urllib.request.Request(art["url"], headers={"User-Agent": "SkyWalker-1-TUI/0.1"})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
data = resp.read()
|
||||
dest.write_bytes(data)
|
||||
print(f" {len(data):,} bytes -> {dest.name}", file=sys.stderr)
|
||||
downloaded += 1
|
||||
except Exception as e:
|
||||
print(f" FAIL: {art['filename']}: {e}", file=sys.stderr)
|
||||
|
||||
# Write CREDITS.md
|
||||
credits = ASSETS_DIR / "CREDITS.md"
|
||||
lines = ["# Splash Art Credits\n", ""]
|
||||
lines.append("All artwork sourced from [16colo.rs](https://16colo.rs) /")
|
||||
lines.append("[Mistigris](https://mistigris.com) art packs.\n")
|
||||
lines.append("| File | Artist | Title | Pack |")
|
||||
lines.append("|------|--------|-------|------|")
|
||||
for art in ART_SOURCES:
|
||||
pack_url = f"https://16colo.rs/pack/{art['pack']}"
|
||||
lines.append(
|
||||
f"| `{art['filename']}` | {art['artist']} "
|
||||
f"| {art['title']} | [{art['pack']}]({pack_url}) |"
|
||||
)
|
||||
lines.append("")
|
||||
credits.write_text("\n".join(lines))
|
||||
|
||||
print(f"\n {downloaded}/{len(ART_SOURCES)} images downloaded to {ASSETS_DIR}", file=sys.stderr)
|
||||
return downloaded
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fetch_all()
|
||||
#!/usr/bin/env python3
|
||||
"""Download splash art from 16colo.rs for SkyWalker-1 TUI.
|
||||
|
||||
Fetches pixel/teletext art from Mistigris packs on 16colo.rs and saves
|
||||
them as bundled assets for the splash screen.
|
||||
|
||||
Usage:
|
||||
python scripts/fetch_art.py
|
||||
"""
|
||||
|
||||
import urllib.request
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ASSETS_DIR = Path(__file__).resolve().parent.parent / "src" / "skywalker_tui" / "assets" / "splash"
|
||||
|
||||
ART_SOURCES = [
|
||||
{
|
||||
"url": "https://16colo.rs/pack/mist0717/raw/ILLARTERATE-SETI-100_02_SATELLITE.PNG",
|
||||
"filename": "seti-satellite.png",
|
||||
"artist": "Illarterate",
|
||||
"title": "S.E.T.I. Satellite",
|
||||
"pack": "mist0717",
|
||||
},
|
||||
{
|
||||
"url": "https://16colo.rs/pack/mist1119/raw/192.168.10.13-DIALTONE.JPG",
|
||||
"filename": "dialtone.jpg",
|
||||
"artist": "192.168.10.13",
|
||||
"title": "Dialtone",
|
||||
"pack": "mist1119",
|
||||
},
|
||||
{
|
||||
"url": "https://16colo.rs/pack/mist0523/raw/BLIPPYPIXEL-SO_FAR_AWAY.GIF",
|
||||
"filename": "so-far-away.gif",
|
||||
"artist": "Blippypixel",
|
||||
"title": "So Far Away",
|
||||
"pack": "mist0523",
|
||||
},
|
||||
{
|
||||
"url": "https://16colo.rs/pack/mist0121/raw/JELLICA_JAKE-PRODIGY.JPG",
|
||||
"filename": "prodigy-out-of-space.jpg",
|
||||
"artist": "Jellica Jake",
|
||||
"title": "Prodigy / Out of Space",
|
||||
"pack": "mist0121",
|
||||
},
|
||||
{
|
||||
"url": "https://16colo.rs/pack/mist1120/raw/BLIPPYPIXEL-SPACE_DOCKER.GIF",
|
||||
"filename": "space-docker.gif",
|
||||
"artist": "Blippypixel",
|
||||
"title": "Space Docker",
|
||||
"pack": "mist1120",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def fetch_all():
|
||||
ASSETS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
downloaded = 0
|
||||
|
||||
for art in ART_SOURCES:
|
||||
dest = ASSETS_DIR / art["filename"]
|
||||
if dest.exists():
|
||||
print(f" exists: {art['filename']}", file=sys.stderr)
|
||||
downloaded += 1
|
||||
continue
|
||||
|
||||
print(f" fetch: {art['filename']} from {art['url']}", file=sys.stderr)
|
||||
try:
|
||||
req = urllib.request.Request(art["url"], headers={"User-Agent": "SkyWalker-1-TUI/0.1"})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
data = resp.read()
|
||||
dest.write_bytes(data)
|
||||
print(f" {len(data):,} bytes -> {dest.name}", file=sys.stderr)
|
||||
downloaded += 1
|
||||
except Exception as e:
|
||||
print(f" FAIL: {art['filename']}: {e}", file=sys.stderr)
|
||||
|
||||
# Write CREDITS.md
|
||||
credits = ASSETS_DIR / "CREDITS.md"
|
||||
lines = ["# Splash Art Credits\n", ""]
|
||||
lines.append("All artwork sourced from [16colo.rs](https://16colo.rs) /")
|
||||
lines.append("[Mistigris](https://mistigris.com) art packs.\n")
|
||||
lines.append("| File | Artist | Title | Pack |")
|
||||
lines.append("|------|--------|-------|------|")
|
||||
for art in ART_SOURCES:
|
||||
pack_url = f"https://16colo.rs/pack/{art['pack']}"
|
||||
lines.append(
|
||||
f"| `{art['filename']}` | {art['artist']} "
|
||||
f"| {art['title']} | [{art['pack']}]({pack_url}) |"
|
||||
)
|
||||
lines.append("")
|
||||
credits.write_text("\n".join(lines))
|
||||
|
||||
print(f"\n {downloaded}/{len(ART_SOURCES)} images downloaded to {ASSETS_DIR}", file=sys.stderr)
|
||||
return downloaded
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fetch_all()
|
||||
|
|
|
|||
|
|
@ -1,247 +1,247 @@
|
|||
#!/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())
|
||||
#!/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())
|
||||
|
|
|
|||
|
|
@ -1,124 +1,124 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Pre-render splash art as ANSI half-block text for instant display.
|
||||
|
||||
Converts source images (PNG/JPG/GIF) into .ans files using Unicode half-block
|
||||
characters with 24-bit ANSI color. This eliminates the runtime Pillow decode
|
||||
and textual-image protocol detection, making splash display instant.
|
||||
|
||||
Each terminal cell renders two vertical pixels using the upper-half-block
|
||||
character (U+2580 '▀') with fg=top_pixel, bg=bottom_pixel for 2x vertical
|
||||
resolution.
|
||||
|
||||
Usage:
|
||||
python scripts/prebake_splash.py [--width 80]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
ASSETS_DIR = Path(__file__).resolve().parent.parent / "src" / "skywalker_tui" / "assets" / "splash"
|
||||
|
||||
# Source images to pre-bake
|
||||
SOURCE_FILES = [
|
||||
"seti-satellite.png",
|
||||
"dialtone.jpg",
|
||||
"so-far-away.gif",
|
||||
"prodigy-out-of-space.jpg",
|
||||
"space-docker.gif",
|
||||
]
|
||||
|
||||
|
||||
def image_to_ansi(img_path: Path, width: int = 80) -> str:
|
||||
"""Convert an image to ANSI half-block art.
|
||||
|
||||
Uses ▀ (upper half block) with fg=top pixel, bg=bottom pixel to get
|
||||
2x vertical resolution. Emits 24-bit ANSI color escape sequences,
|
||||
optimized to only change fg/bg when the color actually changes.
|
||||
"""
|
||||
img = Image.open(img_path)
|
||||
|
||||
# Use first frame for animated GIFs
|
||||
if hasattr(img, "n_frames") and img.n_frames > 1:
|
||||
img.seek(0)
|
||||
|
||||
img = img.convert("RGBA")
|
||||
|
||||
# Resize maintaining aspect ratio, height rounded to even for half-blocks
|
||||
ratio = img.height / img.width
|
||||
pixel_height = int(width * ratio)
|
||||
pixel_height += pixel_height % 2 # ensure even
|
||||
img = img.resize((width, pixel_height), Image.LANCZOS)
|
||||
|
||||
lines = []
|
||||
for y in range(0, pixel_height, 2):
|
||||
chunks = []
|
||||
prev_fg = None
|
||||
prev_bg = None
|
||||
|
||||
for x in range(width):
|
||||
tr, tg, tb, ta = img.getpixel((x, y))
|
||||
if y + 1 < pixel_height:
|
||||
br, bg, bb, ba = img.getpixel((x, y + 1))
|
||||
else:
|
||||
br, bg, bb, ba = 0, 0, 0, 0
|
||||
|
||||
# Treat near-transparent pixels as black
|
||||
if ta < 128:
|
||||
tr, tg, tb = 0, 0, 0
|
||||
if ba < 128:
|
||||
br, bg, bb = 0, 0, 0
|
||||
|
||||
fg = (tr, tg, tb)
|
||||
bk = (br, bg, bb)
|
||||
|
||||
# Only emit escape codes when color changes
|
||||
codes = []
|
||||
if fg != prev_fg:
|
||||
codes.append(f"\033[38;2;{fg[0]};{fg[1]};{fg[2]}m")
|
||||
prev_fg = fg
|
||||
if bk != prev_bg:
|
||||
codes.append(f"\033[48;2;{bk[0]};{bk[1]};{bk[2]}m")
|
||||
prev_bg = bk
|
||||
|
||||
chunks.append("".join(codes) + "\u2580")
|
||||
|
||||
lines.append("".join(chunks) + "\033[0m")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Pre-bake splash art as ANSI half-block text")
|
||||
parser.add_argument("--width", type=int, default=80, help="Output width in columns (default: 80)")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not ASSETS_DIR.exists():
|
||||
print(f"Assets directory not found: {ASSETS_DIR}")
|
||||
return
|
||||
|
||||
for filename in SOURCE_FILES:
|
||||
src = ASSETS_DIR / filename
|
||||
if not src.exists():
|
||||
print(f" SKIP {filename} (not found)")
|
||||
continue
|
||||
|
||||
stem = src.stem
|
||||
dst = ASSETS_DIR / f"{stem}.ans"
|
||||
|
||||
print(f" BAKE {filename} -> {stem}.ans ({args.width} cols) ...", end=" ", flush=True)
|
||||
ansi_text = image_to_ansi(src, width=args.width)
|
||||
dst.write_text(ansi_text)
|
||||
|
||||
# Stats
|
||||
src_kb = src.stat().st_size / 1024
|
||||
dst_kb = dst.stat().st_size / 1024
|
||||
line_count = ansi_text.count("\n") + 1
|
||||
print(f"{src_kb:.1f}KB -> {dst_kb:.1f}KB ({line_count} lines)")
|
||||
|
||||
print("\nDone. Pre-baked .ans files are ready for instant splash display.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
#!/usr/bin/env python3
|
||||
"""Pre-render splash art as ANSI half-block text for instant display.
|
||||
|
||||
Converts source images (PNG/JPG/GIF) into .ans files using Unicode half-block
|
||||
characters with 24-bit ANSI color. This eliminates the runtime Pillow decode
|
||||
and textual-image protocol detection, making splash display instant.
|
||||
|
||||
Each terminal cell renders two vertical pixels using the upper-half-block
|
||||
character (U+2580 '▀') with fg=top_pixel, bg=bottom_pixel for 2x vertical
|
||||
resolution.
|
||||
|
||||
Usage:
|
||||
python scripts/prebake_splash.py [--width 80]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
ASSETS_DIR = Path(__file__).resolve().parent.parent / "src" / "skywalker_tui" / "assets" / "splash"
|
||||
|
||||
# Source images to pre-bake
|
||||
SOURCE_FILES = [
|
||||
"seti-satellite.png",
|
||||
"dialtone.jpg",
|
||||
"so-far-away.gif",
|
||||
"prodigy-out-of-space.jpg",
|
||||
"space-docker.gif",
|
||||
]
|
||||
|
||||
|
||||
def image_to_ansi(img_path: Path, width: int = 80) -> str:
|
||||
"""Convert an image to ANSI half-block art.
|
||||
|
||||
Uses ▀ (upper half block) with fg=top pixel, bg=bottom pixel to get
|
||||
2x vertical resolution. Emits 24-bit ANSI color escape sequences,
|
||||
optimized to only change fg/bg when the color actually changes.
|
||||
"""
|
||||
img = Image.open(img_path)
|
||||
|
||||
# Use first frame for animated GIFs
|
||||
if hasattr(img, "n_frames") and img.n_frames > 1:
|
||||
img.seek(0)
|
||||
|
||||
img = img.convert("RGBA")
|
||||
|
||||
# Resize maintaining aspect ratio, height rounded to even for half-blocks
|
||||
ratio = img.height / img.width
|
||||
pixel_height = int(width * ratio)
|
||||
pixel_height += pixel_height % 2 # ensure even
|
||||
img = img.resize((width, pixel_height), Image.LANCZOS)
|
||||
|
||||
lines = []
|
||||
for y in range(0, pixel_height, 2):
|
||||
chunks = []
|
||||
prev_fg = None
|
||||
prev_bg = None
|
||||
|
||||
for x in range(width):
|
||||
tr, tg, tb, ta = img.getpixel((x, y))
|
||||
if y + 1 < pixel_height:
|
||||
br, bg, bb, ba = img.getpixel((x, y + 1))
|
||||
else:
|
||||
br, bg, bb, ba = 0, 0, 0, 0
|
||||
|
||||
# Treat near-transparent pixels as black
|
||||
if ta < 128:
|
||||
tr, tg, tb = 0, 0, 0
|
||||
if ba < 128:
|
||||
br, bg, bb = 0, 0, 0
|
||||
|
||||
fg = (tr, tg, tb)
|
||||
bk = (br, bg, bb)
|
||||
|
||||
# Only emit escape codes when color changes
|
||||
codes = []
|
||||
if fg != prev_fg:
|
||||
codes.append(f"\033[38;2;{fg[0]};{fg[1]};{fg[2]}m")
|
||||
prev_fg = fg
|
||||
if bk != prev_bg:
|
||||
codes.append(f"\033[48;2;{bk[0]};{bk[1]};{bk[2]}m")
|
||||
prev_bg = bk
|
||||
|
||||
chunks.append("".join(codes) + "\u2580")
|
||||
|
||||
lines.append("".join(chunks) + "\033[0m")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Pre-bake splash art as ANSI half-block text")
|
||||
parser.add_argument("--width", type=int, default=80, help="Output width in columns (default: 80)")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not ASSETS_DIR.exists():
|
||||
print(f"Assets directory not found: {ASSETS_DIR}")
|
||||
return
|
||||
|
||||
for filename in SOURCE_FILES:
|
||||
src = ASSETS_DIR / filename
|
||||
if not src.exists():
|
||||
print(f" SKIP {filename} (not found)")
|
||||
continue
|
||||
|
||||
stem = src.stem
|
||||
dst = ASSETS_DIR / f"{stem}.ans"
|
||||
|
||||
print(f" BAKE {filename} -> {stem}.ans ({args.width} cols) ...", end=" ", flush=True)
|
||||
ansi_text = image_to_ansi(src, width=args.width)
|
||||
dst.write_text(ansi_text)
|
||||
|
||||
# Stats
|
||||
src_kb = src.stat().st_size / 1024
|
||||
dst_kb = dst.stat().st_size / 1024
|
||||
line_count = ansi_text.count("\n") + 1
|
||||
print(f"{src_kb:.1f}KB -> {dst_kb:.1f}KB ({line_count} lines)")
|
||||
|
||||
print("\nDone. Pre-baked .ans files are ready for instant splash display.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue