mirror of
https://github.com/samjage/metro-warden.git
synced 2026-06-06 02:40:43 +00:00
261 lines
8.7 KiB
Python
261 lines
8.7 KiB
Python
"""
|
|
Settings Tab — application configuration form.
|
|
|
|
Reads from the StateStore and writes back on change.
|
|
Supports editing poll intervals, theme options, and plugin enable/disable.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any, Dict
|
|
|
|
from textual.app import ComposeResult
|
|
from textual.binding import Binding
|
|
from textual.widget import Widget
|
|
from textual.widgets import Button, Input, Label, Select, Static, Switch
|
|
|
|
|
|
CONFIG_PATH = Path(__file__).parent.parent.parent / "config" / "defaults.toml"
|
|
|
|
|
|
class SettingRow(Widget):
|
|
"""A single label + control row in the settings form."""
|
|
|
|
DEFAULT_CSS = """
|
|
SettingRow {
|
|
layout: horizontal;
|
|
height: 3;
|
|
align: left middle;
|
|
margin-bottom: 1;
|
|
}
|
|
SettingRow .setting-label {
|
|
width: 28;
|
|
color: $text;
|
|
content-align: left middle;
|
|
}
|
|
SettingRow .setting-control {
|
|
width: 24;
|
|
}
|
|
SettingRow .setting-desc {
|
|
width: 1fr;
|
|
color: $text-muted;
|
|
content-align: left middle;
|
|
padding-left: 2;
|
|
}
|
|
"""
|
|
|
|
def __init__(self, label: str, control: Widget, description: str = "", **kwargs) -> None:
|
|
super().__init__(**kwargs)
|
|
self._label = label
|
|
self._control = control
|
|
self._description = description
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield Static(self._label, classes="setting-label")
|
|
yield self._control
|
|
if self._description:
|
|
yield Static(self._description, classes="setting-desc")
|
|
|
|
|
|
class SettingsTab(Widget):
|
|
"""
|
|
Application settings form.
|
|
|
|
Changes are applied immediately to the state store and persisted
|
|
to config/defaults.toml on save.
|
|
"""
|
|
|
|
DEFAULT_CSS = """
|
|
SettingsTab {
|
|
layout: vertical;
|
|
height: 1fr;
|
|
padding: 1 2;
|
|
overflow-y: auto;
|
|
}
|
|
SettingsTab .tab-title {
|
|
color: $primary;
|
|
text-style: bold;
|
|
height: 1;
|
|
margin-bottom: 1;
|
|
}
|
|
SettingsTab .section-header {
|
|
color: $accent;
|
|
text-style: bold;
|
|
height: 1;
|
|
margin-top: 2;
|
|
margin-bottom: 1;
|
|
}
|
|
SettingsTab .button-row {
|
|
layout: horizontal;
|
|
height: 3;
|
|
margin-top: 2;
|
|
}
|
|
SettingsTab Button {
|
|
margin-right: 2;
|
|
}
|
|
SettingsTab .status-label {
|
|
height: 1;
|
|
color: $success;
|
|
margin-top: 1;
|
|
}
|
|
"""
|
|
|
|
BINDINGS = [Binding("s", "save_settings", "Save")]
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield Static("// SETTINGS — Configuration", classes="tab-title")
|
|
|
|
yield Static("Polling Intervals", classes="section-header")
|
|
yield SettingRow(
|
|
"Network poll interval (s):",
|
|
Input(value="5", id="cfg-network-interval", classes="setting-control"),
|
|
"How often to refresh interface stats",
|
|
)
|
|
yield SettingRow(
|
|
"DNS poll interval (s):",
|
|
Input(value="30", id="cfg-dns-interval", classes="setting-control"),
|
|
"How often to check DNS resolver health",
|
|
)
|
|
yield SettingRow(
|
|
"System poll interval (s):",
|
|
Input(value="3", id="cfg-system-interval", classes="setting-control"),
|
|
"How often to sample CPU / memory",
|
|
)
|
|
yield SettingRow(
|
|
"Firewall poll interval (s):",
|
|
Input(value="60", id="cfg-firewall-interval", classes="setting-control"),
|
|
"How often to re-read firewall rules",
|
|
)
|
|
|
|
yield Static("Plugins", classes="section-header")
|
|
yield SettingRow(
|
|
"Network plugin:",
|
|
Switch(value=True, id="cfg-plugin-network", classes="setting-control"),
|
|
"Monitor network interfaces",
|
|
)
|
|
yield SettingRow(
|
|
"DNS plugin:",
|
|
Switch(value=True, id="cfg-plugin-dns", classes="setting-control"),
|
|
"Monitor DNS resolver health",
|
|
)
|
|
yield SettingRow(
|
|
"System plugin:",
|
|
Switch(value=True, id="cfg-plugin-system", classes="setting-control"),
|
|
"Monitor CPU / memory / disk",
|
|
)
|
|
yield SettingRow(
|
|
"Firewall plugin:",
|
|
Switch(value=True, id="cfg-plugin-firewall", classes="setting-control"),
|
|
"Read firewall rules",
|
|
)
|
|
|
|
yield Static("Display", classes="section-header")
|
|
yield SettingRow(
|
|
"Signal history limit:",
|
|
Input(value="500", id="cfg-signal-limit", classes="setting-control"),
|
|
"Max events shown in Signals tab",
|
|
)
|
|
yield SettingRow(
|
|
"Chronicle buffer:",
|
|
Input(value="2000", id="cfg-chronicle-buffer", classes="setting-control"),
|
|
"Max events retained in Chronicle",
|
|
)
|
|
|
|
with Widget(classes="button-row"):
|
|
yield Button("Save", id="btn-save", variant="primary")
|
|
yield Button("Reset to defaults", id="btn-reset", variant="default")
|
|
|
|
yield Static("", id="settings-status", classes="status-label")
|
|
|
|
def on_mount(self) -> None:
|
|
self._load_from_state()
|
|
|
|
def _load_from_state(self) -> None:
|
|
app = self.app
|
|
if not hasattr(app, "state"):
|
|
return
|
|
mappings = {
|
|
"settings.network_interval": "cfg-network-interval",
|
|
"settings.dns_interval": "cfg-dns-interval",
|
|
"settings.system_interval": "cfg-system-interval",
|
|
"settings.firewall_interval": "cfg-firewall-interval",
|
|
"settings.signal_limit": "cfg-signal-limit",
|
|
"settings.chronicle_buffer": "cfg-chronicle-buffer",
|
|
}
|
|
for state_key, widget_id in mappings.items():
|
|
val = app.state.get(state_key)
|
|
if val is not None:
|
|
try:
|
|
widget = self.query_one(f"#{widget_id}", Input)
|
|
widget.value = str(val)
|
|
except Exception:
|
|
pass
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
if event.button.id == "btn-save":
|
|
self.action_save_settings()
|
|
elif event.button.id == "btn-reset":
|
|
self._reset_to_defaults()
|
|
|
|
def action_save_settings(self) -> None:
|
|
app = self.app
|
|
status = self.query_one("#settings-status", Static)
|
|
if not hasattr(app, "state"):
|
|
status.update("[red]No state store available[/red]")
|
|
return
|
|
|
|
try:
|
|
mappings = {
|
|
"settings.network_interval": ("cfg-network-interval", float),
|
|
"settings.dns_interval": ("cfg-dns-interval", float),
|
|
"settings.system_interval": ("cfg-system-interval", float),
|
|
"settings.firewall_interval": ("cfg-firewall-interval", float),
|
|
"settings.signal_limit": ("cfg-signal-limit", int),
|
|
"settings.chronicle_buffer": ("cfg-chronicle-buffer", int),
|
|
}
|
|
for state_key, (widget_id, cast) in mappings.items():
|
|
widget = self.query_one(f"#{widget_id}", Input)
|
|
app.state.set(state_key, cast(widget.value))
|
|
|
|
# Persist to TOML
|
|
self._persist_toml()
|
|
status.update("[green]Settings saved.[/green]")
|
|
except Exception as exc:
|
|
status.update(f"[red]Error: {exc}[/red]")
|
|
|
|
def _reset_to_defaults(self) -> None:
|
|
defaults = {
|
|
"cfg-network-interval": "5",
|
|
"cfg-dns-interval": "30",
|
|
"cfg-system-interval": "3",
|
|
"cfg-firewall-interval": "60",
|
|
"cfg-signal-limit": "500",
|
|
"cfg-chronicle-buffer": "2000",
|
|
}
|
|
for widget_id, value in defaults.items():
|
|
try:
|
|
self.query_one(f"#{widget_id}", Input).value = value
|
|
except Exception:
|
|
pass
|
|
self.query_one("#settings-status", Static).update("[yellow]Defaults restored. Press Save to apply.[/yellow]")
|
|
|
|
def _persist_toml(self) -> None:
|
|
try:
|
|
import toml
|
|
data = {
|
|
"polling": {
|
|
"network_interval": float(self.query_one("#cfg-network-interval", Input).value),
|
|
"dns_interval": float(self.query_one("#cfg-dns-interval", Input).value),
|
|
"system_interval": float(self.query_one("#cfg-system-interval", Input).value),
|
|
"firewall_interval": float(self.query_one("#cfg-firewall-interval", Input).value),
|
|
},
|
|
"display": {
|
|
"signal_limit": int(self.query_one("#cfg-signal-limit", Input).value),
|
|
"chronicle_buffer": int(self.query_one("#cfg-chronicle-buffer", Input).value),
|
|
},
|
|
}
|
|
CONFIG_PATH.write_text(toml.dumps(data))
|
|
except Exception as exc:
|
|
pass # non-fatal
|