From f329db3020d40362961d52b91498941cb8a56f05 Mon Sep 17 00:00:00 2001 From: Bui Dai Date: Wed, 29 Jul 2026 21:06:20 +0700 Subject: [PATCH 1/3] feat(hypr): cut the session over to the Quickshell locker The lock intent took three paths: the keybind ran hyprlock directly and bypassed logind, the idle timeout went through hypridle's lock_cmd, and pre-sleep went through loginctl. Nothing converged and LockedHint was never set. The keybind now goes through loginctl lock-session, so all three raise one dbus lock event and share one lock_cmd and one fallback chain. lock_cmd asks Quickshell to lock and matches the returned value: qs ipc call prints `Target not found.` and exits 0 when the shell is alive but the lock module failed to load, so exit status cannot distinguish that from success. hyprlock stays as the last link for the one case where a fallback is real -- the shell already dead when the lock is requested. inhibit_sleep is pinned to 3 so suspend waits for a compositor-confirmed lock. auto resolves to 1 here, because hypridle chooses by detecting whether hyprlock is launched before sleep and this config's pre-sleep command is loginctl lock-session -- dropping the inhibitor the moment loginctl returns, which is precisely the race. The four idle timeouts are unchanged. Refs #71 --- README.md | 55 +++++++++++++++++++++++++++++++++++++++ config/hypr/binds.lua | 6 ++++- config/hypr/hypridle.conf | 14 +++++++++- 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b4e6ae3..fec1f59 100644 --- a/README.md +++ b/README.md @@ -208,3 +208,58 @@ 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. 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. } From 539a58adfb9507d5af86b7666f0fb1d5ad96cdde Mon Sep 17 00:00:00 2001 From: Bui Dai Date: Wed, 29 Jul 2026 21:06:52 +0700 Subject: [PATCH 2/3] chore: bump quickshell submodule to the lock screen Without the pointer bump a fresh machine still checks out the old commit, and the lock_cmd added alongside would find no lock target and fall through to hyprlock on every lock. Refs #71 --- config/quickshell | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 78c89ddb22bd53fd5cd545c2db64adfa69a89afc Mon Sep 17 00:00:00 2001 From: Bui Dai Date: Wed, 29 Jul 2026 21:10:46 +0700 Subject: [PATCH 3/3] docs: note that pulling the shell submodule does not reliably reload it Observed during the cutover pull: the watcher fired part-way through the checkout, failed on a module that had not landed yet, and then went quiet, leaving the old generation serving with no sign of a problem. Refs #71 --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index fec1f59..48ad88a 100644 --- a/README.md +++ b/README.md @@ -263,3 +263,16 @@ 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." +```