Files

129 lines
4.8 KiB
QML

pragma Singleton
// ─────────────────────────────────────────────────────────────────────────────
// System vitals — CPU, memory and GPU utilisation.
//
// Replaces the GNOME Vitals extension, which showed exactly these three in
// exactly this order. Everything comes from procfs/sysfs, so there are no
// subprocesses: one timer kicks three async re-reads of very small files and
// the parsing happens in the `loaded` handlers.
// ─────────────────────────────────────────────────────────────────────────────
import Quickshell
import Quickshell.Io
import QtQuick
import qs.config
Singleton {
id: root
// All three are percentages, 0-100.
property real cpu: 0
property real memory: 0
property real gpu: 0
// False when Settings.gpuBusyPath is missing or unreadable (no amdgpu, or a
// different card index). The widget hides the GPU field rather than
// reporting a permanent 0%.
property bool gpuAvailable: false
// /proc/stat is cumulative since boot, so utilisation is only meaningful as
// a delta between two samples. These hold the previous one.
property real _prevTotal: 0
property real _prevIdle: 0
Timer {
interval: Settings.vitalsIntervalMs
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
cpuFile.reload();
memFile.reload();
if (Settings.showGpu)
gpuFile.reload();
}
}
// ── CPU ─────────────────────────────────────────────────────────────────
FileView {
id: cpuFile
path: "/proc/stat"
printErrors: false
onLoaded: root._parseCpu(text())
}
// ── Memory ──────────────────────────────────────────────────────────────
FileView {
id: memFile
path: "/proc/meminfo"
printErrors: false
onLoaded: root._parseMemory(text())
}
// ── GPU ─────────────────────────────────────────────────────────────────
// amdgpu exposes a bare integer 0-100 here.
FileView {
id: gpuFile
path: Settings.gpuBusyPath
printErrors: false
onLoaded: root._parseGpu(text())
onLoadFailed: root.gpuAvailable = false
}
function _parseCpu(text: string): void {
if (!text)
return;
// First line is the aggregate: "cpu user nice system idle iowait ..."
const fields = text.slice(0, text.indexOf("\n")).split(/\s+/).slice(1).map(parseFloat).filter(n => !isNaN(n));
if (fields.length < 5)
return;
// idle + iowait — the kernel counts both as "not doing work".
const idle = fields[3] + fields[4];
let total = 0;
for (let i = 0; i < fields.length; i++)
total += fields[i];
const dTotal = total - root._prevTotal;
const dIdle = idle - root._prevIdle;
root._prevTotal = total;
root._prevIdle = idle;
// The very first sample has no predecessor; reporting it would show the
// since-boot average, which is never what you want.
if (dTotal <= 0)
return;
root.cpu = Math.max(0, Math.min(100, (1 - dIdle / dTotal) * 100));
}
function _parseMemory(text: string): void {
// MemAvailable is the kernel's own estimate of what a new allocation
// could get; a much better "used" basis than MemFree.
const total = root._meminfoField(text, "MemTotal");
const available = root._meminfoField(text, "MemAvailable");
if (total <= 0 || available < 0)
return;
root.memory = Math.max(0, Math.min(100, (1 - available / total) * 100));
}
function _meminfoField(text: string, key: string): real {
const match = text.match(new RegExp("^" + key + ":\\s+(\\d+)", "m"));
return match ? parseFloat(match[1]) : -1;
}
function _parseGpu(text: string): void {
const value = parseInt(text, 10);
if (isNaN(value)) {
root.gpuAvailable = false;
return;
}
root.gpuAvailable = true;
root.gpu = Math.max(0, Math.min(100, value));
}
}