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 <noreply@anthropic.com>
This commit is contained in:
rope 2026-05-26 17:06:46 +01:00
parent d474f87df5
commit 2b9edcb589

View file

@ -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();
lineNum++;
if (lineNum === 1) {
let num = parseInt(trimmed);
if (!isNaN(num) && num >= 0 && num <= 100) {
batteryWidget.batteryLevel = num;
} else if (trimmed.length > 0) {
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