134 lines
4.5 KiB
Python
134 lines
4.5 KiB
Python
"""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
|