mirror of
https://github.com/samjage/metro-warden.git
synced 2026-06-06 01:20:42 +00:00
141 lines
3.9 KiB
Python
141 lines
3.9 KiB
Python
"""
|
|
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} ──")
|