From 334c48e9b4df7bcf42a023bb13bf930ddd30e6d1 Mon Sep 17 00:00:00 2001 From: Benjamin Palko Date: Thu, 24 Jul 2025 10:45:08 -0400 Subject: [PATCH 1/2] copy pasta from caelestia --- services/SystemInfo.qml | 220 ++++++++++++++++++++++++++++++++++++++++ utils/Ref.qml | 9 ++ 2 files changed, 229 insertions(+) create mode 100644 services/SystemInfo.qml create mode 100644 utils/Ref.qml 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-- +} From 2f0e6bbb31d50550a5685cd63ce0e13fd720b84d Mon Sep 17 00:00:00 2001 From: Benjamin Palko Date: Thu, 24 Jul 2025 10:49:40 -0400 Subject: [PATCH 2/2] add storage --- config/Dimensions.qml | 11 +++++++ constants/Icons.qml | 1 + modules/bar/Bar.qml | 4 +++ modules/bar/components/Storage.qml | 49 ++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+) create mode 100644 modules/bar/components/Storage.qml diff --git a/config/Dimensions.qml b/config/Dimensions.qml index df27c14..d768bf8 100644 --- a/config/Dimensions.qml +++ b/config/Dimensions.qml @@ -12,6 +12,7 @@ Singleton { property Mpris mpris: Mpris {} property Clock clock: Clock {} property Pipewire pipewire: Pipewire {} + property Storage storage: Storage {} property Caffeine caffeine: Caffeine {} property Workspace workspace: Workspace {} property Tray tray: Tray {} @@ -56,6 +57,16 @@ Singleton { property int verticalPadding: 6 } + component Storage: QtObject { + id: clock + + property int iconSize: 14 + property int fontSize: 14 + property int height: 30 + property int horizontalPadding: 8 + property int verticalPadding: 6 + } + component Caffeine: QtObject { id: clock diff --git a/constants/Icons.qml b/constants/Icons.qml index aed8f3f..7c947e6 100644 --- a/constants/Icons.qml +++ b/constants/Icons.qml @@ -5,6 +5,7 @@ import Quickshell Singleton { property string brickWall: "\u{E586}" property string coffee: "\u{E09a}" + property string hardDrive: "\u{E0f1}" property string triangle: "\u{E192}" property string triangleDashed: "\u{E642}" } diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index c0a39e5..17d043e 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -104,6 +104,10 @@ Scope { anchors.verticalCenter: parent.verticalCenter } + Storage { + anchors.verticalCenter: parent.verticalCenter + } + Clock { anchors.verticalCenter: parent.verticalCenter } diff --git a/modules/bar/components/Storage.qml b/modules/bar/components/Storage.qml new file mode 100644 index 0000000..01713cd --- /dev/null +++ b/modules/bar/components/Storage.qml @@ -0,0 +1,49 @@ +import QtQuick +import Quickshell +import "../../../config/" +import "../../../constants/" +import "../../../services/" +import "../../../styled/" +import "../../../utils/" + +StyledLabel { + implicitWidth: childrenRect.width + implicitHeight: Dimensions.storage.height + + Ref { + service: SystemInfo + } + + Row { + StyledText { + id: icon + + font.family: Theme.lucide.font.family + font.pixelSize: Dimensions.storage.iconSize + font.bold: true + text: Icons.hardDrive + + anchors.verticalCenter: parent.verticalCenter + topPadding: Dimensions.storage.verticalPadding + bottomPadding: Dimensions.storage.verticalPadding + leftPadding: Dimensions.storage.horizontalPadding + } + + StyledText { + id: text + anchors.verticalCenter: parent.verticalCenter + topPadding: Dimensions.storage.verticalPadding + bottomPadding: Dimensions.storage.verticalPadding + rightPadding: Dimensions.storage.horizontalPadding + + font.pixelSize: Dimensions.storage.fontSize + + text: ` ${(SystemInfo.storagePerc * 100).toFixed()}%` + + SystemClock { + id: clock + precision: SystemClock.Seconds + } + } + } +}