Fix mode panels: Screen → Container with on_show/on_hide lifecycle

Textual's ContentSwitcher expects regular Widget/Container children,
not Screen subclasses. Screen's on_mount fires for all children at
once regardless of visibility, so demo workers for all 5 modes
started simultaneously and competed for the bridge.

Container children get proper on_show/on_hide from ContentSwitcher's
visibility toggling — only the active panel's worker runs.
This commit is contained in:
Ryan Malloy 2026-02-13 04:42:59 -07:00
parent 64c33985a3
commit 8da486719a
5 changed files with 23 additions and 28 deletions

View File

@ -10,8 +10,7 @@ import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "tools")) sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "tools"))
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import Horizontal, Vertical from textual.containers import Container, Horizontal, Vertical
from textual.screen import Screen
from textual.widgets import Label, Input, Button, Static, ProgressBar, Checkbox from textual.widgets import Label, Input, Button, Static, ProgressBar, Checkbox
from textual import work from textual import work
from textual.worker import Worker from textual.worker import Worker
@ -37,7 +36,7 @@ def _alloc_table(start: float, stop: float) -> str:
return "\n".join(lines) return "\n".join(lines)
class LBandScreen(Screen): class LBandScreen(Container):
"""L-band direct input analyzer with allocation annotations.""" """L-band direct input analyzer with allocation annotations."""
DEFAULT_CSS = """ DEFAULT_CSS = """
@ -130,11 +129,11 @@ class LBandScreen(Screen):
yield Button("Sweep", id="lband-sweep-btn", variant="success") yield Button("Sweep", id="lband-sweep-btn", variant="success")
yield Button("Stop", id="lband-stop-btn", variant="error") yield Button("Stop", id="lband-stop-btn", variant="error")
def on_mount(self) -> None: def on_show(self) -> None:
if self._bridge.is_demo: if self._bridge.is_demo and not self._sweeping:
self._start_sweep() self._start_sweep()
def on_unmount(self) -> None: def on_hide(self) -> None:
self._stop_sweep() self._stop_sweep()
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:

View File

@ -6,8 +6,7 @@ sparkline history.
""" """
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import Horizontal, Vertical from textual.containers import Container, Horizontal, Vertical
from textual.screen import Screen
from textual.widgets import Label, Input, Button, Static from textual.widgets import Label, Input, Button, Static
from textual import work from textual import work
from textual.worker import Worker, WorkerState from textual.worker import Worker, WorkerState
@ -16,7 +15,7 @@ from skywalker_tui.widgets.signal_gauge import SignalGauge
from skywalker_tui.widgets.sparkline_widget import SparklineWidget from skywalker_tui.widgets.sparkline_widget import SparklineWidget
class MonitorScreen(Screen): class MonitorScreen(Container):
"""Real-time signal monitor with gauge and sparkline.""" """Real-time signal monitor with gauge and sparkline."""
DEFAULT_CSS = """ DEFAULT_CSS = """
@ -96,12 +95,12 @@ class MonitorScreen(Screen):
yield Button("Start", id="mon-start", variant="success") yield Button("Start", id="mon-start", variant="success")
yield Button("Stop", id="mon-stop", variant="error") yield Button("Stop", id="mon-stop", variant="error")
def on_mount(self) -> None: def on_show(self) -> None:
# Auto-start polling in demo mode # Auto-start polling in demo mode
if self._bridge.is_demo: if self._bridge.is_demo and not self._polling:
self._start_polling() self._start_polling()
def on_unmount(self) -> None: def on_hide(self) -> None:
self._stop_polling() self._stop_polling()
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:

View File

