From 740f4e2c829e46b929f274dae7ab999411600a22 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 6 Jun 2026 17:53:07 +0200 Subject: [PATCH] Fix power panel discharge state --- bin/omarchy-battery-status | 2 +- shell/plugins/panels/power/Model.js | 6 ++++-- shell/plugins/panels/power/Panel.qml | 29 ++++++++++++++++------------ test/shell.d/power-test.sh | 2 ++ 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/bin/omarchy-battery-status b/bin/omarchy-battery-status index c2eaf249..ac81e9d6 100755 --- a/bin/omarchy-battery-status +++ b/bin/omarchy-battery-status @@ -58,7 +58,7 @@ fi charge_holding=false if [[ $ac_online == "true" && -n $threshold_end ]]; then - if [[ $state == "discharging" || $state == "pending-charge" ]]; then + if [[ $state == "pending-charge" ]]; then charge_holding=true elif [[ $state == "fully-charged" ]] && (( percentage < 99 )); then charge_holding=true diff --git a/shell/plugins/panels/power/Model.js b/shell/plugins/panels/power/Model.js index 11901fe8..553d9aef 100644 --- a/shell/plugins/panels/power/Model.js +++ b/shell/plugins/panels/power/Model.js @@ -55,7 +55,8 @@ function chargeThresholdActive(device, onBattery, states) { if (!(d && d.isPresent && !onBattery)) return false var fraction = batteryFraction(d) - if (d.state === s.Discharging || d.state === s.PendingCharge) return true + if (d.state === s.Discharging) return false + if (d.state === s.PendingCharge) return true if (d.state === s.FullyCharged && fraction < 0.99) return true if (d.state !== s.Charging || fraction >= 0.99) return false @@ -74,6 +75,7 @@ function batteryIcon(device, onBattery, states) { if (threshold) return defaultIcons[index] if (d.state === states.FullyCharged) return "󰂅" if (d.state === states.Charging) return chargingIcons[index] + if (d.state === states.Discharging) return defaultIcons[index] if (!onBattery) return "" return defaultIcons[index] } @@ -85,7 +87,7 @@ function modeLabel(device, onBattery, states) { var percentage = d.isPresent ? d.percentage : 0 if (chargeThresholdActive(d, onBattery, states)) return "Threshold" if (!onBattery && percentage >= 1) return "Fully charged" - if (onBattery) return "On battery" + if (onBattery || d.state === states.Discharging) return "On battery" return "Charging" } diff --git a/shell/plugins/panels/power/Panel.qml b/shell/plugins/panels/power/Panel.qml index 5d8869fb..3062eb5e 100644 --- a/shell/plugins/panels/power/Panel.qml +++ b/shell/plugins/panels/power/Panel.qml @@ -41,12 +41,12 @@ Panel { function batteryIcon() { var device = UPower.displayDevice - return Model.batteryIcon(device, UPower.onBattery, upowerStates()) + return Model.batteryIcon(device, root.discharging, upowerStates()) } function modeLabel() { var device = UPower.displayDevice - return Model.modeLabel(device, UPower.onBattery, upowerStates()) + return Model.modeLabel(device, root.discharging, upowerStates()) } function profileIcon(name) { @@ -57,11 +57,15 @@ Panel { var device = UPower.displayDevice return device && device.isPresent && device.state === UPowerDeviceState.FullyCharged && !root.chargeThresholdActive } + readonly property bool discharging: { + var device = UPower.displayDevice + return !!(device && device.isPresent && (UPower.onBattery || device.state === UPowerDeviceState.Discharging)) + } readonly property bool chargeThresholdActive: { var device = UPower.displayDevice - return Model.chargeThresholdActive(device, UPower.onBattery, upowerStates()) + return Model.chargeThresholdActive(device, root.discharging, upowerStates()) } - readonly property bool batteryFull: fullyCharged || (!UPower.onBattery && batteryFraction >= 1) + readonly property bool batteryFull: fullyCharged || (!root.discharging && batteryFraction >= 1) readonly property bool batteryFlowIdle: batteryFull || chargeThresholdActive // 0..1 charge level, used by the visual progress bar. @@ -70,14 +74,12 @@ Panel { return Model.batteryFraction(d) } - readonly property bool batteryLow: UPower.onBattery && batteryFraction > 0 && batteryFraction <= 0.2 readonly property bool charging: { var d = UPower.displayDevice return d && d.isPresent && d.state === UPowerDeviceState.Charging && !root.chargeThresholdActive } readonly property color batteryFillColor: { - if (batteryLow) return Color.urgent return root.bar ? root.bar.foreground : Color.foreground } @@ -111,7 +113,7 @@ Panel { readonly property var activePhrases: { if (fullyCharged) return [] if (charging) return chargingPhrases - if (UPower.onBattery || chargeThresholdActive) return onBatteryPhrases + if (discharging) return onBatteryPhrases return [] } readonly property bool rotatingPhrases: activePhrases.length > 0 @@ -254,7 +256,6 @@ Panel { text: root.batteryIcon() fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27) fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1 - active: root.batteryPresent && UPower.displayDevice.percentage <= 0.2 && UPower.onBattery tooltipText: "" onPressed: function(b) { if (root.batteryPresent) root.toggle() } } @@ -296,7 +297,7 @@ Panel { Text { id: heroIcon text: root.batteryIcon() - color: root.batteryLow ? Color.urgent : root.bar.foreground + color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.display anchors.left: parent.left @@ -340,7 +341,7 @@ Panel { Text { id: heroPercent text: root.batteryInfo.percentage || "—" - color: root.batteryLow ? Color.urgent : root.bar.foreground + color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.displayLarge font.bold: true @@ -408,17 +409,21 @@ Panel { width: (parent.width - parent.spacing) / 2 spacing: Style.spacing.labelGap InfoPair { - label: root.chargeThresholdActive ? "Charge limit" : (UPower.onBattery ? "Time left" : "Time to full") + label: root.chargeThresholdActive ? "Charge limit" : (root.discharging ? "Time left" : "Time to full") value: root.chargeThresholdActive ? (root.batteryInfo.threshold || "-") : (root.batteryFlowIdle ? "-" : (root.batteryInfo.time || "—")) } InfoPair { - label: root.chargeThresholdActive ? "Battery state" : (UPower.onBattery ? "Discharging" : "Charging") + label: root.chargeThresholdActive ? "Battery state" : (root.discharging ? "Discharging" : "Charging") value: root.chargeThresholdActive ? "Holding" : (root.batteryFull ? "-" : (root.batteryInfo.rate || "")) } } } // ---------- Power profile picker ---------- + PanelSeparator { + foreground: root.bar.foreground + } + Column { width: parent.width spacing: Style.space(10) diff --git a/test/shell.d/power-test.sh b/test/shell.d/power-test.sh index 3b26fe2f..66ec5166 100644 --- a/test/shell.d/power-test.sh +++ b/test/shell.d/power-test.sh @@ -24,7 +24,9 @@ assertEqual(power.batteryFraction({ isPresent: true, percentage: 1.5 }), 1, 'pow assert(power.chargeThresholdActive({ isPresent: true, percentage: 0.8, state: states.PendingCharge }, false, states), 'power detects threshold by pending charge state') assert(power.chargeThresholdActive({ isPresent: true, percentage: 0.8, state: states.Charging, changeRate: 0.1, timeToFull: 120 }, false, states), 'power detects threshold by stalled charging') assert(!power.chargeThresholdActive({ isPresent: true, percentage: 0.8, state: states.Charging, changeRate: 1.0, timeToFull: 120 }, false, states), 'power does not flag active charging as threshold') +assert(!power.chargeThresholdActive({ isPresent: true, percentage: 0.5, state: states.Discharging }, false, states), 'power does not flag discharging as threshold') assertEqual(power.modeLabel({ isPresent: true, percentage: 1, state: states.FullyCharged }, false, states), 'Fully charged', 'power labels full battery') assertEqual(power.modeLabel({ isPresent: true, percentage: 0.5, state: states.Discharging }, true, states), 'On battery', 'power labels battery mode') +assertEqual(power.modeLabel({ isPresent: true, percentage: 0.5, state: states.Discharging }, false, states), 'On battery', 'power labels discharging battery mode') assert(power.batteryIcon({ isPresent: true, percentage: 0.4, state: states.Charging }, false, states).length > 0, 'power maps battery icons') JS