Dotfiles_ArchAP/quickshell/.config/quickshell/labfy-sway/systemmonitor/SystemMetrics.qml

177 lines
6.8 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Io
Item {
id: metrics
property bool active: false
property real cpuPercent: -1
property real ramPercent: -1
property real ramUsedGiB: -1
property real ramTotalGiB: -1
property real gpuPercent: -1
property real cpuTemp: -1
property real gpuTemp: -1
property real diskPercent: -1
property real diskUsedGiB: -1
property real diskTotalGiB: -1
property string loadAverage: ""
property string uptimeText: ""
property var previousCpu: null
property string gpuPath: ""
property string cpuTempPath: ""
property string gpuTempPath: ""
function numberFromFile(view, divisor) {
const raw = view.text().trim();
if (!/^[0-9]+$/.test(raw)) return -1;
const value = Number(raw) / divisor;
return Number.isFinite(value) ? value : -1;
}
// CONTRACT: la première lecture CPU constitue seulement la référence ; seul un delta positif produit un pourcentage.
function updateCpu() {
const line = cpuFile.text().split("\n")[0].trim().split(/\s+/);
if (line[0] !== "cpu" || line.length < 6) return;
const values = line.slice(1).map(Number);
if (values.some(value => !Number.isFinite(value))) return;
const total = values.reduce((sum, value) => sum + value, 0);
const idle = values[3] + values[4]; // idle + iowait
if (previousCpu) {
const totalDelta = total - previousCpu.total;
const idleDelta = idle - previousCpu.idle;
if (totalDelta > 0 && idleDelta >= 0)
cpuPercent = Math.max(0, Math.min(100, 100 * (1 - idleDelta / totalDelta)));
}
previousCpu = { total: total, idle: idle };
}
function updateMemory() {
const entries = {};
for (const line of memoryFile.text().split("\n")) {
const match = line.match(/^(MemTotal|MemAvailable):\s+([0-9]+) kB$/);
if (match) entries[match[1]] = Number(match[2]);
}
if (!(entries.MemTotal > 0) || !(entries.MemAvailable >= 0)) return;
const used = Math.max(0, entries.MemTotal - entries.MemAvailable);
ramPercent = Math.min(100, used * 100 / entries.MemTotal);
ramUsedGiB = used / 1048576;
ramTotalGiB = entries.MemTotal / 1048576;
}
function updateLoad() {
const fields = loadFile.text().trim().split(/\s+/);
if (fields.length >= 3 && fields.slice(0, 3).every(value => /^[0-9]+\.[0-9]+$/.test(value)))
loadAverage = fields.slice(0, 3).join(" ");
}
function updateUptime() {
const seconds = Number(uptimeFile.text().trim().split(/\s+/)[0]);
if (!Number.isFinite(seconds) || seconds < 0) return;
const minutes = Math.floor(seconds / 60);
const days = Math.floor(minutes / 1440);
const hours = Math.floor((minutes % 1440) / 60);
uptimeText = days > 0 ? days + " j " + hours + " h" : hours + " h " + minutes % 60 + " min";
}
function sample() {
if (!active) return;
cpuFile.reload();
memoryFile.reload();
gpuFile.reload();
cpuTempFile.reload();
gpuTempFile.reload();
loadFile.reload();
uptimeFile.reload();
}
function refreshDisk() {
if (active && !diskReader.running) diskReader.running = true;
}
onActiveChanged: {
if (active) {
previousCpu = null;
cpuPercent = -1;
gpuPercent = -1;
cpuTemp = -1;
gpuTemp = -1;
gpuPath = "";
cpuTempPath = "";
gpuTempPath = "";
// WHY: les numéros cardN et hwmonN changent au redémarrage.
if (!hardwareResolver.running) hardwareResolver.running = true;
sample();
refreshDisk();
} else {
previousCpu = null;
diskReader.running = false;
}
}
// INVARIANT: aucun échantillonnage ni Process df périodique quand le panneau est fermé.
Timer { interval: 1000; repeat: true; running: metrics.active; onTriggered: metrics.sample() }
Timer { interval: 15000; repeat: true; running: metrics.active; onTriggered: metrics.refreshDisk() }
FileView { id: cpuFile; path: "/proc/stat"; onLoaded: metrics.updateCpu() }
FileView { id: memoryFile; path: "/proc/meminfo"; onLoaded: metrics.updateMemory() }
FileView {
id: gpuFile
path: metrics.gpuPath
printErrors: false
onLoaded: metrics.gpuPercent = metrics.numberFromFile(gpuFile, 1)
}
FileView {
id: cpuTempFile
path: metrics.cpuTempPath
printErrors: false
onLoaded: metrics.cpuTemp = metrics.numberFromFile(cpuTempFile, 1000)
}
FileView {
id: gpuTempFile
path: metrics.gpuTempPath
printErrors: false
onLoaded: metrics.gpuTemp = metrics.numberFromFile(gpuTempFile, 1000)
}
FileView { id: loadFile; path: "/proc/loadavg"; onLoaded: metrics.updateLoad() }
FileView { id: uptimeFile; path: "/proc/uptime"; onLoaded: metrics.updateUptime() }
Process {
id: hardwareResolver
command: ["/usr/bin/python3", Quickshell.shellPath("bin/resolve-hardware.py")]
stdout: StdioCollector { id: hardwareOutput; waitForEnd: true }
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0 || exitStatus !== 0) return;
const paths = hardwareOutput.text.trimEnd().split("\n");
if (paths.length !== 3) return;
// CONTRACT: seuls les chemins sysfs résolus par le helper sont chargés.
if (paths[0].startsWith("/sys/class/drm/")) metrics.gpuPath = paths[0];
if (paths[1].startsWith("/sys/class/hwmon/")) metrics.cpuTempPath = paths[1];
if (paths[2].startsWith("/sys/class/hwmon/")) metrics.gpuTempPath = paths[2];
if (metrics.active) {
gpuFile.reload();
cpuTempFile.reload();
gpuTempFile.reload();
}
}
}
Process {
id: diskReader
command: ["df", "-B1", "--output=size,used,pcent,target", "/"]
stdout: StdioCollector { id: diskOutput; waitForEnd: true }
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0 || exitStatus !== 0) return;
const lines = diskOutput.text.trim().split("\n");
if (lines.length < 2) return;
const fields = lines[lines.length - 1].trim().split(/\s+/);
if (fields.length !== 4 || fields[3] !== "/") return;
const total = Number(fields[0]);
const used = Number(fields[1]);
if (!(total > 0) || !(used >= 0)) return;
metrics.diskTotalGiB = total / 1073741824;
metrics.diskUsedGiB = used / 1073741824;
metrics.diskPercent = Math.min(100, used * 100 / total);
}
}
}