Files
Nix-Vibe/home-manager/modules/quickshell-stats.py
T

182 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""qs-stats: one-shot system stats probe for the QuickShell stats popup.
Emits a single JSON object on stdout:
{
"ts": 1730000000,
"cpu": 12.3,
"ram": {"used": 8.2, "total": 31.9, "pct": 25.7},
"swap": {"used": 0.0, "total": 8.0, "pct": 0.0},
"disk": {"used": 156.4, "total": 934.1, "pct": 16.7},
"load": [2.01, 2.29, 2.10],
"uptime": 7861.0,
"temps": [{"name": "TCPU", "temp": 58.0}, ...],
"freq": 3.8
}
CPU% is computed from two /proc/stat samples ~250ms apart (mostly an idle
sleep: ~free on battery). Memory/swap/disk come from /proc/meminfo +
os.statvfs. Temperatures come from /sys/class/thermal (kernel hwmon zones).
The bar runs this on a slow 15s timer; the stats popup refreshes every 3s
while open.
"""
import json
import os
import sys
import time
CPU_SAMPLE_MS = 250
def cpu_usage():
def sample():
with open("/proc/stat") as f:
parts = f.readline().split()
# cpu user nice system idle iowait irq softirq steal guest guest_nice
vals = list(map(int, parts[1:]))
idle = vals[3] + vals[4]
total = sum(vals)
return idle, total
idle0, total0 = sample()
time.sleep(CPU_SAMPLE_MS / 1000.0)
idle1, total1 = sample()
d_idle = idle1 - idle0
d_total = total1 - total0
if d_total <= 0:
return 0.0
return round(100.0 * (1.0 - d_idle / d_total), 1)
def meminfo_gi():
mem = {}
try:
with open("/proc/meminfo") as f:
for line in f:
key, _, rest = line.partition(":")
val = rest.strip().split()[0]
mem[key] = int(val) # kB
except OSError:
return {}
def gi(kb):
return round(kb / 1024.0 / 1024.0, 1)
ram = {
"total": gi(mem.get("MemTotal", 0)),
# MemAvailable is the honest "swappable minus thrash" number
"available": gi(mem.get("MemAvailable", mem.get("MemFree", 0))),
}
ram["used"] = round(ram["total"] - ram["available"], 1)
ram["pct"] = (
round(100.0 * ram["used"] / ram["total"], 1) if ram["total"] > 0 else 0.0
)
swap = {
"total": gi(mem.get("SwapTotal", 0)),
"free": gi(mem.get("SwapFree", 0)),
}
swap["used"] = round(swap["total"] - swap["free"], 1)
swap["pct"] = (
round(100.0 * swap["used"] / swap["total"], 1) if swap["total"] > 0 else 0.0
)
return ram, swap
def disk_pct(path="/"):
try:
st = os.statvfs(path)
except OSError:
return {"used": 0.0, "total": 0.0, "pct": 0.0}
total = st.f_blocks * st.f_frsize
free = st.f_bavail * st.f_frsize
used = total - free
return {
"used": round(used / 1024.0**3, 1),
"total": round(total / 1024.0**3, 1),
"pct": round(100.0 * used / total, 1) if total > 0 else 0.0,
}
def load_avg():
try:
return [round(float(x), 2) for x in os.getloadavg()]
except OSError:
return []
def uptime():
try:
with open("/proc/uptime") as f:
return round(float(f.read().split()[0]), 1)
except OSError:
return 0.0
def temps():
out = []
base = "/sys/class/thermal"
try:
zones = sorted(os.listdir(base))
except OSError:
return out
for z in zones:
if not z.startswith("thermal_zone"):
continue
tpath = os.path.join(base, z)
typefile = os.path.join(tpath, "type")
tempfile = os.path.join(tpath, "temp")
try:
with open(typefile) as f:
name = f.read().strip()
with open(tempfile) as f:
millideg = int(f.read().strip())
except (OSError, ValueError):
continue
# Skip the ACPI "INT3400 Thermal" umbrella zone (always ~20C, noise)
if "INT3400" in name:
continue
out.append({"name": name, "temp": round(millideg / 1000.0, 0)})
# Keep a sane display order: CPU forward, wifi last
def rank(n):
n = n.lower()
if "cpu" in n or "tctl" in n or "pkg" in n:
return 0
if "sen" in n:
return 1
return 2
out.sort(key=lambda t: (rank(t["name"]), t["name"]))
return out
def scaled_vout():
# Current CPU P-state / GHz (x86-package-temp zone present ⇒ has cpufreq).
try:
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq") as f:
khz = int(f.read().strip())
return round(khz / 1_000_000.0, 2)
except OSError:
return 0.0
def main():
ram, swap = meminfo_gi()
payload = {
"ts": int(time.time()),
"cpu": cpu_usage(),
"ram": ram,
"swap": swap,
"disk": disk_pct(),
"load": load_avg(),
"uptime": uptime(),
"temps": temps(),
"freq": scaled_vout(),
}
sys.stdout.write(json.dumps(payload))
sys.stdout.write("\n")
if __name__ == "__main__":
main()