129 lines
4.8 KiB
QML
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));
|
|
}
|
|
}
|