""" Signals Tab — live scrolling monitor of all bus events. Subscribes to every topic ("**") and appends new events to a scrollable RichLog widget, styled to resemble an oscilloscope or signal analyser. """ from __future__ import annotations import json from datetime import datetime, timezone from typing import Any from textual.app import ComposeResult from textual.binding import Binding from textual.widget import Widget from textual.widgets import Label, RichLog, Static def _preview(data: Any, max_len: int = 80) -> str: """Produce a compact single-line preview of event data.""" if data is None: return "null" try: raw = json.dumps(data, default=str) except Exception: raw = str(data) if len(raw) > max_len: raw = raw[:max_len] + "…" return raw class SignalsTab(Widget): """ Live event signal monitor. Every event published on the bus appears here with: - Timestamp (HH:MM:SS.mmm) - Topic (colour-coded by namespace) - Data preview (truncated JSON) """ DEFAULT_CSS = """ SignalsTab { layout: vertical; height: 1fr; padding: 1 2; } SignalsTab .tab-title { color: $primary; text-style: bold; height: 1; margin-bottom: 1; } SignalsTab RichLog { height: 1fr; border: round $primary; background: $surface; scrollbar-gutter: stable; } SignalsTab .hint { height: 1; color: $text-muted; margin-top: 1; } """ BINDINGS = [ Binding("c", "clear_log", "Clear"), Binding("p", "toggle_pause", "Pause"), ] # Colour map per topic namespace TOPIC_COLOURS = { "network": "cyan", "dns": "blue", "firewall": "red", "system": "green", "state": "yellow", "registry": "magenta", } def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self._paused = False self._event_count = 0 self._sub_id: str | None = None def compose(self) -> ComposeResult: yield Static("// SIGNALS — Live Event Monitor", classes="tab-title") yield RichLog(id="signal-log", highlight=True, markup=True, max_lines=500) yield Static( "Press [bold]c[/bold] to clear | [bold]p[/bold] to pause", classes="hint", ) def on_mount(self) -> None: app = self.app if hasattr(app, "bus"): self._sub_id = app.bus.subscribe("**", self._on_any_event) def on_unmount(self) -> None: app = self.app if hasattr(app, "bus") and self._sub_id: app.bus.unsubscribe(self._sub_id) def _on_any_event(self, topic: str, data: Any) -> None: if self._paused: return self.call_from_thread(self._append_event, topic, data) def _append_event(self, topic: str, data: Any) -> None: self._event_count += 1 log_widget = self.query_one("#signal-log", RichLog) now = datetime.now(timezone.utc) ts = now.strftime("%H:%M:%S") + f".{now.microsecond // 1000:03d}" namespace = topic.split(".")[0] colour = self.TOPIC_COLOURS.get(namespace, "white") preview = _preview(data) # Format: [dim]HH:MM:SS.mmm[/] [colour]topic[/] data line = ( f"[dim]{ts}[/dim] " f"[{colour} bold]{topic:<35}[/{colour} bold] " f"[dim]{preview}[/dim]" ) log_widget.write(line) def action_clear_log(self) -> None: log_widget = self.query_one("#signal-log", RichLog) log_widget.clear() self._event_count = 0 def action_toggle_pause(self) -> None: self._paused = not self._paused log_widget = self.query_one("#signal-log", RichLog) status = "[yellow]PAUSED[/yellow]" if self._paused else "[green]LIVE[/green]" log_widget.write(f" ── {status} ──")