birdcage/tui/src/birdcage_tui/widgets/status_strip.py
Ryan Malloy 145763fcfb Rebuild TUI as 4-tab layout with 10 new widgets
Replace sidebar + 5-screen layout with horizontal tab bar (F1-F4)
and persistent StatusStrip. Consolidate Position + Scan screens into
Signal (F3) with Monitor/Sweep/Sky Map sub-modes via ModeBar.

Screens:
  - F1 Dashboard: system health, tracking panel, quick actions, presets
  - F2 Control: motor tuning, compass rose, preset management
  - F3 Signal: RSSI monitor, 1D sweep (SweepPlot), 2D sky map (heatmap)
  - F4 System: NVS editor with regex filter, EEPROM, firmware info
  - F5 Console: push/pop overlay (no longer a tab)

New widgets: StatusStrip, ModeBar, SweepPlot, QuickActions, PresetList,
ReceiverInfo, MotorTuning, NvsFilter, SystemHealth, TrackingPanel.

Removed: PositionScreen, ScanScreen, DeviceStatusBar (functionality
absorbed into new screens and StatusStrip).

App-level position poll feeds StatusStrip and active screen at ~2 Hz.
Fix shared threading.Event across instances (class-level mutable default).
2026-02-14 18:04:50 -07:00

112 lines
3.3 KiB
Python

"""Status strip -- persistent 1-row connection/position/signal bar."""
from rich.text import Text
from textual.reactive import reactive
from textual.widgets import Static
# Firmware prompt → display color mapping.
_MENU_COLORS: dict[str, str] = {
"TRK>": "#00d4aa",
"MOT>": "#00e060",
"DVB>": "#2080d0",
"NVS>": "#e8a020",
"A3981>": "#00b8c8",
"STEP>": "#40c0a0",
"EE>": "#e8a020",
"OS>": "#8090a0",
"ADC>": "#00b8c8",
"GPIO>": "#40c0a0",
"PEAK>": "#e8c020",
}
class StatusStrip(Static):
"""Persistent status bar showing connection, position, signal, and motor state.
Docked below the header on every screen. Updated by the app-level
position poll and signal monitors.
"""
connected: reactive[bool] = reactive(False)
demo: reactive[bool] = reactive(False)
port: reactive[str] = reactive("/dev/ttyUSB0")
azimuth: reactive[float] = reactive(0.0)
elevation: reactive[float] = reactive(0.0)
rssi: reactive[int] = reactive(-1) # -1 means no data
motor_state: reactive[str] = reactive("IDLE")
fw_menu: reactive[str] = reactive("TRK>")
def render(self) -> Text:
result = Text()
sep = Text(" \u2502 ", style="#1a2a38")
# Connection status
if self.demo:
result.append(" DEMO", style="#e8a020 bold italic")
elif self.connected:
result.append(" CONNECTED", style="#00e060 bold")
result.append(f" {self.port}", style="#506878")
else:
result.append(" OFFLINE", style="#e04040 bold")
result.append_text(sep)
# Position
result.append("AZ ", style="#506878")
result.append(f"{self.azimuth:7.2f}", style="#00d4aa bold")
result.append(" EL ", style="#506878")
result.append(f"{self.elevation:6.2f}", style="#00d4aa bold")
result.append_text(sep)
# Signal (RSSI)
if self.rssi >= 0:
result.append("RSSI ", style="#506878")
result.append(f"{self.rssi}", style="#00b8c8 bold")
else:
result.append("RSSI ", style="#384858")
result.append("---", style="#384858")
result.append_text(sep)
# Motor state
state = self.motor_state
if state == "MOVING":
result.append(state, style="#e8c020 bold")
elif state == "ENGAGED":
result.append(state, style="#00e060")
else:
result.append(state, style="#506878")
result.append_text(sep)
# Firmware context
menu_color = _MENU_COLORS.get(self.fw_menu, "#506878")
result.append(self.fw_menu, style=f"{menu_color}")
return result
def watch_connected(self, _value: bool) -> None:
self.refresh()
def watch_demo(self, _value: bool) -> None:
self.refresh()
def watch_port(self, _value: str) -> None:
self.refresh()
def watch_azimuth(self, _value: float) -> None:
self.refresh()
def watch_elevation(self, _value: float) -> None:
self.refresh()
def watch_rssi(self, _value: int) -> None:
self.refresh()
def watch_motor_state(self, _value: str) -> None:
self.refresh()
def watch_fw_menu(self, _value: str) -> None:
self.refresh()