Skip to content

Session lock/blank awareness: protocol, client UI, and input gate (GNOME) #160

Description

@jonocodes

Session lock/blank awareness: protocol, client UI, and input gate (GNOME)

When GNOME locks or blanks the screen, deckd has no idea. global.display.focus_window goes null, the extension's activeWindowPayload (packaging/gnome-shell/deckd-focus@local/wire-shape.js:26) returns all-nulls, the daemon misses every layout and serves default with is_default: true. The phone shows Home and every press silently does nothing. The client cannot distinguish "screen locked" from "desktop with nothing focused".

Two things make this more than cosmetic:

  • shell: / url: actions still execute while locked. They're subprocesses the daemon spawns; they never need window focus. Launching an app "works" — it just opens behind the shield, so it reads as broken while actually being a lock-screen bypass for arbitrary command execution from any phone that can reach the daemon.
  • key: / type: injection lands on the unlock prompt. uinput is a kernel-level virtual device; the shield cannot tell it from a real keyboard.

So the fix is a daemon-side policy that the UI honestly mirrors — not a client-side banner.

This issue carries the platform-neutral core (protocol, client UI, daemon gate, capability plumbing) plus the GNOME detection. macOS detection is a follow-up that plugs into the same seam.

Two states, not one

The obvious design — one locked boolean driven by org.gnome.ScreenSaver.GetActive — is wrong, because GetActive goes true for a blank that hasn't locked yet, and stays true forever when Automatic Screen Lock is off. Gating input on it would mean deckd refuses all input whenever the screen is blank, and you'd lose the ability to wake your desktop from your phone, which works today.

Split them:

  • blanked — the session isn't on the screen. Drives the client UI takeover only.
  • locked — credentials required. Drives the daemon-side refusal.

Blanked-but-not-locked keeps input enabled: the first press wakes the machine (uinput events are indistinguishable from real hardware to the shield). Show a soft "Screen asleep — press anything to wake" rather than the lock takeover.

blanked locked
GNOME org.gnome.ScreenSaver ActiveChanged (prime with GetActive) org.freedesktop.login1.Session LockedHint
KDE org.freedesktop.ScreenSaver login1 LockedHint
macOS — (see follow-up) — (see follow-up)

login1's LockedHint is the cross-DE half and also catches loginctl lock-session. Verified present on a live GNOME session: gdbus call --session --dest org.gnome.ScreenSaver … GetActive answers.

Wire

protocol.py:95 already defines StateMessage { locked: bool } — exported to TS, wired to nothing. Use it; add blanked. Push on transition and in the initial connect snapshot, so a phone that joins mid-lock isn't stuck showing a stale layout.

Naming collision — fix this first

App.tsx:65 already maps socket status unauthorized → the label "locked" (the deckd password gate). Adding screen-lock with the same word makes "Locked" ambiguous between type the deck password and unlock your desktop. Rename the auth one to "sign-in needed"; reserve "Screen locked" for the session state. Add both terms to CONTEXT.md's Language section.

Client UI

Full-surface takeover, not a banner — the grid is entirely dead, and a greyed grid still invites taps.

  • Lock glyph, "Screen locked", and "Controls resume when you unlock ‹host›". Name the host: it matters when driving two machines from one phone.
  • Keep the connection dot green. The socket is fine. Reusing the disconnected treatment sends people off to restart the daemon.
  • App badge reads "Screen locked" in place of the app name.

Gate per-capability, not globally — split by whether the surface targets the focused window:

Blocked while locked Still works
Layout buttons, jogstrip, trackpad/keyboard, raise-window Now playing (MPRIS), settings, layout editor

Now Playing is deliberate, not an oversight: media players keep running, MPRIS still accepts commands, and a locked screen is exactly when you want play/pause from your phone — same as hardware media keys, which work on a lock screen. Settings is device-local and the editor writes YAML to disk; both are unaffected by lock.

Running programs list: keep the button, disable it, reuse the existing windows-empty slot (RunningWindowsList.tsx:47) with "Screen locked — unlock ‹host› to switch windows." ListWindows still enumerates fine while locked, so a populated list that silently refuses to raise anything is the worst option — but a control that vanishes and reappears reads as a bug. A disabled control with a stated reason teaches the model.

Enforcement

Refuse in _dispatch_press and the key/type/pad/jog paths, next to the existing _injection_blocked guard (server.py:2454) — that guard is already the precedent for "pressed but suppressed" and records a guard_dropped outcome for diagnostics. Reply with an ErrorMessage so a client that missed the state transition still surfaces something real. Add --allow-while-locked to opt out.

Capability plumbing

Add session_lock / session_blank to PlatformBackend.capabilities() and a row to the capability matrix in docs/PLATFORM-PARITY.md. A backend that can't observe lock state must not advertise it, so the client never strands itself in a lock view it can't clear.

tests/test_platform_parity.py only guards the three compositor-axis rows via its _AXIS map, so a new row won't be checked by default — extend _AXIS to cover it, otherwise this adds exactly the doc/code drift that test exists to catch.

Acceptance

  • Locking GNOME flips the phone to the lock view within a focus tick; unlocking restores the previous layout.
  • Blank-with-auto-lock-off shows the "asleep" view and a press wakes the machine.
  • While locked, a shell: button is refused daemon-side and the client shows why.
  • Now Playing still controls a running player while locked.
  • Running-programs button is disabled with the lock-specific empty state.
  • A phone connecting during a lock gets the state in its connect snapshot.
  • --allow-while-locked restores today's behaviour.
  • Parity matrix row added and guarded by _AXIS.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestfocus-watcherActive-window / app-awareness detectionhuman-verification-requiredCode is complete; a human must verify on real hardware / a live session before closinginput-injectionSynthetic input via uinput or platform equivalent

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions