skywalker-1/tui/src/skywalker_tui/app.py

166 lines
5.8 KiB
Python
Raw Normal View History

"""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()