Dotfiles/desktopenvs/hyprdrive/astro-menu/modules/sysmon.py

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}'>-&lt;</span>{bar}"
f"<span foreground='{_ACCENT}'>&gt;-</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,
)