From ad3ba45e3b6601ad726dbfc23244d3d41e2cd0f9 Mon Sep 17 00:00:00 2001 From: Jon Iler Date: Wed, 1 Jul 2026 09:13:40 -0600 Subject: [PATCH 1/2] Add nvidia-optimus plugin Monitor and control an NVIDIA Optimus dGPU on Hyprland: toggle Battery (dGPU RTD3-suspends via AQ_DRM_DEVICES + mesa-only EGL) vs HDMI-Ready, with a theme-tinted eye, wake-free sysfs polling, GPU auto-detection, and a live process list. --- nvidia-optimus/Assets/nvidia-eye.svg | 1 + nvidia-optimus/BarWidget.qml | 81 ++++++++ nvidia-optimus/Main.qml | 109 ++++++++++ nvidia-optimus/Panel.qml | 296 +++++++++++++++++++++++++++ nvidia-optimus/README.md | 102 +++++++++ nvidia-optimus/Settings.qml | 43 ++++ nvidia-optimus/manifest.json | 26 +++ nvidia-optimus/preview.png | Bin 0 -> 44765 bytes nvidia-optimus/scripts/gpu-detect.sh | 67 ++++++ nvidia-optimus/scripts/gpu-stat | 108 ++++++++++ nvidia-optimus/scripts/gpu-toggle | 72 +++++++ 11 files changed, 905 insertions(+) create mode 100644 nvidia-optimus/Assets/nvidia-eye.svg create mode 100644 nvidia-optimus/BarWidget.qml create mode 100644 nvidia-optimus/Main.qml create mode 100644 nvidia-optimus/Panel.qml create mode 100644 nvidia-optimus/README.md create mode 100644 nvidia-optimus/Settings.qml create mode 100644 nvidia-optimus/manifest.json create mode 100644 nvidia-optimus/preview.png create mode 100644 nvidia-optimus/scripts/gpu-detect.sh create mode 100755 nvidia-optimus/scripts/gpu-stat create mode 100755 nvidia-optimus/scripts/gpu-toggle diff --git a/nvidia-optimus/Assets/nvidia-eye.svg b/nvidia-optimus/Assets/nvidia-eye.svg new file mode 100644 index 000000000..35dcbfd28 --- /dev/null +++ b/nvidia-optimus/Assets/nvidia-eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/nvidia-optimus/BarWidget.qml b/nvidia-optimus/BarWidget.qml new file mode 100644 index 000000000..6c32f6066 --- /dev/null +++ b/nvidia-optimus/BarWidget.qml @@ -0,0 +1,81 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +// Bar button: the NVIDIA "eye" mark, tinted to the active Noctalia theme — the accent +// colour (Color.mPrimary) when the dGPU is awake / driving a display, and a neutral +// colour (Color.mOnSurface) when it is asleep. Hidden when no NVIDIA dGPU is present. +NIconButton { + id: root + + property var pluginApi: null + + property ShellScreen screen + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + readonly property var main: pluginApi?.mainInstance + readonly property string rt: main ? main.runtime : "unknown" + readonly property bool ext: main ? main.external : false + readonly property string md: main ? main.mode : "battery" + readonly property bool present: main ? main.present : true + readonly property bool lit: root.ext || root.rt === "active" + + visible: root.present + icon: "" // hidden: we draw the tinted NVIDIA mark below + + tooltipText: { + if (root.ext) + return "dGPU — driving external display"; + var m = (root.md === "hdmi") ? "HDMI-Ready" : "Battery"; + return "dGPU — " + (root.rt === "active" ? "active" : "asleep") + " (" + m + " mode)"; + } + tooltipDirection: BarService.getTooltipDirection(screen?.name) + baseSize: Style.getCapsuleHeightForScreen(screen?.name) + applyUiScale: false + customRadius: Style.radiusL + colorBg: Style.capsuleColor + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + Item { + id: mark + anchors.centerIn: parent + width: Math.round(root.buttonSize * 0.9) + height: width + + Image { + id: eyeImg + anchors.fill: parent + source: Qt.resolvedUrl("Assets/nvidia-eye.svg") + fillMode: Image.PreserveAspectFit + sourceSize.width: mark.width + sourceSize.height: mark.height + smooth: true + mipmap: true + visible: false // shown via the tinting effect below + } + + MultiEffect { + anchors.fill: eyeImg + source: eyeImg + colorization: 1.0 + colorizationColor: root.lit ? Color.mPrimary : Color.mOnSurface + Behavior on colorizationColor { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + + onClicked: { + if (pluginApi) + pluginApi.openPanel(root.screen, this); + } +} diff --git a/nvidia-optimus/Main.qml b/nvidia-optimus/Main.qml new file mode 100644 index 000000000..74740b3e3 --- /dev/null +++ b/nvidia-optimus/Main.qml @@ -0,0 +1,109 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +// Headless logic + polling for the NVIDIA Optimus plugin. +// The background poll runs the bundled `scripts/gpu-stat` in SYSFS-ONLY mode, so it +// never wakes — or holds awake — a sleeping dGPU. Detailed nvidia-smi stats are +// polled by the Panel only while it is open. +Item { + id: root + + property var pluginApi: null + + // Bundled helper scripts, resolved relative to this plugin's directory and run via + // bash (so no execute bit is required after a plain `git clone`). + readonly property string pluginDir: Qt.resolvedUrl(".").toString().replace(/^file:\/\//, "") + readonly property string statCmd: root.pluginDir + "scripts/gpu-stat" + readonly property string toggleCmd: root.pluginDir + "scripts/gpu-toggle" + + property var stats: ({ + "runtime": "unknown", + "mode": "battery", + "pending": "battery", + "external": "none" + }) + + readonly property string runtime: (stats && stats.runtime) ? stats.runtime : "unknown" + readonly property string mode: (stats && stats.mode) ? stats.mode : "battery" // live + readonly property string pending: (stats && stats.pending) ? stats.pending : mode // next login + readonly property bool present: runtime !== "absent" + readonly property bool active: runtime === "active" + readonly property bool external: !!(stats && stats.external && stats.external !== "none") + readonly property bool needsRestart: pending !== mode + property bool busy: false + + function refresh() { + if (statProc.running) + return; + statProc.command = ["bash", root.statCmd]; + statProc.running = true; + } + + function setMode(m) { + busy = true; + ctlProc.command = ["bash", root.toggleCmd, m]; + ctlProc.running = true; + refresh(); + } + + function toggle() { + if (busy) + return; + setMode(pending === "hdmi" ? "battery" : "hdmi"); + } + + function apply() { + applyProc.running = true; // restarts the Hyprland session to pick up the env change + } + + // qs -c noctalia-shell ipc call plugin:nvidia-optimus toggle (open panel) + // qs -c noctalia-shell ipc call plugin:nvidia-optimus mode (flip Battery/HDMI) + IpcHandler { + target: "plugin:nvidia-optimus" + + function toggle(): void { + if (root.pluginApi) + root.pluginApi.withCurrentScreen(screen => root.pluginApi.togglePanel(screen)); + } + + function mode(): void { + root.toggle(); + } + } + + Process { + id: statProc + stdout: StdioCollector { + onStreamFinished: { + try { + root.stats = JSON.parse(this.text); + } catch (e) { + Logger.w("NvidiaOptimus", "Failed to parse gpu-stat output"); + } + } + } + } + + Process { + id: ctlProc + onExited: function (code) { + root.busy = false; + root.refresh(); + } + } + + Process { + id: applyProc + command: ["hyprctl", "dispatch", "exit"] + } + + Timer { + interval: 2000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: root.refresh() + } +} diff --git a/nvidia-optimus/Panel.qml b/nvidia-optimus/Panel.qml new file mode 100644 index 000000000..99f0b5387 --- /dev/null +++ b/nvidia-optimus/Panel.qml @@ -0,0 +1,296 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Widgets + +// Popover: themed NVIDIA header with the Battery⇄HDMI toggle, live dGPU stats, the +// external-display readout, and the list of processes holding the GPU. Detailed +// nvidia-smi stats are polled ONLY while this panel is visible. +Item { + id: root + + property var pluginApi: null + + readonly property var geometryPlaceholder: panelContainer + property real contentPreferredWidth: 360 * Style.uiScaleRatio + property real contentPreferredHeight: (panelCol.implicitHeight + Style.marginL * 2) * Style.uiScaleRatio + readonly property bool allowAttach: true + + anchors.fill: parent + + readonly property var main: pluginApi?.mainInstance + readonly property var s: main ? main.stats : ({}) + readonly property string runtime: (s && s.runtime) ? s.runtime : "unknown" + readonly property bool active: runtime === "active" + readonly property string pending: main ? main.pending : "battery" + readonly property bool needsRestart: main ? main.needsRestart : false + readonly property bool ext: main ? main.external : false + readonly property bool lit: root.ext || root.active + + // Detailed nvidia-smi stats — polled ONLY while this panel is visible, so the + // background bar poll never wakes the dGPU. + property var full: ({}) + + Process { + id: fullProc + stdout: StdioCollector { + onStreamFinished: { + try { + root.full = JSON.parse(this.text); + } catch (e) {} + } + } + } + + Timer { + interval: 2000 + running: root.visible && !!root.main + repeat: true + triggeredOnStart: true + onTriggered: { + if (root.main && !fullProc.running) { + fullProc.command = ["bash", root.main.statCmd, "full"]; + fullProc.running = true; + } + } + } + + function fmt1(n) { + return (Math.round((Number(n) || 0) * 10) / 10).toFixed(1); + } + + function subtitle() { + if (root.needsRestart) + return (root.pending === "hdmi" ? "HDMI-Ready" : "Battery") + " pending — restart to apply"; + if (root.pending === "hdmi") + return root.ext ? "Driving external display" : "HDMI-Ready — dGPU on demand"; + return root.active ? "Battery — dGPU awake" : "Battery — dGPU asleep"; + } + + function statRows() { + var d = root.full || {}; + return [ + { + "icon": "bolt", + "label": "Power draw", + "value": fmt1(d.powerW) + " W" + }, { + "icon": "temperature", + "label": "GPU temp", + "value": (d.tempC || 0) + "°C" + }, { + "icon": "activity", + "label": "GPU util", + "value": (d.util || 0) + "%" + }, { + "icon": "memory", + "label": "VRAM", + "value": fmt1(d.vramUsed) + " / " + fmt1(d.vramTotal) + " GB" + } + ]; + } + + Item { + id: panelContainer + anchors.fill: parent + + ColumnLayout { + id: panelCol + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + // Header: themed NVIDIA mark + title + Battery/HDMI toggle + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + Item { + id: headerMark + implicitWidth: Math.round(Style.fontSizeXXL * 1.7) + implicitHeight: Math.round(Style.fontSizeXXL * 1.2) + width: implicitWidth + height: implicitHeight + + Image { + id: headerEye + anchors.fill: parent + source: Qt.resolvedUrl("Assets/nvidia-eye.svg") + fillMode: Image.PreserveAspectFit + sourceSize.width: parent.width + sourceSize.height: parent.height + smooth: true + mipmap: true + visible: false + } + MultiEffect { + anchors.fill: headerEye + source: headerEye + colorization: 1.0 + colorizationColor: root.lit ? Color.mPrimary : Color.mOnSurface + Behavior on colorizationColor { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + NText { + text: (root.s && root.s.gpuName) ? root.s.gpuName : "NVIDIA dGPU" + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeL + color: Color.mOnSurface + } + + NText { + text: root.subtitle() + pointSize: Style.fontSizeS + color: root.needsRestart ? Color.mError : (root.pending === "hdmi" ? Color.mPrimary : Color.mOnSurfaceVariant) + } + } + + NToggle { + checked: root.pending === "hdmi" + onToggled: { + if (root.main) + root.main.toggle(); + } + } + } + + NDivider { + Layout.fillWidth: true + } + + // Mode context: external display + what is holding the dGPU awake + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + NIcon { + icon: "device-tv" + color: root.ext ? Color.mPrimary : Color.mOnSurfaceVariant + pointSize: Style.fontSizeM + } + NText { + text: "External display" + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + Layout.fillWidth: true + } + NText { + text: root.ext ? ((root.s.external || "?") + " · " + (root.s.externalName || "")) : "none" + color: root.ext ? Color.mPrimary : Color.mOnSurface + pointSize: Style.fontSizeS + font.weight: Style.fontWeightBold + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + visible: root.active + NIcon { + icon: "stack" + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeM + } + NText { + text: "GPU processes" + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + Layout.fillWidth: true + } + NText { + text: (root.full && root.full.heldBy && root.full.heldBy.length > 0) ? root.full.heldBy : "none reported" + color: Color.mOnSurface + pointSize: Style.fontSizeS + font.weight: Style.fontWeightBold + elide: Text.ElideRight + Layout.maximumWidth: 180 * Style.uiScaleRatio + } + } + } + + NDivider { + Layout.fillWidth: true + } + + // Live dGPU stats (dimmed while the dGPU is asleep) + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + opacity: root.active ? 1.0 : 0.4 + + Repeater { + model: root.statRows() + + delegate: RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NIcon { + icon: modelData.icon + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeM + } + NText { + text: modelData.label + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + Layout.fillWidth: true + } + NText { + text: modelData.value + color: Color.mOnSurface + pointSize: Style.fontSizeS + font.weight: Style.fontWeightBold + } + } + } + } + + // Apply banner — only when a mode change is pending + NDivider { + Layout.fillWidth: true + visible: root.needsRestart + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + visible: root.needsRestart + + NText { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: "AQ_DRM_DEVICES is read at session start." + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + NButton { + text: "Apply now (log out)" + fontSize: Style.fontSizeS + backgroundColor: Color.mError + textColor: Color.mOnError + onClicked: { + if (root.main) + root.main.apply(); + } + } + } + } + } +} diff --git a/nvidia-optimus/README.md b/nvidia-optimus/README.md new file mode 100644 index 000000000..0c8c2a77a --- /dev/null +++ b/nvidia-optimus/README.md @@ -0,0 +1,102 @@ +# NVIDIA Optimus — Noctalia plugin + +A bar widget + panel for **[Noctalia](https://github.com/noctalia-dev/noctalia-shell)** (Quickshell) +that monitors and controls an **NVIDIA Optimus dGPU on Hyprland**. + +The NVIDIA "eye" in your bar is **tinted to your theme** — accent colour when the dGPU is +awake or driving a display, neutral when it's asleep — so at a glance you know whether the +discrete GPU is burning battery. Click it for a panel with a **Battery ⇄ HDMI-Ready toggle**, +live power/temp/VRAM, and a list of exactly what's holding the GPU awake (including +container/root processes that `lsof` can't see). + +> **Why this exists:** on Hyprland, an NVIDIA Optimus dGPU often refuses to enter RTD3 +> runtime-suspend even when nothing is using it, quietly draining battery — while the HDMI +> port is frequently wired to the dGPU, so you *can't* just disable it. This plugin gives you +> a one-click way to switch between "let the dGPU sleep" and "I need the external display," +> and shows you the truth about its power state. + +## What it does + +- **Bar icon**: theme-tinted NVIDIA mark. Accent = dGPU awake, neutral = asleep. Hidden if no + NVIDIA dGPU is present. Hover for state + mode; click to open the panel. +- **Battery mode**: drops the dGPU from Hyprland's `AQ_DRM_DEVICES` **and** forces the session + onto mesa-only EGL, so the compositor stops pinning the dGPU — it can finally RTD3-suspend. + (Excluding it from `AQ_DRM_DEVICES` alone is *not* enough; Hyprland + hyprpaper still open the + nvidia nodes via EGL.) No external output in this mode. +- **HDMI-Ready mode**: adds the dGPU back so external displays wired to it work. +- **Live panel**: power draw, temperature, utilisation, VRAM, external-display readout, and the + full list of processes on the GPU (compute *and* graphics, with PID). +- **Zero-overhead monitoring**: the 2-second background poll is **sysfs-only** — it never runs + `nvidia-smi` or `lspci`, so it can never wake or hold-awake a sleeping dGPU. `nvidia-smi` runs + only while the panel is open, and only when the GPU is already awake. + +## Requirements + +- **Hyprland** (uses `AQ_DRM_DEVICES`, an aquamarine/Hyprland env var) +- **Noctalia** ≥ 4.0 (Quickshell) +- An **NVIDIA Optimus laptop** with the proprietary driver and `nvidia-drm.modeset=1` +- Any iGPU (Intel or AMD) — the plugin auto-detects both GPUs from sysfs +- `bash`, `python3`, `lsof`, and (for the panel's live stats) `nvidia-smi` + +## Install + +```bash +git clone https://github.com/joniler/noctalia-nvidia-optimus \ + ~/.config/noctalia/plugins/nvidia-optimus +``` + +Then in Noctalia: **Settings → Plugins**, enable **NVIDIA Optimus**, and add its widget to your +bar (**Settings → Bar**). No execute bit needed — the helper scripts run via `bash`. + +Optional keybind (Hyprland), matching the panel toggle: + +``` +bind = $mod CTRL SHIFT, G, exec, qs -c noctalia-shell ipc call nvidiaOptimus toggle +``` + +## Usage + +- **Click the eye** → panel. The header toggle flips **Battery ⇄ HDMI-Ready**. +- Mode changes are written to `~/.config/hypr/env.conf` (backed up next to it) and **apply on the + next login** — `AQ_DRM_DEVICES` is only read at session start. The panel shows a red + **"Apply now (log out)"** button while a change is pending. +- IPC: `qs -c noctalia-shell ipc call nvidiaOptimus toggle` (open panel) / + `... nvidiaOptimus mode` (flip mode). + +If your Hyprland env file lives somewhere other than `~/.config/hypr/env.conf`, set +`HYPR_ENV_FILE` in that file's own environment before the plugin's scripts run (or edit the +`env_file` helper in `scripts/gpu-detect.sh`). + +## How the modes look in `env.conf` + +```ini +# Battery — dGPU sleeps, no HDMI +env = AQ_DRM_DEVICES,/dev/dri/cardN # iGPU only +env = __EGL_VENDOR_LIBRARY_FILENAMES,/usr/share/glvnd/egl_vendor.d/50_mesa.json + +# HDMI-Ready — external outputs work, dGPU stays awake +env = AQ_DRM_DEVICES,/dev/dri/cardN:/dev/dri/cardM # iGPU + dGPU +``` + +Card nodes are resolved from **PCI addresses** on every switch, so this survives the +`/dev/dri/cardN` renumbering that happens when GPU modes change. + +## Notes & caveats + +- **Hyprland-only.** The mode toggle is built on `AQ_DRM_DEVICES`; it won't do anything on other + compositors. The monitoring half (icon + power state) is harmless everywhere but only useful + with an NVIDIA dGPU. +- **A running CUDA/GL client keeps the dGPU awake** — that's correct behaviour. The panel's + "GPU processes" row tells you which one (e.g. a stray ComfyUI/Ollama container). Battery mode + can't sleep a GPU that something is actively using. +- Opening the panel wakes the dGPU (to read `nvidia-smi`); the **bar icon** is the honest at-rest + indicator, since it never touches the GPU. + +## Credits + +- NVIDIA "eye" glyph derived from the [Papirus icon theme](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme) (GPLv3), recoloured to a single silhouette and tinted at runtime. +- Built for [Noctalia](https://github.com/noctalia-dev/noctalia-shell). + +## License + +MIT © Jon Iler ([@joniler](https://github.com/joniler)) diff --git a/nvidia-optimus/Settings.qml b/nvidia-optimus/Settings.qml new file mode 100644 index 000000000..0f57e7367 --- /dev/null +++ b/nvidia-optimus/Settings.qml @@ -0,0 +1,43 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +// Settings view for the NVIDIA Optimus plugin. +Item { + id: root + + property var pluginApi: null + + implicitHeight: col.implicitHeight + + ColumnLayout { + id: col + anchors.left: parent.left + anchors.right: parent.right + spacing: Style.marginM + + NText { + text: "NVIDIA Optimus" + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeL + color: Color.mOnSurface + } + + NText { + Layout.fillWidth: true + wrapMode: Text.WordWrap + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + text: "The bar toggle flips Hyprland's AQ_DRM_DEVICES between Battery (iGPU only — the NVIDIA dGPU runtime-suspends, no HDMI) and HDMI-Ready (iGPU + dGPU — external outputs work). It edits ~/.config/hypr/env.conf (backed up alongside) and applies on the next login. The bar icon is theme-tinted: accent when the dGPU is awake, neutral when asleep. Stats poll every 2s from a bundled sysfs-only script that never wakes the GPU; nvidia-smi runs only while this panel is open, and only when the GPU is already awake." + } + + NText { + Layout.fillWidth: true + wrapMode: Text.WordWrap + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + text: "Requires: Hyprland (aquamarine), an NVIDIA Optimus laptop with the proprietary driver + nvidia-drm.modeset=1. Override the env file with $HYPR_ENV_FILE if yours lives elsewhere." + } + } +} diff --git a/nvidia-optimus/manifest.json b/nvidia-optimus/manifest.json new file mode 100644 index 000000000..d23163c82 --- /dev/null +++ b/nvidia-optimus/manifest.json @@ -0,0 +1,26 @@ +{ + "id": "nvidia-optimus", + "name": "NVIDIA Optimus", + "version": "1.0.0", + "minNoctaliaVersion": "4.0.0", + "author": "joniler", + "license": "MIT", + "repository": "https://github.com/noctalia-dev/noctalia-plugins", + "description": "Monitor and control an NVIDIA Optimus dGPU on Hyprland — toggle Battery (dGPU sleeps) vs HDMI-Ready, see live power/VRAM, and list what is holding the GPU awake.", + "tags": ["Bar", "Panel", "System", "GPU", "Hyprland", "NVIDIA"], + "entryPoints": { + "main": "Main.qml", + "barWidget": "BarWidget.qml", + "panel": "Panel.qml", + "settings": "Settings.qml" + }, + "dependencies": { + "plugins": [] + }, + "metadata": { + "panel": { + "defaultPosition": "right" + }, + "defaultSettings": {} + } +} diff --git a/nvidia-optimus/preview.png b/nvidia-optimus/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..6d088b2352b6a0b179a1f5884533f9e68a1de0b8 GIT binary patch literal 44765 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/dev/null) || continue # cached attr, does not wake device + case "$cls" in + 0x0300*|0x0302*) ;; # VGA or 3D controller + *) continue ;; + esac + ven=$(cat "$d/vendor" 2>/dev/null) + pci=$(basename "$d") + case "$ven" in + 0x10de) [ -z "$DGPU_PCI" ] && DGPU_PCI="$pci" ;; # NVIDIA + 0x8086|0x1002) [ -z "$IGPU_PCI" ] && IGPU_PCI="$pci" ;; # Intel / AMD + esac + done +} + +# Resolve the /dev/dri/cardN node for a PCI address (cardN numbering is not stable). +card_for() { # $1=pci -> cardN + local c p + for c in /dev/dri/card[0-9]*; do + p=$(basename "$(readlink -f "/sys/class/drm/$(basename "$c")/device" 2>/dev/null)") + [ "$p" = "$1" ] && { basename "$c"; return; } + done +} + +# Marketing name of the dGPU WITHOUT waking it: reads the device ID from sysfs and +# resolves it against the pci.ids database (plain file reads). Falls back to lspci +# (a one-time config-space read) only if pci.ids is unavailable. +gpu_name() { # uses $DGPU_PCI + local vid did f raw="" + vid=$(cat "/sys/bus/pci/devices/$DGPU_PCI/vendor" 2>/dev/null); vid=${vid#0x} + did=$(cat "/sys/bus/pci/devices/$DGPU_PCI/device" 2>/dev/null); did=${did#0x} + for f in /usr/share/hwdata/pci.ids /usr/share/misc/pci.ids; do + [ -r "$f" ] || continue + raw=$(awk -v v="$vid" -v d="$did" ' + /^[0-9a-fA-F]{4} / { invend = ($1 == v) } + invend && index($0, "\t" d " ") == 1 { sub(/^\t[0-9a-fA-F]{4} /, ""); print; exit } + ' "$f") + [ -n "$raw" ] && break + done + [ -z "$raw" ] && raw=$(lspci -s "$DGPU_PCI" 2>/dev/null | sed -E 's/^.*: //') + # Prefer the bracketed marketing name, e.g. "GB205M [GeForce RTX 5070 Ti Mobile]". + case "$raw" in *\[*\]*) raw=$(printf '%s' "$raw" | sed -E 's/.*\[([^]]*)\].*/\1/') ;; esac + raw=$(printf '%s' "$raw" | sed -E 's/GeForce +//; s/ +\(rev.*//; s/ +$//') + [ -n "$raw" ] && printf '%s' "$raw" || printf 'NVIDIA dGPU' +} + +# Path of the mesa EGL vendor JSON (used to force mesa-only EGL in Battery mode). +mesa_egl_json() { + local f + for f in /usr/share/glvnd/egl_vendor.d/*mesa*.json \ + /usr/local/share/glvnd/egl_vendor.d/*mesa*.json; do + [ -e "$f" ] && { echo "$f"; return; } + done + echo "/usr/share/glvnd/egl_vendor.d/50_mesa.json" # sensible default +} + +# Hyprland env file that holds the AQ_DRM_DEVICES / EGL lines. +env_file() { echo "${HYPR_ENV_FILE:-$HOME/.config/hypr/env.conf}"; } diff --git a/nvidia-optimus/scripts/gpu-stat b/nvidia-optimus/scripts/gpu-stat new file mode 100755 index 000000000..090b4bb1d --- /dev/null +++ b/nvidia-optimus/scripts/gpu-stat @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# gpu-stat — emit JSON describing the NVIDIA dGPU power state + current graphics mode. +# Consumed by the noctalia nvidia-optimus plugin (BarWidget + Panel). +# +# Default (background) mode is SYSFS-ONLY and never runs nvidia-smi or lspci, so it +# can be polled every couple of seconds without ever waking — or holding awake — a +# sleeping dGPU. Pass "full" (only while the panel is open) to add nvidia-smi +# power/temp/util/VRAM and the list of processes holding a context (this DOES wake +# the GPU, so the plugin only requests it when the GPU is already awake). +set -uo pipefail + +SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=gpu-detect.sh +. "$SELF_DIR/gpu-detect.sh" + +MODE_ARG="${1:-}" +detect_gpus +ENVFILE="$(env_file)" + +# No NVIDIA dGPU present — emit a sentinel so the UI can hide/disable itself. +if [ -z "${DGPU_PCI:-}" ]; then + echo '{"runtime":"absent","mode":"battery","pending":"battery","external":"none","externalName":"","gpuName":"no dGPU","powerW":0,"tempC":0,"util":0,"vramUsed":0,"vramTotal":0,"heldBy":""}' + exit 0 +fi + +DCARD=$(card_for "$DGPU_PCI") +ICARD=$(card_for "${IGPU_PCI:-}") +DREND=$(basename "$(readlink -f "/dev/dri/by-path/pci-$DGPU_PCI-render" 2>/dev/null)" 2>/dev/null) + +# dGPU runtime power state — reading this sysfs file does NOT wake the GPU. +runtime=$(cat "/sys/bus/pci/devices/$DGPU_PCI/power/runtime_status" 2>/dev/null || echo unknown) + +# Marketing name — resolved wake-free from sysfs + pci.ids (see gpu_name), and +# cached so even that lookup only runs once per session. +NAMECACHE="${XDG_RUNTIME_DIR:-/tmp}/nvidia-optimus.name" +if [ -s "$NAMECACHE" ]; then + gpuName=$(cat "$NAMECACHE") +else + gpuName=$(gpu_name) + printf '%s' "$gpuName" >"$NAMECACHE" 2>/dev/null || true +fi + +# External display: first connected non-eDP connector, and which GPU drives it. +external="none"; externalName="" +for st in /sys/class/drm/card*-*/status; do + conn=$(basename "$(dirname "$st")") + name=${conn#*-} + case "$name" in eDP*) continue;; esac + cardn=${conn%%-*} + # Reading a connector's status can force a probe that RESUMES the GPU. Skip the + # dGPU's own connectors while it is asleep, so monitoring never wakes it. (In HDMI + # mode the dGPU is already awake, so its connectors are read normally.) + [ "$cardn" = "${DCARD:-}" ] && [ "$runtime" != active ] && continue + [ "$(cat "$st" 2>/dev/null)" = connected ] || continue + pci=$(basename "$(readlink -f "/sys/class/drm/$cardn/device" 2>/dev/null)") + case "$pci" in + "$DGPU_PCI") external=NVIDIA ;; + "${IGPU_PCI:-none}") external=iGPU ;; + *) external="?" ;; + esac + externalName=$name + break +done + +# Live mode: is the compositor currently holding the dGPU render node? (lsof is safe.) +mode=battery +if [ -n "${DREND:-}" ] && lsof -t "/dev/dri/$DREND" 2>/dev/null \ + | while read -r pid; do ps -o comm= -p "$pid" 2>/dev/null; done \ + | grep -qiE '^(Hyprland|hyprpaper|quickshell)$'; then + mode=hdmi +fi + +# Pending mode: what env.conf will apply on the next Hyprland start. +pending=battery +grep -qE '^env = AQ_DRM_DEVICES,.*:' "$ENVFILE" 2>/dev/null && pending=hdmi + +# --- detail (nvidia-smi) — ONLY in full mode and ONLY when the GPU is awake --- +powerW=0; tempC=0; util=0; vramUsed=0; vramTotal=0; heldBy="" +if [ "$MODE_ARG" = full ] && [ "$runtime" = active ] && command -v nvidia-smi >/dev/null 2>&1; then + read -r pw tp ut vu vt < <(nvidia-smi \ + --query-gpu=power.draw,temperature.gpu,utilization.gpu,memory.used,memory.total \ + --format=csv,noheader,nounits 2>/dev/null | head -1 | tr -d ',') + powerW=${pw:-0}; tempC=${tp:-0}; util=${ut:-0} + vramUsed=$(awk "BEGIN{printf \"%.2f\", ${vu:-0}/1024}") + vramTotal=$(awk "BEGIN{printf \"%.2f\", ${vt:-0}/1024}") + # Every process with a context on the GPU — compute (C) AND graphics (G), + # including container/root procs that user-level lsof can't see. + heldBy=$(nvidia-smi 2>/dev/null | awk ' + /Processes:/ { p = 1 } + p && $5 ~ /^[0-9]+$/ && ($6 == "C" || $6 == "G") { + name = $7; for (i = 8; i <= NF - 2; i++) name = name " " $i; sub(/.*\//, "", name); + printf "%s%s:%s %s(%s)", (c++ ? ", " : ""), name, $5, $6, $(NF - 1); + }') +fi + +python3 - "$runtime" "$mode" "$pending" "$external" "$externalName" "$gpuName" \ + "$powerW" "$tempC" "$util" "$vramUsed" "$vramTotal" "$heldBy" <<'PY' +import json, sys +keys = ["runtime","mode","pending","external","externalName","gpuName", + "powerW","tempC","util","vramUsed","vramTotal","heldBy"] +d = dict(zip(keys, sys.argv[1:])) +for k in ("powerW","tempC","util","vramUsed","vramTotal"): + try: + d[k] = float(d[k]) + except Exception: + d[k] = 0 +print(json.dumps(d)) +PY diff --git a/nvidia-optimus/scripts/gpu-toggle b/nvidia-optimus/scripts/gpu-toggle new file mode 100755 index 000000000..2d3b335df --- /dev/null +++ b/nvidia-optimus/scripts/gpu-toggle @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# gpu-toggle — switch Hyprland's graphics between: +# battery iGPU only → the NVIDIA dGPU runtime-suspends (best battery, no HDMI) +# hdmi iGPU+dGPU → external outputs on the dGPU work; dGPU stays awake +# +# Battery mode also forces the session onto mesa-only EGL, because excluding the dGPU +# from AQ_DRM_DEVICES is not enough on Hyprland: the compositor + hyprpaper otherwise +# still open the nvidia nodes via EGL, keeping the dGPU out of RTD3 suspend. +# +# Card nodes are resolved from PCI addresses each run, so this is immune to +# /dev/dri/cardN renumbering. Changes apply on the next Hyprland start (log out/in). +# Usage: gpu-toggle [toggle|battery|hdmi|status|apply|info] +set -uo pipefail + +SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=gpu-detect.sh +. "$SELF_DIR/gpu-detect.sh" + +detect_gpus +ENVFILE="$(env_file)" +ICON="$SELF_DIR/../Assets/nvidia-eye.svg" + +notify() { notify-send -a "NVIDIA Optimus" -i "$ICON" "$@" 2>/dev/null || true; } + +ICARD=$(card_for "${IGPU_PCI:-}") +DCARD=$(card_for "${DGPU_PCI:-}") + +current() { grep -qE '^env = AQ_DRM_DEVICES,.*:' "$ENVFILE" 2>/dev/null && echo hdmi || echo battery; } + +set_mode() { # $1 = battery|hdmi + [ -f "$ENVFILE" ] || { notify -u critical "NVIDIA Optimus" "Hyprland env file not found: $ENVFILE"; exit 1; } + [ -n "${ICARD:-}" ] || { notify -u critical "NVIDIA Optimus" "Could not resolve the iGPU card node."; exit 1; } + local val + if [ "$1" = hdmi ]; then + [ -n "${DCARD:-}" ] || { notify -u critical "NVIDIA Optimus" "Could not resolve the NVIDIA dGPU card node."; exit 1; } + val="/dev/dri/$ICARD:/dev/dri/$DCARD" + else + val="/dev/dri/$ICARD" + fi + + cp -f "$ENVFILE" "$ENVFILE.nvidia-optimus.bak" + if grep -qE '^env = AQ_DRM_DEVICES,' "$ENVFILE"; then + sed -i -E "s|^env = AQ_DRM_DEVICES,.*|env = AQ_DRM_DEVICES,$val|" "$ENVFILE" + else + printf 'env = AQ_DRM_DEVICES,%s\n' "$val" >>"$ENVFILE" + fi + + # Manage the mesa-only EGL restriction (Battery mode only). + local egl="env = __EGL_VENDOR_LIBRARY_FILENAMES,$(mesa_egl_json)" + sed -i -E '/^env = __EGL_VENDOR_LIBRARY_FILENAMES,/d' "$ENVFILE" + if [ "$1" = battery ]; then + sed -i "/^env = AQ_DRM_DEVICES,/a $egl" "$ENVFILE" + fi +} + +banner() { # $1 = battery|hdmi + if [ "$1" = hdmi ]; then + notify "NVIDIA Optimus → HDMI-Ready" "dGPU will drive external displays. Log out & back in to apply." + else + notify "NVIDIA Optimus → Battery" "dGPU will sleep when idle (no HDMI). Log out & back in to apply." + fi +} + +case "${1:-toggle}" in + battery) set_mode battery; banner battery ;; + hdmi) set_mode hdmi; banner hdmi ;; + toggle|"") if [ "$(current)" = hdmi ]; then set_mode battery; banner battery; else set_mode hdmi; banner hdmi; fi ;; + status) current ;; + apply) notify "NVIDIA Optimus" "Restarting the session to apply…"; hyprctl dispatch exit ;; + info) notify "NVIDIA Optimus — pending: $(current)" "dGPU=${DCARD:-?} iGPU=${ICARD:-?}" ;; + *) echo "usage: gpu-toggle [toggle|battery|hdmi|status|apply|info]" >&2; exit 1 ;; +esac From b851aff5ae82c18673c16ef96dd602ed61ea74cf Mon Sep 17 00:00:00 2001 From: Jon Iler Date: Wed, 1 Jul 2026 09:31:50 -0600 Subject: [PATCH 2/2] =?UTF-8?q?nvidia-optimus:=20i18n=20=E2=80=94=20route?= =?UTF-8?q?=20all=20user-facing=20text=20through=20pluginApi.tr()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses code-quality review: adds i18n/en.json and replaces hardcoded strings in Panel/Settings/BarWidget with pluginApi?.tr() lookups. --- nvidia-optimus/BarWidget.qml | 11 ++++++++--- nvidia-optimus/Panel.qml | 36 +++++++++++++++++++++--------------- nvidia-optimus/Settings.qml | 6 +++--- nvidia-optimus/i18n/en.json | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 nvidia-optimus/i18n/en.json diff --git a/nvidia-optimus/BarWidget.qml b/nvidia-optimus/BarWidget.qml index 6c32f6066..2a41f8e46 100644 --- a/nvidia-optimus/BarWidget.qml +++ b/nvidia-optimus/BarWidget.qml @@ -30,10 +30,15 @@ NIconButton { icon: "" // hidden: we draw the tinted NVIDIA mark below tooltipText: { + var api = root.pluginApi; if (root.ext) - return "dGPU — driving external display"; - var m = (root.md === "hdmi") ? "HDMI-Ready" : "Battery"; - return "dGPU — " + (root.rt === "active" ? "active" : "asleep") + " (" + m + " mode)"; + return api?.tr("bar.tooltip-external"); + var state = (root.rt === "active") ? api?.tr("common.state-active") : api?.tr("common.state-asleep"); + var mode = (root.md === "hdmi") ? api?.tr("common.mode-hdmi") : api?.tr("common.mode-battery"); + return api?.tr("bar.tooltip", { + "state": state, + "mode": mode + }); } tooltipDirection: BarService.getTooltipDirection(screen?.name) baseSize: Style.getCapsuleHeightForScreen(screen?.name) diff --git a/nvidia-optimus/Panel.qml b/nvidia-optimus/Panel.qml index 99f0b5387..d0ca4e926 100644 --- a/nvidia-optimus/Panel.qml +++ b/nvidia-optimus/Panel.qml @@ -63,31 +63,37 @@ Item { } function subtitle() { - if (root.needsRestart) - return (root.pending === "hdmi" ? "HDMI-Ready" : "Battery") + " pending — restart to apply"; + var api = root.pluginApi; + if (root.needsRestart) { + var m = (root.pending === "hdmi") ? api?.tr("common.mode-hdmi") : api?.tr("common.mode-battery"); + return api?.tr("panel.subtitle-pending", { + "mode": m + }); + } if (root.pending === "hdmi") - return root.ext ? "Driving external display" : "HDMI-Ready — dGPU on demand"; - return root.active ? "Battery — dGPU awake" : "Battery — dGPU asleep"; + return root.ext ? api?.tr("panel.subtitle-driving") : api?.tr("panel.subtitle-hdmi"); + return root.active ? api?.tr("panel.subtitle-battery-awake") : api?.tr("panel.subtitle-battery-asleep"); } function statRows() { var d = root.full || {}; + var api = root.pluginApi; return [ { "icon": "bolt", - "label": "Power draw", + "label": api?.tr("panel.stat-power"), "value": fmt1(d.powerW) + " W" }, { "icon": "temperature", - "label": "GPU temp", + "label": api?.tr("panel.stat-temp"), "value": (d.tempC || 0) + "°C" }, { "icon": "activity", - "label": "GPU util", + "label": api?.tr("panel.stat-util"), "value": (d.util || 0) + "%" }, { "icon": "memory", - "label": "VRAM", + "label": api?.tr("panel.stat-vram"), "value": fmt1(d.vramUsed) + " / " + fmt1(d.vramTotal) + " GB" } ]; @@ -144,7 +150,7 @@ Item { spacing: 0 NText { - text: (root.s && root.s.gpuName) ? root.s.gpuName : "NVIDIA dGPU" + text: (root.s && root.s.gpuName) ? root.s.gpuName : root.pluginApi?.tr("panel.title-fallback") font.weight: Style.fontWeightBold pointSize: Style.fontSizeL color: Color.mOnSurface @@ -184,13 +190,13 @@ Item { pointSize: Style.fontSizeM } NText { - text: "External display" + text: root.pluginApi?.tr("panel.external") color: Color.mOnSurfaceVariant pointSize: Style.fontSizeS Layout.fillWidth: true } NText { - text: root.ext ? ((root.s.external || "?") + " · " + (root.s.externalName || "")) : "none" + text: root.ext ? ((root.s.external || "?") + " · " + (root.s.externalName || "")) : root.pluginApi?.tr("common.none") color: root.ext ? Color.mPrimary : Color.mOnSurface pointSize: Style.fontSizeS font.weight: Style.fontWeightBold @@ -207,13 +213,13 @@ Item { pointSize: Style.fontSizeM } NText { - text: "GPU processes" + text: root.pluginApi?.tr("panel.gpu-processes") color: Color.mOnSurfaceVariant pointSize: Style.fontSizeS Layout.fillWidth: true } NText { - text: (root.full && root.full.heldBy && root.full.heldBy.length > 0) ? root.full.heldBy : "none reported" + text: (root.full && root.full.heldBy && root.full.heldBy.length > 0) ? root.full.heldBy : root.pluginApi?.tr("panel.none-reported") color: Color.mOnSurface pointSize: Style.fontSizeS font.weight: Style.fontWeightBold @@ -275,13 +281,13 @@ Item { NText { Layout.fillWidth: true wrapMode: Text.WordWrap - text: "AQ_DRM_DEVICES is read at session start." + text: root.pluginApi?.tr("panel.apply-note") pointSize: Style.fontSizeXS color: Color.mOnSurfaceVariant } NButton { - text: "Apply now (log out)" + text: root.pluginApi?.tr("panel.apply-button") fontSize: Style.fontSizeS backgroundColor: Color.mError textColor: Color.mOnError diff --git a/nvidia-optimus/Settings.qml b/nvidia-optimus/Settings.qml index 0f57e7367..d420041af 100644 --- a/nvidia-optimus/Settings.qml +++ b/nvidia-optimus/Settings.qml @@ -18,7 +18,7 @@ Item { spacing: Style.marginM NText { - text: "NVIDIA Optimus" + text: root.pluginApi?.tr("settings.title") font.weight: Style.fontWeightBold pointSize: Style.fontSizeL color: Color.mOnSurface @@ -29,7 +29,7 @@ Item { wrapMode: Text.WordWrap pointSize: Style.fontSizeS color: Color.mOnSurfaceVariant - text: "The bar toggle flips Hyprland's AQ_DRM_DEVICES between Battery (iGPU only — the NVIDIA dGPU runtime-suspends, no HDMI) and HDMI-Ready (iGPU + dGPU — external outputs work). It edits ~/.config/hypr/env.conf (backed up alongside) and applies on the next login. The bar icon is theme-tinted: accent when the dGPU is awake, neutral when asleep. Stats poll every 2s from a bundled sysfs-only script that never wakes the GPU; nvidia-smi runs only while this panel is open, and only when the GPU is already awake." + text: root.pluginApi?.tr("settings.description") } NText { @@ -37,7 +37,7 @@ Item { wrapMode: Text.WordWrap pointSize: Style.fontSizeXS color: Color.mOnSurfaceVariant - text: "Requires: Hyprland (aquamarine), an NVIDIA Optimus laptop with the proprietary driver + nvidia-drm.modeset=1. Override the env file with $HYPR_ENV_FILE if yours lives elsewhere." + text: root.pluginApi?.tr("settings.requirements") } } } diff --git a/nvidia-optimus/i18n/en.json b/nvidia-optimus/i18n/en.json new file mode 100644 index 000000000..08786d87e --- /dev/null +++ b/nvidia-optimus/i18n/en.json @@ -0,0 +1,35 @@ +{ + "common": { + "state-active": "active", + "state-asleep": "asleep", + "mode-hdmi": "HDMI-Ready", + "mode-battery": "Battery", + "none": "none" + }, + "bar": { + "tooltip-external": "dGPU — driving external display", + "tooltip": "dGPU — {state} ({mode} mode)" + }, + "panel": { + "title-fallback": "NVIDIA dGPU", + "subtitle-pending": "{mode} pending — restart to apply", + "subtitle-driving": "Driving external display", + "subtitle-hdmi": "HDMI-Ready — dGPU on demand", + "subtitle-battery-awake": "Battery — dGPU awake", + "subtitle-battery-asleep": "Battery — dGPU asleep", + "external": "External display", + "gpu-processes": "GPU processes", + "none-reported": "none reported", + "stat-power": "Power draw", + "stat-temp": "GPU temp", + "stat-util": "GPU util", + "stat-vram": "VRAM", + "apply-note": "AQ_DRM_DEVICES is read at session start.", + "apply-button": "Apply now (log out)" + }, + "settings": { + "title": "NVIDIA Optimus", + "description": "The bar toggle flips Hyprland's AQ_DRM_DEVICES between Battery (iGPU only — the NVIDIA dGPU runtime-suspends, no HDMI) and HDMI-Ready (iGPU + dGPU — external outputs work). It edits ~/.config/hypr/env.conf (backed up alongside) and applies on the next login. The bar icon is theme-tinted: accent when the dGPU is awake, neutral when asleep. Stats poll every 2s from a bundled sysfs-only script that never wakes the GPU; nvidia-smi runs only while this panel is open, and only when the GPU is already awake.", + "requirements": "Requires: Hyprland (aquamarine), an NVIDIA Optimus laptop with the proprietary driver + nvidia-drm.modeset=1. Override the env file with $HYPR_ENV_FILE if yours lives elsewhere." + } +}