diff --git a/README.md b/README.md index 7324f9a..cab99da 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/modules/Bar.qml b/modules/Bar.qml index 809edad..39d3f0e 100644 --- a/modules/Bar.qml +++ b/modules/Bar.qml @@ -201,6 +201,7 @@ PanelWindow { // Stats M.BarGroup { + M.Gpu {} M.Cpu { visible: M.Modules.cpu.enable } diff --git a/modules/Gpu.qml b/modules/Gpu.qml new file mode 100644 index 0000000..5f43c4b --- /dev/null +++ b/modules/Gpu.qml @@ -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 + } + } +} diff --git a/modules/Modules.qml b/modules/Modules.qml index 70a2d5d..4a0053e 100644 --- a/modules/Modules.qml +++ b/modules/Modules.qml @@ -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("{}") diff --git a/modules/SystemStats.qml b/modules/SystemStats.qml index 9ee7adf..a668be4 100644 --- a/modules/SystemStats.qml +++ b/modules/SystemStats.qml @@ -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; diff --git a/nix/hm-module.nix b/nix/hm-module.nix index 33cbc0f..7911c9b 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -87,6 +87,7 @@ in "bluetooth" "network" "powerProfile" + "gpu" "cpu" "memory" "idleInhibitor"