diff --git a/services/SystemInfo.qml b/services/SystemInfo.qml new file mode 100644 index 0000000..18921e7 --- /dev/null +++ b/services/SystemInfo.qml @@ -0,0 +1,220 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + property real cpuPerc + property real cpuTemp + property string gpuType: "NONE" + property real gpuPerc + property real gpuTemp + property real memUsed + property real memTotal + readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0 + property real storageUsed + property real storageTotal + property real storagePerc: storageTotal > 0 ? storageUsed / storageTotal : 0 + + property real lastCpuIdle + property real lastCpuTotal + + property int refCount + + 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 { + running: root.refCount > 0 + interval: 3000 + repeat: true + triggeredOnStart: true + onTriggered: { + stat.reload(); + meminfo.reload(); + storage.running = true; + gpuUsage.running = true; + sensors.running = true; + } + } + + 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: ["sh", "-c", "df | grep '^/dev/' | awk '{print $1, $3, $4}'"] + stdout: StdioCollector { + onStreamFinished: { + const deviceMap = new Map(); + + for (const line of text.trim().split("\n")) { + if (line.trim() === "") + continue; + + const parts = line.trim().split(/\s+/); + if (parts.length >= 3) { + const device = parts[0]; + const used = parseInt(parts[1], 10) || 0; + const avail = parseInt(parts[2], 10) || 0; + + // Only keep the entry with the largest total space for each device + if (!deviceMap.has(device) || (used + avail) > (deviceMap.get(device).used + deviceMap.get(device).avail)) { + deviceMap.set(device, { + used: used, + avail: avail + }); + } + } + } + + let totalUsed = 0; + let totalAvail = 0; + + for (const [device, stats] of deviceMap) { + totalUsed += stats.used; + totalAvail += stats.avail; + } + + root.storageUsed = totalUsed; + root.storageTotal = totalUsed + totalAvail; + } + } + } + + Process { + id: gpuTypeCheck + + running: true + command: ["sh", "-c", "if ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; elif command -v nvidia-smi >/dev/null; then echo NVIDIA; else echo NONE; fi"] + stdout: StdioCollector { + onStreamFinished: root.gpuType = text.trim() + } + } + + Process { + id: gpuUsage + + command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : root.gpuType === "NVIDIA" ? ["nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu", "--format=csv,noheader,nounits"] : ["echo"] + stdout: StdioCollector { + onStreamFinished: { + 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] = text.trim().split(","); + root.gpuPerc = parseInt(usage, 10) / 100; + root.gpuTemp = parseInt(temp, 10); + } else { + root.gpuPerc = 0; + root.gpuTemp = 0; + } + } + } + } + + Process { + id: sensors + + command: ["sensors"] + environment: ({ + LANG: "C", + LC_ALL: "C" + }) + 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; + } + } + } +} diff --git a/utils/Ref.qml b/utils/Ref.qml new file mode 100644 index 0000000..679f52f --- /dev/null +++ b/utils/Ref.qml @@ -0,0 +1,9 @@ +import Quickshell +import QtQuick + +QtObject { + required property Singleton service + + Component.onCompleted: service.refCount++ + Component.onDestruction: service.refCount-- +}