diff --git a/README.md b/README.md index b4e6ae3..48ad88a 100644 --- a/README.md +++ b/README.md @@ -208,3 +208,71 @@ write audit records — the tally and the lockout both still work, and Quickshel produce the same line for the same reason. And `pam_unix` does not log successful authentications at the default level, so a clean run leaves no positive trace in the journal; absence of a success line is not evidence of failure. + +## Session locking + +`loginctl lock-session` is the single entry point. The keybind, the 300 s idle timeout +and pre-sleep all raise the same dbus lock event, so there is one lock command, one +fallback chain, and logind's `LockedHint` is set whichever path fired. Nothing invokes a +locker directly, and nothing calls `qs ipc call` and then suspends. + +hypridle's `lock_cmd` asks Quickshell to lock and **matches the returned value**, not the +exit status. Measured on this machine: `qs ipc call` prints `Target not found.` and exits +**0** when the shell is alive but the lock module failed to load, and only exits 255 when +no instance is running at all — so exit status cannot tell that failure apart from +success. `grep -qx true` can. + +`inhibit_sleep = 3` makes suspend wait for a compositor-confirmed session lock. `auto` +resolves to 1 here — hypridle picks between the two by detecting whether hyprlock is +launched before sleep, and this config's pre-sleep command is `loginctl lock-session` — +which would drop the inhibitor the moment `loginctl` returns, before anything is on +screen. Pinning to 3 closes that race at the cost of the lock/unlock hooks, which are +unused. + +**hyprlock stays installed**, with `config/hypr/hyprlock.conf` and `/etc/pam.d/hyprlock` +left alone. It is the last link in `lock_cmd` for the one case where a fallback is real: +the shell already dead when the lock is *requested*, so nothing holds the lock yet and no +compositor flag is involved. Once Quickshell holds the lock, no second client can take it +over — see `config/quickshell/docs/research/lock-client-death.md` for recovery from a +session left locked with no client. + +Hyprland's session-lock restore (`misc:allow_session_lock_restore`) stays **off**. It +would let a new lock client adopt an existing lock, which is a way back into a locked +session rather than a way out of a dead one. + +### One-time steps on a new machine + +The PAM install line above, plus the avatar the lock screen reads: + +```bash +ln -sf ~/Pictures/profile/bird_profile.png ~/.face +``` + +`~/.face` is the convention SDDM, GDM and LightDM already read, so one file drives the +lock screen and any future display manager instead of letting them drift. Missing or +unreadable, the lock screen draws a themed `person` symbol rather than a broken image. + +### Applying a change + +`hypridle.conf` and `binds.lua` are read at startup, not watched: + +```bash +pkill hypridle; setsid hypridle >/dev/null 2>&1 & # autostarted by Hyprland, not a user unit +hyprctl reload # picks up binds.lua +``` + +`hyprctl dispatch exec hypridle` does **not** work here, for the reason given in the safe +test procedure above. + +Pulling the `config/quickshell` submodule does **not** reliably reload the running shell. +Observed on the cutover pull: the watcher fired part-way through the checkout, failed with +`module "qs.modules.lock" is not installed` because `shell.qml` had landed before the +module directory, and then went quiet — leaving the shell serving the *old* generation with +no sign anything was wrong. git also writes many files by rename, which this watcher +ignores. Force a clean reload by rewriting one file's contents in place, and confirm it took: + +```bash +cd ~/.config/quickshell && cp shell.qml /tmp/s && cat /tmp/s > shell.qml +qs log | tail -5 # want a trailing "Configuration Loaded", not an error +qs ipc call lock isLocked # want "false", not "Target not found." +``` diff --git a/config/hypr/binds.lua b/config/hypr/binds.lua index f9db46e..25ebcdb 100644 --- a/config/hypr/binds.lua +++ b/config/hypr/binds.lua @@ -16,7 +16,11 @@ hl.bind("SUPER + D", hl.dsp.exec_cmd("discord")) -- ------------------------------------------------------------------------- -- Session Actions -- ------------------------------------------------------------------------- -hl.bind(meh .. " + L", hl.dsp.exec_cmd("hyprlock")) +-- Through logind rather than a locker directly, so the keybind, the idle timeout and +-- pre-sleep all converge on one dbus lock event, one lock_cmd and one fallback chain — +-- and logind's LockedHint gets set. Routing through hypridle is the cost; a dead +-- hypridle already means no idle lock and no lock before suspend. +hl.bind(meh .. " + L", hl.dsp.exec_cmd("loginctl lock-session")) hl.bind(meh .. " + N", hl.dsp.exec_cmd("makoctl dismiss -a")) -- ------------------------------------------------------------------------- diff --git a/config/hypr/hypridle.conf b/config/hypr/hypridle.conf index c09beb0..3f329a8 100644 --- a/config/hypr/hypridle.conf +++ b/config/hypr/hypridle.conf @@ -1,5 +1,17 @@ +# `auto` resolves to 1 ("wait for before_sleep_cmd") here, because hypridle picks +# between 1 and 3 by detecting whether hyprlock is launched before sleep — and this +# config's pre-sleep command is `loginctl lock-session`. The inhibitor would drop the +# moment loginctl returns, before anything is on screen. Pinning to 3 waits for a +# compositor-confirmed session lock instead. Cost: it disables the lock/unlock hooks, +# neither of which is used here. general { - lock_cmd = pidof hyprlock || hyprlock # avoid starting multiple hyprlock instances. + inhibit_sleep = 3 + # Matched on the returned value, not the exit status: `qs ipc call` exits 0 even + # for `Target not found.`, so exit status cannot tell a shell whose lock module + # failed to load from a successful lock. This is the one place a hyprlock fallback + # is real — the shell is already dead when the lock is requested, so nothing holds + # the lock yet. `pidof` prevents a double launch. + lock_cmd = qs ipc call lock lock | grep -qx true || pidof hyprlock || hyprlock before_sleep_cmd = loginctl lock-session # lock before suspend. after_sleep_cmd = hyprctl dispatch 'hl.dsp.dpms({ action = "enable" })' # to avoid having to press a key twice to turn on the display. } diff --git a/config/quickshell b/config/quickshell index d5e8285..1e19de2 160000 --- a/config/quickshell +++ b/config/quickshell @@ -1 +1 @@ -Subproject commit d5e828539a3fdae2aace0d89032e11b814d1bac0 +Subproject commit 1e19de2e39bc2b0fd7458470789bd801baf19f8e