diff --git a/bin/omarchy-system-sleep-lock b/bin/omarchy-system-sleep-lock index 2ebf79c1..ec4d5748 100755 --- a/bin/omarchy-system-sleep-lock +++ b/bin/omarchy-system-sleep-lock @@ -10,12 +10,17 @@ if [[ ! $wait_attempts =~ ^[0-9]+$ ]] || (( wait_attempts < 1 )); then wait_attempts=100 fi -omarchy-hyprland-monitor-internal recover >/dev/null 2>&1 || true -omarchy-hyprland-monitor-internal-mirror recover >/dev/null 2>&1 || true +recover_internal_monitor_toggles() { + omarchy-hyprland-monitor-internal recover >/dev/null 2>&1 || true + omarchy-hyprland-monitor-internal-mirror recover >/dev/null 2>&1 || true +} +recover_internal_monitor_toggles omarchy-shell lock lock >/dev/null 2>&1 || exit 1 for (( attempt = 0; attempt < wait_attempts; attempt++ )); do + (( attempt % 5 == 0 )) && recover_internal_monitor_toggles + status=$(omarchy-shell lock status 2>/dev/null || true) if jq -e '.secure == true' <<<"$status" >/dev/null 2>&1; then diff --git a/shell/plugins/lock/Service.qml b/shell/plugins/lock/Service.qml index 06d6a7b0..ac1bed40 100644 --- a/shell/plugins/lock/Service.qml +++ b/shell/plugins/lock/Service.qml @@ -17,6 +17,7 @@ Item { readonly property string currentBackgroundLink: stateHome + "/omarchy/current/background" property bool lockRequested: false + property bool pendingSessionLock: false property bool authenticatingPassword: false property bool fingerprintAuthenticating: false property bool passwordPamConfigured: false @@ -34,6 +35,37 @@ Item { readonly property bool locked: lockRequested || sessionLock.locked || sessionLock.secure readonly property bool authenticating: authenticatingPassword || fingerprintAuthenticating + function realScreenCount() { + var screens = Quickshell.screens || [] + var count = 0 + + for (var i = 0; i < screens.length; i++) { + var screen = screens[i] + if (screen && screen.name && screen.width > 0 && screen.height > 0) count += 1 + } + + return count + } + + function hasRealScreen() { + return realScreenCount() > 0 + } + + function requestSessionLock() { + if (!lockRequested || sessionLock.locked || sessionLock.secure) return + + if (!hasRealScreen()) { + if (!pendingSessionLock) logEvent("lock-pending: no-real-screen") + pendingSessionLock = true + if (!pendingSessionLockTimer.running) pendingSessionLockTimer.start() + return + } + + pendingSessionLock = false + pendingSessionLockTimer.stop() + sessionLock.locked = true + } + function refreshBackground() { if (!readlinkProc.running) readlinkProc.running = true } @@ -68,9 +100,9 @@ Item { resetAuthenticationState() lockRequested = true - sessionLock.locked = true idleBlankTimer.restart() logEvent("lock-requested") + requestSessionLock() Qt.callLater(function() { root.refreshBackground() @@ -84,6 +116,8 @@ Item { if (!root.locked && !lockRequested) return lockRequested = false + pendingSessionLock = false + pendingSessionLockTimer.stop() resetAuthenticationState() idleBlankTimer.stop() sessionLock.locked = false @@ -161,14 +195,25 @@ Item { onSecureStateChanged: { root.logEvent("secure=" + secure) - if (secure) root.startFingerprint() + if (secure) { + root.pendingSessionLock = false + pendingSessionLockTimer.stop() + root.startFingerprint() + } } onLockStateChanged: { root.logEvent("session-locked=" + locked) + if (locked) { + root.pendingSessionLock = false + pendingSessionLockTimer.stop() + } + if (!locked && root.lockRequested) { root.lockRequested = false + root.pendingSessionLock = false + pendingSessionLockTimer.stop() root.resetAuthenticationState() root.runWake() } @@ -316,6 +361,18 @@ Item { onTriggered: if (root.lockRequested && !root.authenticating) root.runBlank() } + Timer { + id: pendingSessionLockTimer + interval: 100 + repeat: true + onTriggered: root.requestSessionLock() + } + + Connections { + target: Quickshell + function onScreensChanged() { root.requestSessionLock() } + } + onAuthenticatingChanged: { if (!lockRequested) return if (authenticating) idleBlankTimer.stop() @@ -353,8 +410,10 @@ Item { return JSON.stringify({ locked: root.locked, requested: root.lockRequested, + pending: root.pendingSessionLock, sessionLocked: sessionLock.locked, secure: sessionLock.secure, + realScreens: root.realScreenCount(), passwordPam: root.passwordPamConfigured, fingerprint: root.fingerprintConfigured, authenticating: root.authenticating, diff --git a/test/shell.d/monitor-recovery-test.sh b/test/shell.d/monitor-recovery-test.sh index 7ff6c361..e11c6f39 100755 --- a/test/shell.d/monitor-recovery-test.sh +++ b/test/shell.d/monitor-recovery-test.sh @@ -6,6 +6,7 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" monitor_watch="$ROOT/bin/omarchy-hyprland-monitor-watch" sleep_lock="$ROOT/bin/omarchy-system-sleep-lock" +lock_service="$ROOT/shell/plugins/lock/Service.qml" grep -F 'recover_after_monitor_removal' "$monitor_watch" >/dev/null grep -F 'sleep 1' "$monitor_watch" >/dev/null @@ -15,4 +16,10 @@ pass "monitor watcher retries internal monitor recovery after removal" grep -F 'omarchy-hyprland-monitor-internal recover >/dev/null 2>&1 || true' "$sleep_lock" >/dev/null grep -F 'omarchy-hyprland-monitor-internal-mirror recover >/dev/null 2>&1 || true' "$sleep_lock" >/dev/null -pass "sleep lock recovers internal monitor toggles before suspend" +grep -F '(( attempt % 5 == 0 )) && recover_internal_monitor_toggles' "$sleep_lock" >/dev/null +pass "sleep lock recovers internal monitor toggles while waiting for secure lock" + +grep -F 'lock-pending: no-real-screen' "$lock_service" >/dev/null +grep -F 'function onScreensChanged() { root.requestSessionLock() }' "$lock_service" >/dev/null +grep -F 'realScreens: root.realScreenCount()' "$lock_service" >/dev/null +pass "lock service defers session lock until a real screen exists"