From 5c618f1514ec091a83abf39209be49e446518c97 Mon Sep 17 00:00:00 2001 From: Fimeg Date: Fri, 31 Jul 2026 21:25:05 -0400 Subject: [PATCH] lock: hoist the lock out of the Config.ready loaders so a reload can adopt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PanelLoader is a LazyLoader gated on Config.ready, behind a second one. On reload both are shut when quickshell propagates, so LazyLoader has no item to hand its successor and the new WlSessionLock never adopts the live compositor lock. Lock is a direct child of ShellRoot now, as the boot-order doc already asked. Ungated means Config may not have loaded, and touchKeypad reads false from the adapter defaults — which would bind the desktop surface on the phone and cannot be changed once the lock is active. The choice rides the reload too. --- surfaces/quickshell/modules/ii/lock/Lock.qml | 33 ++++++++++++++++++- .../panelFamilies/SouveraineFamily.qml | 17 +++++++++- surfaces/quickshell/shell.qml | 18 ++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) 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.