"""Native macOS overlay window that draws a pulsing border around a target window.""" import math import objc from Cocoa import ( NSApplication, NSWindow, NSBorderlessWindowMask, NSColor, NSView, NSBezierPath, NSTimer, NSScreen, ) from Foundation import NSInsetRect from Quartz import ( CGWindowListCopyWindowInfo, kCGWindowListOptionOnScreenOnly, kCGNullWindowID, kCGWindowOwnerPID, kCGWindowBounds, kCGWindowLayer, kCGWindowNumber, ) from cursor_flasher.config import Config def hex_to_nscolor(hex_str: str, alpha: float = 1.0) -> NSColor: """Convert a hex color string to NSColor.""" hex_str = hex_str.lstrip("#") r = int(hex_str[0:2], 16) / 255.0 g = int(hex_str[2:4], 16) / 255.0 b = int(hex_str[4:6], 16) / 255.0 return NSColor.colorWithCalibratedRed_green_blue_alpha_(r, g, b, alpha) class PulseBorderView(NSView): """Custom view that draws a pulsing border rectangle.""" def initWithFrame_config_(self, frame, config): self = objc.super(PulseBorderView, self).initWithFrame_(frame) if self is None: return None self._config = config self._phase = 0.0 return self def drawRect_(self, rect): opacity_range = self._config.pulse_opacity_max - self._config.pulse_opacity_min alpha = self._config.pulse_opacity_min + opacity_range * ( 0.5 + 0.5 * math.sin(self._phase) ) color = hex_to_nscolor(self._config.pulse_color, alpha) color.setStroke() width = self._config.pulse_width inset = width / 2.0 bounds = objc.super(PulseBorderView, self).bounds() inset_rect = NSInsetRect(bounds, inset, inset) path = NSBezierPath.bezierPathWithRect_(inset_rect) path.setLineWidth_(width) path.stroke() def setPhase_(self, phase): self._phase = phase self.setNeedsDisplay_(True) 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._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 pulsing overlays around every window belonging to `pid`.""" self._target_pid = pid frames = self._get_all_window_frames(pid) if not frames: return self._sync_overlays(frames) for entry in self._overlays: entry.window.orderFrontRegardless() self._start_animation() def hide(self) -> None: """Hide all overlay windows.""" self._stop_animation() for entry in self._overlays: entry.window.orderOut_(None) def update_positions(self) -> None: """Reposition overlays to track current Cursor window positions.""" if self._target_pid is None: return frames = self._get_all_window_frames(self._target_pid) self._sync_overlays(frames) 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 ) 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])) view = PulseBorderView.alloc().initWithFrame_config_( content_rect, self._config ) window.setContentView_(view) return _OverlayEntry(window, view) def _start_animation(self) -> None: if self._timer is not None: return interval = 1.0 / 30.0 # 30 fps self._timer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_( interval, self, "_tick:", None, True ) def _stop_animation(self) -> None: if self._timer is not None: self._timer.invalidate() self._timer = None self._phase = 0.0 @objc.python_method def _tick_impl(self): speed = self._config.pulse_speed step = (2.0 * math.pi) / (speed * 30.0) self._phase += step for entry in self._overlays: entry.view.setPhase_(self._phase) self.update_positions() def _tick_(self, timer) -> None: self._tick_impl() 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 [] screen_height = NSScreen.mainScreen().frame().size.height frames = [] for info in window_list: if info.get(kCGWindowOwnerPID) != pid: continue if info.get(kCGWindowLayer, 999) != 0: continue bounds = info.get(kCGWindowBounds) if bounds is None: continue w = bounds["Width"] h = bounds["Height"] if w < 100 or h < 100: continue x = bounds["X"] y = screen_height - bounds["Y"] - h frames.append(((x, y), (w, h))) return frames