Keep NSScreen list current across sleep/wake and display changes

Register for NSApplicationDidChangeScreenParametersNotification and
NSWorkspaceDidWakeNotification so the daemon refreshes NSScreen.screens()
after external monitors connect/disconnect or the system wakes from sleep.

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-11 03:06:47 -04:00
parent 730f6ec1cf
commit 23fe6ac101

View File

@@ -6,7 +6,10 @@ import signal
import socket
import time
from Cocoa import NSApplication, NSRunLoop, NSDate
from Cocoa import (
NSApplication, NSRunLoop, NSDate,
NSNotificationCenter, NSObject, NSScreen, NSWorkspace,
)
from Quartz import (
CGEventSourceSecondsSinceLastEventType,
kCGEventSourceStateHIDSystemState,
@@ -34,6 +37,26 @@ SOCKET_PATH = os.path.join(SOCKET_DIR, "flasher.sock")
INPUT_DISMISS_GRACE = 0.5
class _DisplayObserver(NSObject):
"""Listens for macOS display-change and wake notifications.
Registering for NSApplicationDidChangeScreenParametersNotification forces
AppKit to keep NSScreen.screens() current in long-running daemon processes.
Without this, the screen list can go stale after sleep/wake cycles, causing
modes like "allscreens" to miss external displays.
"""
def screenParametersChanged_(self, notification):
screens = NSScreen.screens()
count = len(screens) if screens else 0
logger.info("Display configuration changed — %d screen(s) detected", count)
def workspaceDidWake_(self, notification):
screens = NSScreen.screens()
count = len(screens) if screens else 0
logger.info("System woke from sleep — %d screen(s) detected", count)
def _get_system_appearance() -> str:
"""Return "dark" or "light" based on the current macOS appearance."""
app = NSApplication.sharedApplication()
@@ -71,11 +94,13 @@ class FlasherDaemon:
self._pending_approvals: dict[str, _PendingApproval] = {}
self._active_pulses: dict[str, _ActivePulse] = {}
self._cursor_was_frontmost: bool = False
self._display_observer: NSObject | None = None
def run(self) -> None:
NSApplication.sharedApplication()
self._running = True
self._setup_socket()
self._setup_display_notifications()
signal.signal(signal.SIGTERM, self._handle_signal)
signal.signal(signal.SIGINT, self._handle_signal)
@@ -111,6 +136,34 @@ class FlasherDaemon:
self._server.listen(5)
self._server.setblocking(False)
def _setup_display_notifications(self) -> None:
"""Subscribe to macOS display-change and wake events.
This is required for NSScreen.screens() to stay current in a
long-running daemon. Without these observers, AppKit may not process
screen-configuration changes after sleep/wake, leaving the screen
list stale until the process is restarted.
"""
self._display_observer = _DisplayObserver.alloc().init()
NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
self._display_observer,
"screenParametersChanged:",
"NSApplicationDidChangeScreenParametersNotification",
None,
)
NSWorkspace.sharedWorkspace().notificationCenter().addObserver_selector_name_object_(
self._display_observer,
"workspaceDidWake:",
"NSWorkspaceDidWakeNotification",
None,
)
screens = NSScreen.screens()
count = len(screens) if screens else 0
logger.info("Display notifications registered — %d screen(s) currently", count)
def _check_socket(self) -> None:
if self._server is None:
return
@@ -313,6 +366,12 @@ class FlasherDaemon:
return [window_frame]
def _cleanup(self) -> None:
if self._display_observer is not None:
NSNotificationCenter.defaultCenter().removeObserver_(self._display_observer)
NSWorkspace.sharedWorkspace().notificationCenter().removeObserver_(
self._display_observer
)
self._display_observer = None
self.overlay.hide()
if self._server is not None:
self._server.close()