Add radar scope, splash screen, Star Wars easter egg, and harden TUI
New features: - P1 green phosphor radar scope widget on Track screen with LUT-optimized rendering - Splash screen with pre-baked ANSI half-block art from 16colo.rs/Mistigris - Star Wars ASCII animation via telnet (ctrl+w) with IPv6 happy eyeballs and offline fallback - Dark Side / New Hope toast notifications on theme toggle - Kitty terminal detection with cat emoji on splash Robustness (from Apollo code review): - Circuit breaker in track loop after 10 consecutive errors - Input validation for frequency, symbol rate, step size across scan/spectrum/track - Consolidated sys.path manipulation into __init__.py - Radar scope pre-computes dist/angle LUT per pixel on resize Cleanup: - Removed unused imports across lband, monitor, scan, signal_gauge - Moved Pillow/textual-image to optional dev deps (splash uses pre-baked ANSI) - Added 41-test pytest suite covering telnet IAC parsing, radar geometry, splash assets
This commit is contained in:
parent
8da486719a
commit
6dcb6b693a
33 changed files with 2076 additions and 86 deletions
|
|
@ -9,13 +9,6 @@ with Textual's built-in App.mode / _current_mode / _screen_stacks system.
|
|||
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add tools directory to path for skywalker_lib import
|
||||
_tools_dir = str(Path(__file__).resolve().parent.parent.parent.parent / "tools")
|
||||
if _tools_dir not in sys.path:
|
||||
sys.path.insert(0, _tools_dir)
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
|
|
@ -57,14 +50,17 @@ class SkyWalkerApp(App):
|
|||
Binding("f5", "rf_mode('track')", "Track", show=True),
|
||||
Binding("q", "quit", "Quit", show=True),
|
||||
Binding("d", "toggle_dark", "Theme", show=True),
|
||||
Binding("ctrl+w", "starwars", "Star Wars", show=False),
|
||||
]
|
||||
|
||||
def __init__(self, bridge: USBBridge, initial_mode: str = "spectrum"):
|
||||
def __init__(self, bridge: USBBridge, initial_mode: str = "spectrum",
|
||||
show_splash: bool = True):
|
||||
super().__init__()
|
||||
self._bridge = bridge
|
||||
self._initial_rf_mode = initial_mode
|
||||
self._active_rf_mode = initial_mode
|
||||
self._rf_screens: dict[str, object] = {}
|
||||
self._show_splash = show_splash
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
|
|
@ -81,18 +77,35 @@ class SkyWalkerApp(App):
|
|||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
# Initialize status bar
|
||||
# Initialize status bar (lightweight)
|
||||
status = self.query_one(DeviceStatusBar)
|
||||
status.update_status(self._bridge)
|
||||
|
||||
# Install all mode screens into the content switcher
|
||||
if self._show_splash:
|
||||
# Push splash FIRST, then init mode screens behind it.
|
||||
# Two-tick chain: tick 1 = splash renders, tick 2 = heavy work.
|
||||
self.call_later(self._push_splash)
|
||||
else:
|
||||
self.call_later(self._init_mode_screens)
|
||||
|
||||
def _push_splash(self) -> None:
|
||||
"""Push splash screen, then defer heavy mode screen init."""
|
||||
from skywalker_tui.screens.splash import SplashScreen
|
||||
try:
|
||||
self.push_screen(SplashScreen())
|
||||
except Exception:
|
||||
pass
|
||||
# Mode screens mount behind the splash overlay — pre-baked ANSI art
|
||||
# renders instantly so no delay needed before heavy work starts
|
||||
self.call_later(self._init_mode_screens)
|
||||
|
||||
def _init_mode_screens(self) -> None:
|
||||
"""Mount all 5 mode screens into the content switcher."""
|
||||
switcher = self.query_one("#content-area", ContentSwitcher)
|
||||
for mode_key, (_label, cls) in MODES.items():
|
||||
screen = cls(self._bridge, id=f"screen-{mode_key}")
|
||||
self._rf_screens[mode_key] = screen
|
||||
switcher.mount(screen)
|
||||
|
||||
# Activate initial mode
|
||||
self.action_rf_mode(self._initial_rf_mode)
|
||||
|
||||
def action_rf_mode(self, mode: str) -> None:
|
||||
|
|
@ -110,7 +123,7 @@ class SkyWalkerApp(App):
|
|||
btn.remove_class("-active")
|
||||
self.query_one(f"#btn-{mode}", Button).add_class("-active")
|
||||
|
||||
self.sub_title = f"DVB-S RF Tool — {MODES[mode][0]}"
|
||||
self.sub_title = f"DVB-S RF Tool \u2014 {MODES[mode][0]}"
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle sidebar mode button clicks."""
|
||||
|
|
@ -122,6 +135,25 @@ class SkyWalkerApp(App):
|
|||
|
||||
def action_toggle_dark(self) -> None:
|
||||
self.dark = not self.dark
|
||||
if self.dark:
|
||||
self.notify(
|
||||
"Welcome to the Dark Side.",
|
||||
title="The Force is strong with this one",
|
||||
severity="warning",
|
||||
timeout=4,
|
||||
)
|
||||
else:
|
||||
self.notify(
|
||||
"The Force awakens.",
|
||||
title="A New Hope",
|
||||
severity="information",
|
||||
timeout=4,
|
||||
)
|
||||
|
||||
def action_starwars(self) -> None:
|
||||
"""Easter egg: stream ASCII Star Wars from telnet."""
|
||||
from skywalker_tui.screens.starwars import StarWarsScreen
|
||||
self.push_screen(StarWarsScreen())
|
||||
|
||||
|
||||
def main():
|
||||
|
|
@ -133,6 +165,10 @@ def main():
|
|||
"--demo", action="store_true",
|
||||
help="Use synthetic signal data (no hardware required)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-splash", action="store_true",
|
||||
help="Skip the splash screen on startup",
|
||||
)
|
||||
parser.add_argument(
|
||||
"mode", nargs="?", default="spectrum",
|
||||
choices=list(MODES.keys()),
|
||||
|
|
@ -158,7 +194,11 @@ def main():
|
|||
print("Use --demo for synthetic signal data.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
app = SkyWalkerApp(bridge=bridge, initial_mode=args.mode)
|
||||
app = SkyWalkerApp(
|
||||
bridge=bridge,
|
||||
initial_mode=args.mode,
|
||||
show_splash=not args.no_splash,
|
||||
)
|
||||
try:
|
||||
app.run()
|
||||
finally:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue