mirror of
https://github.com/samjage/metro-warden.git
synced 2026-06-06 04:30:42 +00:00
Initial commit: Metro Warden TUI network operations center
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Registry Tab — displays loaded plugins and their status.
|
||||
|
||||
Subscribes to registry.plugin.* events to show live plugin state.
|
||||
Allows loading/unloading plugins interactively.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import DataTable, Footer, Label, Static
|
||||
|
||||
|
||||
class RegistryTab(Widget):
|
||||
"""
|
||||
Plugin registry viewer.
|
||||
|
||||
Shows all discovered plugins with their version, status, module path,
|
||||
and load timestamp. Pressing Enter on a row toggles load/unload.
|
||||
"""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
RegistryTab {
|
||||
layout: vertical;
|
||||
height: 1fr;
|
||||
padding: 1 2;
|
||||
}
|
||||
RegistryTab .tab-title {
|
||||
color: $primary;
|
||||
text-style: bold;
|
||||
height: 1;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
RegistryTab DataTable {
|
||||
height: 1fr;
|
||||
border: round $primary;
|
||||
}
|
||||
RegistryTab .hint {
|
||||
height: 1;
|
||||
color: $text-muted;
|
||||
margin-top: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("r", "refresh_registry", "Refresh"),
|
||||
Binding("enter", "toggle_plugin", "Toggle"),
|
||||
]
|
||||
|
||||
COLUMNS = [
|
||||
("Name", 14),
|
||||
("Version", 9),
|
||||
("Status", 11),
|
||||
("Tags", 22),
|
||||
("Loaded At", 22),
|
||||
("Module", 32),
|
||||
("Error", 30),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static("// REGISTRY — Plugin Manifest", classes="tab-title")
|
||||
yield DataTable(id="registry-table", zebra_stripes=True, cursor_type="row")
|
||||
yield Static(
|
||||
"Press [bold]r[/bold] to refresh | [bold]Enter[/bold] to toggle load",
|
||||
classes="hint",
|
||||
)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
table = self.query_one("#registry-table", DataTable)
|
||||
for col_label, width in self.COLUMNS:
|
||||
table.add_column(col_label, width=width)
|
||||
|
||||
app = self.app
|
||||
if hasattr(app, "bus"):
|
||||
app.bus.subscribe("registry.plugin.*", self._on_registry_event)
|
||||
self._refresh_table()
|
||||
|
||||
def _on_registry_event(self, topic: str, data: Any) -> None:
|
||||
self.call_from_thread(self._refresh_table)
|
||||
|
||||
def _refresh_table(self) -> None:
|
||||
table = self.query_one("#registry-table", DataTable)
|
||||
table.clear()
|
||||
app = self.app
|
||||
if not hasattr(app, "registry"):
|
||||
return
|
||||
for record in sorted(app.registry.all_records(), key=lambda r: r.name):
|
||||
loaded_at = record.loaded_at.strftime("%Y-%m-%d %H:%M:%S") if record.loaded_at else "—"
|
||||
table.add_row(
|
||||
record.name,
|
||||
record.version,
|
||||
record.status.name,
|
||||
", ".join(record.tags) or "—",
|
||||
loaded_at,
|
||||
record.module_path,
|
||||
record.error or "—",
|
||||
)
|
||||
|
||||
def action_refresh_registry(self) -> None:
|
||||
self._refresh_table()
|
||||
|
||||
def action_toggle_plugin(self) -> None:
|
||||
table = self.query_one("#registry-table", DataTable)
|
||||
row_key = table.cursor_row
|
||||
if row_key is None:
|
||||
return
|
||||
# Get plugin name from the focused row
|
||||
try:
|
||||
cell_value = table.get_cell_at((row_key, 0))
|
||||
except Exception:
|
||||
return
|
||||
plugin_name = str(cell_value)
|
||||
app = self.app
|
||||
if not hasattr(app, "registry"):
|
||||
return
|
||||
record = app.registry.get(plugin_name)
|
||||
if record is None:
|
||||
return
|
||||
from core.registry import PluginStatus
|
||||
if record.status == PluginStatus.ACTIVE:
|
||||
app.registry.unload(plugin_name)
|
||||
else:
|
||||
app.registry.load(plugin_name)
|
||||
self._refresh_table()
|
||||
Reference in New Issue
Block a user