feat(gpu): add gpu monitor module (amd sysfs + nvidia-smi, usage/vram/temp)

This commit is contained in:
Damocles 2026-04-17 11:15:55 +02:00
parent a2966f51ab
commit ff1644c0bd
6 changed files with 301 additions and 2 deletions

View file

@ -104,7 +104,7 @@ programs.nova-shell.modules = {
Each module is an object with `enable` (default `true`) and optional extra
settings. Full list: `workspaces`, `tray`, `windowTitle`, `clock`,
`notifications`, `mpris`, `volume`, `bluetooth`, `backlight`, `network`,
`powerProfile`, `idleInhibitor`, `weather`, `temperature`, `cpu`, `memory`,
`powerProfile`, `idleInhibitor`, `weather`, `temperature`, `gpu`, `cpu`, `memory`,
`disk`, `battery`, `power`, `backgroundOverlay`, `overviewBackdrop`.
### Theme

View file

@ -201,6 +201,7 @@ PanelWindow {
// Stats
M.BarGroup {
M.Gpu {}
M.Cpu {
visible: M.Modules.cpu.enable
}

276
modules/Gpu.qml Normal file
View file

@ -0,0 +1,276 @@
import QtQuick
import Quickshell
import "." as M
M.BarSection {
id: root
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
tooltip: ""
visible: M.Modules.gpu.enable && M.SystemStats.gpuAvailable
property bool _pinned: false
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
readonly property bool _showPanel: _anyHover || _pinned
on_AnyHoverChanged: {
if (_anyHover)
_unpinTimer.stop();
else if (_pinned)
_unpinTimer.start();
}
Timer {
id: _unpinTimer
interval: 500
onTriggered: root._pinned = false
}
function _loadColor(pct) {
const t = Math.max(0, Math.min(100, pct)) / 100;
const a = t < 0.5 ? M.Theme.base0B : M.Theme.base0A;
const b = t < 0.5 ? M.Theme.base0A : M.Theme.base08;
const u = t < 0.5 ? t * 2 : (t - 0.5) * 2;
return Qt.rgba(a.r + (b.r - a.r) * u, a.g + (b.g - a.g) * u, a.b + (b.b - a.b) * u, 1);
}
function _fmt(gb) {
return gb >= 10 ? gb.toFixed(1) + "G" : gb.toFixed(2) + "G";
}
M.BarIcon {
icon: "\uDB84\uDCB0"
color: root._loadColor(M.SystemStats.gpuUsage)
anchors.verticalCenter: parent.verticalCenter
TapHandler {
cursorShape: Qt.PointingHandCursor
onTapped: root._pinned = !root._pinned
}
}
M.BarLabel {
label: M.SystemStats.gpuUsage + "%"
minText: "100%"
color: root._loadColor(M.SystemStats.gpuUsage)
anchors.verticalCenter: parent.verticalCenter
TapHandler {
cursorShape: Qt.PointingHandCursor
onTapped: root._pinned = !root._pinned
}
}
M.HoverPanel {
id: hoverPanel
showPanel: root._showPanel
screen: QsWindow.window?.screen ?? null
anchorItem: root
accentColor: root.accentColor
panelNamespace: "nova-gpu"
panelTitle: "GPU"
contentWidth: 240
// Header vendor + usage%
Item {
width: parent.width
height: 28
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: M.SystemStats.gpuVendor.toUpperCase()
color: M.Theme.base03
font.pixelSize: M.Theme.fontSize - 3
font.family: M.Theme.fontFamily
font.letterSpacing: 1
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: M.SystemStats.gpuUsage + "%"
color: root._loadColor(M.SystemStats.gpuUsage)
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
font.bold: true
}
}
// Usage bar
Item {
width: parent.width
height: 14
Item {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
height: 6
Rectangle {
anchors.fill: parent
color: M.Theme.base02
radius: 3
}
Rectangle {
width: parent.width * Math.min(1, M.SystemStats.gpuUsage / 100)
height: parent.height
color: root._loadColor(M.SystemStats.gpuUsage)
radius: 3
Behavior on width {
enabled: root._showPanel
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
}
}
// Usage history sparkline
Canvas {
id: _sparkline
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
height: 36
property var _hist: M.SystemStats.gpuHistory
on_HistChanged: if (root._showPanel)
requestPaint()
Connections {
target: root
function on_ShowPanelChanged() {
if (root._showPanel)
_sparkline.requestPaint();
}
}
onPaint: {
const ctx = getContext("2d");
if (!ctx)
return;
ctx.clearRect(0, 0, width, height);
const d = _hist;
if (!d.length)
return;
const maxSamples = 60;
const bw = width / maxSamples;
const offset = maxSamples - d.length;
for (let i = 0; i < d.length; i++) {
const barH = Math.max(1, height * d[i] / 100);
const col = root._loadColor(d[i]);
ctx.fillStyle = col.toString();
ctx.fillRect((offset + i) * bw, height - barH, Math.max(1, bw - 0.5), barH);
}
}
}
// VRAM section
Rectangle {
width: parent.width - 16
height: 1
anchors.horizontalCenter: parent.horizontalCenter
color: M.Theme.base03
}
Item {
width: parent.width
height: 22
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "VRAM"
color: M.Theme.base03
font.pixelSize: M.Theme.fontSize - 3
font.family: M.Theme.fontFamily
font.letterSpacing: 1
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root._fmt(M.SystemStats.gpuVramUsedGb) + " / " + root._fmt(M.SystemStats.gpuVramTotalGb)
color: root.accentColor
font.pixelSize: M.Theme.fontSize - 1
font.family: M.Theme.fontFamily
font.bold: true
}
}
Item {
width: parent.width
height: 12
Item {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
height: 5
Rectangle {
anchors.fill: parent
color: M.Theme.base02
radius: 2
}
Rectangle {
width: M.SystemStats.gpuVramTotalGb > 0 ? parent.width * Math.min(1, M.SystemStats.gpuVramUsedGb / M.SystemStats.gpuVramTotalGb) : 0
height: parent.height
color: root.accentColor
radius: 2
Behavior on width {
enabled: root._showPanel
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
}
}
// Temperature row
Item {
width: parent.width
height: 22
visible: M.SystemStats.gpuTempC > 0
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "Temp"
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: M.SystemStats.gpuTempC + "\u00B0C"
color: M.SystemStats.gpuTempC > 85 ? M.Theme.base08 : M.SystemStats.gpuTempC > 70 ? M.Theme.base0A : M.Theme.base05
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
}
}
Item {
width: 1
height: 4
}
}
}

View file

@ -58,6 +58,9 @@ QtObject {
warm: 80,
hot: 90
})
property var gpu: ({
enable: true
})
property var cpu: ({
enable: true
})
@ -94,7 +97,7 @@ QtObject {
// All module keys that have an enable flag used to default-enable anything
// not explicitly mentioned in modules.json
readonly property var _moduleKeys: ["workspaces", "tray", "windowTitle", "clock", "notifications", "mpris", "volume", "bluetooth", "backlight", "network", "powerProfile", "idleInhibitor", "weather", "temperature", "cpu", "memory", "disk", "battery", "privacy", "screenCorners", "power", "backgroundOverlay", "overviewBackdrop"]
readonly property var _moduleKeys: ["workspaces", "tray", "windowTitle", "clock", "notifications", "mpris", "volume", "bluetooth", "backlight", "network", "powerProfile", "idleInhibitor", "weather", "temperature", "gpu", "cpu", "memory", "disk", "battery", "privacy", "screenCorners", "power", "backgroundOverlay", "overviewBackdrop"]
// Fallback: if modules.json doesn't exist, enable everything
Component.onCompleted: _apply("{}")

View file

@ -34,6 +34,15 @@ QtObject {
property int tempCelsius: 0
property var tempHistory: [] // 150 samples @ 4s each 10 min
// GPU
property bool gpuAvailable: false
property string gpuVendor: ""
property int gpuUsage: 0
property real gpuVramUsedGb: 0
property real gpuVramTotalGb: 0
property int gpuTempC: 0
property var gpuHistory: [] // 60 samples @ ~4-8s each 4-8 min
// Memory
property int memPercent: 0
property real memUsedGb: 0
@ -86,6 +95,15 @@ QtObject {
root.tempCelsius = ev.celsius;
const th = root.tempHistory.concat([ev.celsius]);
root.tempHistory = th.length > 150 ? th.slice(th.length - 150) : th;
} else if (ev.type === "gpu") {
root.gpuAvailable = true;
root.gpuVendor = ev.vendor;
root.gpuUsage = ev.usage;
root.gpuVramUsedGb = ev.vram_used_gb;
root.gpuVramTotalGb = ev.vram_total_gb;
root.gpuTempC = ev.temp_c;
const gh = root.gpuHistory.concat([ev.usage]);
root.gpuHistory = gh.length > 60 ? gh.slice(gh.length - 60) : gh;
} else if (ev.type === "mem") {
root.memPercent = ev.percent;
root.memUsedGb = ev.used_gb;

View file

@ -87,6 +87,7 @@ in
"bluetooth"
"network"
"powerProfile"
"gpu"
"cpu"
"memory"
"idleInhibitor"