diff --git a/surfaces/quickshell/modules/ii/lock/Lock.qml b/surfaces/quickshell/modules/ii/lock/Lock.qml index 8b12cc4..ef81de6 100644 --- a/surfaces/quickshell/modules/ii/lock/Lock.qml +++ b/surfaces/quickshell/modules/ii/lock/Lock.qml @@ -116,7 +116,38 @@ LockScreen { } } - lockSurface: Config.options.lock.touchKeypad ? touchSurfaceComponent : desktopSurfaceComponent + // Which surface, decided WITHOUT waiting for the config file. + // + // `Config.options` is a JsonAdapter, so it answers with its QML defaults + // from the instant it exists — `touchKeypad` reads `false` until `onLoaded` + // flips `ready`. That is survivable at boot, where `initIfReady()` waits for + // `Config.ready` before requesting a lock, and fatal on a reload, where the + // lock is adopted at construction: the surface would bind to the DESKTOP + // keypad on the phone, and `WlSessionLock.surfaceComponent` cannot be + // changed while the lock is active — quickshell qCritical's and keeps the + // old one. The result is a lock screen that will not take your PIN, which + // is the worst of the three failures on this path because you cannot get + // back in. + // + // So the last known-good answer rides through the reload in-process, and + // Config takes over the moment it is genuinely loaded. + PersistentProperties { + id: lockPrefs + reloadableId: "souveraineLockPrefs" + property bool touchKeypad: false + } + + Connections { + target: Config + function onReadyChanged() { + if (Config.ready) + lockPrefs.touchKeypad = Config.options.lock.touchKeypad; + } + } + + lockSurface: (Config.ready ? Config.options.lock.touchKeypad : lockPrefs.touchKeypad) + ? touchSurfaceComponent + : desktopSurfaceComponent property Component desktopSurfaceComponent: LockSurface { context: root.context diff --git a/surfaces/quickshell/panelFamilies/SouveraineFamily.qml b/surfaces/quickshell/panelFamilies/SouveraineFamily.qml index 82e2063..538f158 100644 --- a/surfaces/quickshell/panelFamilies/SouveraineFamily.qml +++ b/surfaces/quickshell/panelFamilies/SouveraineFamily.qml @@ -47,7 +47,22 @@ Scope { PanelLoader { component: Background {} } PanelLoader { component: Cheatsheet {} } PanelLoader { extraCondition: Config.options.dock.enable; component: Dock {} } - PanelLoader { component: Lock {} } + // Lock is NOT here. It is a direct child of ShellRoot in shell.qml. + // + // It lived behind this LazyLoader until 2026-07-31, and that is the whole + // of TASK-48's shell half. `PanelLoader` is `active: Config.ready`, and + // this family sits behind a second `Config.ready` LazyLoader — so on a + // scene reload both gates are shut at the moment quickshell propagates + // reloads. `LazyLoader::onReload` finds `mItem == nullptr`, skips + // propagation entirely, and incubates a fresh Lock milliseconds later with + // no predecessor. The new `WlSessionLock` therefore never adopts the + // outgoing one's compositor lock, and the reload either crashes on the + // denied re-acquire or sends `unlock_and_destroy` and drops the session. + // + // No amount of `reloadableId` fixes that: the object was not there to be + // matched. `session-authority-boot-order.md` Phase A.1 already called for + // this hoist, for the adjacent reason — nothing may render before the lock + // decision. `BootBloom` is hoisted in shell.qml on exactly this argument. PanelLoader { component: MediaControls {} } // Desktop notifications belong in the right-hand notification panel. // Keeping a transient popup surface loaded on desktop let some shell diff --git a/surfaces/quickshell/shell.qml b/surfaces/quickshell/shell.qml index 55b9ef3..7b1c030 100644 --- a/surfaces/quickshell/shell.qml +++ b/surfaces/quickshell/shell.qml @@ -12,6 +12,7 @@ // is owned outright. import "modules/common" +import "modules/ii/lock" import "modules/souveraine/boot" import "services" import "panelFamilies" @@ -36,6 +37,23 @@ ShellRoot { // master on its first frame; LockScreen clears GlobalStates.bootBloomActive. BootBloom {} + // The lock, top-level and ungated, for both of the reasons the docs give. + // + // Boot (`session-authority-boot-order.md` Phase A.1): no surface may + // precede the lock decision. Behind `Config.ready` it could, and once did — + // an NM password dialog rendered before the lockscreen. + // + // Reload (TASK-48): a `LazyLoader` whose `active` is false when quickshell + // propagates reloads has no item to hand its successor, so the new + // `WlSessionLock` never adopts the live compositor lock. As a direct child + // of `ShellRoot` — itself a `ReloadPropagator` — this matches its + // predecessor positionally, with no `reloadableId` needed, and the adopt + // path in `LockScreen.qml` finally has something to adopt. + // + // It must stay a direct child. Wrapping it in any conditional loader + // reintroduces both failures at once. + Lock {} + // A normal XDG app window in this process. Its desktop/sidebar launchers // call the `settings` IPC target; no second Quickshell/session scope is // created.