diff --git a/bin/omarchy-powerprofiles-init b/bin/omarchy-powerprofiles-init index a8dd1cc6..cdf9c1c3 100755 --- a/bin/omarchy-powerprofiles-init +++ b/bin/omarchy-powerprofiles-init @@ -2,8 +2,4 @@ # omarchy:summary=Set the correct power profile on boot based on current AC/battery state. -if omarchy-battery-present && ! omarchy-power-present; then - omarchy-powerprofiles-set battery -else - omarchy-powerprofiles-set ac -fi +exec omarchy-powerprofiles-set autodetect diff --git a/bin/omarchy-powerprofiles-set b/bin/omarchy-powerprofiles-set index bebd18d2..54872140 100755 --- a/bin/omarchy-powerprofiles-set +++ b/bin/omarchy-powerprofiles-set @@ -1,45 +1,63 @@ #!/bin/bash -# omarchy:summary=Set the power profile to the requested level, falling back to balanced -# omarchy:args=[autodetect|ac|battery] +# omarchy:summary=Set and remember the power profile for AC or battery use +# omarchy:args=[autodetect|ac|battery] [power-saver|balanced|performance] -action="${1-}" +action="${1:-autodetect}" +requested_profile="${2:-}" -# Auto-detect when called with no argument: treat any Mains or USB -# power-supply device reporting online=1 as "on AC". This handles -# USB-C only laptops where the legacy AC device may not fire udev -# events, and also avoids false negatives from per-port USB-C devices -# that are present-but-empty (online=0) while another port supplies power. -if [[ -z $action || $action == "autodetect" ]]; then - # On plug/unplug, udev fires the rule before sysfs `online` is updated - # on some laptops (notably Lenovo Yoga Pro 7 with USB-C charging). A - # short settle delay lets the kernel update before we read state. - sleep 0.3 +usage() { + echo "Usage: omarchy-powerprofiles-set [autodetect|ac|battery] [power-saver|balanced|performance]" >&2 + exit 1 +} - action=battery - for ps in /sys/class/power_supply/*; do - [[ -r $ps/online && -r $ps/type ]] || continue - type=$(cat "$ps/type") - [[ $type == "Mains" || $type == "USB" ]] || continue - if [[ $(cat "$ps/online") == "1" ]]; then +(( $# <= 2 )) || usage + +case "$action" in + autodetect) + # Use the same signal as the shell (UPower.onBattery) so every entry point + # saves and restores the profile under the same ac/battery key. + if [[ $(busctl get-property org.freedesktop.UPower /org/freedesktop/UPower org.freedesktop.UPower OnBattery 2>/dev/null) == "b true" ]]; then + action=battery + else action=ac - break fi - done -fi + ;; + ac | battery) ;; + *) usage ;; +esac mapfile -t profiles < <(powerprofilesctl list | awk '/^\s*[* ]\s*[a-zA-Z0-9\-]+:$/ { gsub(/^[*[:space:]]+|:$/,""); print }') -case "$action" in - ac) - # Prefer performance, fall back to balanced - if [[ " ${profiles[*]} " == *" performance "* ]]; then - powerprofilesctl set performance - else - powerprofilesctl set balanced - fi - ;; - battery) - powerprofilesctl set balanced - ;; -esac \ No newline at end of file +profile_available() { + [[ " ${profiles[*]} " == *" $1 "* ]] +} + +state_dir="${OMARCHY_POWERPROFILES_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/powerprofiles}" +state_file="$state_dir/$action" + +if [[ -n $requested_profile ]]; then + profile_available "$requested_profile" || { + echo "Power profile is not available: $requested_profile" >&2 + exit 1 + } + + profile="$requested_profile" +elif [[ -r $state_file ]]; then + profile=$(<"$state_file") +fi + +if [[ -z ${profile:-} ]] || ! profile_available "$profile"; then + if [[ $action == "ac" ]] && profile_available performance; then + profile=performance + else + profile=balanced + fi +fi + +powerprofilesctl set "$profile" || exit + +if [[ -n $requested_profile ]]; then + mkdir -p "$state_dir" + printf '%s\n' "$requested_profile" >"$state_file" +fi diff --git a/etc/udev/rules.d/99-omarchy-power-profile.rules b/etc/udev/rules.d/99-omarchy-power-profile.rules deleted file mode 100644 index 37a707cc..00000000 --- a/etc/udev/rules.d/99-omarchy-power-profile.rules +++ /dev/null @@ -1,2 +0,0 @@ -SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="/usr/bin/systemd-run --no-block --collect --property=After=power-profiles-daemon.service /usr/bin/omarchy-powerprofiles-set" -SUBSYSTEM=="power_supply", ATTR{type}=="USB", RUN+="/usr/bin/systemd-run --no-block --collect --property=After=power-profiles-daemon.service /usr/bin/omarchy-powerprofiles-set" diff --git a/shell/plugins/menu/Menu.qml b/shell/plugins/menu/Menu.qml index 70c51817..4d7e31a8 100644 --- a/shell/plugins/menu/Menu.qml +++ b/shell/plugins/menu/Menu.qml @@ -228,7 +228,7 @@ Item { "power-profiles": { script: "current=$(powerprofilesctl get 2>/dev/null); omarchy-powerprofiles-list 2>/dev/null | while read -r p; do [[ -z $p ]] && continue; printf '%s\\t%s\\t%s\\n' \"$p\" \"$p\" \"$current\"; done", icon: "\udb81\udc0b", - actionFor: function(value) { return "powerprofilesctl set '" + value.replace(/'/g, "'\\''") + "'" }, + actionFor: function(value) { return "omarchy-powerprofiles-set autodetect '" + value.replace(/'/g, "'\\''") + "'" }, keywordsFor: function(value) { return value + " power profile" } } }) diff --git a/shell/plugins/panels/power/Panel.qml b/shell/plugins/panels/power/Panel.qml index e5fb1f49..2aad1ac3 100644 --- a/shell/plugins/panels/power/Panel.qml +++ b/shell/plugins/panels/power/Panel.qml @@ -157,7 +157,7 @@ Panel { function setProfile(profile) { if (!profile || actionProc.running) return - actionProc.command = ["powerprofilesctl", "set", profile] + actionProc.command = ["omarchy-powerprofiles-set", root.discharging ? "battery" : "ac", profile] actionProc.running = true } diff --git a/shell/plugins/services/battery/Service.qml b/shell/plugins/services/battery/Service.qml index c2ab7625..37fdf5c7 100644 --- a/shell/plugins/services/battery/Service.qml +++ b/shell/plugins/services/battery/Service.qml @@ -11,6 +11,7 @@ Item { property string omarchyPath: Quickshell.env("OMARCHY_PATH") readonly property int batteryThreshold: 10 + property string pendingPowerSource: "" PersistentProperties { id: persisted @@ -41,8 +42,24 @@ Item { warningProcess.running = true } + function applyPowerProfile() { + pendingPowerSource = UPower.onBattery ? "battery" : "ac" + if (!powerProfileProcess.running) runPendingPowerProfile() + } + + function runPendingPowerProfile() { + powerProfileProcess.command = ["omarchy-powerprofiles-set", pendingPowerSource] + pendingPowerSource = "" + powerProfileProcess.running = true + } + Process { id: warningProcess } + Process { + id: powerProfileProcess + onExited: if (root.pendingPowerSource !== "") root.runPendingPowerProfile() + } + Timer { interval: 30000 running: true @@ -53,6 +70,9 @@ Item { Connections { target: UPower - function onOnBatteryChanged() { root.checkBattery() } + function onOnBatteryChanged() { + root.checkBattery() + root.applyPowerProfile() + } } } diff --git a/test/shell.d/powerprofiles-set-test.sh b/test/shell.d/powerprofiles-set-test.sh new file mode 100755 index 00000000..e71de864 --- /dev/null +++ b/test/shell.d/powerprofiles-set-test.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' EXIT + +mkdir -p "$tmp_dir/bin" "$tmp_dir/state" + +cat >"$tmp_dir/bin/powerprofilesctl" <<'EOF' +#!/bin/bash + +if [[ $1 == "list" ]]; then + printf ' power-saver:\n* balanced:\n performance:\n' +elif [[ $1 == "set" ]]; then + [[ ${POWERPROFILES_SET_FAIL:-0} == "0" ]] || exit 1 + printf '%s\n' "$2" >>"$POWERPROFILES_LOG" +fi +EOF +chmod +x "$tmp_dir/bin/powerprofilesctl" + +cat >"$tmp_dir/bin/busctl" <<'EOF' +#!/bin/bash + +if [[ ${ON_BATTERY:-0} == "1" ]]; then + echo "b true" +else + echo "b false" +fi +EOF +chmod +x "$tmp_dir/bin/busctl" + +export PATH="$tmp_dir/bin:$ROOT/bin:$PATH" +export POWERPROFILES_LOG="$tmp_dir/calls" +export OMARCHY_POWERPROFILES_STATE_DIR="$tmp_dir/state" + +"$ROOT/bin/omarchy-powerprofiles-set" ac balanced +[[ $(<"$tmp_dir/state/ac") == "balanced" ]] || fail "power profile stores AC preference" +[[ $(tail -n 1 "$tmp_dir/calls") == "balanced" ]] || fail "power profile applies selected AC preference" +pass "power profile stores and applies AC preference" + +"$ROOT/bin/omarchy-powerprofiles-set" ac +[[ $(tail -n 1 "$tmp_dir/calls") == "balanced" ]] || fail "power profile restores AC preference" +pass "power profile restores AC preference" + +if POWERPROFILES_SET_FAIL=1 "$ROOT/bin/omarchy-powerprofiles-set" ac performance; then + fail "power profile reports a failed selection" +fi +[[ $(<"$tmp_dir/state/ac") == "balanced" ]] || fail "power profile preserves preference after failed selection" +pass "power profile persists only successful selections" + +"$ROOT/bin/omarchy-powerprofiles-set" battery performance +[[ $(<"$tmp_dir/state/battery") == "performance" ]] || fail "power profile stores battery preference" +pass "power profile stores battery preference separately" + +"$ROOT/bin/omarchy-powerprofiles-set" ac +[[ $(tail -n 1 "$tmp_dir/calls") == "balanced" ]] || fail "battery preference does not replace AC preference" +pass "power profile keeps AC and battery preferences separate" + +ON_BATTERY=1 "$ROOT/bin/omarchy-powerprofiles-set" +[[ $(tail -n 1 "$tmp_dir/calls") == "performance" ]] || fail "autodetect restores battery preference" +pass "power profile autodetect restores battery preference" + +rm "$tmp_dir/state/ac" +ON_BATTERY=0 "$ROOT/bin/omarchy-powerprofiles-set" +[[ $(tail -n 1 "$tmp_dir/calls") == "performance" ]] || fail "power profile uses performance as AC default" +pass "power profile retains performance as AC default" + +"$ROOT/bin/omarchy-powerprofiles-set" ac power-saver +"$ROOT/bin/omarchy-powerprofiles-init" +[[ $(tail -n 1 "$tmp_dir/calls") == "power-saver" ]] || fail "init restores the autodetected preference" +pass "power profile init restores the autodetected preference" + +rg -F '["omarchy-powerprofiles-set", pendingPowerSource]' "$ROOT/shell/plugins/services/battery/Service.qml" >/dev/null || + fail "battery service applies profiles through Omarchy command" +pass "battery service applies profiles through Omarchy command" + +rg -F 'omarchy-powerprofiles-set autodetect' "$ROOT/shell/plugins/menu/Menu.qml" >/dev/null || + fail "power profile menu persists selections through Omarchy command" +pass "power profile menu persists selections through Omarchy command"