Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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."
```
6 changes: 5 additions & 1 deletion config/hypr/binds.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

-- -------------------------------------------------------------------------
Expand Down
14 changes: 13 additions & 1 deletion config/hypr/hypridle.conf
Original file line number Diff line number Diff line change
@@ -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.
}
Expand Down
2 changes: 1 addition & 1 deletion config/quickshell
Submodule quickshell updated 54 files
+10 −0 .github/workflows/smoke.yml
+22 −0 AGENTS.md
+4 −0 assets/icons/auth-fingerprint-symbolic.svg
+567 −0 docs/research/lock-client-death.md
+700 −0 docs/research/pam-policy-options.md
+156 −0 docs/research/reload-while-locked.md
+592 −0 docs/research/wlsessionlock-pam-semantics.md
+1 −1 modules/bar/StatusIcons.qml
+54 −0 modules/common/Appearance.qml
+151 −0 modules/lock/LockAuth.js
+299 −0 modules/lock/LockAuthArea.qml
+60 −0 modules/lock/LockBackground.qml
+67 −0 modules/lock/LockClock.qml
+71 −0 modules/lock/LockFingerprint.js
+160 −0 modules/lock/LockFingerprint.qml
+32 −0 modules/lock/LockModule.qml
+95 −0 modules/lock/LockPower.js
+119 −0 modules/lock/LockPowerControls.qml
+114 −0 modules/lock/LockProfile.qml
+54 −0 modules/lock/LockReveal.js
+135 −0 modules/lock/LockStatusCluster.qml
+290 −0 modules/lock/LockSurface.qml
+88 −0 scripts/dev-nested.sh
+56 −0 services/KeyboardLayout.qml
+33 −0 services/KeyboardLayoutParse.js
+488 −0 services/Lock.qml
+293 −0 services/LockLogic.js
+14 −0 shell.qml
+45 −0 spikes/reload-lock/README.md
+68 −0 spikes/reload-lock/edits.py
+150 −0 spikes/reload-lock/nested/LockModule.qml
+6 −0 spikes/reload-lock/nested/shell.qml
+149 −0 spikes/reload-lock/run.sh
+155 −0 spikes/reload-lock/shell.qml
+76 −0 tests/fixtures/lock-auth-area/shell.qml
+50 −0 tests/fixtures/lock-clock/shell.qml
+56 −0 tests/fixtures/lock-fingerprint/shell.qml
+43 −0 tests/fixtures/lock-power-controls/shell.qml
+35 −0 tests/fixtures/lock-status-cluster/shell.qml
+92 −0 tests/keyboard-layout.test.mjs
+93 −0 tests/lock-auth-area.sh
+127 −0 tests/lock-auth.test.mjs
+94 −0 tests/lock-clock.sh
+94 −0 tests/lock-clock.test.mjs
+108 −0 tests/lock-fingerprint.sh
+163 −0 tests/lock-fingerprint.test.mjs
+376 −0 tests/lock-logic.test.mjs
+140 −0 tests/lock-motion.test.mjs
+61 −0 tests/lock-power-controls.sh
+143 −0 tests/lock-power.test.mjs
+57 −0 tests/lock-reveal.test.mjs
+66 −0 tests/lock-status-cluster.sh
+137 −0 tests/lock-status-cluster.test.mjs
+110 −0 tests/lock-surface-composition.test.mjs
Loading