"""SkyWalker-1 TUI — main application. Provides mode switching between 5 RF operating modes via a sidebar and F-key shortcuts. Each mode is a Screen subclass that manages its own workers. Note: We use "rf_mode" terminology for our 5 operating modes to avoid colliding 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 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 MODES = { "spectrum": ("F1 Spectrum", SpectrumScreen), "scan": ("F2 Scan", ScanScreen), "monitor": ("F3 Monitor", MonitorScreen), "lband": ("F4 L-Band", LBandScreen), "track": ("F5 Track", TrackScreen), } 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("q", "quit", "Quit", show=True), Binding("d", "toggle_dark", "Theme", show=True), ] def __init__(self, bridge: USBBridge, initial_mode: str = "spectrum"): super().__init__() self._bridge = bridge self._initial_rf_mode = initial_mode self._active_rf_mode = initial_mode self._rf_screens: dict[str, object] = {} 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 status = self.query_one(DeviceStatusBar) status.update_status(self._bridge) # Install all 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: """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 — {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.dark = not self.dark 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( "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) try: app.run() finally: bridge.close()