#!/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" <"$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 OWN schema default, # not to the AC value. This section once pinned the opposite -- protecting a # deliberate AC setting from being "overridden" by unplugging -- but the # Power page's battery card shows the schema defaults for unwritten battery # keys, so the AC fallback made the generator disagree with what the screen # said: a fresh laptop displayed "suspend at 20 minutes on battery" and # generated no suspend listener at all. Whatever the sliders show is what # must be generated; a person who wants battery to match AC sets it so, on # the card that has said the real values all along. stub_hw 0 1 run_powered '{"screenBlankMinutes":30,"lockMinutes":45}' grep -q 'timeout = 120' "$generated" \ || fail 'an unset battery blank did not use the schema default the Power page displays' grep -q 'timeout = 300' "$generated" \ || fail 'an unset battery lock did not use the schema default the Power page displays' grep -q 'systemctl suspend' "$generated" \ || fail 'an unset battery suspend generated no suspend listener -- the discharged-in-a-bag bug, back again' # 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'