Current state of main at 0240060 feat(hp-laptop): install TeleportFling from its flake. History intentionally collapsed to a single commit; this repo mirrors only the latest state.
182 lines
4.9 KiB
Python
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() |