#!/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()