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
@ -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' },
|
||||
|
||||
220
site/src/assets/tui/dark-mode.svg
Normal file
|
After Width: | Height: | Size: 95 KiB |
232
site/src/assets/tui/lband.svg
Normal file
|
After Width: | Height: | Size: 173 KiB |
231
site/src/assets/tui/monitor.svg
Normal file
|
After Width: | Height: | Size: 119 KiB |
222
site/src/assets/tui/scan.svg
Normal file
|
After Width: | Height: | Size: 101 KiB |
221
site/src/assets/tui/spectrum.svg
Normal file
|
After Width: | Height: | Size: 96 KiB |
2311
site/src/assets/tui/splash.svg
Normal file
|
After Width: | Height: | Size: 596 KiB |
217
site/src/assets/tui/starwars.svg
Normal file
|
After Width: | Height: | Size: 64 KiB |
224
site/src/assets/tui/track.svg
Normal file
|
After Width: | Height: | Size: 322 KiB |
178
site/src/content/docs/tools/tui.mdx
Normal file
@ -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.
|
||||
|
||||
<Badge text="Requires custom firmware v3.02.0+" variant="caution" />
|
||||
|
||||

|
||||
|
||||
## 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` |
|
||||
|
||||
<Aside type="tip">
|
||||
Demo mode generates realistic-looking synthetic data for every screen, making it useful for familiarization, documentation, and development without satellite hardware.
|
||||
</Aside>
|
||||
|
||||
---
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| <kbd>F1</kbd> | Spectrum analyzer |
|
||||
| <kbd>F2</kbd> | Transponder scanner |
|
||||
| <kbd>F3</kbd> | Signal monitor |
|
||||
| <kbd>F4</kbd> | L-Band analyzer |
|
||||
| <kbd>F5</kbd> | Carrier tracker |
|
||||
| <kbd>d</kbd> | Toggle dark/light theme |
|
||||
| <kbd>q</kbd> | Quit |
|
||||
| <kbd>Ctrl+W</kbd> | Easter egg |
|
||||
| <kbd>Esc</kbd> | 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 <Badge text="F1" variant="note" />
|
||||
|
||||
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 <Badge text="F2" variant="note" />
|
||||
|
||||
Three-phase automated transponder search: coarse sweep, fine sweep around peaks, and blind scan at each refined candidate across a range of symbol rates.
|
||||
|
||||

|
||||
|
||||
<Steps>
|
||||
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
|
||||
</Steps>
|
||||
|
||||
- Progress indicator for each phase
|
||||
- Results table with frequency, symbol rate, and lock status
|
||||
- Supports Ku-band, C-band, and custom LO configurations
|
||||
|
||||
### Monitor <Badge text="F3" variant="note" />
|
||||
|
||||
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 <Badge text="F4" variant="note" />
|
||||
|
||||
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
|
||||
|
||||
<Aside type="caution">
|
||||
Connect an L-band antenna or filtered preamp directly to the F-type input. LNB power is explicitly disabled in this mode to protect non-LNB equipment.
|
||||
</Aside>
|
||||
|
||||
### Track <Badge text="F5" variant="note" />
|
||||
|
||||
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 <kbd>d</kbd> 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 <kbd>Ctrl+W</kbd> 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 <kbd>Esc</kbd> 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
|
||||
151
tui/scripts/generate_screenshots.py
Normal file
@ -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())
|
||||
@ -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",
|
||||
|
||||