Skip to content

macOS: detect session lock and feed the session-state seam #161

Description

@jonocodes

macOS: detect session lock and feed the session-state seam

Follow-up to #160, which carries the protocol (StateMessage), the client lock view, the daemon-side input gate, and the session_lock / session_blank capability plumbing. This issue is detection only: implement the macOS half behind that seam.

Depends on #160 landing first.

What happens on a Mac today

A different failure mode from GNOME, and slightly worse. _FOCUS_SCRIPT (platform_macos.py:103) asks System Events for the frontmost process. Under the lock screen that's loginwindow — a non-null identity, not GNOME's all-nulls. It matches no layout, so the phone still lands on Home, but focused_app.app_id = "loginwindow" flows through and the is_default program-suffix (App.tsx:486) renders the badge as "Home (loginwindow)". So the Mac doesn't merely fail to say "locked" — it puts a mystery process name on screen.

Detection: CGSessionCopyCurrentDictionary

Use Quartz.CGSessionCopyCurrentDictionary() and check for the CGSSessionScreenIsLocked key. It fits this backend unusually well:

  • Quartz is already lazily imported (_load_quartz, platform_macos.py:56) — no new dependency.
  • No permission grant, unlike the Accessibility consent dance MacKeySink._check_accessibility has to do for injection.
  • It's a cheap dict read, so it hangs off the existing ~100ms focus poll rather than needing new machinery.

The push-based alternative is NSDistributedNotificationCenter with com.apple.screenIsLocked / com.apple.screenIsUnlocked. Skip it — it needs a live CFRunLoop, and bridging that into the asyncio daemon costs more than it saves when the focus loop is already polling.

Do not implement blanked on macOS

There's no equivalent dict key for "screensaver running". It would need com.apple.screensaver.didstart (same runloop problem) or shelling out to pgrep ScreenSaverEngine. Advertise session_lock only and let the absence of session_blank mean the Mac never shows the "asleep" view — the repo's own rule that absence is a designed empty state, not a crash.

Note this makes macOS's screensaver/lock split genuinely independent in a way GNOME's isn't: with "require password after screensaver" set to anything but immediately, the screensaver runs while the session stays unlocked and input just dismisses it. Gating on locked alone is the correct behaviour there.

Fast user switching — cheap win, same dict

While in that dictionary: kCGSSessionOnConsoleKey false means another user is at the console. deckd's session is alive but nothing it does reaches a screen. Same UI takeover, different copy — "Another user is signed in." One extra key lookup, and it closes a state that would otherwise be indistinguishable from a freeze.

Security note: half of this is already handled

macOS Secure Event Input blocks osascript keystroke and synthetic CGEvents from unprivileged processes while the lock screen's password field has focus. So the keystroke-injection half of #160's threat model is already covered by the OS — the Linux uinput exposure is the sharper one.

But shell: and url: actions are identical on both platforms: subprocesses the daemon spawns, never touching the event system, running fine under a lock screen. The daemon-side gate still earns its place on macOS; it's the command-execution half it's buying.

Verification

Everything here is unverified. It was designed from the code on a Linux box; nobody has run CGSessionCopyCurrentDictionary against a live locked Mac. Per docs/PLATFORM-PARITY.md's evidence levels this lands as unverified and needs a dated hardware run in the macOS table before it can be treated as working. Apply human-verification-required when the code is complete.

Acceptance

  • Locking a Mac flips the phone to the lock view; unlocking restores the previous layout.
  • MacFocusBackend.capabilities() advertises session_lock and not session_blank.
  • Fast-user-switch away shows the "another user is signed in" state.
  • A shell: button is refused daemon-side while locked.
  • "Home (loginwindow)" can no longer appear on the badge.
  • Capability matrix row added in docs/PLATFORM-PARITY.md.
  • Dated hardware run recorded in the macOS verification table.

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 detection

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions