Skip to content

Leave a locked session's display to the lock screen - #9152

Open
Pillumz wants to merge 1 commit into
omacom:quattrofrom
Pillumz:fix/idle-no-wake-while-locked
Open

Leave a locked session's display to the lock screen#9152
Pillumz wants to merge 1 commit into
omacom:quattrofrom
Pillumz:fix/idle-no-wake-while-locked

Conversation

@Pillumz

@Pillumz Pillumz commented Aug 30, 2026

Copy link
Copy Markdown

What happens

Lock the session and let the lock screen blank the display. Later the panel comes back on at the lock screen with nobody at the machine, and stays on until someone touches the keyboard or mouse - on an OLED, that is the lock screen burning in for hours. omarchy debug idle shows a single idle-monitor: activeidle-cycle-cancel: activityprocess-start: wake omarchy-system-wake with no input anywhere around it.

Why

The idle service treats its IdleMonitor resuming as user activity and runs omarchy-system-wake, which turns DPMS back on. But the monitor is respectInhibitors: true, and Hyprland's ext-idle-notify implementation sends resumed to an idled notification the moment an idle inhibitor appears (src/protocols/IdleNotify.cpp: update() while inhibited calls reset(), which sends resumed). So any client taking an idle inhibitor - a browser starting media on a background workspace, a wake lock, an XWayland app suspending the screensaver - counts as activity with nobody there.

Unlocked that is harmless. Locked it is the whole bug: the lock screen only re-arms its blank timer for pointer, click or key events delivered to its own surface, and none happened, so the display stays lit indefinitely.

Reproduced on Hyprland 0.56.2 / Omarchy 4.0.1 with nobody at the desk: lock, wait for idle-monitor: idle, then start a silent mpv clip into the locked session from SSH. Hyprland logs New idle inhibitor registered, the idle service logs active and runs the wake in the same second, the panel lights, and it is still lit after mpv exits. An evdev witness on every input device saw nothing throughout. The same run before the monitor idles does nothing, as the Hyprland code predicts.

Fix

While the session is locked, the display belongs to the lock screen, so cancelIdleCycle() no longer runs omarchy-system-wake; it logs wake-skipped: session-locked instead and still ends the cycle. The decision lives in IdleModel.wakeAfterIdle() so it is unit-testable, and sessionLocked is added to omarchy-shell idle status since omarchy debug idle prints that and not the lock's status.

The lock state comes from the lock service in-process, shell.firstPartyServiceFor("omarchy.lock"), the same call the Stay Awake, DND, night-light and media widgets use for their services. The omarchy-shell lock isLocked shell-out on the screensaver line answers from the same locked property, but only after a subprocess round trip, and it reads as unlocked whenever the IPC call fails; a synchronous decision needs the direct read. The idle service never blanks a display itself - the only DPMS-off in the tree is the lock's own blank timer - so skipping its wake cannot strand a display that nothing else can restore: Hyprland's key_press_enables_dpms / mouse_move_enables_dpms light the panel on keys, motion and gestures, the lock surface's own click and key handlers run the lock's wake, and finishUnlock runs it unconditionally (fingerprint unlock at a blanked panel included). Clamshell reconciliation, which omarchy-system-wake also does, has the lid switch bind, omarchy-hyprland-monitor-watch and the sleep lock as independent owners.

Why this layer: making the lock screen re-blank on a resume it did not ask for would still flash the panel; refusing inside omarchy-system-wake while locked would break the lock's own wake, which runs the same command; the Hyprland behaviour is protocol-legal and not ours to change; and respectInhibitors: false would bring the screensaver back over video playback.

One deliberate consequence: Hyprland does not enable DPMS for scroll (axis) or bare button events, only for keys, motion and gesture starts, so a scroll-wheel nudge at a blanked lock screen used to light the panel only through this accidental idle wake (and then never blanked again). After this change it does nothing; a click or any pointer motion still wakes. If a scroll wake is wanted, it belongs in LockView as a wheel handler that emits wakeRequested, which I am happy to follow up with.

#7131 adds a byte-identical lockService line with its own comment and a lockInFlight property; #7132 reads the same service through firstPartyServiceFor inside a function. Whichever lands second conflicts in the property block and in statusJson, and the two names should collapse into one.

Tests

  • test/shell.d/idle-test.sh: unit cases for wakeAfterIdle, plus source assertions that keep the wiring honest on machines where the compositor fixture has to skip.
  • test/shell.d/idle-lock-wake-test.sh (new, compositor-backed like the lock fixtures): loads the real Service.qml into a throwaway quickshell with a stand-in shell whose services register by reassignment, as shell.qml does. The lock service is registered after the idle service and the fixture checks sessionLocked follows, so a future serviceFor that caches would fail here rather than silently disable the fix. Then it drives handleActiveSignal() twice: locked, no wake process is spawned and the skip is logged; unlocked, the wake process spawns and a PATH-shadowed omarchy-system-wake actually runs (marker file). The fake config uses day-long timeouts and the service is destroyed once the wake exits, so an orphaned fixture can never lock the developer's session; the test skips if the login shell would not resolve the shadow (a machine dev-linked to another checkout); the launch is under timeout 15. Against the unpatched service the fixture fails on all eight assertions.
  • ./test/shell: the same six pre-existing environmental failures with and without this change (packaging checkout, Claude usage collector, network guard, bar icon geometry); everything else passes.

The idle service runs omarchy-system-wake whenever its idle monitor reports
activity during a cycle it started. While the session is locked that is the
wrong reflex: the lock screen already lights the panel for input that reaches
it and blanks it again five seconds later, and the idle monitor resuming is
not evidence of input there. Hyprland's ext-idle-notify sends "resumed" the
moment an idle inhibitor appears (IdleNotify.cpp, update() -> reset() while
inhibited), so a browser starting media on a background workspace, or any
client taking a wake lock, wakes a blanked lock screen with nobody at the
machine - and nothing blanks it again until someone touches it. On an OLED
that is the lock screen burning in overnight.

Gate the wake on the lock service's state, read in-process the way the bar
indicators read theirs, and log the skipped wake so `omarchy debug idle`
shows what happened. The cycle itself still ends; only the display is left
alone. Reading the lock in-process keeps the answer synchronous: the
shell-out on the screensaver line reports the same locked property, but only
after a round trip, and reads as unlocked whenever the call fails.

Reproduced on Hyprland 0.56.2 / Omarchy 4.0.1 by locking, waiting for the
monitor to idle, then playing a silent mpv clip into the locked session: the
panel lit within a second and stayed lit after mpv exited, with zero evdev
input in between. The new fixture test drives the real Service.qml against
the compositor with a stand-in lock service registered after the idle
service, the way shell.qml may load them, and a shadowed omarchy-system-wake:
locked, no wake process is spawned; unlocked, the command runs as before.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant