GPU plugin
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
#include "gpu.hpp"
|
||||
|
||||
#include "sensorslib.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <qdir.h>
|
||||
#include <qfile.h>
|
||||
#include <qregularexpression.h>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QTimer>
|
||||
|
||||
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<QString> 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
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include "tickingservice.hpp"
|
||||
|
||||
#include <qprocess.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
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
|
||||
@@ -1,4 +1,11 @@
|
||||
#include "tickingservice.hpp"
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QTimer>
|
||||
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user