From 6610919a58661cd165c9c8b46527a2e0ac3b7536 Mon Sep 17 00:00:00 2001 From: cottongin Date: Wed, 11 Mar 2026 03:07:12 -0400 Subject: [PATCH] Show overlay on all Spaces and alongside fullscreen apps Set canJoinAllSpaces + fullScreenAuxiliary on overlay windows so the border renders regardless of which Space or fullscreen app is active. In window mode, fall back to screen-edge border when Cursor isn't frontmost to avoid a floating rectangle on other Spaces. Made-with: Cursor --- src/cursor_flasher/daemon.py | 9 +++++- src/cursor_flasher/overlay.py | 6 ++++ tests/test_daemon.py | 53 ++++++++++++++++++++++++++++++++--- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/cursor_flasher/daemon.py b/src/cursor_flasher/daemon.py index 4c4d132..4a15a5f 100644 --- a/src/cursor_flasher/daemon.py +++ b/src/cursor_flasher/daemon.py @@ -357,12 +357,19 @@ class FlasherDaemon: self._last_flash[workspace] = now def _resolve_frames(self, window_frame: tuple) -> list[tuple]: - """Return frame(s) based on flash_mode config.""" + """Return frame(s) based on flash_mode config. + + In "window" mode, falls back to screen frame when Cursor is not + frontmost (e.g. different Space or behind fullscreen app) to avoid + drawing a floating rectangle at stale coordinates. + """ mode = self.config.flash_mode if mode == "allscreens": return all_screen_frames() if mode == "screen": return [screen_frame_for_window(window_frame)] + if not is_cursor_frontmost(): + return [screen_frame_for_window(window_frame)] return [window_frame] def _cleanup(self) -> None: diff --git a/src/cursor_flasher/overlay.py b/src/cursor_flasher/overlay.py index c875611..4d8cd4d 100644 --- a/src/cursor_flasher/overlay.py +++ b/src/cursor_flasher/overlay.py @@ -11,6 +11,8 @@ from Cocoa import ( NSView, NSBezierPath, NSTimer, + NSWindowCollectionBehaviorCanJoinAllSpaces, + NSWindowCollectionBehaviorFullScreenAuxiliary, ) from Foundation import NSInsetRect @@ -161,6 +163,10 @@ class OverlayManager: window.setOpaque_(False) window.setBackgroundColor_(NSColor.clearColor()) window.setLevel_(2147483631) + window.setCollectionBehavior_( + NSWindowCollectionBehaviorCanJoinAllSpaces + | NSWindowCollectionBehaviorFullScreenAuxiliary + ) window.setIgnoresMouseEvents_(True) window.setHasShadow_(False) diff --git a/tests/test_daemon.py b/tests/test_daemon.py index 40d3527..c5c9d50 100644 --- a/tests/test_daemon.py +++ b/tests/test_daemon.py @@ -122,6 +122,7 @@ class TestFlasherDaemon: window = {"title": "my-project", "frame": ((0, 0), (800, 600))} with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \ + patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \ patch("cursor_flasher.daemon.play_alert"), \ patch(PATCH_APPEARANCE, return_value="light"): daemon._handle_message( @@ -132,6 +133,46 @@ class TestFlasherDaemon: "/path", [((0, 0), (800, 600))], daemon.config.light.completed ) + def test_window_mode_falls_back_to_screen_when_cursor_not_frontmost(self): + """Window mode falls back to screen frame when Cursor isn't frontmost.""" + daemon = self._make_daemon(approval_delay=0.0, flash_mode="window") + window = {"title": "proj", "frame": ((0, 0), (800, 600))} + screen = ((0, 0), (1920, 1080)) + + daemon._handle_message( + json.dumps({"workspace": "/path", "event": "preToolUse", "tool": "Shell"}).encode() + ) + + with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \ + patch("cursor_flasher.daemon.screen_frame_for_window", return_value=screen), \ + patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=False), \ + patch("cursor_flasher.daemon.play_alert"), \ + patch(PATCH_APPEARANCE, return_value="dark"): + daemon._check_pending() + + daemon.overlay.add_pulse.assert_called_once_with( + "/path", [screen], daemon.config.dark.running + ) + + def test_stop_falls_back_to_screen_when_cursor_not_frontmost(self): + """Stop flash in window mode falls back to screen when Cursor isn't frontmost.""" + daemon = self._make_daemon(flash_mode="window") + window = {"title": "proj", "frame": ((0, 0), (800, 600))} + screen = ((0, 0), (1920, 1080)) + + with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \ + patch("cursor_flasher.daemon.screen_frame_for_window", return_value=screen), \ + patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=False), \ + patch("cursor_flasher.daemon.play_alert"), \ + patch(PATCH_APPEARANCE, return_value="dark"): + daemon._handle_message( + json.dumps({"workspace": "/path", "event": "stop"}).encode() + ) + + daemon.overlay.add_flash.assert_called_once_with( + "/path", [screen], daemon.config.dark.completed + ) + def test_allscreens_mode_uses_all_screens(self): daemon = self._make_daemon(flash_mode="allscreens", approval_delay=0.0) window = {"title": "my-project", "frame": ((0, 0), (800, 600))} @@ -380,7 +421,7 @@ class TestFlasherDaemon: ) with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \ - patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=False), \ + patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \ patch("cursor_flasher.daemon.play_alert") as mock_alert, \ patch(PATCH_APPEARANCE, return_value="dark"): daemon._check_pending() @@ -395,6 +436,7 @@ class TestFlasherDaemon: window = {"title": "proj", "frame": ((0, 0), (800, 600))} with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \ + patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \ patch("cursor_flasher.daemon.play_alert") as mock_alert, \ patch(PATCH_APPEARANCE, return_value="dark"): daemon._handle_message( @@ -411,6 +453,7 @@ class TestFlasherDaemon: window = {"title": "proj", "frame": ((0, 0), (800, 600))} with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \ + patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \ patch("cursor_flasher.daemon.play_alert") as mock_alert, \ patch(PATCH_APPEARANCE, return_value="dark"): daemon._handle_message( @@ -434,7 +477,7 @@ class TestFlasherDaemon: ) with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \ - patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=False), \ + patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \ patch("cursor_flasher.daemon.play_alert"), \ patch(PATCH_APPEARANCE, return_value="dark"): daemon._check_pending() @@ -448,6 +491,7 @@ class TestFlasherDaemon: daemon._active_pulses.clear() with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \ + patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \ patch("cursor_flasher.daemon.play_alert"), \ patch(PATCH_APPEARANCE, return_value="dark"): daemon._handle_message( @@ -475,7 +519,7 @@ class TestFlasherDaemon: ) with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \ - patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=False), \ + patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \ patch("cursor_flasher.daemon.play_alert"), \ patch(PATCH_APPEARANCE, return_value="light"): daemon._check_pending() @@ -500,7 +544,7 @@ class TestFlasherDaemon: ) with patch("cursor_flasher.daemon.find_window_by_workspace", side_effect=[win_a, win_b]), \ - patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=False), \ + patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \ patch("cursor_flasher.daemon.play_alert"), \ patch(PATCH_APPEARANCE, return_value="dark"): daemon._check_pending() @@ -533,6 +577,7 @@ class TestFlasherDaemon: window = {"title": "project-a", "frame": ((0, 0), (800, 600))} with patch("cursor_flasher.daemon.find_window_by_workspace", return_value=window), \ + patch("cursor_flasher.daemon.is_cursor_frontmost", return_value=True), \ patch("cursor_flasher.daemon.play_alert"), \ patch(PATCH_APPEARANCE, return_value="dark"): daemon._handle_message(