@ -8,8 +8,7 @@ import struct
import time import time
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import Horizontal, Vertical from textual.containers import Container, Horizontal, Vertical
from textual.screen import Screen
from textual.widgets import Label, Input, Button, Static, ProgressBar from textual.widgets import Label, Input, Button, Static, ProgressBar
from textual import work from textual import work
from textual.worker import Worker from textual.worker import Worker
@ -18,7 +17,7 @@ from skywalker_tui.widgets.spectrum_plot import SpectrumPlot
from skywalker_tui.widgets.frequency_table import FrequencyTable from skywalker_tui.widgets.frequency_table import FrequencyTable
class ScanScreen(Screen): class ScanScreen(Container):
"""Multi-phase transponder scanner with progress and results table.""" """Multi-phase transponder scanner with progress and results table."""
DEFAULT_CSS = """ DEFAULT_CSS = """
@ -111,7 +110,7 @@ class ScanScreen(Screen):
yield Button("Scan", id="scan-start-btn", variant="success") yield Button("Scan", id="scan-start-btn", variant="success")
yield Button("Stop", id="scan-stop-btn", variant="error") yield Button("Stop", id="scan-stop-btn", variant="error")
def on_unmount(self) -> None: def on_hide(self) -> None:
self._stop_scan() self._stop_scan()
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:

View File

@ -5,8 +5,7 @@ waterfall beneath it. Uses threaded workers for the blocking USB sweep.
""" """
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import Horizontal, Vertical from textual.containers import Container, Horizontal, Vertical
from textual.screen import Screen
from textual.widgets import Label, Input, Button, Static, ProgressBar, Checkbox from textual.widgets import Label, Input, Button, Static, ProgressBar, Checkbox
from textual import work from textual import work
from textual.worker import Worker from textual.worker import Worker
@ -15,7 +14,7 @@ from skywalker_tui.widgets.spectrum_plot import SpectrumPlot
from skywalker_tui.widgets.waterfall import WaterfallDisplay from skywalker_tui.widgets.waterfall import WaterfallDisplay
class SpectrumScreen(Screen): class SpectrumScreen(Container):
"""Spectrum analyzer with bar chart and optional waterfall.""" """Spectrum analyzer with bar chart and optional waterfall."""
DEFAULT_CSS = """ DEFAULT_CSS = """
@ -94,11 +93,11 @@ class SpectrumScreen(Screen):
yield Button("Sweep", id="spec-sweep-btn", variant="success") yield Button("Sweep", id="spec-sweep-btn", variant="success")
yield Button("Stop", id="spec-stop-btn", variant="error") yield Button("Stop", id="spec-stop-btn", variant="error")
def on_mount(self) -> None: def on_show(self) -> None:
if self._bridge.is_demo: if self._bridge.is_demo and not self._sweeping:
self._start_sweep() self._start_sweep()
def on_unmount(self) -> None: def on_hide(self) -> None:
self._stop_sweep() self._stop_sweep()
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:

View File

@ -12,8 +12,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import Horizontal, Vertical from textual.containers import Container, Horizontal, Vertical
from textual.screen import Screen
from textual.widgets import Label, Input, Button, Static, RichLog from textual.widgets import Label, Input, Button, Static, RichLog
from textual import work from textual import work
from textual.worker import Worker from textual.worker import Worker
@ -22,7 +21,7 @@ from skywalker_tui.widgets.signal_gauge import SignalGauge
from skywalker_tui.widgets.sparkline_widget import SparklineWidget from skywalker_tui.widgets.sparkline_widget import SparklineWidget
class TrackScreen(Screen): class TrackScreen(Container):
"""Long-running carrier tracker with event log and export.""" """Long-running carrier tracker with event log and export."""
DEFAULT_CSS = """ DEFAULT_CSS = """
@ -137,11 +136,11 @@ class TrackScreen(Screen):
yield Button("Export CSV", id="trk-csv-btn") yield Button("Export CSV", id="trk-csv-btn")
yield Button("Export JSONL", id="trk-jsonl-btn") yield Button("Export JSONL", id="trk-jsonl-btn")
def on_mount(self) -> None: def on_show(self) -> None:
if self._bridge.is_demo: if self._bridge.is_demo and not self._tracking:
self._start_tracking() self._start_tracking()
def on_unmount(self) -> None: def on_hide(self) -> None:
self._stop_tracking() self._stop_tracking()
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None: