Skip to content

Display pinning: park on sleep, retry-on-wake with backoff#3

Open
imaznation wants to merge 1 commit into
kemalandic:mainfrom
imaznation:proposal/display-pinning-robustness
Open

Display pinning: park on sleep, retry-on-wake with backoff#3
imaznation wants to merge 1 commit into
kemalandic:mainfrom
imaznation:proposal/display-pinning-robustness

Conversation

@imaznation
Copy link
Copy Markdown

Symptom

On a multi-monitor kiosk setup, after sleep/wake or screen-lock/unlock, the kiosk window can end up on the main display instead of the configured secondary target. macOS surfaces the wake notifications before NSScreen.screens reflects the secondary display being re-enumerated (USB-C / DisplayPort handshakes take a few seconds), so a single repin call right at wake-time finds no target and falls back to whatever screen exists — typically main. Once it's there, macOS often keeps it there across the next wake.

Changes

Makes window pinning robust across the wake transition.

Wake side

  • Observes NSApplication.didChangeScreenParameters plus NSWorkspace.didWake / screensDidWake / sessionDidBecomeActive. Any of these triggers repinDashboard.
  • repinDashboard schedules retryRepin with a backoff [0, 0.5, 1.5, 3, 6, 12, 30, 60, 60, 60s] — try right away, then progressively wait for the target to re-enumerate.
  • Each attempt: if the configured target screen exists (or no specific target is configured), show() the window; otherwise hide() (orderOut) so it stays parked off-screen and retry.

Sleep / lock side

  • Observes NSWorkspace.willSleep / screensDidSleep / sessionDidResignActive — any of those calls parkDashboard which orderOuts the window. That way the wake transition starts with the window already off-screen instead of macOS relocating it to main before our wake handler fires.

Window controller

DashboardWindowController gains a hide() method to support both sides of this cycle without releasing the window.

Notes

The backoff hard ceiling (10 attempts, ~4 minutes) means a target that doesn't come back leaves the window parked rather than the user finding it on the wrong screen. No behavior change in the steady state — only changes the transient handling around sleep/wake.

Symptom on a multi-monitor kiosk setup: after sleep/wake or
screen-lock/unlock, the kiosk window could end up on the main display
instead of the configured secondary target. macOS surfaces the wake
notifications *before* NSScreen.screens reflects the secondary display
being re-enumerated (USB-C / DisplayPort handshakes take a few seconds),
so a single repin call right at wake-time finds no target and falls
back to whatever screen exists, typically main.

This change makes window pinning robust across the wake transition:

Wake side:
  - Observe NSApplication.didChangeScreenParameters plus
    NSWorkspace.didWake, screensDidWake, sessionDidBecomeActive. Any
    of those triggers `repinDashboard`.
  - `repinDashboard` schedules `retryRepin` with a backoff
    [0, 0.5, 1.5, 3, 6, 12, 30, 60, 60, 60s] — try right away, then
    progressively wait for the target to re-enumerate.
  - Each attempt: if the configured target screen exists (or no
    specific target is configured), `show()` the window; otherwise
    `hide()` (orderOut) so it stays parked off-screen and retry.

Sleep / lock side:
  - Observe NSWorkspace.willSleep, screensDidSleep,
    sessionDidResignActive — any of those calls `parkDashboard` which
    orderOuts the window. That way the wake transition starts with the
    window already off-screen instead of macOS relocating it to main
    before our wake handler fires.

DashboardWindowController gains a `hide()` method to support both
sides of this cycle without releasing the window.

The backoff hard ceiling (10 attempts, ~ 4 minutes) means a target
that doesn't come back leaves the window parked rather than the user
finding it on the wrong screen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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