Files
Panama/config/dot/quickshell/services/LidPolicy.qml
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

79 lines
3.4 KiB
QML

pragma Singleton
// ─────────────────────────────────────────────────────────────────────────────
// Closing the lid on a docked machine should not put it to sleep.
//
// logind handles the lid correctly except for one case it cannot see: an
// external display means a closed lid is a docked machine rather than one
// being put in a bag. Its own "docked" test looks for an ACPI docking station,
// which 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 disconnects; logind does the rest, including the suspend, and
// hypridle's before_sleep_cmd already locks the session on the way down.
//
// This service exists only to notice the topology changing. The decision
// itself is in bin/panama-lid, so a machine with no lid and no external
// display costs one process start that exits immediately.
//
// Failure is safe in the direction that matters. If the guard dies, or this
// service never starts, the inhibitor is released and logind's default returns:
// a docked laptop suspends when the lid closes, which is merely annoying. The
// alternative design -- a logind 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 the building.
// ─────────────────────────────────────────────────────────────────────────────
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-lid"
// Whether the inhibitor is currently held. Reported by the Power page and
// by panama-doctor; nothing depends on it to make a decision.
readonly property bool inhibited: guard.running
// Same rule panama-hw uses: eDP, LVDS and DSI are the built-in panel and
// everything else arrived through a cable. Computed here rather than asked
// of the helper so a topology change is noticed without spawning anything.
readonly property bool externalConnected: {
const screens = Quickshell.screens ?? [];
for (const screen of screens) {
const name = String(screen?.name ?? "");
if (name === "")
continue;
if (!/eDP|LVDS|DSI/i.test(name))
return true;
}
return false;
}
onExternalConnectedChanged: root.reconcile()
function reconcile(): void {
// Restarting rather than toggling: the helper re-checks whether this
// machine is a laptop at all, and a process that is already holding
// the right inhibitor costs nothing to leave alone.
if (root.externalConnected) {
if (!guard.running)
guard.running = true;
} else if (guard.running) {
guard.running = false;
}
}
// Long-lived by design: it holds the inhibitor for as long as it runs, and
// exits immediately on a machine that should not have one.
Process {
id: guard
command: [root.helperPath, "guard"]
}
Component.onCompleted: root.reconcile()
}