From a37f6075d1e60c2c193c057a4ad8153480a336f9 Mon Sep 17 00:00:00 2001 From: zach Date: Mon, 22 Jun 2026 23:13:18 +0200 Subject: [PATCH] GPU plugin --- Helpers/SystemUsage.qml | 269 ----------------- Modules/Dashboard/Dash/Resources.qml | 8 +- Modules/Resources.qml | 8 +- Modules/Resources/Content.qml | 12 +- Plugins/ZShell/Services/CMakeLists.txt | 1 + Plugins/ZShell/Services/gpu.cpp | 332 +++++++++++++++++++++ Plugins/ZShell/Services/gpu.hpp | 88 ++++++ Plugins/ZShell/Services/tickingservice.cpp | 43 +++ 8 files changed, 478 insertions(+), 283 deletions(-) delete mode 100644 Helpers/SystemUsage.qml create mode 100644 Plugins/ZShell/Services/gpu.cpp create mode 100644 Plugins/ZShell/Services/gpu.hpp diff --git a/Helpers/SystemUsage.qml b/Helpers/SystemUsage.qml deleted file mode 100644 index b13402d..0000000 --- a/Helpers/SystemUsage.qml +++ /dev/null @@ -1,269 +0,0 @@ -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; - } - } - } -} diff --git a/Modules/Dashboard/Dash/Resources.qml b/Modules/Dashboard/Dash/Resources.qml index c76fd6e..12c4096 100644 --- a/Modules/Dashboard/Dash/Resources.qml +++ b/Modules/Dashboard/Dash/Resources.qml @@ -24,8 +24,8 @@ Item { service: Cpu } - Ref { - service: SystemUsage + ServiceRef { + service: Gpu } ColumnLayout { @@ -52,13 +52,13 @@ Item { Resource { fgColor: DynamicColors.palette.m3tertiary icon: "gamepad" - value: SystemUsage.gpuPerc + value: Gpu.percentage } Resource { fgColor: DynamicColors.palette.m3primary icon: "host" - value: SystemUsage.gpuMemUsed + value: Gpu.memoryUsed / Gpu.memoryTotal } Resource { diff --git a/Modules/Resources.qml b/Modules/Resources.qml index b0194d0..94805d8 100644 --- a/Modules/Resources.qml +++ b/Modules/Resources.qml @@ -34,8 +34,8 @@ CustomRect { implicitHeight: root.implicitHeight spacing: Appearance.spacing.smaller - Ref { - service: SystemUsage + ServiceRef { + service: Gpu } ServiceRef { @@ -70,7 +70,7 @@ CustomRect { icon: "gamepad" iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3tertiary - percentage: SystemUsage.gpuPerc + percentage: Gpu.percentage } Resource { @@ -78,7 +78,7 @@ CustomRect { icon: "developer_board" iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3primary - percentage: SystemUsage.gpuMemUsed + percentage: Gpu.memoryUsed / Gpu.memoryTotal } } } diff --git a/Modules/Resources/Content.qml b/Modules/Resources/Content.qml index f18867f..55bcff1 100644 --- a/Modules/Resources/Content.qml +++ b/Modules/Resources/Content.qml @@ -55,18 +55,18 @@ Item { WrappedLoader { id: gpuCard - active: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "" + active: Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None sourceComponent: HeroCard { accent: DynamicColors.palette.m3secondary icon: "desktop_windows" label: qsTr("GPU") - subLabel: SystemUsage.gpuName - temperature: SystemUsage.gpuTemp - usage: SystemUsage.gpuPerc + subLabel: Gpu.name + temperature: Gpu.temperature + usage: Gpu.percentage - Ref { - service: SystemUsage + ServiceRef { + service: Gpu } } } diff --git a/Plugins/ZShell/Services/CMakeLists.txt b/Plugins/ZShell/Services/CMakeLists.txt index 0ff7714..7d1d2ff 100644 --- a/Plugins/ZShell/Services/CMakeLists.txt +++ b/Plugins/ZShell/Services/CMakeLists.txt @@ -14,6 +14,7 @@ qml_module(ZShell-services sensorslib.hpp sensorslib.cpp usagefmt.hpp usagefmt.cpp cpu.hpp cpu.cpp + gpu.hpp gpu.cpp memory.hpp memory.cpp diskinfo.hpp diskinfo.cpp storage.hpp storage.cpp diff --git a/Plugins/ZShell/Services/gpu.cpp b/Plugins/ZShell/Services/gpu.cpp new file mode 100644 index 0000000..9a236cd --- /dev/null +++ b/Plugins/ZShell/Services/gpu.cpp @@ -0,0 +1,332 @@ +#include "gpu.hpp" + +#include "sensorslib.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ZShell::services { + +namespace { + +constexpr const char* kTypeDetectScript = + "if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; 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"; + +constexpr const char* kNameDetectScript = "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null" + " || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' -f1" + " || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1"; + +} // namespace + +Gpu::Gpu(QObject* parent) + : TickingService(parent) { + QString configPath = QDir::homePath() + QStringLiteral("/.config/zshell/config.json"); + + auto reloadConfig = [this, configPath]() { + QFile file(configPath); + if (file.open(QIODevice::ReadOnly)) { + QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + if (!doc.isNull()) { + QJsonObject services = doc.object().value("services").toObject(); + setUserType(parseType(services.value("gpuType").toString())); + } + } + }; + + reloadConfig(); + + static QFileSystemWatcher* watcher = new QFileSystemWatcher(); + if (!watcher->files().contains(configPath)) { + QObject::connect(watcher, &QFileSystemWatcher::fileChanged, this, [this, configPath]() { + QTimer::singleShot(100, this, [this, configPath]() { + QFile file(configPath); + if (file.exists()) { + QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + if (!doc.isNull()) { + QJsonObject services = doc.object().value("services").toObject(); + setUserType(parseType(services.value("gpuType").toString())); + } + } + }); + }); + watcher->addPath(configPath); + } + + // Detection must run before any ServiceRef appears: callers may gate the ref on + // `type !== Gpu.None`, which would otherwise deadlock the detection. + if (m_userType == Auto) { + detectTypeOnce(); + } + detectNameOnce(); +} + +Gpu::Type Gpu::type() const { + return m_userType == Auto ? m_autoType : m_userType; +} + +Gpu::Type Gpu::userType() const { + return m_userType; +} + +Gpu::Type Gpu::autoType() const { + return m_autoType; +} + +QString Gpu::name() const { + return m_name; +} + +qreal Gpu::percentage() const { + return m_percentage; +} + +qreal Gpu::temperature() const { + return m_temperature; +} + +qreal Gpu::memoryUsed() const { + return m_memoryUsed; +} + +qreal Gpu::memoryTotal() const { + return m_memoryTotal; +} + +void Gpu::setUserType(Type value) { + if (value == m_userType) { + return; + } + const Type prevDerived = type(); + m_userType = value; + Q_EMIT userTypeChanged(); + if (type() != prevDerived) { + Q_EMIT typeChanged(); + } +} + +void Gpu::setAutoType(Type value) { + if (value == m_userType) { + return; + } + const Type prevDerived = type(); + m_autoType = value; + Q_EMIT autoTypeChanged(); + if (type() != prevDerived) { + Q_EMIT typeChanged(); + } +} + +void Gpu::setName(QString value) { + if (value == m_name) { + return; + } + m_name = std::move(value); + Q_EMIT nameChanged(); +} + +void Gpu::setMemoryUsed(qreal value) { + if (m_memoryUsed == value) return; + m_memoryUsed = value; + Q_EMIT memoryUsedChanged(); +} + +void Gpu::setMemoryTotal(qreal value) { + if (m_memoryTotal == value) return; + m_memoryTotal = value; + Q_EMIT memoryTotalChanged(); +} + +void Gpu::tick() { + const Type t = type(); + if (t == Generic) { + readGenericUsage(); + readGpuTemperature(); + } else if (t == Nvidia) { + startNvidiaUsage(); + } else { + if (std::abs(m_percentage) > 0.0001) { + m_percentage = 0.0; + Q_EMIT percentageChanged(); + } + if (std::abs(m_temperature) > 0.05) { + m_temperature = 0.0; + Q_EMIT temperatureChanged(); + } + } +} + +void Gpu::detectTypeOnce() { + if (m_typeProc) { + return; + } + m_typeProc = new QProcess(this); + QObject::connect(m_typeProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) { + const QByteArray out = m_typeProc->readAllStandardOutput().trimmed(); + if (!out.isEmpty()) { + setAutoType(parseType(QString::fromLatin1(out))); + } + m_typeProc->deleteLater(); + m_typeProc = nullptr; + }); + m_typeProc->start(QStringLiteral("sh"), { QStringLiteral("-c"), QString::fromLatin1(kTypeDetectScript) }); +} + +void Gpu::detectNameOnce() { + if (m_nameProc) { + return; + } + m_nameProc = new QProcess(this); + QObject::connect(m_nameProc, &QProcess::finished, this, [this](int, QProcess::ExitStatus) { + const QString output = QString::fromUtf8(m_nameProc->readAllStandardOutput()).trimmed(); + if (!output.isEmpty()) { + const QString lower = output.toLower(); + if (lower.contains(QStringLiteral("nvidia")) || lower.contains(QStringLiteral("geforce")) || + lower.contains(QStringLiteral("rtx")) || lower.contains(QStringLiteral("gtx")) || + lower.contains(QStringLiteral("rx"))) { + setName(cleanName(output)); + } else { + static const QRegularExpression bracketRe(QStringLiteral("\\[([^\\]]+)\\][^\\[]*$")); + const auto bracket = bracketRe.match(output); + if (bracket.hasMatch()) { + setName(cleanName(bracket.captured(1))); + } else { + static const QRegularExpression colonRe(QStringLiteral(":\\s*(.+)")); + const auto colon = colonRe.match(output); + if (colon.hasMatch()) { + setName(cleanName(colon.captured(1))); + } + } + } + } + m_nameProc->deleteLater(); + m_nameProc = nullptr; + }); + m_nameProc->start(QStringLiteral("sh"), { QStringLiteral("-c"), QString::fromLatin1(kNameDetectScript) }); +} + +void Gpu::readGenericUsage() { + const QStringList paths = + QDir(QStringLiteral("/sys/class/drm")) + .entryList(QStringList() << QStringLiteral("card*"), QDir::Dirs | QDir::NoDotAndDotDot); + qreal sum = 0.0; + int count = 0; + for (const QString& card : paths) { + QFile f(QStringLiteral("/sys/class/drm/%1/device/gpu_busy_percent").arg(card)); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + continue; + } + bool ok = false; + const qreal v = f.readAll().trimmed().toDouble(&ok); + f.close(); + if (ok) { + sum += v; + ++count; + } + } + const qreal newPerc = count > 0 ? sum / count / 100.0 : 0.0; + if (std::abs(newPerc - m_percentage) > 0.0001) { + m_percentage = newPerc; + Q_EMIT percentageChanged(); + } +} + +void Gpu::startNvidiaUsage() { + if (m_nvidiaProc) { + return; + } + m_nvidiaProc = new QProcess(this); + QObject::connect(m_nvidiaProc, &QProcess::readyReadStandardOutput, this, [this]() { + while (m_nvidiaProc->canReadLine()) { + const QByteArray out = m_nvidiaProc->readLine(); + const QString output = QString::fromUtf8(out).trimmed(); + if (output.isEmpty()) + continue; + + const QList parts = output.split(','); + if (parts.size() < 4) + return; + + bool ok1 = false; + bool ok2 = false; + bool ok3 = false; + bool ok4 = false; + const qreal usage = parts.at(0).trimmed().toDouble(&ok1) / 100.0; + const qreal temp = parts.at(1).trimmed().toDouble(&ok2); + const qreal memUsed = parts.at(2).trimmed().toDouble(&ok3); + const qreal memTotal = parts.at(3).trimmed().toDouble(&ok4); + + if (ok1 && std::abs(usage - m_percentage) > 0.0001) { + m_percentage = usage; + Q_EMIT percentageChanged(); + } + if (ok2 && std::abs(temp - m_temperature) > 0.05) { + m_temperature = temp; + Q_EMIT temperatureChanged(); + } + if (ok3) { + setMemoryUsed(memUsed); + } + if (ok4) { + setMemoryTotal(memTotal); + } + } + }); + + QObject::connect(m_nvidiaProc, &QProcess::finished, this, [this]() { + m_nvidiaProc->deleteLater(); + m_nvidiaProc = nullptr; + }); + + QObject::connect(m_nvidiaProc, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) { + m_nvidiaProc->deleteLater(); + m_nvidiaProc = nullptr; + }); + + m_nvidiaProc->start(QStringLiteral("nvidia-smi"), { QStringLiteral("--query-gpu=utilization.gpu,temperature.gpu,memory.used,memory.total"), + QStringLiteral("--format=csv,noheader,nounits"), + QStringLiteral("-lms"), + QString::number(updateInterval()) }); +} + +void Gpu::readGpuTemperature() { + const auto t = sensorslib::gpuPciAverageTemp(); + const qreal newTemp = t.value_or(0.0); + if (std::abs(newTemp - m_temperature) > 0.05) { + m_temperature = newTemp; + Q_EMIT temperatureChanged(); + } +} + +Gpu::Type Gpu::parseType(const QString& s) { + const QString u = s.trimmed().toUpper(); + if (u.isEmpty()) { + return Auto; + } + if (u == QStringLiteral("NVIDIA")) { + return Nvidia; + } + if (u == QStringLiteral("GENERIC")) { + return Generic; + } + return None; +} + +QString Gpu::cleanName(QString s) { + static const QRegularExpression noise( + QStringLiteral("\\(R\\)|\\(TM\\)|Graphics"), QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression spaces(QStringLiteral("\\s+")); + s.replace(noise, QString()); + s.replace(spaces, QStringLiteral(" ")); + return s.trimmed(); +} + +} // namespace ZShell::services diff --git a/Plugins/ZShell/Services/gpu.hpp b/Plugins/ZShell/Services/gpu.hpp new file mode 100644 index 0000000..e8cf84d --- /dev/null +++ b/Plugins/ZShell/Services/gpu.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include "tickingservice.hpp" + +#include +#include + +namespace ZShell::services { + +class Gpu : public TickingService { +Q_OBJECT +QML_ELEMENT +QML_SINGLETON + +public: +enum Type { + Auto, // user override is empty (config "") — defer to detected autoType + None, // no usable GPU + Nvidia, // queried via nvidia-smi + Generic, // queried via /sys/class/drm/card*/device/gpu_busy_percent +}; +Q_ENUM(Type) + +private: +Q_PROPERTY(Type type READ type NOTIFY typeChanged) +Q_PROPERTY(Type userType READ userType NOTIFY userTypeChanged) +Q_PROPERTY(Type autoType READ autoType NOTIFY autoTypeChanged) +Q_PROPERTY(QString name READ name NOTIFY nameChanged) +Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged) +Q_PROPERTY(qreal temperature READ temperature NOTIFY temperatureChanged) +Q_PROPERTY(qreal memoryUsed READ memoryUsed NOTIFY memoryUsedChanged) +Q_PROPERTY(qreal memoryTotal READ memoryTotal NOTIFY memoryTotalChanged) + +public: +explicit Gpu(QObject* parent = nullptr); + +[[nodiscard]] Type type() const; +[[nodiscard]] Type userType() const; +[[nodiscard]] Type autoType() const; +[[nodiscard]] QString name() const; +[[nodiscard]] qreal percentage() const; +[[nodiscard]] qreal temperature() const; +[[nodiscard]] qreal memoryUsed() const; +[[nodiscard]] qreal memoryTotal() const; + +signals: +void typeChanged(); +void userTypeChanged(); +void autoTypeChanged(); +void nameChanged(); +void percentageChanged(); +void temperatureChanged(); +void memoryUsedChanged(); +void memoryTotalChanged(); + +protected: +void tick() override; + +private: +void detectTypeOnce(); +void detectNameOnce(); +void readGenericUsage(); +void startNvidiaUsage(); +void readGpuTemperature(); + +void setUserType(Type value); +void setAutoType(Type value); +void setName(QString value); +void setMemoryUsed(qreal value); +void setMemoryTotal(qreal value); + +[[nodiscard]] static Type parseType(const QString& s); +[[nodiscard]] static QString cleanName(QString s); + +Type m_userType = Auto; +Type m_autoType = None; +QString m_name; +qreal m_percentage = 0.0; +qreal m_temperature = 0.0; +qreal m_memoryUsed = 0.0; +qreal m_memoryTotal = 0.0; + +QProcess* m_typeProc = nullptr; +QProcess* m_nameProc = nullptr; +QProcess* m_nvidiaProc = nullptr; +}; + +} // namespace ZShell::services diff --git a/Plugins/ZShell/Services/tickingservice.cpp b/Plugins/ZShell/Services/tickingservice.cpp index 5611a6c..4485d86 100644 --- a/Plugins/ZShell/Services/tickingservice.cpp +++ b/Plugins/ZShell/Services/tickingservice.cpp @@ -1,4 +1,11 @@ #include "tickingservice.hpp" +#include +#include +#include +#include +#include +#include +#include namespace ZShell::services { @@ -9,6 +16,42 @@ TickingService::TickingService(QObject* parent) QObject::connect(m_timer, &QTimer::timeout, this, [this] { tick(); }); + + QString configPath = QDir::homePath() + QStringLiteral("/.config/zshell/config.json"); + + auto reloadConfig = [this, configPath]() { + QFile file(configPath); + if (file.open(QIODevice::ReadOnly)) { + QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + if (!doc.isNull()) { + QJsonObject dashboard = doc.object().value("dashboard").toObject(); + if (dashboard.contains("resourceUpdateInterval")) { + applyInterval(dashboard.value("resourceUpdateInterval").toInt(1000)); + } + } + } + }; + + reloadConfig(); + + static auto* watcher = new QFileSystemWatcher(); + if (!watcher->files().contains(configPath)) { + QObject::connect(watcher, &QFileSystemWatcher::fileChanged, this, [this, configPath]() { + QTimer::singleShot(100, this, [this, configPath]() { + QFile file(configPath); + if (file.exists()) { + QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + if (!doc.isNull()) { + QJsonObject dashboard = doc.object().value("dashboard").toObject(); + if (dashboard.contains("resourceUpdateInterval")) { + applyInterval(dashboard.value("resourceUpdateInterval").toInt(1000)); + } + } + } + }); + }); + watcher->addPath(configPath); + } } int TickingService::updateInterval() const {