feat: support multiple Cursor windows in overlay

Renamed OverlayWindow to OverlayManager. Now discovers all on-screen
windows for the Cursor PID and creates/reuses/hides overlay windows
dynamically to match. Filters out tiny windows (<100px).
Verified: detects 3 windows across monitors.

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-10 02:44:04 -04:00
parent f967575ebc
commit c1d10efe7b
2 changed files with 86 additions and 45 deletions

View File

@@ -5,13 +5,13 @@ import time
from Cocoa import NSApplication, NSRunLoop, NSDate
from cursor_flasher.config import Config
from cursor_flasher.overlay import OverlayWindow
from cursor_flasher.overlay import OverlayManager
from cursor_flasher.detector import CursorDetector
app = NSApplication.sharedApplication()
config = Config()
overlay = OverlayWindow(config)
overlay = OverlayManager(config)
detector = CursorDetector()
pid = detector._find_cursor_pid()

View File

@@ -12,7 +12,7 @@ from Cocoa import (
NSTimer,
NSScreen,
)
from Foundation import NSInsetRect, NSMakeRect
from Foundation import NSInsetRect
from Quartz import (
CGWindowListCopyWindowInfo,
kCGWindowListOptionOnScreenOnly,
@@ -20,6 +20,7 @@ from Quartz import (
kCGWindowOwnerPID,
kCGWindowBounds,
kCGWindowLayer,
kCGWindowNumber,
)
from cursor_flasher.config import Config
@@ -67,63 +68,100 @@ class PulseBorderView(NSView):
self.setNeedsDisplay_(True)
class OverlayWindow:
"""Manages the overlay window lifecycle."""
class _OverlayEntry:
"""A single overlay window + view pair."""
__slots__ = ("window", "view")
def __init__(self, window: NSWindow, view: PulseBorderView):
self.window = window
self.view = view
class OverlayManager:
"""Manages overlay windows for all Cursor windows belonging to a PID.
Creates one transparent overlay per Cursor window and keeps them
positioned and animated in sync.
"""
def __init__(self, config: Config):
self._config = config
self._window: NSWindow | None = None
self._view: PulseBorderView | None = None
self._overlays: list[_OverlayEntry] = []
self._timer: NSTimer | None = None
self._phase = 0.0
self._target_pid: int | None = None
def show(self, pid: int) -> None:
"""Show the pulsing overlay around the window belonging to `pid`."""
"""Show pulsing overlays around every window belonging to `pid`."""
self._target_pid = pid
frame = self._get_window_frame(pid)
if frame is None:
frames = self._get_all_window_frames(pid)
if not frames:
return
if self._window is None:
self._create_window(frame)
else:
self._window.setFrame_display_(frame, True)
self._view.setFrame_(((0, 0), (frame[1][0], frame[1][1])))
self._sync_overlays(frames)
for entry in self._overlays:
entry.window.orderFrontRegardless()
self._window.orderFrontRegardless()
self._start_animation()
def hide(self) -> None:
"""Hide and clean up the overlay."""
"""Hide all overlay windows."""
self._stop_animation()
if self._window is not None:
self._window.orderOut_(None)
for entry in self._overlays:
entry.window.orderOut_(None)
def update_position(self) -> None:
"""Reposition overlay to match current Cursor window position."""
if self._target_pid is None or self._window is None:
def update_positions(self) -> None:
"""Reposition overlays to track current Cursor window positions."""
if self._target_pid is None:
return
frame = self._get_window_frame(self._target_pid)
if frame is not None:
self._window.setFrame_display_(frame, True)
self._view.setFrame_(((0, 0), (frame[1][0], frame[1][1])))
frames = self._get_all_window_frames(self._target_pid)
self._sync_overlays(frames)
def _create_window(self, frame) -> None:
self._window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
def _sync_overlays(self, frames: list[tuple]) -> None:
"""Ensure we have exactly len(frames) overlays, positioned correctly.
Reuses existing overlay windows where possible, creates new ones
if more windows appeared, and hides extras if windows closed.
"""
needed = len(frames)
existing = len(self._overlays)
for i in range(needed):
frame = frames[i]
content_rect = ((0, 0), (frame[1][0], frame[1][1]))
if i < existing:
entry = self._overlays[i]
entry.window.setFrame_display_(frame, True)
entry.view.setFrame_(content_rect)
else:
entry = self._create_overlay(frame)
self._overlays.append(entry)
entry.window.orderFrontRegardless()
for i in range(needed, existing):
self._overlays[i].window.orderOut_(None)
if needed < existing:
self._overlays = self._overlays[:needed]
def _create_overlay(self, frame) -> _OverlayEntry:
window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
frame, NSBorderlessWindowMask, 2, False # NSBackingStoreBuffered
)
self._window.setOpaque_(False)
self._window.setBackgroundColor_(NSColor.clearColor())
self._window.setLevel_(25) # Above normal windows
self._window.setIgnoresMouseEvents_(True)
self._window.setHasShadow_(False)
window.setOpaque_(False)
window.setBackgroundColor_(NSColor.clearColor())
window.setLevel_(25) # Above normal windows
window.setIgnoresMouseEvents_(True)
window.setHasShadow_(False)
content_rect = ((0, 0), (frame[1][0], frame[1][1]))
self._view = PulseBorderView.alloc().initWithFrame_config_(
view = PulseBorderView.alloc().initWithFrame_config_(
content_rect, self._config
)
self._window.setContentView_(self._view)
window.setContentView_(view)
return _OverlayEntry(window, view)
def _start_animation(self) -> None:
if self._timer is not None:
@@ -144,22 +182,23 @@ class OverlayWindow:
speed = self._config.pulse_speed
step = (2.0 * math.pi) / (speed * 30.0)
self._phase += step
if self._view is not None:
self._view.setPhase_(self._phase)
self.update_position()
for entry in self._overlays:
entry.view.setPhase_(self._phase)
self.update_positions()
def _tick_(self, timer) -> None:
self._tick_impl()
def _get_window_frame(self, pid: int):
"""Get the screen frame of the main window for the given PID."""
def _get_all_window_frames(self, pid: int) -> list[tuple]:
"""Get screen frames for all on-screen windows belonging to `pid`."""
window_list = CGWindowListCopyWindowInfo(
kCGWindowListOptionOnScreenOnly, kCGNullWindowID
)
if not window_list:
return None
return []
screen_height = NSScreen.mainScreen().frame().size.height
frames = []
for info in window_list:
if info.get(kCGWindowOwnerPID) != pid:
@@ -169,10 +208,12 @@ class OverlayWindow:
bounds = info.get(kCGWindowBounds)
if bounds is None:
continue
x = bounds["X"]
y = screen_height - bounds["Y"] - bounds["Height"]
w = bounds["Width"]
h = bounds["Height"]
return ((x, y), (w, h))
if w < 100 or h < 100:
continue
x = bounds["X"]
y = screen_height - bounds["Y"] - h
frames.append(((x, y), (w, h)))
return None
return frames