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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user