pragma Singleton import Quickshell import Quickshell.Io import QtQuick import qs.Config Singleton { id: root property string autoGpuType: "NONE" property string cpuName: "" property real cpuPerc property real cpuTemp // Individual disks: Array of { mount, used, total, free, perc } property var disks: [] 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 real lastCpuIdle property real lastCpuTotal readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0 property real memTotal property real memUsed property int refCount readonly property real storagePerc: { let totalUsed = 0; let totalSize = 0; for (const disk of disks) { totalUsed += disk.used; totalSize += disk.total; } return totalSize > 0 ? totalUsed / totalSize : 0; } 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: { stat.reload(); meminfo.reload(); storage.running = false; if (root.gpuName === "GENERIC") gpuUsage.running = true; sensors.running = true; } } FileView { id: cpuinfoInit path: "/proc/cpuinfo" onLoaded: { const nameMatch = text().match(/model name\s*:\s*(.+)/); if (nameMatch) root.cpuName = root.cleanCpuName(nameMatch[1]); } } FileView { id: stat path: "/proc/stat" onLoaded: { const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/); if (data) { const stats = data.slice(1).map(n => parseInt(n, 10)); const total = stats.reduce((a, b) => a + b, 0); const idle = stats[3] + (stats[4] ?? 0); const totalDiff = total - root.lastCpuTotal; const idleDiff = idle - root.lastCpuIdle; root.cpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0; root.lastCpuTotal = total; root.lastCpuIdle = idle; } } } FileView { id: meminfo path: "/proc/meminfo" onLoaded: { const data = text(); root.memTotal = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1; root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0; } } Process { id: storage command: ["lsblk", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE", "-P"] stdout: StdioCollector { onStreamFinished: { const diskMap = {}; // Map disk name -> { name, totalSize, used, fsTotal } const lines = text.trim().split("\n"); for (const line of lines) { if (line.trim() === "") continue; const nameMatch = line.match(/NAME="([^"]+)"/); const sizeMatch = line.match(/SIZE="([^"]+)"/); const typeMatch = line.match(/TYPE="([^"]+)"/); const fsusedMatch = line.match(/FSUSED="([^"]*)"/); const fssizeMatch = line.match(/FSSIZE="([^"]*)"/); if (!nameMatch || !typeMatch) continue; const name = nameMatch[1]; const type = typeMatch[1]; const size = parseInt(sizeMatch?.[1] || "0", 10); const fsused = parseInt(fsusedMatch?.[1] || "0", 10); const fssize = parseInt(fssizeMatch?.[1] || "0", 10); if (type === "disk") { // Skip zram (swap) devices if (name.startsWith("zram")) continue; // Initialize disk entry if (!diskMap[name]) { diskMap[name] = { name: name, totalSize: size, used: 0, fsTotal: 0 }; } } else if (type === "part") { // Find parent disk (remove trailing numbers/p+numbers) let parentDisk = name.replace(/p?\d+$/, ""); // For nvme devices like nvme0n1p1, parent is nvme0n1 if (name.match(/nvme\d+n\d+p\d+/)) parentDisk = name.replace(/p\d+$/, ""); // Aggregate partition usage to parent disk if (diskMap[parentDisk]) { diskMap[parentDisk].used += fsused; diskMap[parentDisk].fsTotal += fssize; } } } const diskList = []; let totalUsed = 0; let totalSize = 0; for (const diskName of Object.keys(diskMap).sort()) { const disk = diskMap[diskName]; // Use filesystem total if available, otherwise use disk size const total = disk.fsTotal > 0 ? disk.fsTotal : disk.totalSize; const used = disk.used; const perc = total > 0 ? used / total : 0; // Convert bytes to KiB for consistency with formatKib diskList.push({ mount: disk.name // Using 'mount' property for compatibility , used: used / 1024, total: total / 1024, free: (total - used) / 1024, perc: perc }); totalUsed += used; totalSize += total; } root.disks = diskList; } } } 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: gpuUsageNvidia command: ["/usr/bin/nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits", "-lms", "1000"] running: true stderr: SplitParser { onRead: data => { console.log("nvidia-smi stderr:", String(data).trim()); } } stdout: SplitParser { onRead: data => { const [usage, temp, mem] = String(data).trim().split(/\s*,\s*/); root.gpuPerc = parseInt(usage, 10) / 100; root.gpuTemp = parseInt(temp, 10); root.gpuMemUsed = parseInt(mem, 10) / root.gpuMemTotal; } } onExited: (exitCode, exitStatus) => { console.log("gpuUsageNvidia exited:", exitCode, exitStatus); } onStarted: console.log("gpuUsageNvidia started, pid =", processId) } Process { id: gpuUsage command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : ["echo"] stdout: StdioCollector { onStreamFinished: { console.log("this is running"); if (root.gpuType === "GENERIC") { const percs = text.trim().split("\n"); const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0); root.gpuPerc = sum / percs.length / 100; } else if (root.gpuType === "NVIDIA") { const [usage, temp, mem] = text.trim().split(","); root.gpuPerc = parseInt(usage, 10) / 100; root.gpuTemp = parseInt(temp, 10); root.gpuMemUsed = parseInt(mem, 10) / root.gpuMemTotal; } 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 cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/); if (!cpuTemp) // If AMD Tdie pattern failed, try fallback on Tctl cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/); if (cpuTemp) root.cpuTemp = parseFloat(cpuTemp[1]); if (root.gpuType !== "GENERIC") return; 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; } } } }