Dotfiles/desktopenvs/hyprlua/astal-menu/ui/statsbar.py

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