196 lines
6.5 KiB
Python
196 lines
6.5 KiB
Python
"""System-monitor quad ("Ship Systems"): the machine's vitals reskinned as a
|
|
starship diagnostic panel. A small backend script (backend/sysmon.sh) reports
|
|
usage + temperature for each subsystem, renamed in the ship's language:
|
|
|
|
CPU -> Thrusters
|
|
GPU -> Hyperdrive
|
|
RAM -> Life Support Systems
|
|
Storage -> Cargo Hold (one entry per mounted drive)
|
|
|
|
Each is drawn as a bracketed usage meter (-<████░░░░>-) with a temperature
|
|
readout (-( 54°C )-). Rows are laid out in a Gtk.Grid so every column lines up
|
|
regardless of the (variable-width) nerd-font icons. The compact cell shows the
|
|
four headline subsystems (first drive only); the expanded view lists every drive
|
|
with its capacity.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import gi
|
|
|
|
gi.require_version("Gtk", "4.0")
|
|
from gi.repository import GLib, Gtk # noqa: E402
|
|
|
|
from lib.proc import run_text
|
|
from module_base import ModuleContext, ModuleInstance, ModuleSpec
|
|
from paths import BACKEND_DIR
|
|
|
|
_SCRIPT = str(BACKEND_DIR / "sysmon.sh")
|
|
_MONO = "Agave Nerd Font Mono"
|
|
|
|
# palette (kept in sync by eye with style/_colors.css / the hologram violets)
|
|
_MAGENTA = "#EB00A6"
|
|
_ACCENT = "#E40046"
|
|
_DIM_EMPTY = "#3B2A6E" # unfilled meter cells
|
|
_DIM_TEXT = "#9385C9" # detail line
|
|
_COOL = "#22D3EE"
|
|
_WARM = "#F5A623"
|
|
_HOT = "#E40046"
|
|
_GOOD = "#2BE08A"
|
|
|
|
# category -> nerd-font glyph (verified to render in Agave Nerd Font)
|
|
_ICONS = {
|
|
"cpu": chr(0xf135), # nf-fa-rocket
|
|
"gpu": chr(0xf0e7), # nf-fa-bolt
|
|
"ram": chr(0xf004), # nf-fa-heart
|
|
"disk": chr(0xf0a0), # nf-fa-hdd_o
|
|
}
|
|
|
|
|
|
def _usage_color(pct: int) -> str:
|
|
if pct >= 85:
|
|
return _HOT
|
|
if pct >= 60:
|
|
return _WARM
|
|
return _GOOD
|
|
|
|
|
|
def _temp_color(temp: int) -> str:
|
|
if temp >= 85:
|
|
return _HOT
|
|
if temp >= 65:
|
|
return _WARM
|
|
return _COOL
|
|
|
|
|
|
def _esc(text: str) -> str:
|
|
return GLib.markup_escape_text(text)
|
|
|
|
|
|
class _SystemMonitor(Gtk.Box):
|
|
REFRESH_MS = 2500
|
|
|
|
def __init__(self, ctx: ModuleContext, compact: bool):
|
|
super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
|
self.ctx = ctx
|
|
self.compact = compact
|
|
self.add_css_class("sysmon-view")
|
|
self.set_valign(Gtk.Align.CENTER)
|
|
self._size = 10 if compact else 12
|
|
self._bar_w = 9 if compact else 16
|
|
self._timer: int | None = None
|
|
|
|
self._grid = Gtk.Grid()
|
|
self._grid.set_row_spacing(3 if compact else 7)
|
|
self._grid.set_column_spacing(12)
|
|
self.append(self._grid)
|
|
|
|
# -- lifecycle ---------------------------------------------------------
|
|
def start(self) -> None:
|
|
self.refresh()
|
|
if self._timer is None:
|
|
self._timer = GLib.timeout_add(self.REFRESH_MS, self._tick)
|
|
|
|
def stop(self) -> None:
|
|
if self._timer is not None:
|
|
GLib.source_remove(self._timer)
|
|
self._timer = None
|
|
|
|
def _tick(self) -> bool:
|
|
self.refresh()
|
|
return True
|
|
|
|
def refresh(self) -> None:
|
|
run_text([_SCRIPT], self._on_result)
|
|
|
|
def _on_result(self, ok: bool, out: str, _err: str) -> None:
|
|
if not ok:
|
|
return
|
|
rows = []
|
|
for line in out.splitlines():
|
|
parts = line.split("\t")
|
|
if len(parts) < 5:
|
|
continue
|
|
rows.append(dict(zip(("cat", "name", "usage", "temp", "detail"), parts)))
|
|
if self.compact:
|
|
# headline subsystems + only the first (root) drive
|
|
trimmed, seen_disk = [], False
|
|
for r in rows:
|
|
if r["cat"] == "disk":
|
|
if seen_disk:
|
|
continue
|
|
seen_disk = True
|
|
trimmed.append(r)
|
|
rows = trimmed
|
|
self._render(rows)
|
|
|
|
# -- rendering ---------------------------------------------------------
|
|
def _clear(self) -> None:
|
|
child = self._grid.get_first_child()
|
|
while child is not None:
|
|
nxt = child.get_next_sibling()
|
|
self._grid.remove(child)
|
|
child = nxt
|
|
|
|
def _span(self, markup: str, xalign: float = 0.0) -> Gtk.Label:
|
|
lbl = Gtk.Label(xalign=xalign)
|
|
lbl.add_css_class("sysmon-row")
|
|
lbl.set_markup(f"<span font_family='{_MONO}' size='{int(self._size * 1024)}'>{markup}</span>")
|
|
return lbl
|
|
|
|
def _render(self, rows: list[dict]) -> None:
|
|
self._clear()
|
|
for i, r in enumerate(rows):
|
|
try:
|
|
usage = int(r["usage"])
|
|
except ValueError:
|
|
usage = 0
|
|
fill_col = _usage_color(usage)
|
|
filled = max(0, min(self._bar_w, round(usage / 100 * self._bar_w)))
|
|
bar = (f"<span foreground='{fill_col}'>{'█' * filled}</span>"
|
|
f"<span foreground='{_DIM_EMPTY}'>{'░' * (self._bar_w - filled)}</span>")
|
|
meter = (f"<span foreground='{_ACCENT}'>-<</span>{bar}"
|
|
f"<span foreground='{_ACCENT}'>>-</span>")
|
|
try:
|
|
temp = int(r["temp"])
|
|
temp_txt, temp_col = f"{temp}°C", _temp_color(temp)
|
|
except ValueError:
|
|
temp_txt, temp_col = "--°C", _DIM_TEXT
|
|
temp_field = (f"<span foreground='{_ACCENT}'>-(</span> "
|
|
f"<span foreground='{temp_col}'>{temp_txt:>5}</span> "
|
|
f"<span foreground='{_ACCENT}'>)-</span>")
|
|
|
|
icon = _ICONS.get(r["cat"], "")
|
|
self._grid.attach(self._span(f"<span foreground='{_MAGENTA}'>{icon}</span>"), 0, i, 1, 1)
|
|
self._grid.attach(self._span(f"<span foreground='{_MAGENTA}'>{_esc(r['name'])}</span>"), 1, i, 1, 1)
|
|
self._grid.attach(self._span(meter), 2, i, 1, 1)
|
|
self._grid.attach(self._span(f"<span foreground='{fill_col}'>{usage:>3}%</span>", 1.0), 3, i, 1, 1)
|
|
self._grid.attach(self._span(temp_field), 4, i, 1, 1)
|
|
if not self.compact:
|
|
self._grid.attach(
|
|
self._span(f"<span foreground='{_DIM_TEXT}'>{_esc(r['detail'])}</span>"), 5, i, 1, 1)
|
|
|
|
|
|
def build(ctx: ModuleContext) -> ModuleInstance:
|
|
compact = _SystemMonitor(ctx, compact=True)
|
|
expanded = _SystemMonitor(ctx, compact=False)
|
|
|
|
def on_show() -> None:
|
|
compact.start()
|
|
expanded.start()
|
|
|
|
def on_hide() -> None:
|
|
compact.stop()
|
|
expanded.stop()
|
|
|
|
return ModuleInstance(compact=compact, expanded=expanded,
|
|
scroll_expanded=True, on_show=on_show, on_hide=on_hide)
|
|
|
|
|
|
SPEC = ModuleSpec(
|
|
id="sysmon",
|
|
title="Ship Systems",
|
|
icon=chr(0xf085), # nf-fa-cogs
|
|
build=build,
|
|
)
|