Files
Panama/tests/setup/lid-contract
T
Gabriel Brown 50a99a5ad0 Closing the lid at a desk is not closing it in a bag
logind handles the lid correctly except for the one case it cannot
see: an external display means a closed lid is a docked machine, not
one being put away. Its own docked test looks for an ACPI docking
station that modern hardware does not have.

Panama does not take the lid over to fix that. It holds a logind
handle-lid-switch inhibitor while an external display is connected and
releases it when the last one goes, which needs no lid watcher, no
polling, and no drop-in. The direction it fails in is the point: if the
guard dies, logind's default comes back and a docked laptop suspends,
which is annoying. A drop-in setting HandleLidSwitch=ignore plus a
watcher of our own fails the other way, leaving a lid that does nothing
at all on a machine being carried out of a building.

Locking on the way down needed no work: hypridle's before_sleep_cmd
already runs loginctl lock-session, so a lid-close suspend is a locked
suspend. The contract fails anything that duplicates it.

Not yet verified against a real lid, which is stated in the helper's
header rather than implied by silence. The decision logic, the
inhibitor's shape, and every machine that should hold none of it are
covered.
2026-08-21 22:36:36 -04:00

148 lines
6.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# What closing the lid does.
#
# One sentence: closing the lid should suspend, unless there is an external
# monitor, in which case the machine is docked and should keep working.
#
# The mechanism is a logind `handle-lid-switch` inhibitor held while an
# external display is connected, rather than a lid watcher of Panama's own.
# That choice is the thing most worth pinning, because the obvious alternative
# fails in the dangerous direction: a drop-in setting HandleLidSwitch=ignore
# plus a watcher that dies leaves a laptop whose lid does nothing at all, being
# carried out of a building. An inhibitor that dies gives back logind's
# default, which is merely an annoying suspend at a desk.
#
# So this asserts, in order of how much it would cost to get wrong:
#
# 1. No logind drop-in is shipped. Panama never takes the lid over.
# 2. A machine with no reason for an inhibitor holds none: a desktop, and an
# undocked laptop that should suspend normally.
# 3. A docked laptop holds one, and it is the right kind.
# 4. Locking on the way down is not this code's job -- hypridle's
# before_sleep_cmd already does it -- and must not be quietly duplicated.
#
# Driven with stubbed predicates. The end-to-end behavior of a real lid needs a
# machine with a lid; see the header of the helper.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
helper="$repo_dir/config/dot/quickshell/scripts/panama-lid"
service="$repo_dir/config/dot/quickshell/services/LidPolicy.qml"
shell_qml="$repo_dir/config/dot/quickshell/shell.qml"
hypridle="$repo_dir/config/dot/hypr/hypridle.conf"
findings=()
note() { findings+=("$1"); }
[[ -x "$helper" ]] || { printf 'lid contract: %s is not executable\n' "$helper" >&2; exit 1; }
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
calls="$work/calls"
fake="$work/fake"
mkdir -p "$fake/bin"
# ── 1. Panama never takes the lid over ───────────────────────────────────────
if compgen -G "$repo_dir/config/copy/etc/systemd/logind.conf.d/*" >/dev/null 2>&1; then
note 'a logind drop-in is shipped; the inhibitor approach exists so the lid is never left doing nothing'
fi
# Comments are stripped first: these files explain at length what they
# deliberately do NOT do, and prose naming HandleLidSwitch or lock-session is
# documentation rather than behavior.
uncommented() { grep -rhv '^[[:space:]]*#' "$@" 2>/dev/null; }
uncommented "$repo_dir/config" | grep -q 'HandleLidSwitch' \
&& note 'something sets HandleLidSwitch, which takes the lid away from logind'
# ── The stubs ────────────────────────────────────────────────────────────────
# panama-hw answers whatever this fixture says. $1 laptop, $2 external monitor,
# as exit codes: 0 yes, 1 no.
stub_hw() {
cat >"$fake/bin/panama-hw" <<STUB
#!/usr/bin/env bash
case "\$1" in
laptop) exit $1 ;;
external-monitor) exit $2 ;;
lid-closed) exit 1 ;;
clamshell) exit 1 ;;
*) exit 1 ;;
esac
STUB
chmod +x "$fake/bin/panama-hw"
}
# systemd-inhibit records how it was called instead of holding anything.
cat >"$fake/systemd-inhibit" <<STUB
#!/usr/bin/env bash
printf '%s\n' "\$*" >>"$calls"
STUB
chmod +x "$fake/systemd-inhibit"
guard() {
: >"$calls"
PATH="$fake:$PATH" PANAMA_PATH="$fake" "$helper" guard >/dev/null 2>&1
}
# ── 2. Machines that should hold nothing ─────────────────────────────────────
stub_hw 1 0 # a desktop: not a laptop, external monitor present
guard || note 'the guard failed on a desktop instead of exiting cleanly'
[[ -s "$calls" ]] && note 'a desktop held a lid inhibitor, which it has no lid to inhibit'
stub_hw 0 1 # a laptop with no external display: must suspend normally
guard || note 'the guard failed on an undocked laptop instead of exiting cleanly'
[[ -s "$calls" ]] \
&& note 'an undocked laptop held a lid inhibitor, so closing it in a bag would not suspend'
# ── 3. A docked laptop holds the right one ───────────────────────────────────
stub_hw 0 0 # a laptop with an external display
guard
[[ -s "$calls" ]] || note 'a docked laptop held no inhibitor, so closing the lid would suspend mid-work'
recorded="$(cat "$calls" 2>/dev/null)"
grep -q -- '--what=handle-lid-switch' <<<"$recorded" \
|| note 'the inhibitor is not a handle-lid-switch inhibitor, so logind would still act on the lid'
grep -q -- '--mode=block' <<<"$recorded" \
|| note 'the inhibitor is a delay rather than a block, so logind would suspend anyway after its timeout'
grep -q -- '--who=panama-lid' <<<"$recorded" \
|| note 'the inhibitor does not identify itself, so systemd-inhibit --list cannot explain who is holding it'
grep -q -- '--why=' <<<"$recorded" \
|| note 'the inhibitor states no reason'
# ── 4. Locking on the way down stays hypridle's job ──────────────────────────
grep -q 'before_sleep_cmd' "$hypridle" \
|| note 'the shipped hypridle config no longer locks before sleep, which is what makes a lid-close suspend a locked one'
uncommented "$helper" | grep -q 'loginctl lock-session\|hyprlock' \
&& note 'the lid helper locks the session itself, duplicating what hypridle already does on every sleep'
# ── The service that drives it ───────────────────────────────────────────────
[[ -r "$service" ]] || note 'LidPolicy.qml is missing, so nothing notices a display being connected'
grep -q 'eDP|LVDS|DSI' "$service" \
|| note 'the service does not distinguish the built-in panel from an external display'
grep -q 'Connections { target: LidPolicy }' "$shell_qml" \
|| note 'nothing keeps LidPolicy alive, so it would only start when some page happened to reference it'
# Status is machine-readable, for the Power page and panama-doctor.
stub_hw 0 0
status="$(PATH="$fake:$PATH" PANAMA_PATH="$fake" "$helper" status 2>/dev/null)"
if command -v jq >/dev/null 2>&1; then
jq -e . >/dev/null 2>&1 <<<"$status" || note 'status does not emit valid JSON'
for key in laptop lidClosed externalMonitor clamshell inhibited; do
jq -e "has(\"$key\")" >/dev/null 2>&1 <<<"$status" || note "status omits $key"
done
fi
if (( ${#findings[@]} > 0 )); then
printf 'lid contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'lid contract: PASS\n'