"""A minimalist system-stats line for the top of the panel. CPU / RAM / GPU / disk utilisation plus network up/down rate, refreshed on a timer. Everything is read straight from /proc and /sys on the main thread — these are local file reads that take microseconds, so no subprocess or worker thread is needed. """ from __future__ import annotations import glob import os import time import gi gi.require_version("Gtk", "4.0") from gi.repository import GLib, Gtk # noqa: E402 _GPU_FILES = glob.glob("/sys/class/drm/card*/device/gpu_busy_percent") _NET_SKIP = ("lo", "docker", "veth", "br-", "virbr", "tun", "tap") def _human(rate: float) -> str: for unit in ("B", "K", "M", "G"): if rate < 1024 or unit == "G": return f"{rate:4.1f}{unit}" rate /= 1024 return f"{rate:4.1f}G" class Statsbar(Gtk.Box): def __init__(self): super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=18, halign=Gtk.Align.CENTER) self.add_css_class("statsbar") # short text labels rather than nerd glyphs (some don't exist in the font and # render as tofu); keeps the line legible on any theme. self._cpu = self._cell("CPU") self._ram = self._cell("MEM") self._gpu = self._cell("GPU") self._disk = self._cell("DISK") self._down = self._cell("↓") self._up = self._cell("↑") self._prev_cpu: tuple[int, int] | None = None self._prev_net: tuple[int, int] | None = None self._prev_t: float | None = None self._refresh() self._source = GLib.timeout_add_seconds(2, self._tick) def _cell(self, icon: str) -> Gtk.Label: box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) ic = Gtk.Label(label=icon) ic.add_css_class("stat-icon") val = Gtk.Label(label="…") val.add_css_class("stat-value") box.append(ic) box.append(val) self.append(box) return val def _tick(self) -> bool: self._refresh() return GLib.SOURCE_CONTINUE # -- readers ----------------------------------------------------------- @staticmethod def _cpu_pct(prev) -> tuple[float, tuple[int, int]]: parts = [int(x) for x in open("/proc/stat").readline().split()[1:]] idle = parts[3] + parts[4] # idle + iowait total = sum(parts) if prev is None: return 0.0, (total, idle) dt, di = total - prev[0], idle - prev[1] pct = (1 - di / dt) * 100 if dt > 0 else 0.0 return max(0.0, min(100.0, pct)), (total, idle) @staticmethod def _ram_pct() -> float: mem = {} for line in open("/proc/meminfo"): k, _, v = line.partition(":") mem[k] = int(v.split()[0]) if len(mem) > 4 and "MemAvailable" in mem: break total, avail = mem.get("MemTotal", 1), mem.get("MemAvailable", 0) return (1 - avail / total) * 100 @staticmethod def _gpu_pct() -> float: best = 0 for f in _GPU_FILES: try: best = max(best, int(open(f).read().strip())) except (OSError, ValueError): pass return float(best) @staticmethod def _disk_pct() -> float: s = os.statvfs("/") return (1 - s.f_bfree / s.f_blocks) * 100 if s.f_blocks else 0.0 @staticmethod def _net_bytes() -> tuple[int, int]: rx = tx = 0 with open("/proc/net/dev") as fh: for line in fh.readlines()[2:]: name, _, rest = line.partition(":") name = name.strip() if name.startswith(_NET_SKIP): continue cols = rest.split() rx += int(cols[0]) tx += int(cols[8]) return rx, tx def _refresh(self) -> None: cpu, self._prev_cpu = self._cpu_pct(self._prev_cpu) self._cpu.set_text(f"{cpu:2.0f}%") self._ram.set_text(f"{self._ram_pct():2.0f}%") self._gpu.set_text(f"{self._gpu_pct():2.0f}%") self._disk.set_text(f"{self._disk_pct():2.0f}%") now = time.monotonic() rx, tx = self._net_bytes() if self._prev_net is not None and self._prev_t is not None and now > self._prev_t: dt = now - self._prev_t self._down.set_text(_human((rx - self._prev_net[0]) / dt) + "/s") self._up.set_text(_human((tx - self._prev_net[1]) / dt) + "/s") self._prev_net, self._prev_t = (rx, tx), now