diff --git a/site/astro.config.mjs b/site/astro.config.mjs
index e95db97..1bb3a94 100644
--- a/site/astro.config.mjs
+++ b/site/astro.config.mjs
@@ -32,9 +32,9 @@ export default defineConfig({
plugins: [starlightImageZoom(), starlightLinksValidator()],
social: [
{
- icon: 'github',
- label: 'GitHub',
- href: 'https://github.com/placeholder/skywalker-1',
+ icon: 'external',
+ label: 'Git Repository',
+ href: 'https://git.supported.systems/warehack.ing/skywalker-1',
},
],
customCss: ['./src/styles/custom.css'],
@@ -113,6 +113,7 @@ export default defineConfig({
{ label: 'Firmware Loader', slug: 'tools/firmware-loader' },
{ label: 'Tuning', slug: 'tools/tuning' },
{ label: 'SkyWalker RF Tool', slug: 'tools/skywalker' },
+ { label: 'SkyWalker TUI', slug: 'tools/tui' },
{ label: 'EEPROM Utilities', slug: 'tools/eeprom-utilities' },
{ label: 'Debugging', slug: 'tools/debugging' },
{ label: 'TS Analyzer', slug: 'tools/ts-analyzer' },
diff --git a/site/src/assets/tui/dark-mode.svg b/site/src/assets/tui/dark-mode.svg
new file mode 100644
index 0000000..cfaf6d8
--- /dev/null
+++ b/site/src/assets/tui/dark-mode.svg
@@ -0,0 +1,220 @@
+
diff --git a/site/src/assets/tui/lband.svg b/site/src/assets/tui/lband.svg
new file mode 100644
index 0000000..3ff9183
--- /dev/null
+++ b/site/src/assets/tui/lband.svg
@@ -0,0 +1,232 @@
+
diff --git a/site/src/assets/tui/monitor.svg b/site/src/assets/tui/monitor.svg
new file mode 100644
index 0000000..c14bbdb
--- /dev/null
+++ b/site/src/assets/tui/monitor.svg
@@ -0,0 +1,231 @@
+
diff --git a/site/src/assets/tui/scan.svg b/site/src/assets/tui/scan.svg
new file mode 100644
index 0000000..db08639
--- /dev/null
+++ b/site/src/assets/tui/scan.svg
@@ -0,0 +1,222 @@
+
diff --git a/site/src/assets/tui/spectrum.svg b/site/src/assets/tui/spectrum.svg
new file mode 100644
index 0000000..38750c5
--- /dev/null
+++ b/site/src/assets/tui/spectrum.svg
@@ -0,0 +1,221 @@
+
diff --git a/site/src/assets/tui/splash.svg b/site/src/assets/tui/splash.svg
new file mode 100644
index 0000000..ffdd855
--- /dev/null
+++ b/site/src/assets/tui/splash.svg
@@ -0,0 +1,2311 @@
+
diff --git a/site/src/assets/tui/starwars.svg b/site/src/assets/tui/starwars.svg
new file mode 100644
index 0000000..7a79963
--- /dev/null
+++ b/site/src/assets/tui/starwars.svg
@@ -0,0 +1,217 @@
+
diff --git a/site/src/assets/tui/track.svg b/site/src/assets/tui/track.svg
new file mode 100644
index 0000000..186d388
--- /dev/null
+++ b/site/src/assets/tui/track.svg
@@ -0,0 +1,224 @@
+
diff --git a/site/src/content/docs/tools/tui.mdx b/site/src/content/docs/tools/tui.mdx
new file mode 100644
index 0000000..614ac7f
--- /dev/null
+++ b/site/src/content/docs/tools/tui.mdx
@@ -0,0 +1,178 @@
+---
+title: SkyWalker TUI
+description: Interactive terminal dashboard for spectrum analysis, transponder scanning, signal monitoring, L-band analysis, and carrier tracking.
+---
+
+import { Tabs, TabItem, Steps, Aside, Badge } from '@astrojs/starlight/components';
+
+The SkyWalker TUI is a full-screen terminal dashboard built on [Textual](https://textual.textualize.io/) that wraps all five RF operating modes into a single interactive interface. Sidebar navigation, F-key shortcuts, real-time widget updates, and a dark/light theme toggle replace the separate CLI subcommands with a unified experience.
+
+
+
+
+
+## Installation
+
+The TUI is distributed as the `skywalker-tui` package from the `tui/` directory in the repository.
+
+```bash title="Install with uv"
+cd tui && uv sync
+```
+
+```bash title="Run with hardware"
+sudo uv run skywalker-tui
+```
+
+```bash title="Run in demo mode (no hardware)"
+uv run skywalker-tui --demo
+```
+
+### CLI Flags
+
+| Flag | Description |
+|------|-------------|
+| `--demo` | Synthetic signal data — no SkyWalker-1 USB device needed |
+| `--no-splash` | Skip the startup splash screen |
+| `--verbose, -v` | Verbose USB logging (hardware mode only) |
+| `MODE` | Initial mode: `spectrum` (default), `scan`, `monitor`, `lband`, `track` |
+
+
+
+---
+
+## Keyboard Shortcuts
+
+| Key | Action |
+|-----|--------|
+| F1 | Spectrum analyzer |
+| F2 | Transponder scanner |
+| F3 | Signal monitor |
+| F4 | L-Band analyzer |
+| F5 | Carrier tracker |
+| d | Toggle dark/light theme |
+| q | Quit |
+| Ctrl+W | Easter egg |
+| Esc | Dismiss overlay / skip splash |
+
+The sidebar buttons mirror the F-key shortcuts — click or press to switch modes. The active mode is highlighted in the sidebar.
+
+---
+
+## Screens
+
+Each mode is a self-contained screen with its own widgets, workers, and layout. Switching modes pauses the previous screen's polling and starts the new one.
+
+### Spectrum
+
+Sweep spectrum analyzer across the 950–2150 MHz IF range. Renders a bar chart of signal power at each frequency step with a scrolling waterfall display below.
+
+
+
+- Bar chart with color-coded signal levels (blue → green → yellow → red)
+- Waterfall display showing power over time
+- Peak detection markers above the noise floor
+- Configurable sweep range, step size, and dwell time
+
+### Scan
+
+Three-phase automated transponder search: coarse sweep, fine sweep around peaks, and blind scan at each refined candidate across a range of symbol rates.
+
+
+
+
+1. **Coarse sweep** at 10 MHz steps to identify candidate carriers
+2. **Fine sweep** at 2 MHz steps around each detected peak
+3. **Blind scan** at each refined peak, trying symbol rates from min to max
+
+
+- Progress indicator for each phase
+- Results table with frequency, symbol rate, and lock status
+- Supports Ku-band, C-band, and custom LO configurations
+
+### Monitor
+
+Real-time signal strength display for hands-free dish alignment. Tunes to a single frequency and polls continuously with visual feedback.
+
+
+
+- Signal gauge with lock/unlock status indicator
+- Sparkline history of recent signal strength samples
+- SNR and AGC readouts
+- Designed for dish alignment — visual feedback without needing to read numbers
+
+### L-Band
+
+Direct RF input analysis in the 950–2150 MHz range with LNB power disabled. Annotates known L-band frequency allocations.
+
+
+
+- Same sweep engine as Spectrum mode, but with `lnb_lo=0` and LNB voltage off
+- Frequency allocation overlays for Amateur 23cm, GNSS, Inmarsat, Iridium, MetSat
+- Detects carrier presence regardless of modulation compatibility
+
+
+
+### Track
+
+Carrier/beacon tracker with timestamped logging. Tracks a single frequency and detects lock/unlock transitions over time.
+
+
+
+- Radar scope display showing signal history
+- Sparkline for signal power trend
+- Event log with timestamped lock/unlock transitions
+- Frequency drift detection
+
+---
+
+## Easter Eggs
+
+### Dark Side Toggle
+
+Press d to toggle between dark and light themes. Switching to dark mode triggers a notification:
+
+> *"Welcome to the Dark Side."*
+> — The Force is strong with this one
+
+
+
+Switching back to light mode:
+
+> *"The Force awakens."*
+> — A New Hope
+
+### Star Wars
+
+Press Ctrl+W to open the Star Wars overlay. It attempts to connect to `towel.blinkenlights.nl:23` for the classic ASCII Star Wars telnet animation. If the connection fails (port 23 is blocked on many networks), a built-in opening crawl plays instead — complete with a SkyWalker-1 themed storyline and Star Destroyer ASCII art.
+
+
+
+Press Esc to close the overlay and return to the main dashboard.
+
+### Kitty Cat
+
+When running inside the [Kitty terminal](https://sw.kovidgoyal.net/kitty/), the splash screen adds a subtle 🐱 to the title bar. A small nod to the terminal that made it all possible.
+
+---
+
+## Splash Screen
+
+On startup, the TUI displays a randomly selected piece of ASCII art from the [16colo.rs / Mistigris](https://16colo.rs/) archives — pre-baked as ANSI half-block art for instant rendering. The splash auto-dismisses after 5 seconds or on any keypress.
+
+
+
+Use `--no-splash` to skip it.
+
+---
+
+## See Also
+
+- [SkyWalker RF Tool](/tools/skywalker/) — CLI version of the same 5 modes (text-only output, scriptable)
+- [Spectrum Analysis](/tools/spectrum-analysis/) — practical how-to guides for each operating mode
+- [Signal Monitoring](/bcm4500/signal-monitoring/) — SIGNAL_MONITOR register protocol and data format
+- [Tuning Tool](/tools/tuning/) — primary tuning, LNB control, and transport stream capture
+- [RF Coverage](/hardware/rf-coverage/) — frequency coverage and antenna considerations
diff --git a/tui/scripts/generate_screenshots.py b/tui/scripts/generate_screenshots.py
new file mode 100644
index 0000000..68a77cf
--- /dev/null
+++ b/tui/scripts/generate_screenshots.py
@@ -0,0 +1,151 @@
+#!/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())
diff --git a/tui/src/skywalker_tui/app.py b/tui/src/skywalker_tui/app.py
index 74d9c45..b77ac85 100644
--- a/tui/src/skywalker_tui/app.py
+++ b/tui/src/skywalker_tui/app.py
@@ -134,8 +134,10 @@ class SkyWalkerApp(App):
self.action_rf_mode(mode)
def action_toggle_dark(self) -> None:
- self.dark = not self.dark
- if self.dark:
+ self.theme = (
+ "textual-dark" if self.theme == "textual-light" else "textual-light"
+ )
+ if self.current_theme.dark:
self.notify(
"Welcome to the Dark Side.",
title="The Force is strong with this one",