From 2b9edcb5893d4cee746d06ee0a11ff6b4d50967b Mon Sep 17 00:00:00 2001 From: rope Date: Tue, 26 May 2026 17:06:46 +0100 Subject: [PATCH] quickshell: add battery dropdown with power draw and profiles Click battery to see charge %, wattage draw, and switch between power-saver/balanced/performance profiles via power-profiles-daemon. Co-Authored-By: Claude Opus 4.6 --- settings/hyprland.nix | 176 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 169 insertions(+), 7 deletions(-) diff --git a/settings/hyprland.nix b/settings/hyprland.nix index 6ae2c56..3edffd3 100644 --- a/settings/hyprland.nix +++ b/settings/hyprland.nix @@ -677,6 +677,8 @@ in property int batteryLevel: 0 property bool charging: false property string batteryIcon: "\u{f008e}" + property real powerDraw: 0.0 + property string powerProfile: "balanced" function updateIcon() { if (charging) { batteryIcon = "\u{f0084}"; return; } @@ -689,30 +691,52 @@ in } Timer { - interval: 10000 + interval: 5000 running: true repeat: true triggeredOnStart: true - onTriggered: batteryProc.running = true + onTriggered: { batteryProc.running = true; profileProc.running = true; } } Process { id: batteryProc - command: ["sh", "-c", "cat /sys/class/power_supply/BAT0/capacity; cat /sys/class/power_supply/BAT0/status"] + command: ["sh", "-c", "cat /sys/class/power_supply/BAT0/capacity; cat /sys/class/power_supply/BAT0/status; cat /sys/class/power_supply/BAT0/power_now 2>/dev/null || echo 0"] stdout: SplitParser { + property int lineNum: 0 onRead: data => { let trimmed = data.trim(); - let num = parseInt(trimmed); - if (!isNaN(num) && num >= 0 && num <= 100) { - batteryWidget.batteryLevel = num; - } else if (trimmed.length > 0) { + lineNum++; + if (lineNum === 1) { + let num = parseInt(trimmed); + if (!isNaN(num)) batteryWidget.batteryLevel = num; + } else if (lineNum === 2) { batteryWidget.charging = (trimmed === "Charging"); + } else if (lineNum === 3) { + let uw = parseInt(trimmed); + if (!isNaN(uw)) batteryWidget.powerDraw = uw / 1000000.0; + lineNum = 0; } batteryWidget.updateIcon(); } } } + Process { + id: profileProc + command: ["${pkgs.power-profiles-daemon}/bin/powerprofilesctl", "get"] + stdout: SplitParser { + onRead: data => { + batteryWidget.powerProfile = data.trim(); + } + } + } + + Process { + id: setProfileProc + property string target: "balanced" + command: ["${pkgs.power-profiles-daemon}/bin/powerprofilesctl", "set", target] + } + Row { anchors.verticalCenter: parent.verticalCenter spacing: 4 @@ -737,6 +761,22 @@ in font.pixelSize: 14 } } + + MouseArea { + anchors.fill: parent + onClicked: { + if (batteryDropdown.justDismissed) return; + if (batteryDropdown.visible) { + batteryDropdown.visible = false; + } else { + batteryProc.running = true; + profileProc.running = true; + let pos = batteryWidget.mapToItem(bar.contentItem, batteryWidget.width / 2, 0); + batteryDropdown.dropdownX = pos.x; + batteryDropdown.visible = true; + } + } + } } ''} @@ -1128,6 +1168,128 @@ in } } + ${lib.optionalString isMacbook '' + // Battery dropdown + BarDropdown { + id: batteryDropdown + fullWidth: batteryDropdownCol.width + 24 + fullHeight: batteryDropdownCol.height + 16 + + Column { + id: batteryDropdownCol + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 10 + width: 200 + spacing: 8 + + // Battery status + Row { + width: parent.width + spacing: 8 + + Text { + text: batteryWidget.batteryIcon + color: "#${c.base05}" + font.family: "FiraMono Nerd Font" + font.pixelSize: 18 + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + Text { + text: batteryWidget.batteryLevel + "%" + (batteryWidget.charging ? " — Charging" : "") + color: "#${c.base05}" + font.family: "FiraMono Nerd Font" + font.pixelSize: 13 + font.weight: Font.Medium + } + Text { + text: batteryWidget.powerDraw.toFixed(1) + " W" + color: "#${c.base04}" + font.family: "FiraMono Nerd Font" + font.pixelSize: 11 + } + } + } + + // Separator + Rectangle { + width: parent.width - 10 + anchors.horizontalCenter: parent.horizontalCenter + height: 1 + color: "#${c.base03}" + } + + // Power profile label + Text { + text: "Power Profile" + color: "#${c.base03}" + font.family: "FiraMono Nerd Font" + font.pixelSize: 11 + } + + // Profile buttons + Row { + width: parent.width + spacing: 4 + + Repeater { + model: [ + { name: "power-saver", label: "\u{f0425}", tip: "Saver" }, + { name: "balanced", label: "\u{f0376}", tip: "Balanced" }, + { name: "performance", label: "\u{f0e0e}", tip: "Performance" } + ] + + Rectangle { + required property var modelData + width: (parent.width - 8) / 3 + height: 36 + radius: 6 + color: batteryWidget.powerProfile === modelData.name + ? "#${c.base02}" : profMouse.containsMouse + ? "#${c.base01}" : "transparent" + border.width: batteryWidget.powerProfile === modelData.name ? 1 : 0 + border.color: "#${c.base03}" + + Column { + anchors.centerIn: parent + spacing: 1 + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: modelData.label + color: batteryWidget.powerProfile === modelData.name + ? "#${c.base0D}" : "#${c.base05}" + font.family: "FiraMono Nerd Font" + font.pixelSize: 14 + } + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: modelData.tip + color: "#${c.base04}" + font.family: "FiraMono Nerd Font" + font.pixelSize: 9 + } + } + + MouseArea { + id: profMouse + anchors.fill: parent + hoverEnabled: true + onClicked: { + setProfileProc.target = modelData.name; + setProfileProc.running = true; + batteryWidget.powerProfile = modelData.name; + } + } + } + } + } + } + } + ''} + // Calendar popup BarDropdown { id: calPopup