"""SkyWalker-1 TUI — main application. Provides mode switching between 8 operating modes via a sidebar and F-key shortcuts. Each mode is a Container subclass that manages its own workers. Note: We use "rf_mode" terminology for our operating modes to avoid colliding with Textual's built-in App.mode / _current_mode / _screen_stacks system. """ import argparse import sys from textual.app import App, ComposeResult from textual.binding import Binding from textual.containers import Horizontal, Vertical from textual.widgets import Header, Footer, Button, Label, Static, ContentSwitcher from skywalker_tui.bridge import USBBridge from skywalker_tui.demo import DemoDevice from skywalker_tui.widgets.status_bar import DeviceStatusBar from skywalker_tui.screens.spectrum import SpectrumScreen from skywalker_tui.screens.scan import ScanScreen from skywalker_tui.screens.monitor import MonitorScreen from skywalker_tui.screens.lband import LBandScreen from skywalker_tui.screens.track import TrackScreen from skywalker_tui.screens.device import DeviceScreen from skywalker_tui.screens.stream import StreamScreen from skywalker_tui.screens.config import ConfigScreen from skywalker_tui.screens.motor import MotorScreen from skywalker_tui.screens.survey import SurveyScreen MODES = { "spectrum": ("F1 Spectrum", SpectrumScreen), "scan": ("F2 Scan", ScanScreen), "monitor": ("F3 Monitor", MonitorScreen), "lband": ("F4 L-Band", LBandScreen), "track": ("F5 Track", TrackScreen), "device": ("F6 Device", DeviceScreen), "stream": ("F7 Stream", StreamScreen), "config": ("F8 Config", ConfigScreen), "motor": ("F9 Motor", MotorScreen), "survey": ("F10 Survey", SurveyScreen), } class SkyWalkerApp(App): """Textual TUI for Genpix SkyWalker-1 DVB-S receiver.""" TITLE = "SkyWalker-1" SUB_TITLE = "DVB-S RF Tool" CSS_PATH = "theme.tcss" BINDINGS = [ Binding("f1", "rf_mode('spectrum')", "Spectrum", show=True), Binding("f2", "rf_mode('scan')", "Scan", show=True), Binding("f3", "rf_mode('monitor')", "Monitor", show=True), Binding("f4", "rf_mode('lband')", "L-Band", show=True), Binding("f5", "rf_mode('track')", "Track", show=True), Binding("f6", "rf_mode('device')", "Device", show=True), Binding("f7", "rf_mode('stream')", "Stream", show=True), Binding("f8", "rf_mode('config')", "Config", show=True), Binding("f9", "rf_mode('motor')", "Motor", show=True), Binding("f10", "rf_mode('survey')", "Survey", 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", 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() with Horizontal(): with Vertical(id="sidebar"): yield Label("[bold #00d4aa]SkyWalker-1[/]", classes="sidebar-heading") yield Label("[#506878]DVB-S RF Tool[/]", classes="sidebar-heading") yield Static("") for mode_key, (label, _cls) in MODES.items(): yield Button(label, id=f"btn-{mode_key}", classes="mode-button") yield Static("") yield DeviceStatusBar(self._bridge) yield ContentSwitcher(id="content-area") yield Footer() def on_mount(self) -> None: # Initialize status bar (lightweight) status = self.query_one(DeviceStatusBar) status.update_status(self._bridge) 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 8 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) self.action_rf_mode(self._initial_rf_mode) def action_rf_mode(self, mode: str) -> None: """Switch to a different RF operating mode.""" if mode not in MODES: return self._active_rf_mode = mode switcher = self.query_one("#content-area", ContentSwitcher) switcher.current = f"screen-{mode}" # Update sidebar button highlights for mode_key in MODES: btn = self.query_one(f"#btn-{mode_key}", Button) btn.remove_class("-active") self.query_one(f"#btn-{mode}", Button).add_class("-active") 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.""" btn_id = event.button.id or "" if btn_id.startswith("btn-"): mode = btn_id[4:] if mode in MODES: self.action_rf_mode(mode) def action_toggle_dark(self) -> None: 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", 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(): parser = argparse.ArgumentParser( prog="skywalker-tui", description="Textual TUI for Genpix SkyWalker-1 DVB-S receiver", ) parser.add_argument( "--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()), help="Initial mode (default: spectrum)", ) parser.add_argument( "-v", "--verbose", action="store_true", help="Verbose USB logging (hardware mode only)", ) args = parser.parse_args() if args.demo: device = DemoDevice() bridge = USBBridge(device) else: try: from skywalker_lib import SkyWalker1 device = SkyWalker1(verbose=args.verbose) device.open() bridge = USBBridge(device) except Exception as e: print(f"Cannot open SkyWalker-1: {e}", file=sys.stderr) print("Use --demo for synthetic signal data.", file=sys.stderr) sys.exit(1) app = SkyWalkerApp( bridge=bridge, initial_mode=args.mode, show_splash=not args.no_splash, ) try: app.run() finally: bridge.close()