#!/usr/bin/env bash # What closing the lid should do. # # panama-lid status what this machine would do right now, as JSON # panama-lid guard hold the inhibitor while clamshell use is possible # # The rule is 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 implementation is deliberately not a lid watcher. logind already handles # the lid perfectly well; what it cannot do is notice that an external display # makes a closed lid mean something different, because its own "docked" test # looks for an ACPI docking station that modern hardware does not have. # # So Panama does not take the lid over. It holds a `handle-lid-switch` # inhibitor while an external monitor is connected, and releases it when the # last one goes away. logind does everything else, which has three properties # worth having: # # * Nothing has to watch the lid, and no polling loop exists. # * If this ever fails or is killed, the inhibitor is released and the # machine goes back to logind's default. The failure mode is "a docked # laptop suspends", not "the lid does nothing at all", which is the failure # mode a logind drop-in would have. # * Locking before sleep is already handled: hypridle's before_sleep_cmd # runs `loginctl lock-session`, so a suspend from a closed lid is a locked # suspend without anything here being involved. # # Started and stopped by services/LidPolicy.qml on display topology changes. # # NOT YET VERIFIED ON A LAPTOP. The logic and the inhibitor are testable on any # machine and are covered by tests/setup/lid-contract, but the end-to-end # behavior of closing a real lid on a docked machine needs a machine with a # lid. Everything here fails toward logind's default, so the untested path is # "keeps working while docked" rather than anything that could strand a session. set -uo pipefail PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}" HW="$PANAMA_PATH/bin/panama-hw" answer() { "$@" && printf 'true' || printf 'false'; } cmd_status() { local inhibited=false systemd-inhibit --list 2>/dev/null | grep -q 'panama-lid' && inhibited=true printf '{"laptop":%s,"lidClosed":%s,"externalMonitor":%s,"clamshell":%s,"inhibited":%s}\n' \ "$(answer "$HW" laptop)" \ "$(answer "$HW" lid-closed)" \ "$(answer "$HW" external-monitor)" \ "$(answer "$HW" clamshell)" \ "$inhibited" } # Holds the inhibitor for as long as it runs. Exits immediately, holding # nothing, when this machine has no reason to want one -- a desktop has no lid # to inhibit and an undocked laptop should suspend normally. cmd_guard() { "$HW" laptop || exit 0 "$HW" external-monitor || exit 0 exec systemd-inhibit \ --what=handle-lid-switch \ --who=panama-lid \ --why="An external display is connected, so a closed lid is a docked machine rather than one being put away" \ --mode=block \ sleep infinity } # The internal panel's connector name, from the compositor: eDP, LVDS and # DSI are built in, everything else arrived through a cable -- the same rule # panama-hw and LidPolicy use. internal_connector() { hyprctl -j monitors all 2>/dev/null \ | jq -r '[.[] | select(.name | test("^(eDP|LVDS|DSI)"; "i"))][0].name // empty' } # Bound to the lid switch by keybinds.lua. Closing a DOCKED lid turns the # internal panel off, so nothing renders inside a closed shell and no # workspace strands on an invisible output; an undocked lid does nothing here # because logind is already suspending the machine. Failure direction: if the # disable never runs, the panel stays lit inside the shell -- wasteful, not # harmful. cmd_close() { "$HW" laptop || exit 0 "$HW" external-monitor || exit 0 local internal internal="$(internal_connector)" [[ -n "$internal" ]] || exit 0 hyprctl eval "hl.monitor({ output = \"$internal\", disable = true })" >/dev/null } # Re-enable on open, preferring what the person chose for this panel in # Settings (the same displays store monitors.lua reads at startup) and # falling back to the panel's preferred mode. cmd_open() { "$HW" laptop || exit 0 local internal entry mode scale internal="$(internal_connector)" [[ -n "$internal" ]] || exit 0 entry="$(jq -c --arg name "$internal" '.displays[$name] // empty' \ "${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json" 2>/dev/null)" if [[ -n "$entry" ]]; then mode="$(jq -r '.mode' <<<"$entry")" scale="$(jq -r '.scale' <<<"$entry")" hyprctl eval "hl.monitor({ output = \"$internal\", mode = \"$mode\", position = \"auto\", scale = $scale })" >/dev/null else hyprctl eval "hl.monitor({ output = \"$internal\", mode = \"preferred\", position = \"auto\", scale = \"auto\" })" >/dev/null fi } case "${1:-status}" in status) cmd_status ;; guard) cmd_guard ;; close) cmd_close ;; open) cmd_open ;; -h|--help) cat <<'USAGE' usage: panama-lid [status|guard|close|open] status what closing the lid would do right now, as JSON guard hold a handle-lid-switch inhibitor while an external display is connected; exits immediately on a machine that needs none close docked lid closed: turn the internal panel off (bound to the lid switch by keybinds.lua); does nothing undocked open lid opened: turn the internal panel back on, restoring the mode and scale chosen in Settings USAGE ;; *) printf 'panama-lid: unknown command: %s\n' "$1" >&2; exit 2 ;; esac