diff --git a/bin/omarchy-brightness-keyboard b/bin/omarchy-brightness-keyboard index f323b94f53a..c716a5d38b5 100755 --- a/bin/omarchy-brightness-keyboard +++ b/bin/omarchy-brightness-keyboard @@ -25,18 +25,66 @@ if [[ -z $device ]]; then exit 1 fi +STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy" +STATE_FILE="$STATE_DIR/keyboard-brightness" + +save_persistent_state() { + local val="$1" + if (( val > 0 )); then + mkdir -p "$STATE_DIR" + echo "$val" > "$STATE_FILE" + fi +} + +get_persistent_state() { + if [[ -s "$STATE_FILE" ]]; then + cat "$STATE_FILE" 2>/dev/null || echo 0 + else + echo 0 + fi +} + +current_brightness="$(brightnessctl -d "$device" get 2>/dev/null || echo 0)" +max_brightness="$(brightnessctl -d "$device" max 2>/dev/null || echo 1)" + if [[ $direction == "off" ]]; then - brightnessctl -sd "$device" set 0 >/dev/null + # Guard: Only save state if current brightness is positive so we never overwrite with 0 + if (( current_brightness > 0 )); then + save_persistent_state "$current_brightness" + brightnessctl -sd "$device" set 0 >/dev/null 2>&1 + else + brightnessctl -d "$device" set 0 >/dev/null 2>&1 + fi exit 0 elif [[ $direction == "restore" ]]; then - brightnessctl -rd "$device" >/dev/null + saved_val=$(get_persistent_state) + runtime_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" + brightnessctl_file="$runtime_dir/brightnessctl/leds/$device" + + + # Check if brightnessctl temporary file has a positive value + if [[ -f "$brightnessctl_file" ]]; then + b_val=$(cat "$brightnessctl_file" 2>/dev/null || echo 0) + if (( b_val > 0 )); then + brightnessctl -rd "$device" >/dev/null 2>&1 + exit 0 + fi + fi + + # If brightnessctl file was missing or zeroed, restore from persistent state + if (( saved_val > 0 )); then + brightnessctl -d "$device" set "$saved_val" >/dev/null 2>&1 + exit 0 + fi + + # Fallback to sensible default (30% of max) if no previous non-zero state is recorded + fallback_step=$(( max_brightness * 30 / 100 )) + (( fallback_step < 1 )) && fallback_step=1 + brightnessctl -d "$device" set "$fallback_step" >/dev/null 2>&1 + save_persistent_state "$fallback_step" exit 0 fi -# Get current and max brightness to determine step size. -max_brightness="$(brightnessctl -d "$device" max)" -current_brightness="$(brightnessctl -d "$device" get)" - # Calculate step as 10% of max brightness. Keyboards with many levels (e.g. 512) # need larger steps; keyboards with few levels (e.g. 3) fall back to step=1. step=$(( max_brightness / 10 )) @@ -53,6 +101,9 @@ else (( new_brightness < 0 )) && new_brightness=0 fi -# Set the new brightness. +# Set the new brightness and persist it if non-zero brightnessctl -d "$device" set "$new_brightness" >/dev/null +if (( new_brightness > 0 )); then + save_persistent_state "$new_brightness" +fi (( no_osd )) || omarchy-osd -i keyboard -p "$(( new_brightness * 100 / max_brightness ))" diff --git a/default/systemd/system-sleep/keyboard-backlight b/default/systemd/system-sleep/keyboard-backlight index 014ab4a8335..375ff3e214c 100644 --- a/default/systemd/system-sleep/keyboard-backlight +++ b/default/systemd/system-sleep/keyboard-backlight @@ -2,19 +2,36 @@ # Turn off keyboard backlight before hibernate to prevent hang on power-off. # The ASUS keyboard controller can block S4 shutdown if LEDs are active. +# On resume (post), restore the previous backlight brightness. sleep_action=${SYSTEMD_SLEEP_ACTION:-$2} +device="" +for candidate in /sys/class/leds/*kbd_backlight*; do + if [[ -e "$candidate" ]]; then + device="$(basename "$candidate")" + break + fi +done + +[[ -n "$device" ]] || exit 0 + +STATE_DIR="/var/lib/systemd/backlight" +STATE_FILE="$STATE_DIR/keyboard-sleep-state" + if [[ $1 == "pre" && $sleep_action == "hibernate" ]]; then - device="" - for candidate in /sys/class/leds/*kbd_backlight*; do - if [[ -e "$candidate" ]]; then - device="$(basename "$candidate")" - break + current=$(cat "/sys/class/leds/$device/brightness" 2>/dev/null || echo 0) + if (( current > 0 )); then + mkdir -p "$STATE_DIR" + echo "$current" > "$STATE_FILE" + fi + brightnessctl -d "$device" set 0 >/dev/null 2>&1 +elif [[ $1 == "post" && $sleep_action == "hibernate" ]]; then + if [[ -s "$STATE_FILE" ]]; then + target=$(cat "$STATE_FILE" 2>/dev/null || echo 0) + if (( target > 0 )); then + brightnessctl -d "$device" set "$target" >/dev/null 2>&1 fi - done - - if [[ -n "$device" ]]; then - brightnessctl -d "$device" set 0 >/dev/null 2>&1 fi fi + diff --git a/test/shell.d/brightness-keyboard-test.sh b/test/shell.d/brightness-keyboard-test.sh new file mode 100755 index 00000000000..f6e0fb9b454 --- /dev/null +++ b/test/shell.d/brightness-keyboard-test.sh @@ -0,0 +1,115 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +mock_bin="$test_tmp/bin" +call_log="$test_tmp/calls" +runtime_dir="$test_tmp/runtime" +state_dir="$test_tmp/state" +mkdir -p "$mock_bin" "$runtime_dir" "$state_dir" + +mock_sys_leds="/sys/class/leds" + +# Mock brightnessctl +cat >"$mock_bin/brightnessctl" <<'SH' +#!/bin/bash +printf 'brightnessctl %s\n' "$*" >>"$CALL_LOG" + +device="" +args=("$@") +for (( i=0; i<${#args[@]}; i++ )); do + if [[ ${args[i]} == "-d" && $(( i + 1 )) -lt ${#args[@]} ]]; then + device="${args[i+1]}" + fi +done + +if [[ $* == *" max"* ]]; then + echo "${MOCK_MAX:-1000}" +elif [[ $* == *" get"* ]]; then + echo "${MOCK_CURRENT:-500}" +elif [[ $* == *" -sd "* ]]; then + # Simulate saving current brightness and setting target + echo "${MOCK_CURRENT:-500}" > "$MOCK_SAVED_FILE" +elif [[ $* == *" -rd "* ]]; then + if [[ -s "$MOCK_SAVED_FILE" ]]; then + saved=$(cat "$MOCK_SAVED_FILE") + echo "$saved" + fi +fi +SH + +cat >"$mock_bin/omarchy-osd" <<'SH' +#!/bin/bash +printf 'omarchy-osd %s\n' "$*" >>"$CALL_LOG" +SH + +chmod +x "$mock_bin"/* + +saved_file="$runtime_dir/brightnessctl/leds/:white:kbd_backlight" +mkdir -p "$(dirname "$saved_file")" +export MOCK_SAVED_FILE="$saved_file" + + +run_kbd() { + CALL_LOG="$call_log" \ + XDG_RUNTIME_DIR="$runtime_dir" \ + XDG_STATE_HOME="$state_dir" \ + PATH="$mock_bin:$ROOT/bin:$PATH" \ + "$ROOT/bin/omarchy-brightness-keyboard" "$@" +} + +# Test 1: off with positive brightness calls brightnessctl -sd +: >"$call_log" +MOCK_CURRENT=500 MOCK_MAX=1000 run_kbd off +grep -Fq 'brightnessctl -sd :white:kbd_backlight set 0' "$call_log" || + fail "off saves state when current brightness is positive" +pass "off saves state when current brightness is positive" + +# Test 2: off when brightness is already 0 calls brightnessctl -d set 0 (omits -s to protect saved state) +: >"$call_log" +MOCK_CURRENT=0 MOCK_MAX=1000 run_kbd off +grep -Fq 'brightnessctl -d :white:kbd_backlight set 0' "$call_log" || + fail "off does not overwrite saved state when current brightness is already 0" +! grep -Fq 'brightnessctl -sd' "$call_log" || + fail "off must not pass -s when current brightness is 0" +pass "off avoids zero-clobbering when backlight is already 0" + +# Test 3: up increments by 10% and persists state +: >"$call_log" +MOCK_CURRENT=500 MOCK_MAX=1000 run_kbd up +grep -Fq 'brightnessctl -d :white:kbd_backlight set 600' "$call_log" || + fail "up steps brightness by 10%" +[[ $(cat "$state_dir/omarchy/keyboard-brightness" 2>/dev/null) == "600" ]] || + fail "up persists new brightness level" +pass "up steps brightness by 10% and persists target" + +# Test 4: down decrements by 10% and persists state +: >"$call_log" +MOCK_CURRENT=500 MOCK_MAX=1000 run_kbd down +grep -Fq 'brightnessctl -d :white:kbd_backlight set 400' "$call_log" || + fail "down steps brightness down by 10%" +[[ $(cat "$state_dir/omarchy/keyboard-brightness" 2>/dev/null) == "400" ]] || + fail "down persists new brightness level" +pass "down steps brightness down by 10% and persists target" + +# Test 5: restore with persistent state recovers saved target +: >"$call_log" +echo "750" > "$state_dir/omarchy/keyboard-brightness" +rm -f "$saved_file" +MOCK_CURRENT=0 MOCK_MAX=1000 run_kbd restore +grep -Fq 'brightnessctl -d :white:kbd_backlight set 750' "$call_log" || + fail "restore uses persistent state when run file is absent" +pass "restore recovers target brightness from persistent state" + +# Test 6: restore without any saved state falls back to 30% default +: >"$call_log" +rm -f "$state_dir/omarchy/keyboard-brightness" "$saved_file" +MOCK_CURRENT=0 MOCK_MAX=1000 run_kbd restore +grep -Fq 'brightnessctl -d :white:kbd_backlight set 300' "$call_log" || + fail "restore falls back to 30% of max when no state exists" +pass "restore falls back to 30% when no state exists"