// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. pragma Singleton import Quickshell import Quickshell.Io import QtQuick import QtQml import "../.." import "Perf.js" as P // The numbers behind the performance panel and the status pill's icon. CPU, // RAM, temperatures and disks are sampled always, so the icon can warn while // the panel is closed; per-core load, the network rate, VRAM and the fan only // while `detailed`, which is any PerfPanel existing (island or drawer). // /proc and /sys are FileViews reloaded on a timer, no subprocess; df, the // fdinfo scan, ip and curl are Processes. Singleton { id: root // PerfPanel instances alive; each counts itself in and out. property int viewers: 0 readonly property bool detailed: viewers > 0 // ponytail: host constants, as conky had them. One mount per NFS server // (Library and Slackware are one export, so are shared and backup); br0 // is a bridge, so VM-to-host copies show in the graph; xe puts no VRAM // capacity in sysfs. Edit here when the hardware changes. readonly property var mounts: ["/", "/home", "/data", "/mnt/nfs/Library", "/mnt/nfs/shared"] readonly property string iface: "br0" readonly property real vramTotal: 12 * 1024 * 1024 * 1024 readonly property string gpuName: "ARC B580" // Conky's table. Ceilings are per sensor: 70C is unremarkable on a CPU // package and alarming on an NVMe. GPU and VRAM are the GPU card's; the // rest are the CPU card's rows. readonly property var sensors: [ { chip: "k10temp", file: "temp1_input", label: "CPU", warn: 75, crit: 90 }, { chip: "k10temp", file: "temp3_input", label: "CCD1", warn: 75, crit: 90 }, { chip: "nvme", file: "temp1_input", label: "NVME", warn: 60, crit: 70 }, { chip: "gigabyte_wmi", file: "temp2_input", label: "BOARD", warn: 60, crit: 70 }, { chip: "gigabyte_wmi", file: "temp3_input", label: "BOARD", warn: 60, crit: 70 }, { chip: "xe", file: "temp2_input", label: "GPU", warn: 75, crit: 85 }, { chip: "xe", file: "temp3_input", label: "VRAM", warn: 80, crit: 90 }, ] property var hwmon: ({}) // chip name -> /sys/class/hwmon/hwmonN property string cpuModel: "" property var load: null // aggregate percent property var cores: [] // percent per core, while detailed property var mem: null // {total, used} bytes property var temps: [] // [{label, value, warn, crit}], value null if unread property var disks: [] // Perf.js df rows property var vram: null // bytes, while detailed property var fan: null // rpm, while detailed property var rx: [] // bytes/s, newest last, while detailed property var tx: [] property string lan: "" property var pub: null // {ip, fetched} property real now: Date.now() / 1000 readonly property var ip: P.ipCheck(root.pub, root.now, Prefs.pinnedIp) // The icon's dot. CPU load is left out: spikes would flicker it, and // sustained load shows up as heat. readonly property string severity: P.worst( root.temps.map(t => P.band(t.value, t.warn, t.crit)) .concat([P.band(root.mem ? 100 * root.mem.used / root.mem.total : null, 85, 95)]) .concat(root.disks.map(d => P.band(d.pct, 90, 95))) .concat([root.ip.mismatch ? "crit" : "ok"])) readonly property string pubipPath: (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env("HOME") + "/.cache") + "/udt/pubip.txt" property var prevCpu: [] property var prevNet: null // {rx, tx, t} function num(view) { const v = parseInt(view.text(), 10); return isNaN(v) ? null : v; } // Measured from the last attempt, not the last success, so a failing // fetch (rate limited, offline) still waits 30 minutes; and never over // one already running, which would kill it before it could answer. property real pubAttempt: 0 function fetchPubip() { const now = Date.now() / 1000; if (pubProc.running || !P.due(root.pub, now, 1800) || now - root.pubAttempt < 1800) return; root.pubAttempt = now; pubProc.running = true; } onDetailedChanged: { if (detailed) { if (!root.lan) lanProc.running = true; root.fetchPubip(); } else { root.cores = []; root.rx = []; root.tx = []; root.prevNet = null; root.vram = null; root.fan = null; } } // --- /proc and /sys --------------------------------------------------- FileView { id: stat; path: "/proc/stat"; blockLoading: true } FileView { id: meminfo; path: "/proc/meminfo"; blockLoading: true } FileView { path: "/proc/cpuinfo" blockLoading: true onLoaded: root.cpuModel = P.cpuModel(text()) } Instantiator { id: tempViews model: root.sensors delegate: FileView { required property var modelData path: root.hwmon[modelData.chip] ? root.hwmon[modelData.chip] + "/" + modelData.file : "" blockLoading: true printErrors: false } } FileView { id: fanView path: root.hwmon.xe ? root.hwmon.xe + "/fan1_input" : "" blockLoading: true printErrors: false } FileView { id: rxView path: "/sys/class/net/" + root.iface + "/statistics/rx_bytes" blockLoading: true printErrors: false } FileView { id: txView path: "/sys/class/net/" + root.iface + "/statistics/tx_bytes" blockLoading: true printErrors: false } Process { running: true command: ["sh", "-c", "grep -H . /sys/class/hwmon/hwmon*/name"] stdout: StdioCollector { onStreamFinished: root.hwmon = P.hwmonMap(text) } } // CPU, RAM and temperatures always; the fan and VRAM while detailed. Timer { interval: 2000 running: true repeat: true triggeredOnStart: true onTriggered: { root.now = Date.now() / 1000; stat.reload(); const cur = P.cpuTimes(stat.text()); root.load = P.load(root.prevCpu[0], cur[0]); root.cores = root.detailed ? cur.slice(1).map((c, i) => P.load(root.prevCpu[i + 1], c) ?? 0) : []; root.prevCpu = cur; meminfo.reload(); root.mem = P.meminfo(meminfo.text()); const t = []; for (let i = 0; i < root.sensors.length; i++) { const s = root.sensors[i]; const v = tempViews.objectAt(i); let value = null; if (v && v.path) { v.reload(); const n = root.num(v); value = n === null ? null : n / 1000; } t.push({ label: s.label, value: value, warn: s.warn, crit: s.crit }); } root.temps = t; if (root.detailed) { if (fanView.path) { fanView.reload(); root.fan = root.num(fanView); } if (!vramProc.running) vramProc.running = true; } } } // ponytail: a shell glob over every fd on the system, ~14ms; only while // the panel is open. Only the user's own processes are readable, so a // system service's GPU use is not counted, as in conky. Process { id: vramProc command: ["sh", "-c", "grep -sH -e '^drm-driver' -e '^drm-client-id' " + "-e '^drm-resident-vram0' /proc/[0-9]*/fdinfo/* 2>/dev/null"] stdout: StdioCollector { onStreamFinished: root.vram = P.vram(text) } } // --- network ------------------------------------------------------------ Timer { interval: 1000 running: root.detailed repeat: true triggeredOnStart: true onTriggered: { rxView.reload(); txView.reload(); const cur = { rx: root.num(rxView), tx: root.num(txView), t: Date.now() / 1000 }; const p = root.prevNet; if (p) { const r = P.rate(p.rx, cur.rx, cur.t - p.t); const s = P.rate(p.tx, cur.tx, cur.t - p.t); if (r !== null) root.rx = P.push(root.rx, r, 60); if (s !== null) root.tx = P.push(root.tx, s, 60); } root.prevNet = cur; } } Process { id: lanProc command: ["ip", "-4", "-o", "addr", "show", root.iface] stdout: StdioCollector { onStreamFinished: root.lan = P.lanAddr(text) } } // --- disks -------------------------------------------------------------- // df under timeout: an unreachable NFS server blocks statfs, and the // whole call then yields nothing, so the last rows are kept. Timer { interval: 60000 running: true repeat: true triggeredOnStart: true onTriggered: { dfProc.running = false; dfProc.running = true; } } Process { id: dfProc command: ["timeout", "-k", "2", "5", "/usr/bin/df", "-P", "-B1"].concat(root.mounts) stdout: StdioCollector { onStreamFinished: { const rows = P.df(text); if (rows.length) root.disks = rows; } } } // --- public IP ---------------------------------------------------------- // conky's cache file, so the address survives a restart and the fetch // keeps conky's 30 minute pace. Checked every minute, fetched when due. FileView { id: pubFile path: root.pubipPath atomicWrites: true watchChanges: true printErrors: false onFileChanged: reload() onLoaded: root.pub = P.parsePubip(text()) } Timer { interval: 60000 running: true repeat: true onTriggered: root.fetchPubip() } // Anything but an IPv4 address (a failure, an error page) leaves the // cache alone: the old value then goes stale rather than mismatching. Process { id: pubProc command: ["curl", "-4fsS", "--max-time", "10", "https://ipinfo.io/ip"] stdout: StdioCollector { onStreamFinished: { const ip = text.trim(); if (!P.isIPv4(ip)) return; const at = Math.floor(Date.now() / 1000); root.pub = { ip: ip, fetched: at }; pubFile.setText(P.formatPubip(ip, at)); } } } }