pragma Singleton import Quickshell import Quickshell.Io import QtQuick // System metrics gathered by reading /proc and /sys directly from QML via // FileView, polled by a Timer. No persistent helper process and no per-tick // subprocess churn (awk/df/cat). Two exceptions that have no virtual-file // equivalent: hardware discovery (one-shot at startup) and disk usage // (df, refreshed infrequently). Singleton { id: root // --- live values, updated each tick --- property real cpu: 0 // % property real cpuFreq: 0 // MHz, max across all cores property real mem: 0 // % property int temp: 0 // °C property int disk: 0 // % used on / property string iface: "" // active interface name property real rxRate: 0 // bytes/sec down property real txRate: 0 // bytes/sec up property int battery: -1 // %, -1 if none property string batteryStatus: "unknown" // per-core utilization, 0..100, index = core number property var coreLoads: [] property int coreCount: 0 readonly property bool hasBattery: battery >= 0 // poll interval in seconds readonly property int interval: 1 // discovered sysfs paths (filled once by discover.sh) property string tempPath: "" property string batPath: "" // --- previous-sample state for delta computations --- property double _prevCpuTotal: 0 property double _prevCpuIdle: 0 property var _prevCoreTotal: [] property var _prevCoreIdle: [] property double _prevRx: 0 property double _prevTx: 0 property string _prevIface: "" property int _tick: 0 // ---- file readers ------------------------------------------------------ // blockLoading makes reload() synchronous so text() is fresh on the // same tick. These are tiny virtual files, so the cost is negligible. FileView { id: statView; path: "/proc/stat"; blockLoading: true } FileView { id: cpuInfoView; path: "/proc/cpuinfo"; blockLoading: true } FileView { id: memView; path: "/proc/meminfo"; blockLoading: true } FileView { id: routeView; path: "/proc/net/route"; blockLoading: true } FileView { id: tempView path: root.tempPath blockLoading: true } FileView { id: batCapView path: root.batPath ? root.batPath + "/capacity" : "" blockLoading: true } FileView { id: batStatusView path: root.batPath ? root.batPath + "/status" : "" blockLoading: true } // network counters: path follows the active interface FileView { id: rxView; blockLoading: true } FileView { id: txView; blockLoading: true } // ---- parsing ----------------------------------------------------------- // Utilization from a /proc/stat "cpu..." field array, given the previous // total/idle for that line. Returns { load, total, idle }. function _cpuLineLoad(f, prevTotal, prevIdle) { let total = 0; for (let i = 0; i < f.length; i++) total += f[i]; const idle = f[3] + f[4]; // idle + iowait const dt = total - prevTotal; const di = idle - prevIdle; let load = 0; if (prevTotal !== 0 && dt > 0) load = Math.max(0, Math.min(100, (1 - di / dt) * 100)); return { load: load, total: total, idle: idle }; } function _parseCpu() { const text = statView.text(); if (!text) return; const lines = text.split("\n"); const loads = []; const prevT = root._prevCoreTotal.slice(); const prevI = root._prevCoreIdle.slice(); for (let li = 0; li < lines.length; li++) { const ln = lines[li]; if (!ln.startsWith("cpu")) break; // cpu lines lead /proc/stat const parts = ln.trim().split(/\s+/); const f = parts.slice(1).map(Number); if (f.length < 5) continue; if (parts[0] === "cpu") { // aggregate -> overall % const r = root._cpuLineLoad(f, root._prevCpuTotal, root._prevCpuIdle); root.cpu = r.load; root._prevCpuTotal = r.total; root._prevCpuIdle = r.idle; } else { // "cpuN" -> per-core const idx = parseInt(parts[0].slice(3)); if (isNaN(idx)) continue; const r = root._cpuLineLoad(f, prevT[idx] || 0, prevI[idx] || 0); loads[idx] = Math.round(r.load); // integer %: stable, lets the redraw guard below short-circuit prevT[idx] = r.total; prevI[idx] = r.idle; } } root._prevCoreTotal = prevT; root._prevCoreIdle = prevI; // coreLoads is a var (array): reassigning always fires the change // signal and re-evaluates every bar binding. Only assign when a core's // value actually changed so unchanged ticks trigger no redraw. if (!_sameLoads(loads, root.coreLoads)) { root.coreLoads = loads; root.coreCount = loads.length; } } function _sameLoads(a, b) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; return true; } // Highest current per-core frequency from /proc/cpuinfo's "cpu MHz" lines. function _parseFreq() { const text = cpuInfoView.text(); if (!text) return; let max = 0; const lines = text.split("\n"); for (let i = 0; i < lines.length; i++) { if (!lines[i].startsWith("cpu MHz")) continue; const v = parseFloat(lines[i].split(":")[1]); if (!isNaN(v) && v > max) max = v; } root.cpuFreq = max; } function _parseMem() { const text = memView.text(); if (!text) return; let mt = 0, ma = 0; const lines = text.split("\n"); for (let i = 0; i < lines.length; i++) { const ln = lines[i]; if (ln.startsWith("MemTotal:")) mt = parseInt(ln.split(/\s+/)[1]); else if (ln.startsWith("MemAvailable:")) { ma = parseInt(ln.split(/\s+/)[1]); break; } } if (mt > 0) root.mem = (mt - ma) / mt * 100; } function _parseTemp() { if (!root.tempPath) { root.temp = 0; return; } const text = tempView.text(); if (!text) return; const v = parseInt(text.trim()); if (!isNaN(v)) root.temp = Math.round(v / 1000); } function _parseBattery() { if (!root.batPath) { root.battery = -1; root.batteryStatus = "unknown"; return; } const cap = batCapView.text(); const st = batStatusView.text(); if (cap) { const v = parseInt(cap.trim()); if (!isNaN(v)) root.battery = v; } if (st) root.batteryStatus = st.trim() || "unknown"; } // Active interface = device of the default route (destination 00000000). function _activeIface() { const text = routeView.text(); if (!text) return ""; const lines = text.split("\n"); for (let i = 1; i < lines.length; i++) { // skip header const cols = lines[i].trim().split(/\s+/); if (cols.length >= 2 && cols[1] === "00000000") return cols[0]; } return ""; } function _parseNet() { const ifc = root._activeIface(); root.iface = ifc; if (!ifc) { root.rxRate = 0; root.txRate = 0; root._prevIface = ""; return; } const base = "/sys/class/net/" + ifc + "/statistics/"; rxView.path = base + "rx_bytes"; txView.path = base + "tx_bytes"; rxView.reload(); txView.reload(); const rx = parseInt((rxView.text() || "0").trim()); const tx = parseInt((txView.text() || "0").trim()); if (ifc === root._prevIface && root._prevRx > 0) { root.rxRate = Math.max(0, (rx - root._prevRx) / root.interval); root.txRate = Math.max(0, (tx - root._prevTx) / root.interval); } else { root.rxRate = 0; root.txRate = 0; } root._prevRx = rx; root._prevTx = tx; root._prevIface = ifc; } function _tickOnce() { statView.reload(); cpuInfoView.reload(); memView.reload(); routeView.reload(); if (root.tempPath) tempView.reload(); if (root.batPath) { batCapView.reload(); batStatusView.reload(); } _parseCpu(); _parseFreq(); _parseMem(); _parseTemp(); _parseBattery(); _parseNet(); // disk has no virtual file; refresh via df every 30 ticks. if (root._tick % 30 === 0) dfProc.running = true; root._tick++; } Timer { interval: root.interval * 1000 running: true repeat: true triggeredOnStart: true onTriggered: root._tickOnce() } // ---- one-shot hardware discovery -------------------------------------- Process { id: discover command: ["bash", Quickshell.shellPath("services/discover.sh")] running: true stdout: SplitParser { splitMarker: "\n" onRead: line => { if (!line) return; const sp = line.indexOf(" "); if (sp < 0) return; const key = line.slice(0, sp); const val = line.slice(sp + 1).trim(); if (key === "TEMP") root.tempPath = val; else if (key === "BAT") root.batPath = val; } } } // ---- disk usage (df, low cadence) ------------------------------------- Process { id: dfProc command: ["df", "-P", "/"] stdout: StdioCollector { onStreamFinished: { const lines = (this.text || "").trim().split("\n"); if (lines.length < 2) return; const cols = lines[lines.length - 1].trim().split(/\s+/); if (cols.length >= 5) { const v = parseInt(cols[4].replace("%", "")); if (!isNaN(v)) root.disk = v; } } } } // Human-readable byte rate with kB as the smallest unit, e.g. "1.2M", "8K". function fmtRate(bytes) { const u = ["K", "M", "G"]; let i = 0, v = bytes / 1024; // start in kB while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; } return (v >= 100 ? v.toFixed(0) : v.toFixed(1)) + u[i]; } }