270 lines
7.1 KiB
QML
270 lines
7.1 KiB
QML
pragma Singleton
|
|
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
import qs.Config
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
property string autoGpuType: "NONE"
|
|
property real gpuMemTotal: 0
|
|
property real gpuMemUsed
|
|
property string gpuName
|
|
property real gpuPerc
|
|
property real gpuTemp
|
|
readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
|
|
property int refCount
|
|
|
|
function cleanCpuName(name: string): string {
|
|
return name.replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/CPU/gi, "").replace(/\d+th Gen /gi, "").replace(/\d+nd Gen /gi, "").replace(/\d+rd Gen /gi, "").replace(/\d+st Gen /gi, "").replace(/Core /gi, "").replace(/Processor/gi, "").replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
function cleanGpuName(name: string): string {
|
|
return name.replace(/NVIDIA GeForce /gi, "").replace(/NVIDIA /gi, "").replace(/AMD Radeon /gi, "").replace(/AMD /gi, "").replace(/Intel /gi, "").replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/Graphics/gi, "").replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
function formatKib(kib: real): var {
|
|
const mib = 1024;
|
|
const gib = 1024 ** 2;
|
|
const tib = 1024 ** 3;
|
|
|
|
if (kib >= tib)
|
|
return {
|
|
value: kib / tib,
|
|
unit: "TiB"
|
|
};
|
|
if (kib >= gib)
|
|
return {
|
|
value: kib / gib,
|
|
unit: "GiB"
|
|
};
|
|
if (kib >= mib)
|
|
return {
|
|
value: kib / mib,
|
|
unit: "MiB"
|
|
};
|
|
return {
|
|
value: kib,
|
|
unit: "KiB"
|
|
};
|
|
}
|
|
|
|
Timer {
|
|
interval: Config.dashboard.resourceUpdateInterval
|
|
repeat: true
|
|
running: root.refCount > 0
|
|
triggeredOnStart: true
|
|
|
|
onTriggered: {
|
|
if (root.gpuType === "GENERIC") {
|
|
gpuUsage.running = true;
|
|
sensors.running = true;
|
|
}
|
|
|
|
if (root.gpuType === "GENERIC" && root.gpuMemTotal === 0)
|
|
oneshotMemAmd.running = true;
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: gpuNameDetect
|
|
|
|
command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || lspci 2>/dev/null | grep -i 'vga\\|3d\\|display' | head -1"]
|
|
running: true
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
const output = text.trim();
|
|
if (!output)
|
|
return;
|
|
|
|
// Check if it's from nvidia-smi (clean GPU name)
|
|
if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) {
|
|
root.gpuName = root.cleanGpuName(output);
|
|
} else {
|
|
// Parse lspci output: extract name from brackets or after colon
|
|
const bracketMatch = output.match(/\[([^\]]+)\]/);
|
|
if (bracketMatch) {
|
|
root.gpuName = root.cleanGpuName(bracketMatch[1]);
|
|
} else {
|
|
const colonMatch = output.match(/:\s*(.+)/);
|
|
if (colonMatch)
|
|
root.gpuName = root.cleanGpuName(colonMatch[1]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: gpuTypeCheck
|
|
|
|
command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"]
|
|
running: !Config.services.gpuType
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.autoGpuType = text.trim()
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: oneshotMem
|
|
|
|
command: ["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"]
|
|
running: root.gpuType === "NVIDIA" && root.gpuMemTotal === 0
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
root.gpuMemTotal = Number(this.text.trim());
|
|
oneshotMem.running = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: oneshotMemAmd
|
|
|
|
command: ["sh", "-c", "cat /sys/class/drm/card*/device/mem_info_vram_total"]
|
|
running: root.gpuType === "GENERIC" && root.gpuMemTotal === 0
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
const values = text.trim().split("\n").map(v => parseInt(v, 10)).filter(v => Number.isFinite(v));
|
|
|
|
if (values.length > 0) {
|
|
const totalBytes = values.reduce((a, b) => a + b, 0);
|
|
root.gpuMemTotal = totalBytes / (1024 * 1024);
|
|
}
|
|
|
|
oneshotMemAmd.running = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: gpuUsageNvidia
|
|
|
|
command: ["/usr/bin/nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits", "-lms", "1000"]
|
|
running: root.refCount > 0 && root.gpuType === "NVIDIA"
|
|
|
|
stdout: SplitParser {
|
|
onRead: data => {
|
|
const parts = String(data).trim().split(/\s*,\s*/);
|
|
if (parts.length < 3)
|
|
return;
|
|
|
|
const usageRaw = parseInt(parts[0], 10);
|
|
const tempRaw = parseInt(parts[1], 10);
|
|
const memRaw = parseInt(parts[2], 10);
|
|
|
|
if (!Number.isFinite(usageRaw) || !Number.isFinite(tempRaw) || !Number.isFinite(memRaw))
|
|
return;
|
|
|
|
const newGpuPerc = Math.max(0, Math.min(1, usageRaw / 100));
|
|
const newGpuTemp = tempRaw;
|
|
const newGpuMemUsed = root.gpuMemTotal > 0 ? Math.max(0, Math.min(1, memRaw / root.gpuMemTotal)) : 0;
|
|
|
|
// Only publish meaningful changes to avoid needless binding churn / repaints
|
|
if (Math.abs(root.gpuPerc - newGpuPerc) >= 0.01)
|
|
root.gpuPerc = newGpuPerc;
|
|
|
|
if (Math.abs(root.gpuTemp - newGpuTemp) >= 1)
|
|
root.gpuTemp = newGpuTemp;
|
|
|
|
if (Math.abs(root.gpuMemUsed - newGpuMemUsed) >= 0.01)
|
|
root.gpuMemUsed = newGpuMemUsed;
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: gpuUsage
|
|
|
|
command: root.gpuType === "GENERIC" ? ["sh", "-c", "paste -d ' ' /sys/class/drm/card*/device/gpu_busy_percent /sys/class/drm/card*/device/mem_info_vram_used"] : ["echo"]
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
if (root.gpuType === "GENERIC") {
|
|
const lines = text.trim().split("\n");
|
|
|
|
let percSum = 0;
|
|
let memSum = 0;
|
|
let count = 0;
|
|
|
|
for (const line of lines) {
|
|
const parts = line.trim().split(/\s+/);
|
|
if (parts.length < 2)
|
|
continue;
|
|
|
|
const gpuBusy = parseInt(parts[0], 10);
|
|
const memUsed = parseInt(parts[1], 10);
|
|
|
|
if (!Number.isFinite(gpuBusy) || !Number.isFinite(memUsed))
|
|
continue;
|
|
|
|
percSum += gpuBusy;
|
|
memSum += memUsed;
|
|
count++;
|
|
}
|
|
|
|
if (count > 0) {
|
|
// GPU usage %
|
|
root.gpuPerc = (percSum / count) / 100;
|
|
|
|
// VRAM usage (bytes → MiB → normalized)
|
|
const memUsedMiB = memSum / (1024 * 1024);
|
|
|
|
const newGpuMemUsed = root.gpuMemTotal > 0 ? Math.max(0, Math.min(1, memUsedMiB / root.gpuMemTotal)) : 0;
|
|
|
|
if (Math.abs(root.gpuMemUsed - newGpuMemUsed) >= 0.01)
|
|
root.gpuMemUsed = newGpuMemUsed;
|
|
}
|
|
} else {
|
|
root.gpuPerc = 0;
|
|
root.gpuTemp = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: sensors
|
|
|
|
command: ["sensors"]
|
|
environment: ({
|
|
LANG: "C.UTF-8",
|
|
LC_ALL: "C.UTF-8"
|
|
})
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
let eligible = false;
|
|
let sum = 0;
|
|
let count = 0;
|
|
|
|
for (const line of text.trim().split("\n")) {
|
|
if (line === "Adapter: PCI adapter")
|
|
eligible = true;
|
|
else if (line === "")
|
|
eligible = false;
|
|
else if (eligible) {
|
|
let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
|
|
if (!match)
|
|
// Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs)
|
|
match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
|
|
|
|
if (match) {
|
|
sum += parseFloat(match[2]);
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
root.gpuTemp = count > 0 ? sum / count : 0;
|
|
}
|
|
}
|
|
}
|
|
}
|