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 socket
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from Cocoa import NSApplication, NSRunLoop, NSDate
|
from Cocoa import (
|
||||||
|
NSApplication, NSRunLoop, NSDate,
|
||||||
|
NSNotificationCenter, NSObject, NSScreen, NSWorkspace,
|
||||||
|
)
|
||||||
from Quartz import (
|
from Quartz import (
|
||||||
CGEventSourceSecondsSinceLastEventType,
|
CGEventSourceSecondsSinceLastEventType,
|
||||||
kCGEventSourceStateHIDSystemState,
|
kCGEventSourceStateHIDSystemState,
|
||||||
@@ -34,6 +37,26 @@ SOCKET_PATH = os.path.join(SOCKET_DIR, "flasher.sock")
|
|||||||
INPUT_DISMISS_GRACE = 0.5
|
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:
|
def _get_system_appearance() -> str:
|
||||||
"""Return "dark" or "light" based on the current macOS appearance."""
|
"""Return "dark" or "light" based on the current macOS appearance."""
|
||||||
app = NSApplication.sharedApplication()
|
app = NSApplication.sharedApplication()
|
||||||
@@ -71,11 +94,13 @@ class FlasherDaemon:
|
|||||||
self._pending_approvals: dict[str, _PendingApproval] = {}
|
self._pending_approvals: dict[str, _PendingApproval] = {}
|
||||||
self._active_pulses: dict[str, _ActivePulse] = {}
|
self._active_pulses: dict[str, _ActivePulse] = {}
|
||||||
self._cursor_was_frontmost: bool = False
|
self._cursor_was_frontmost: bool = False
|
||||||
|
self._display_observer: NSObject | None = None
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
NSApplication.sharedApplication()
|
NSApplication.sharedApplication()
|
||||||
self._running = True
|
self._running = True
|
||||||
self._setup_socket()
|
self._setup_socket()
|
||||||
|
self._setup_display_notifications()
|
||||||
|
|
||||||
signal.signal(signal.SIGTERM, self._handle_signal)
|
signal.signal(signal.SIGTERM, self._handle_signal)
|
||||||
signal.signal(signal.SIGINT, self._handle_signal)
|
signal.signal(signal.SIGINT, self._handle_signal)
|
||||||
@@ -111,6 +136,34 @@ class FlasherDaemon:
|
|||||||
self._server.listen(5)
|
self._server.listen(5)
|
||||||
self._server.setblocking(False)
|
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:
|
def _check_socket(self) -> None:
|
||||||
if self._server is None:
|
if self._server is None:
|
||||||
return
|
return
|
||||||
@@ -313,6 +366,12 @@ class FlasherDaemon:
|
|||||||
return [window_frame]
|
return [window_frame]
|
||||||
|
|
||||||
def _cleanup(self) -> None:
|
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()
|
self.overlay.hide()
|
||||||
if self._server is not None:
|
if self._server is not None:
|
||||||
self._server.close()
|
self._server.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user