pragma Singleton import QtQuick import Quickshell.Io import "." as M QtObject { id: root // ── CPU ────────────────────────────────────────────────────────────── property int cpuUsage: 0 property real cpuFreqGhz: 0 property var cpuCores: [] // [{usage, freq_ghz, history:[]}] — only rebuilt while coreConsumers > 0 property int coreConsumers: 0 onCoreConsumersChanged: { if (coreConsumers > 0) _coreGraceTimer.stop(); else _coreGraceTimer.start(); } property var _coreGraceTimer: Timer { interval: 30000 onTriggered: root.cpuCores = root.cpuCores.map(() => ({ "usage": 0, "freq_ghz": 0, "history": [] })) } property var cpuCoreMaxFreq: [] property var cpuCoreTypes: [] // ── Temperature ────────────────────────────────────────────────────── property int tempCelsius: 0 property var tempHistory: [] // 150 samples @ 4s each ≈ 10 min // ── GPU ────────────────────────────────────────────────────────────── property bool gpuAvailable: false property string gpuVendor: "" property int gpuUsage: 0 property real gpuVramUsedGb: 0 property real gpuVramTotalGb: 0 property int gpuTempC: 0 property var gpuHistory: [] // 60 samples @ ~4-8s each ≈ 4-8 min // ── Memory ─────────────────────────────────────────────────────────── property int memPercent: 0 property real memUsedGb: 0 property real memTotalGb: 0 property real memAvailGb: 0 property real memCachedGb: 0 property real memBuffersGb: 0 property var memHistory: [] // ── Disk ───────────────────────────────────────────────────────────── property var diskMounts: [] property int diskRootPct: 0 // nova-stats stream (cpu + mem) property var _statsProc: Process { running: true command: { const ms = M.Modules.statsDaemon.interval; return ms > 0 ? ["nova-stats", "--interval", ms.toString()] : ["nova-stats"]; } stdout: SplitParser { splitMarker: "\n" onRead: line => { try { const ev = JSON.parse(line); if (ev.type === "cpu") { root.cpuUsage = ev.usage; root.cpuFreqGhz = ev.freq_ghz; if (root.coreConsumers > 0) { const histLen = 16; const prev = root.cpuCores; root.cpuCores = ev.cores.map((c, i) => { const oldHist = prev[i]?.history ?? []; const hist = oldHist.concat([c.usage]); return { usage: c.usage, freq_ghz: c.freq_ghz, history: hist.length > histLen ? hist.slice(hist.length - histLen) : hist }; }); } else if (root.cpuCores.length !== ev.cores.length) { // Keep count in sync so panel can size correctly before consumers activate root.cpuCores = ev.cores.map(c => ({ "usage": c.usage, "freq_ghz": c.freq_ghz, "history": [] })); } } else if (ev.type === "temp") { root.tempCelsius = ev.celsius; const th = root.tempHistory.concat([ev.celsius]); root.tempHistory = th.length > 150 ? th.slice(th.length - 150) : th; } else if (ev.type === "gpu") { root.gpuAvailable = true; root.gpuVendor = ev.vendor; root.gpuUsage = ev.usage; root.gpuVramUsedGb = ev.vram_used_gb; root.gpuVramTotalGb = ev.vram_total_gb; root.gpuTempC = ev.temp_c; const gh = root.gpuHistory.concat([ev.usage]); root.gpuHistory = gh.length > 60 ? gh.slice(gh.length - 60) : gh; } else if (ev.type === "mem") { root.memPercent = ev.percent; root.memUsedGb = ev.used_gb; root.memTotalGb = ev.total_gb; root.memAvailGb = ev.avail_gb; root.memCachedGb = ev.cached_gb; root.memBuffersGb = ev.buffers_gb; const h = root.memHistory.concat([ev.percent]); root.memHistory = h.length > 30 ? h.slice(h.length - 30) : h; } } catch (e) {} } } } // One-time: per-core max freq property var _maxFreqProc: Process { running: true command: ["sh", "-c", "for f in /sys/devices/system/cpu/cpu[0-9]*/cpufreq/cpuinfo_max_freq; do [ -f \"$f\" ] && cat \"$f\" || echo 0; done 2>/dev/null"] stdout: StdioCollector { onStreamFinished: { root.cpuCoreMaxFreq = text.trim().split("\n").filter(l => l).map(l => parseInt(l) / 1e6); } } } // One-time: P/E-core topology property var _coreTypesProc: Process { running: true command: ["sh", "-c", "for d in /sys/devices/system/cpu/cpu[0-9]*/topology/core_type; do [ -f \"$d\" ] && cat \"$d\" || echo Performance; done 2>/dev/null"] stdout: StdioCollector { onStreamFinished: { root.cpuCoreTypes = text.trim().split("\n").filter(l => l).map(l => l.trim()); } } } // Disk via df property var _diskProc: Process { id: diskProc running: true command: ["sh", "-c", "df -x tmpfs -x devtmpfs -x squashfs -x efivarfs -x overlay -B1 --output=target,size,used 2>/dev/null | awk 'NR>1 && $2+0>0 {print $1\"|\"$2\"|\"$3}'"] stdout: StdioCollector { onStreamFinished: { const lines = text.trim().split("\n").filter(l => l); const mounts = []; for (const line of lines) { const parts = line.split("|"); if (parts.length < 3) continue; const total = parseInt(parts[1]); const used = parseInt(parts[2]); if (total <= 0) continue; mounts.push({ "target": parts[0], "pct": Math.round(used / total * 100), "usedBytes": used, "totalBytes": total }); } root.diskMounts = mounts; const rm = mounts.find(m => m.target === "/"); if (rm) root.diskRootPct = rm.pct; } } } property var _diskTimer: Timer { interval: M.Modules.disk.interval || 30000 running: true repeat: true onTriggered: diskProc.running = true } }