An idle screen costs a screen on wall power and the rest of your afternoon on battery, so they should not be the same number. hypridle has no concept of a power source -- one config, one set of timeouts -- so rather than maintaining two configs and swapping them, panama-idle builds the single config from whichever key set applies, and IdleLock rebuilds it when the charger comes or goes. That runs through the same 400ms debounce a settings change uses, so a loose charger cannot restart hypridle in a loop. The battery keys fall back to their AC counterparts rather than to the schema defaults. Without that, unplugging would silently override a deliberately long timing with a shipped short one, which is the kind of thing you would notice only by losing work. A machine with no battery reads none of it and generates exactly what it generated before. The contract pins that alongside the two obvious directions, and was checked by sabotaging the detection to confirm it fails rather than passing vacuously.
161 lines
8.2 KiB
Bash
Executable File
161 lines
8.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# scripts/panama-idle generates hypridle's configuration from Panama's settings.
|
|
#
|
|
# This is the one generator that can cost the user their automatic screen lock,
|
|
# so the properties that matter are: zero means never (rather than "immediately",
|
|
# which a naive template would produce), values are clamped even when the
|
|
# settings file has been hand-edited, and a missing or corrupt file still yields
|
|
# a working configuration rather than an empty one.
|
|
|
|
set -euo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
helper="$repo_dir/config/dot/quickshell/scripts/panama-idle"
|
|
work="$(mktemp -d /tmp/panama-idle-contract.XXXXXX)"
|
|
|
|
fail() {
|
|
printf 'idle config contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
cleanup() { rm -rf "$work"; }
|
|
trap cleanup EXIT
|
|
|
|
generated="$work/state/panama/hypridle.conf"
|
|
|
|
run_with() {
|
|
# Isolated config AND state, so this never touches the real generated file
|
|
# and never restarts the user's hypridle: `apply` only restarts when the
|
|
# systemd drop-in exists, and it cannot exist under this temporary root.
|
|
mkdir -p "$work/config/panama"
|
|
printf '%s' "$1" >"$work/config/panama/settings.json"
|
|
XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" "$helper" apply
|
|
}
|
|
|
|
# ── Shipped defaults ─────────────────────────────────────────────────────────
|
|
run_with '{}'
|
|
grep -q 'timeout = 300' "$generated" || fail 'default screen blank is not 5 minutes'
|
|
grep -q 'timeout = 600' "$generated" || fail 'default lock is not 10 minutes'
|
|
grep -q 'before_sleep_cmd' "$generated" || fail 'lock before sleep missing by default'
|
|
grep -q 'systemctl suspend' "$generated" && fail 'automatic suspend is on by default'
|
|
|
|
# ── Zero means never, not immediately ────────────────────────────────────────
|
|
run_with '{"screenBlankMinutes":0,"lockMinutes":0,"suspendMinutes":0}'
|
|
grep -q 'timeout = 0' "$generated" && fail 'a zero timeout was written as an immediate trigger'
|
|
# `dpms` also appears in general.after_sleep_cmd, which is correct and must
|
|
# stay; what must not exist is any listener at all.
|
|
grep -qE '^listener' "$generated" && fail 'a listener was written when everything is set to never'
|
|
grep -q 'after_sleep_cmd' "$generated" || fail 'the general block was lost when all timers are off'
|
|
|
|
# ── Values are honored ──────────────────────────────────────────────────────
|
|
run_with '{"screenBlankMinutes":2,"lockMinutes":7,"suspendMinutes":45}'
|
|
grep -q 'timeout = 120' "$generated" || fail '2 minute blank not honored'
|
|
grep -q 'timeout = 420' "$generated" || fail '7 minute lock not honored'
|
|
grep -q 'timeout = 2700' "$generated" || fail '45 minute suspend not honored'
|
|
grep -q 'systemctl suspend' "$generated" || fail 'suspend listener missing when set'
|
|
|
|
# ── lockOnSleep off removes the pre-sleep lock ───────────────────────────────
|
|
run_with '{"lockOnSleep":false}'
|
|
grep -q 'before_sleep_cmd' "$generated" && fail 'pre-sleep lock present when disabled'
|
|
|
|
# ── Hand-edited nonsense is clamped, not passed through ──────────────────────
|
|
run_with '{"screenBlankMinutes":99999,"lockMinutes":-40,"suspendMinutes":"soon"}'
|
|
grep -q 'timeout = 7200' "$generated" || fail 'an above-range blank was not clamped to the maximum'
|
|
grep -q 'timeout = 0' "$generated" && fail 'a negative lock produced an immediate trigger'
|
|
grep -q 'systemctl suspend' "$generated" && fail 'a non-numeric suspend produced a listener'
|
|
|
|
# ── A corrupt or absent settings file still yields a working config ──────────
|
|
run_with '{ not json at all'
|
|
grep -q 'timeout = 300' "$generated" || fail 'a corrupt settings file did not fall back to defaults'
|
|
rm -f "$work/config/panama/settings.json"
|
|
XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" "$helper" apply
|
|
grep -q 'timeout = 300' "$generated" || fail 'an absent settings file did not fall back to defaults'
|
|
|
|
# ── status reports what it generated ─────────────────────────────────────────
|
|
status="$(XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" "$helper" status)"
|
|
jq -e '.managed == false and .blankMinutes == 5 and .lockMinutes == 10' <<<"$status" >/dev/null \
|
|
|| fail "status did not report the generated values: $status"
|
|
|
|
# ── Power source ─────────────────────────────────────────────────────────────
|
|
#
|
|
# hypridle has no concept of AC versus battery, so there is one config and it
|
|
# is rebuilt from the other set of keys when the charger comes or goes. Three
|
|
# things must hold, and the third is the one that protects every existing
|
|
# desktop:
|
|
#
|
|
# 1. On battery, the battery keys win.
|
|
# 2. A battery key that was never written falls back to its AC counterpart,
|
|
# not to the schema's shorter shipped default -- otherwise unplugging
|
|
# would silently override a deliberately long setting.
|
|
# 3. A machine with no battery reads none of them, and generates exactly
|
|
# what it generated before any of this existed.
|
|
#
|
|
# panama-hw is stubbed rather than the sysfs tree, because this is testing
|
|
# which keys panama-idle chooses, not how the hardware is detected. That is
|
|
# the hardware predicates contract's job.
|
|
|
|
stub_hw() {
|
|
mkdir -p "$work/fake/bin"
|
|
cat >"$work/fake/bin/panama-hw" <<STUB
|
|
#!/usr/bin/env bash
|
|
case "\$1" in
|
|
battery) exit $1 ;;
|
|
ac) exit $2 ;;
|
|
*) exit 1 ;;
|
|
esac
|
|
STUB
|
|
chmod +x "$work/fake/bin/panama-hw"
|
|
}
|
|
|
|
run_powered() {
|
|
mkdir -p "$work/config/panama"
|
|
printf '%s' "$1" >"$work/config/panama/settings.json"
|
|
XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" \
|
|
PANAMA_PATH="$work/fake" "$helper" apply
|
|
}
|
|
|
|
settings='{"screenBlankMinutes":30,"lockMinutes":45,"suspendMinutes":0,
|
|
"screenBlankMinutesBattery":2,"lockMinutesBattery":5,"suspendMinutesBattery":20}'
|
|
|
|
# On battery: battery keys.
|
|
stub_hw 0 1
|
|
run_powered "$settings"
|
|
grep -q 'timeout = 120' "$generated" || fail 'on battery, the battery blank timing was not used'
|
|
grep -q 'timeout = 300' "$generated" || fail 'on battery, the battery lock timing was not used'
|
|
grep -q 'systemctl suspend' "$generated" || fail 'on battery, the battery suspend listener is missing'
|
|
grep -q 'Power source at generation: battery' "$generated" \
|
|
|| fail 'the generated config does not record that it was built for battery'
|
|
|
|
# Plugged in: AC keys, and no suspend, even though the battery set has one.
|
|
stub_hw 0 0
|
|
run_powered "$settings"
|
|
grep -q 'timeout = 1800' "$generated" || fail 'on wall power, the AC blank timing was not used'
|
|
grep -q 'timeout = 2700' "$generated" || fail 'on wall power, the AC lock timing was not used'
|
|
grep -q 'systemctl suspend' "$generated" && fail 'on wall power, a battery-only suspend listener was written'
|
|
|
|
# A battery key that was never written falls back to its AC counterpart.
|
|
stub_hw 0 1
|
|
run_powered '{"screenBlankMinutes":30,"lockMinutes":45}'
|
|
grep -q 'timeout = 1800' "$generated" \
|
|
|| fail 'an unset battery blank did not fall back to the AC value, so unplugging would override a deliberate setting'
|
|
grep -q 'timeout = 2700' "$generated" \
|
|
|| fail 'an unset battery lock did not fall back to the AC value'
|
|
|
|
# No battery at all: the battery keys are never consulted, even when present.
|
|
stub_hw 1 0
|
|
run_powered "$settings"
|
|
grep -q 'timeout = 1800' "$generated" || fail 'a machine with no battery did not use the AC blank timing'
|
|
grep -q 'timeout = 120' "$generated" && fail 'a machine with no battery read the battery keys'
|
|
grep -q 'Power source at generation: wall power' "$generated" \
|
|
|| fail 'a machine with no battery did not record wall power'
|
|
|
|
status="$(XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" \
|
|
PANAMA_PATH="$work/fake" "$helper" status)"
|
|
jq -e '.powerSource == "ac"' <<<"$status" >/dev/null \
|
|
|| fail "status did not report the power source: $status"
|
|
|
|
trap - EXIT
|
|
cleanup
|
|
printf 'idle config contract: PASS\n'
|