@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co |
| 4 | 4 | |
| 5 | 5 | ## What this is |
| 6 | 6 | |
| 7 | | -ers is a macOS window border renderer for the tarmac window manager. It draws colored overlay borders around application windows using private SkyLight framework APIs. macOS Tahoe only. |
| 7 | +ers is a macOS window border renderer for the tarmac window manager. It draws colored overlay borders around application windows. Borders are NSWindows backed by a CAShapeLayer; window discovery and event subscription still go through private SkyLight (SLS) FFI. macOS Tahoe only, Apple Silicon. |
| 8 | 8 | |
| 9 | 9 | ## Build & Run |
| 10 | 10 | |
@@ -17,32 +17,36 @@ cargo run -- -w 6.0 # custom border width (default: 4.0) |
| 17 | 17 | cargo run -- <wid> # border a specific window ID |
| 18 | 18 | ``` |
| 19 | 19 | |
| 20 | | -No tests — verification is visual. Use `RUST_LOG=debug` for tracing output. |
| 20 | +Default log level is `info`. Use `RUST_LOG=ers=debug` for the full trace (focus changes, hide/unhide, sync, hotplug, etc.). |
| 21 | 21 | |
| 22 | 22 | ## Architecture |
| 23 | 23 | |
| 24 | | -Three source files (~1300 lines total): |
| 24 | +Four source files: |
| 25 | 25 | |
| 26 | | -- **`src/main.rs`** — `BorderMap` struct manages overlay lifecycle. Event loop batches window events with 150ms debounce, then processes creates/destroys/moves/resizes. Focus detection recolors borders (active=white, inactive=gray). Main thread runs CFRunLoop; events dispatch from a background thread via mpsc. |
| 26 | +- **`src/main.rs`** — `BorderMap` manages overlay lifecycle: discover, add_fresh, sync_overlay, hide/unhide, reconcile. Main thread runs a CFRunLoop with a CFRunLoopTimer; events dispatched from SLS event handlers go through an mpsc channel and get processed in a 16–120ms batch. update_focus polls the front window each tick. |
| 27 | 27 | |
| 28 | | -- **`src/skylight.rs`** — FFI bindings for private macOS frameworks: SkyLight (CGS window creation, event registration), CoreGraphics (drawing), CoreFoundation (collections, RunLoop). All types `repr(C)`. |
| 28 | +- **`src/nswindow_overlay.rs`** — `OverlayWindow` wraps `NSWindow + CAShapeLayer`. The NSWindow has `sharingType = .none` (the only mechanism Tahoe's screenshot picker honors — see "screenshot exclusion" below). The CAShapeLayer draws a stroked rounded-rect border that matches the target window's bounds plus the configured `border_width`. Coordinate conversion CG→Cocoa uses `CGDisplayBounds(CGMainDisplayID())` for the primary screen height (NSScreen caches and returns stale data after monitor hotplug). |
| 29 | 29 | |
| 30 | | -- **`src/events.rs`** — Event enum and SLSRegisterNotifyProc callbacks. Filters out the renderer's own windows to prevent feedback loops. Sends events over mpsc channel. |
| 30 | +- **`src/skylight.rs`** — FFI bindings: SLS (window discovery, bounds, ordering, events), CoreFoundation (CFArray, CFRunLoop, CFRunLoopTimer), CGDisplayRegisterReconfigurationCallback (hotplug detection). |
| 31 | 31 | |
| 32 | | -- **`build.rs`** — Links SkyLight (private framework), CoreGraphics, CoreFoundation. |
| 32 | +- **`src/events.rs`** — SLS event registration. Filters out our own NSWindows by owner pid before forwarding via mpsc. |
| 33 | 33 | |
| 34 | 34 | ## Critical macOS Tahoe constraints |
| 35 | 35 | |
| 36 | 36 | These are hard-won discoveries from debugging undocumented APIs: |
| 37 | 37 | |
| 38 | | -1. **SLSCopyManagedDisplaySpaces poisons SLSNewWindow** — calling it on ANY connection corrupts window creation on ALL connections. Use `CGWindowListCopyWindowInfo` instead. |
| 38 | +1. **Screenshot exclusion requires NSWindow + sharingType=.none**. Tahoe's `screencaptureui` enumerates windows via `_SLSCopyWindowsWithOptionsAndTagsAndSpaceOptions` + `_CGSGetWindowTags` (verified by `otool` against the binary), and the SLS-side `SLSSetWindowSharingState` / SLS tag bits do NOT propagate to that path for raw SLS-only windows. Verified empirically with `screencapture -l <wid>`: SLS overlays are captured normally; NSWindows with `.none` sharingType return "could not create image from window". |
| 39 | 39 | |
| 40 | | -2. **Use the process main connection** — `SLSMainConnectionID()` for window creation, drawing, ordering, and event registration (current code already does this). Past notes mentioned a "fresh SLSNewConnection per border" requirement; that has not been needed since the current architecture stabilized. |
| 40 | +2. **`SLSCopyManagedDisplaySpaces` poisons `SLSNewWindow`** — calling it on ANY connection corrupts window creation on ALL connections, process-wide. Use `CGWindowListCopyWindowInfo` for window discovery instead. |
| 41 | 41 | |
| 42 | | -3. **Create windows at final size** — the 1×1-then-reshape pattern breaks on Tahoe. Create at correct position/size immediately. |
| 42 | +3. **`NSScreen.screens` returns stale data after monitor hotplug** until something internal triggers a refresh. For the CG→Cocoa Y-flip we need the live primary-screen height, so use `CGDisplayBounds(CGMainDisplayID())` directly. Don't rely on `[NSScreen screens][0]`. |
| 43 | 43 | |
| 44 | | -4. **Draw before setting tags** — CGContext from `SLWindowContextCreate` must be used to draw BEFORE setting window tags/shadow. Re-obtaining context later for redraws uses the border's own connection. |
| 44 | +4. **`orderWindow:relativeTo:` re-shows an off-screen NSWindow as a side effect.** In active-only mode, `sync_overlay` must NOT call `order_above` on hidden non-focused overlays just because their target moved (e.g. tarmac stack peek-out positions shift on every cycle) — otherwise every stacked window's overlay pops back onto the screen. |
| 45 | + |
| 46 | +5. **CAShapeLayer state can be reset by macOS during display sleep/wake**, leaving the layer at a default tiny frame at the layer origin even though the NSWindow's `frame` survives. `BorderMap::refresh_all_layers` is called once a second from the periodic reconcile (and on hotplug) to re-apply each layer's frame and path. |
| 47 | + |
| 48 | +6. **AX-driven moves don't reliably fire SLS WINDOW_MOVE notifications**, so during stack cycles a stored overlay can be at stale coordinates relative to its target. `update_focus` calls `sync_overlay` on both the old and new focused targets to pull live SLS bounds before un/hiding. |
| 45 | 49 | |
| 46 | 50 | ## Dependencies |
| 47 | 51 | |
| 48 | | -Only `serde`/`serde_json` (JSON parsing of window info) and `tracing`/`tracing-subscriber` (logging). No external runtime dependencies beyond macOS frameworks. |
| 52 | +`serde`/`serde_json` (window-info parsing), `tracing`/`tracing-subscriber` (logging), and the `objc2` family for AppKit/QuartzCore/CoreGraphics/CoreFoundation bindings. No runtime dependencies beyond macOS frameworks. |