The audit's fourth tier -- what a Framework owner reaches for in the first week and found missing. The power button stops being an instant, unconfirmed poweroff: a shipped logind drop-in tells the daemon to stand down and the compositor binds the key to the power menu, the way GNOME turns it into a question. Holding it still hard-cuts through firmware. change-settings restarts logind so the change applies without waiting for a boot, and the Power page says what the button does now. The function row fills in: F10 (XF86RFKill) toggles airplane mode through a new panama-osd verb that blocks or unblocks every radio and says which way it went; F9 (XF86Display) opens the Displays page, the honest action until mirroring exists. And the lid becomes a switch bind: closing a docked lid turns the internal panel off so nothing renders inside a closed shell and no workspace strands on an invisible output, and opening it restores the panel with the mode and scale chosen in Settings. panama-lid owns both decisions; undocked machines suspend via logind before any of it matters. The battery gets an endgame. On battery the screen dims to 30% two-thirds of the way to blanking -- GNOME's single largest idle battery saver -- and restores exactly the level it saved. At the urgent threshold the machine suspends after a fifteen-second grace, cancelled by plugging in, because a suspend preserves the session for days and a hard cut at 0% preserves nothing; "Only warn" remains a choice on the Power page. Hibernate joins the power menu, but only where logind answers CanHibernate with yes -- an entry that fails silently is worse than none. And brightness stops being two code paths: the Displays page now embeds the same control the quick-settings panel uses, so the built-in backlight and DDC/CI monitors share one surface that withdraws itself where neither exists. The lid contract narrows to what its principle protects -- a HandleLidSwitch drop-in -- so deliberate policy for other keys can ship. Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
152 lines
7.0 KiB
Bash
Executable File
152 lines
7.0 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 ───────────────────────────────────────
|
|
|
|
# Specifically the LID. Other keys may be configured there -- the power-key
|
|
# drop-in is deliberate policy with a compositor bind behind it -- but a
|
|
# HandleLidSwitch line would replace the inhibitor design this file protects.
|
|
if compgen -G "$repo_dir/config/copy/etc/systemd/logind.conf.d/*" >/dev/null 2>&1 \
|
|
&& sed 's/#.*//' "$repo_dir"/config/copy/etc/systemd/logind.conf.d/* | grep -q 'HandleLidSwitch'; then
|
|
note 'a logind drop-in sets HandleLidSwitch; 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'
|