Comparing changes

Choose two branches to see what's changed or to start a new pull request.

base: v0.2.0
compare: research/tahoe-screenshot-exclusion
Create pull request
Able to merge. These branches can be automatically merged.
34 commits 6 files changed 2 contributors

Commits on research/tahoe-screenshot-exclusion

.gitignoremodified
@@ -1,1 +1,6 @@
11
 docs/
2
+CLAUDE.md
3
+AGENTS.md
4
+.claude/
5
+.refs/
6
+.fackr/
CLAUDE.mddeleted
@@ -1,48 +0,0 @@
1
-# CLAUDE.md
2
-
3
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
-## What this is
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.
8
-
9
-## Build & Run
10
-
11
-```bash
12
-cargo build              # debug build
13
-cargo build --release    # release build
14
-cargo run                # run (borders all windows)
15
-cargo run -- --list      # list on-screen windows with IDs and bounds
16
-cargo run -- -w 6.0      # custom border width (default: 4.0)
17
-cargo run -- <wid>       # border a specific window ID
18
-```
19
-
20
-No tests — verification is visual. Use `RUST_LOG=debug` for tracing output.
21
-
22
-## Architecture
23
-
24
-Three source files (~1300 lines total):
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.
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)`.
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.
31
-
32
-- **`build.rs`** — Links SkyLight (private framework), CoreGraphics, CoreFoundation.
33
-
34
-## Critical macOS Tahoe constraints
35
-
36
-These are hard-won discoveries from debugging undocumented APIs:
37
-
38
-1. **SLSCopyManagedDisplaySpaces poisons SLSNewWindow** — calling it on ANY connection corrupts window creation on ALL connections. Use `CGWindowListCopyWindowInfo` instead.
39
-
40
-2. **Fresh SLS connection per border** — each overlay needs its own `SLSNewConnection`. Required for reliable rendering.
41
-
42
-3. **Create windows at final size** — the 1×1-then-reshape pattern breaks on Tahoe. Create at correct position/size immediately.
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.
45
-
46
-## Dependencies
47
-
48
-Only `serde`/`serde_json` (JSON parsing of window info) and `tracing`/`tracing-subscriber` (logging). No external runtime dependencies beyond macOS frameworks.
Cargo.tomlmodified
@@ -1,6 +1,6 @@
11
 [package]
22
 name = "ers"
3
-version = "0.2.0"
3
+version = "0.4.0"
44
 edition = "2024"
55
 description = "Window border renderer for tarmac"
66
 
@@ -10,3 +10,35 @@ serde = { version = "1", features = ["derive"] }
1010
 serde_json = "1"
1111
 tracing = "0.1"
1212
 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
13
+objc2 = { version = "0.6", default-features = false }
14
+objc2-foundation = { version = "0.3", default-features = false, features = [
15
+    "NSGeometry",
16
+    "NSString",
17
+    "NSValue",
18
+] }
19
+objc2-app-kit = { version = "0.3", default-features = false, features = [
20
+    "NSApplication",
21
+    "NSResponder",
22
+    "NSView",
23
+    "NSWindow",
24
+    "NSColor",
25
+    "NSColorSpace",
26
+    "NSGraphics",
27
+    "NSScreen",
28
+    "NSRunningApplication",
29
+    "objc2-core-foundation",
30
+] }
31
+objc2-quartz-core = { version = "0.3", default-features = false, features = [
32
+    "CALayer",
33
+    "CAShapeLayer",
34
+    "objc2-core-foundation",
35
+] }
36
+objc2-core-foundation = { version = "0.3", default-features = false, features = [
37
+    "CFCGTypes",
38
+] }
39
+objc2-core-graphics = { version = "0.3", default-features = false, features = [
40
+    "CGColor",
41
+    "CGColorSpace",
42
+    "CGPath",
43
+    "CGDirectDisplay",
44
+] }
src/main.rsmodified
1276 lines changed — click to load
@@ -2,6 +2,7 @@
22
 
33
 mod events;
44
 mod skylight;
5
+mod nswindow_overlay;
56
 
67
 use events::Event;
78
 use skylight::*;
@@ -13,11 +14,41 @@ use std::sync::mpsc;
1314
 use tracing::debug;
1415
 
1516
 static SIGNAL_STOP_REQUESTED: AtomicBool = AtomicBool::new(false);
16
-
17
-/// Per-overlay state: the connection it was created on + its wid.
17
+const MIN_TRACKED_WINDOW_SIZE: f64 = 4.0;
18
+const GEOMETRY_EPSILON: f64 = 0.5;
19
+const WINDOW_ATTRIBUTE_REAL: u64 = 1 << 1;
20
+const WINDOW_TAG_DOCUMENT: u64 = 1 << 0;
21
+const WINDOW_TAG_FLOATING: u64 = 1 << 1;
22
+const WINDOW_TAG_ATTACHED: u64 = 1 << 7;
23
+const WINDOW_TAG_IGNORES_CYCLE: u64 = 1 << 18;
24
+const WINDOW_TAG_MODAL: u64 = 1 << 31;
25
+const WINDOW_TAG_REAL_SURFACE: u64 = 1 << 58;
26
+
27
+/// Per-overlay state: an NSWindow drawing the rounded-rect border via
28
+/// CAShapeLayer. Replaces the old SLS-only overlay window — see
29
+/// nswindow_overlay.rs for the rationale (screencaptureui on Tahoe
30
+/// only honors NSWindow.sharingType, not SLS sharing-state nor tag
31
+/// bits, for raw SLS-only windows).
1832
 struct Overlay {
19
-    cid: CGSConnectionID,
20
-    wid: u32,
33
+    window: nswindow_overlay::OverlayWindow,
34
+}
35
+
36
+impl Overlay {
37
+    fn wid(&self) -> u32 {
38
+        self.window.wid()
39
+    }
40
+    fn bounds(&self) -> CGRect {
41
+        CGRect {
42
+            origin: CGPoint {
43
+                x: self.window.bounds_cg_x,
44
+                y: self.window.bounds_cg_y,
45
+            },
46
+            size: CGSize {
47
+                width: self.window.bounds_cg_w,
48
+                height: self.window.bounds_cg_h,
49
+            },
50
+        }
51
+    }
2152
 }
2253
 
2354
 fn window_area(bounds: CGRect) -> f64 {
@@ -39,6 +70,116 @@ fn is_same_window_surface(a: CGRect, b: CGRect) -> bool {
3970
     smaller > 0.0 && intersection_area(a, b) / smaller >= 0.9
4071
 }
4172
 
73
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74
+enum SurfacePreference {
75
+    KeepExisting,
76
+    ReplaceExisting,
77
+}
78
+
79
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80
+struct WindowMetadata {
81
+    parent_wid: u32,
82
+    tags: u64,
83
+    attributes: u64,
84
+}
85
+
86
+fn surface_preference(existing: CGRect, candidate: CGRect) -> Option<SurfacePreference> {
87
+    if !is_same_window_surface(existing, candidate) {
88
+        return None;
89
+    }
90
+
91
+    if window_area(candidate) > window_area(existing) {
92
+        Some(SurfacePreference::ReplaceExisting)
93
+    } else {
94
+        Some(SurfacePreference::KeepExisting)
95
+    }
96
+}
97
+
98
+fn minimum_trackable_dimension(border_width: f64) -> f64 {
99
+    border_width.max(MIN_TRACKED_WINDOW_SIZE)
100
+}
101
+
102
+fn is_trackable_window(bounds: CGRect, border_width: f64) -> bool {
103
+    let min_dimension = minimum_trackable_dimension(border_width);
104
+    bounds.size.width >= min_dimension && bounds.size.height >= min_dimension
105
+}
106
+
107
+fn origin_changed(a: CGRect, b: CGRect) -> bool {
108
+    (a.origin.x - b.origin.x).abs() > GEOMETRY_EPSILON
109
+        || (a.origin.y - b.origin.y).abs() > GEOMETRY_EPSILON
110
+}
111
+
112
+fn size_changed(a: CGRect, b: CGRect) -> bool {
113
+    (a.size.width - b.size.width).abs() > GEOMETRY_EPSILON
114
+        || (a.size.height - b.size.height).abs() > GEOMETRY_EPSILON
115
+}
116
+
117
+fn is_suitable_window_metadata(metadata: WindowMetadata) -> bool {
118
+    metadata.parent_wid == 0
119
+        && ((metadata.attributes & WINDOW_ATTRIBUTE_REAL) != 0
120
+            || (metadata.tags & WINDOW_TAG_REAL_SURFACE) != 0)
121
+        && (metadata.tags & WINDOW_TAG_ATTACHED) == 0
122
+        && (metadata.tags & WINDOW_TAG_IGNORES_CYCLE) == 0
123
+        && ((metadata.tags & WINDOW_TAG_DOCUMENT) != 0
124
+            || ((metadata.tags & WINDOW_TAG_FLOATING) != 0
125
+                && (metadata.tags & WINDOW_TAG_MODAL) != 0))
126
+}
127
+
128
+fn query_window_metadata(cid: CGSConnectionID, wid: u32) -> Option<WindowMetadata> {
129
+    unsafe {
130
+        let window_ref = cfarray_of_cfnumbers(
131
+            (&wid as *const u32).cast(),
132
+            std::mem::size_of::<u32>(),
133
+            1,
134
+            kCFNumberSInt32Type,
135
+        );
136
+        if window_ref.is_null() {
137
+            return None;
138
+        }
139
+
140
+        let query = SLSWindowQueryWindows(cid, window_ref, 0x0);
141
+        CFRelease(window_ref);
142
+        if query.is_null() {
143
+            return None;
144
+        }
145
+
146
+        let iterator = SLSWindowQueryResultCopyWindows(query);
147
+        CFRelease(query);
148
+        if iterator.is_null() {
149
+            return None;
150
+        }
151
+
152
+        let metadata = if SLSWindowIteratorAdvance(iterator) {
153
+            Some(WindowMetadata {
154
+                parent_wid: SLSWindowIteratorGetParentID(iterator),
155
+                tags: SLSWindowIteratorGetTags(iterator),
156
+                attributes: SLSWindowIteratorGetAttributes(iterator),
157
+            })
158
+        } else {
159
+            None
160
+        };
161
+
162
+        CFRelease(iterator);
163
+        metadata
164
+    }
165
+}
166
+
167
+fn is_suitable_window(cid: CGSConnectionID, wid: u32) -> bool {
168
+    match query_window_metadata(cid, wid) {
169
+        Some(metadata) => {
170
+            let suitable = is_suitable_window_metadata(metadata);
171
+            if !suitable {
172
+                debug!(
173
+                    "[window_filter] rejecting wid={} parent={} tags={:#x} attributes={:#x}",
174
+                    wid, metadata.parent_wid, metadata.tags, metadata.attributes
175
+                );
176
+            }
177
+            suitable
178
+        }
179
+        None => false,
180
+    }
181
+}
182
+
42183
 fn cf_string_from_static(name: &std::ffi::CStr) -> CFStringRef {
43184
     unsafe { CFStringCreateWithCString(ptr::null(), name.as_ptr().cast(), kCFStringEncodingUTF8) }
44185
 }
@@ -58,12 +199,19 @@ struct BorderMap {
58199
     active_color: (f64, f64, f64, f64),
59200
     inactive_color: (f64, f64, f64, f64),
60201
     active_only: bool,
202
+    mtm: objc2::MainThreadMarker,
61203
 }
62204
 
63205
 impl BorderMap {
64
-    fn new(cid: CGSConnectionID, own_pid: i32, border_width: f64) -> Self {
206
+    fn new(
207
+        cid: CGSConnectionID,
208
+        own_pid: i32,
209
+        border_width: f64,
210
+        mtm: objc2::MainThreadMarker,
211
+    ) -> Self {
65212
         Self {
66213
             overlays: HashMap::new(),
214
+            mtm,
67215
             main_cid: cid,
68216
             own_pid,
69217
             border_width,
@@ -84,24 +232,39 @@ impl BorderMap {
84232
     }
85233
 
86234
     fn is_overlay(&self, wid: u32) -> bool {
87
-        self.overlays.values().any(|o| o.wid == wid)
235
+        self.overlays.values().any(|o| o.wid() == wid)
88236
     }
89237
 
90
-    /// Add border (batch mode, uses main cid).
238
+    /// Add border using the standard filtering path.
91239
     fn add_batch(&mut self, target_wid: u32) {
92
-        if self.overlays.contains_key(&target_wid) {
93
-            return;
94
-        }
95
-        let color = self.color_for(target_wid);
96
-        if let Some((cid, wid)) = create_overlay(
97
-            self.main_cid,
98
-            target_wid,
99
-            self.border_width,
100
-            self.radius,
101
-            color,
102
-        ) {
103
-            self.overlays.insert(target_wid, Overlay { cid, wid });
240
+        self.add_fresh(target_wid);
241
+    }
242
+
243
+    fn surface_replacements(&self, target_wid: u32, bounds: CGRect) -> Option<Vec<u32>> {
244
+        let mut replacements = Vec::new();
245
+
246
+        for &existing_wid in self.overlays.keys() {
247
+            if existing_wid == target_wid {
248
+                continue;
249
+            }
250
+
251
+            unsafe {
252
+                let mut existing_bounds = CGRect::default();
253
+                if SLSGetWindowBounds(self.main_cid, existing_wid, &mut existing_bounds)
254
+                    != kCGErrorSuccess
255
+                {
256
+                    continue;
257
+                }
258
+
259
+                match surface_preference(existing_bounds, bounds) {
260
+                    Some(SurfacePreference::KeepExisting) => return None,
261
+                    Some(SurfacePreference::ReplaceExisting) => replacements.push(existing_wid),
262
+                    None => {}
263
+                }
264
+            }
104265
         }
266
+
267
+        Some(replacements)
105268
     }
106269
 
107270
     /// Add border (event mode). Uses main_cid — fresh connections create
@@ -126,120 +289,187 @@ impl BorderMap {
126289
             if pid == self.own_pid {
127290
                 return;
128291
             }
292
+            if !is_suitable_window(self.main_cid, target_wid) {
293
+                return;
294
+            }
129295
 
130296
             let mut bounds = CGRect::default();
131297
             SLSGetWindowBounds(self.main_cid, target_wid, &mut bounds);
132
-            if bounds.size.width < 50.0 || bounds.size.height < 50.0 {
298
+            if !is_trackable_window(bounds, self.border_width) {
133299
                 return;
134300
             }
135301
             bounds
136302
         };
137303
 
138
-        // Skip container windows: if a tracked window is at the same position,
139
-        // keep the smaller one (content) and skip the larger one (container)
140
-        let cx = bounds.origin.x + bounds.size.width / 2.0;
141
-        let cy = bounds.origin.y + bounds.size.height / 2.0;
142
-        let area = window_area(bounds);
143
-        for &existing_wid in self.overlays.keys() {
144
-            unsafe {
145
-                let mut eb = CGRect::default();
146
-                if SLSGetWindowBounds(self.main_cid, existing_wid, &mut eb) != kCGErrorSuccess {
147
-                    continue;
148
-                }
149
-                let ecx = eb.origin.x + eb.size.width / 2.0;
150
-                let ecy = eb.origin.y + eb.size.height / 2.0;
151
-                if (cx - ecx).abs() < 30.0 && (cy - ecy).abs() < 30.0 {
152
-                    let earea = window_area(eb);
153
-                    if area >= earea {
154
-                        return;
155
-                    } // new window is container, skip
156
-                }
157
-            }
304
+        let Some(replacements) = self.surface_replacements(target_wid, bounds) else {
305
+            return;
306
+        };
307
+
308
+        for wid in replacements {
309
+            self.remove(wid);
158310
         }
159311
 
160312
         let color = self.color_for(target_wid);
161
-        if let Some((cid, wid)) = create_overlay(
162
-            self.main_cid,
163
-            target_wid,
313
+        if let Some(window) = nswindow_overlay::OverlayWindow::new(
314
+            bounds.origin.x,
315
+            bounds.origin.y,
316
+            bounds.size.width,
317
+            bounds.size.height,
164318
             self.border_width,
165319
             self.radius,
166320
             color,
321
+            self.mtm,
167322
         ) {
168
-            self.overlays.insert(target_wid, Overlay { cid, wid });
323
+            window.order_above(target_wid);
324
+            self.overlays.insert(target_wid, Overlay { window });
169325
         }
170326
     }
171327
 
172
-    fn remove_all(&mut self) {
173
-        let wids: Vec<u32> = self.overlays.keys().copied().collect();
174
-        for wid in wids {
175
-            self.remove(wid);
328
+    fn remove(&mut self, target_wid: u32) {
329
+        if let Some(overlay) = self.overlays.remove(&target_wid) {
330
+            debug!(
331
+                "[remove] target={} overlay_wid={} dropping NSWindow",
332
+                target_wid,
333
+                overlay.wid()
334
+            );
335
+            // OverlayWindow's Drop runs orderOut + close.
336
+            drop(overlay);
337
+        } else {
338
+            debug!("[remove] target={} not tracked", target_wid);
176339
         }
177340
     }
178341
 
179
-    fn remove(&mut self, target_wid: u32) {
180
-        if let Some(overlay) = self.overlays.remove(&target_wid) {
181
-            unsafe {
182
-                // Move off-screen first (most reliable hide on Tahoe)
183
-                let offscreen = CGPoint {
184
-                    x: -99999.0,
185
-                    y: -99999.0,
186
-                };
187
-                SLSMoveWindow(overlay.cid, overlay.wid, &offscreen);
188
-                SLSSetWindowAlpha(overlay.cid, overlay.wid, 0.0);
189
-                SLSOrderWindow(overlay.cid, overlay.wid, 0, 0);
190
-                SLSReleaseWindow(overlay.cid, overlay.wid);
191
-                if overlay.cid != self.main_cid {
192
-                    SLSReleaseConnection(overlay.cid);
193
-                }
342
+    /// Reconcile a tracked overlay against its target window.
343
+    fn sync_overlay(&mut self, target_wid: u32) -> bool {
344
+        if !self.overlays.contains_key(&target_wid) {
345
+            return false;
346
+        }
347
+
348
+        let mut bounds = CGRect::default();
349
+        unsafe {
350
+            if SLSGetWindowBounds(self.main_cid, target_wid, &mut bounds) != kCGErrorSuccess {
351
+                // Window is gone (destroyed). Reap the overlay.
352
+                debug!(
353
+                    "[sync_overlay] target={} SLSGetWindowBounds failed — reaping overlay",
354
+                    target_wid
355
+                );
356
+                self.remove(target_wid);
357
+                return true;
358
+            }
359
+
360
+            if !is_suitable_window(self.main_cid, target_wid) {
361
+                self.remove(target_wid);
362
+                return true;
363
+            }
364
+
365
+            if !is_trackable_window(bounds, self.border_width) {
366
+                self.remove(target_wid);
367
+                return true;
194368
             }
195369
         }
196
-    }
197370
 
198
-    /// Move overlay to match target's current position (no recreate).
199
-    fn reposition(&self, target_wid: u32) {
200
-        if let Some(overlay) = self.overlays.get(&target_wid) {
201
-            unsafe {
202
-                let mut bounds = CGRect::default();
203
-                if SLSGetWindowBounds(overlay.cid, target_wid, &mut bounds) != kCGErrorSuccess {
204
-                    return;
371
+        let active_only = self.active_only;
372
+        let focused = self.focused_wid;
373
+
374
+        if let Some(overlay) = self.overlays.get_mut(&target_wid) {
375
+            let prev = overlay.bounds();
376
+            if size_changed(prev, bounds) || origin_changed(prev, bounds) {
377
+                debug!(
378
+                    "[sync_overlay] target={} geometry ({:.1},{:.1},{:.1},{:.1}) -> ({:.1},{:.1},{:.1},{:.1})",
379
+                    target_wid,
380
+                    prev.origin.x,
381
+                    prev.origin.y,
382
+                    prev.size.width,
383
+                    prev.size.height,
384
+                    bounds.origin.x,
385
+                    bounds.origin.y,
386
+                    bounds.size.width,
387
+                    bounds.size.height
388
+                );
389
+                overlay.window.set_bounds(
390
+                    bounds.origin.x,
391
+                    bounds.origin.y,
392
+                    bounds.size.width,
393
+                    bounds.size.height,
394
+                );
395
+                // orderWindow:relativeTo: re-shows an off-screen window
396
+                // as a side effect. In active_only mode, non-focused
397
+                // overlays must remain hidden — otherwise stack peek
398
+                // positions cause every stacked window's overlay to
399
+                // pop onto the screen as their bounds shift.
400
+                if !active_only || target_wid == focused {
401
+                    overlay.window.order_above(target_wid);
205402
                 }
206
-                let bw = self.border_width;
207
-                let origin = CGPoint {
208
-                    x: bounds.origin.x - bw,
209
-                    y: bounds.origin.y - bw,
210
-                };
211
-                SLSMoveWindow(overlay.cid, overlay.wid, &origin);
212403
             }
213404
         }
405
+
406
+        false
214407
     }
215408
 
216
-    /// Recreate overlay at new size.
217
-    fn recreate(&mut self, target_wid: u32) {
218
-        if !self.overlays.contains_key(&target_wid) {
219
-            return;
409
+    fn reconcile_tracked(&mut self) -> bool {
410
+        let tracked: Vec<u32> = self.overlays.keys().copied().collect();
411
+        let mut changed = false;
412
+
413
+        for wid in tracked {
414
+            changed |= self.sync_overlay(wid);
220415
         }
221
-        self.remove(target_wid);
222
-        self.add_fresh(target_wid);
223
-        if self.active_only && target_wid != self.focused_wid {
224
-            self.hide(target_wid);
416
+
417
+        changed
418
+    }
419
+
420
+    /// Re-apply each overlay's CAShapeLayer geometry. Called on a slow
421
+    /// periodic schedule (and on hotplug) to repair layer state that
422
+    /// macOS occasionally resets during display sleep/wake without
423
+    /// changing the NSWindow's frame — sync_overlay won't fix it on
424
+    /// its own because the SLS bounds match what we already stored.
425
+    fn refresh_all_layers(&self) {
426
+        for overlay in self.overlays.values() {
427
+            overlay.window.reapply_layer();
225428
         }
226
-        self.subscribe_target(target_wid);
227429
     }
228430
 
229
-    fn hide(&self, target_wid: u32) {
230
-        if let Some(o) = self.overlays.get(&target_wid) {
431
+    /// Re-apply set_bounds for every tracked overlay even when the
432
+    /// stored CG bounds match the current SLS bounds. After a display
433
+    /// reconfiguration the cocoa frame depends on the (possibly new)
434
+    /// primary screen height, so unchanged CG bounds still need their
435
+    /// cocoa frame recomputed.
436
+    fn reconcile_all_force(&mut self) {
437
+        let tracked: Vec<u32> = self.overlays.keys().copied().collect();
438
+        let active_only = self.active_only;
439
+        let focused = self.focused_wid;
440
+        for wid in tracked {
441
+            let mut bounds = CGRect::default();
231442
             unsafe {
232
-                SLSOrderWindow(o.cid, o.wid, 0, 0);
443
+                if SLSGetWindowBounds(self.main_cid, wid, &mut bounds) != kCGErrorSuccess {
444
+                    self.remove(wid);
445
+                    continue;
446
+                }
447
+            }
448
+            if let Some(overlay) = self.overlays.get_mut(&wid) {
449
+                overlay.window.set_bounds(
450
+                    bounds.origin.x,
451
+                    bounds.origin.y,
452
+                    bounds.size.width,
453
+                    bounds.size.height,
454
+                );
455
+                if !active_only || wid == focused {
456
+                    overlay.window.order_above(wid);
457
+                }
233458
             }
234459
         }
235460
     }
236461
 
462
+    fn hide(&self, target_wid: u32) {
463
+        if let Some(o) = self.overlays.get(&target_wid) {
464
+            debug!("[hide] target={} overlay_wid={}", target_wid, o.wid());
465
+            o.window.order_out();
466
+        }
467
+    }
468
+
237469
     fn unhide(&self, target_wid: u32) {
238470
         if let Some(o) = self.overlays.get(&target_wid) {
239
-            unsafe {
240
-                SLSSetWindowLevel(o.cid, o.wid, 0);
241
-                SLSOrderWindow(o.cid, o.wid, 1, target_wid);
242
-            }
471
+            debug!("[unhide] target={} overlay_wid={}", target_wid, o.wid());
472
+            o.window.order_above(target_wid);
243473
         }
244474
     }
245475
 
@@ -266,38 +496,63 @@ impl BorderMap {
266496
     /// Redraw an existing overlay with a new color (no destroy/recreate).
267497
     fn redraw(&self, target_wid: u32) {
268498
         if let Some(overlay) = self.overlays.get(&target_wid) {
269
-            unsafe {
270
-                let mut bounds = CGRect::default();
271
-                if SLSGetWindowBounds(overlay.cid, target_wid, &mut bounds) != kCGErrorSuccess {
272
-                    return;
273
-                }
274
-                let bw = self.border_width;
275
-                let ow = bounds.size.width + 2.0 * bw;
276
-                let oh = bounds.size.height + 2.0 * bw;
277
-
278
-                let ctx = SLWindowContextCreate(overlay.cid, overlay.wid, ptr::null());
279
-                if ctx.is_null() {
280
-                    return;
281
-                }
282
-
283
-                let color = self.color_for(target_wid);
284
-                draw_border(ctx, ow, oh, bw, self.radius, color);
285
-                SLSFlushWindowContentRegion(overlay.cid, overlay.wid, ptr::null());
286
-                CGContextRelease(ctx);
287
-            }
499
+            overlay.window.set_color(self.color_for(target_wid));
288500
         }
289501
     }
290502
 
291503
     /// Detect focused window and update border colors if focus changed.
292504
     fn update_focus(&mut self) {
293505
         let front = get_front_window(self.own_pid);
294
-        if front == 0 || front == self.focused_wid {
506
+        if front == 0 {
507
+            return;
508
+        }
509
+        if front == self.focused_wid {
510
+            // Same focus as last poll. But a freshly-spawned window may
511
+            // have been focused before its SLS state was complete enough
512
+            // to pass the add_fresh filter — retry on every poll until
513
+            // it sticks.
514
+            if !self.overlays.contains_key(&front) {
515
+                self.add_fresh(front);
516
+                if self.overlays.contains_key(&front) {
517
+                    debug!("[focus-retry] front={} now tracked", front);
518
+                    self.subscribe_target(front);
519
+                    if self.active_only {
520
+                        self.unhide(front);
521
+                    }
522
+                }
523
+            }
295524
             return;
296525
         }
297526
 
298527
         let old = self.focused_wid;
299528
         self.focused_wid = front;
300
-        debug!("[focus] {} -> {}", old, front);
529
+
530
+        // tarmac-style workspace switching can swap focus to a window
531
+        // that wasn't visible (and therefore not discovered) at ers
532
+        // startup. Discover_windows only enumerates on-current-space
533
+        // windows; tarmac stages other workspaces' windows in a hidden
534
+        // state ers never picked up. If focus lands on such a wid,
535
+        // create an overlay for it on demand.
536
+        let new_target = !self.overlays.contains_key(&front);
537
+        debug!(
538
+            "[focus] {} -> {} {}(tracked targets: {:?})",
539
+            old,
540
+            front,
541
+            if new_target { "[NEW] " } else { "" },
542
+            self.overlays.keys().collect::<Vec<_>>()
543
+        );
544
+        if new_target {
545
+            self.add_fresh(front);
546
+            self.subscribe_target(front);
547
+        }
548
+
549
+        // Pull both overlays' positions to the targets' current SLS bounds
550
+        // before un/hiding. AX-driven moves during a stack cycle frequently
551
+        // don't fire SLS WINDOW_MOVE notifications, so a stored overlay
552
+        // can be at stale coordinates. SLSGetWindowBounds (inside
553
+        // sync_overlay) is real-time and doesn't wait for a notification.
554
+        self.sync_overlay(old);
555
+        self.sync_overlay(front);
301556
 
302557
         if self.active_only {
303558
             self.hide(old);
@@ -333,14 +588,9 @@ impl BorderMap {
333588
         }
334589
         for (&target_wid, o) in &self.overlays {
335590
             if target_wid == self.focused_wid {
336
-                unsafe {
337
-                    SLSSetWindowLevel(o.cid, o.wid, 0);
338
-                    SLSOrderWindow(o.cid, o.wid, 1, target_wid);
339
-                }
591
+                o.window.order_above(target_wid);
340592
             } else {
341
-                unsafe {
342
-                    SLSOrderWindow(o.cid, o.wid, 0, 0);
343
-                }
593
+                o.window.order_out();
344594
             }
345595
         }
346596
     }
@@ -410,6 +660,10 @@ fn get_front_window(own_pid: i32) -> u32 {
410660
                 continue;
411661
             }
412662
 
663
+            if !is_suitable_window(SLSMainConnectionID(), wid) {
664
+                continue;
665
+            }
666
+
413667
             // Track first non-self window as fallback (z-order based)
414668
             if fallback_wid == 0 {
415669
                 fallback_wid = wid;
@@ -499,10 +753,13 @@ fn print_help() {
499753
 }
500754
 
501755
 fn main() {
756
+    let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
757
+        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("ers=info"));
502758
     tracing_subscriber::fmt()
503
-        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
759
+        .with_env_filter(env_filter)
504760
         .with_writer(std::io::stderr)
505761
         .init();
762
+    debug!("[main] ers starting, pid={}", std::process::id());
506763
 
507764
     let args: Vec<String> = std::env::args().collect();
508765
 
@@ -534,6 +791,13 @@ fn main() {
534791
 
535792
     let active_only = args.iter().any(|s| s == "--active-only");
536793
 
794
+    // Initialize NSApplication on the main thread before we touch any
795
+    // AppKit APIs. NSWindow operations (used by nswindow_overlay) all
796
+    // require a main-thread context.
797
+    let mtm = nswindow_overlay::init_application();
798
+    nswindow_overlay::log_screens(mtm);
799
+    register_display_hotplug_callback();
800
+
537801
     let cid = unsafe { SLSMainConnectionID() };
538802
     let own_pid = unsafe {
539803
         let mut pid: i32 = 0;
@@ -548,7 +812,7 @@ fn main() {
548812
     setup_event_port(cid);
549813
 
550814
     // Discover and create borders
551
-    let mut borders = BorderMap::new(cid, own_pid, border_width);
815
+    let mut borders = BorderMap::new(cid, own_pid, border_width, mtm);
552816
     borders.radius = radius;
553817
     borders.active_color = active_color;
554818
     borders.inactive_color = inactive_color;
@@ -612,179 +876,306 @@ fn main() {
612876
         );
613877
     }
614878
 
615
-    // Process events on background thread with coalescing
616
-    let running_bg = Arc::clone(&running);
617
-    let handle = std::thread::spawn(move || {
618
-        use std::collections::HashSet;
619
-        use std::time::{Duration, Instant};
620
-
621
-        // Persist across batches: windows we know about but haven't bordered yet.
622
-        // Value is the time the window was first seen — only promote after 100ms
623
-        // so tarmac has time to position them.
624
-        let mut pending: HashMap<u32, Instant> = HashMap::new();
625
-
626
-        while running_bg.load(Ordering::Relaxed) {
627
-            let first = match rx.recv_timeout(Duration::from_millis(100)) {
628
-                Ok(e) => e,
629
-                Err(mpsc::RecvTimeoutError::Timeout) => continue,
630
-                Err(mpsc::RecvTimeoutError::Disconnected) => break,
631
-            };
632
-
633
-            std::thread::sleep(std::time::Duration::from_millis(16));
634
-
635
-            let mut events = vec![first];
636
-            while let Ok(e) = rx.try_recv() {
637
-                events.push(e);
638
-            }
879
+    // Process events on the main thread via a CFRunLoopTimer.
880
+    // BorderMap holds Retained<NSWindow> handles, which are
881
+    // !Send/!Sync — AppKit calls must originate from the main thread.
882
+    // Stash state in thread_local for the C callback to access.
883
+    MAIN_STATE.with(|cell| {
884
+        *cell.borrow_mut() = Some(MainState {
885
+            borders,
886
+            rx,
887
+            pending: HashMap::new(),
888
+            batch_events: Vec::new(),
889
+            batch_first_seen: None,
890
+        });
891
+    });
639892
 
640
-            let mut moved: HashSet<u32> = HashSet::new();
641
-            let mut resized: HashSet<u32> = HashSet::new();
642
-            let mut destroyed: HashSet<u32> = HashSet::new();
643
-            let mut needs_resubscribe = false;
644
-
645
-            for event in events {
646
-                match event {
647
-                    Event::Move(wid) => {
648
-                        if !borders.is_overlay(wid) {
649
-                            moved.insert(wid);
650
-                        }
651
-                    }
652
-                    Event::Resize(wid) => {
653
-                        if !borders.is_overlay(wid) {
654
-                            resized.insert(wid);
655
-                        }
656
-                    }
657
-                    Event::Close(wid) | Event::Destroy(wid) => {
658
-                        if !borders.is_overlay(wid) {
659
-                            destroyed.insert(wid);
660
-                            pending.remove(&wid);
661
-                        }
662
-                    }
663
-                    Event::Create(wid) => {
664
-                        if !borders.is_overlay(wid) {
665
-                            pending.entry(wid).or_insert_with(Instant::now);
666
-                            borders.subscribe_target(wid);
667
-                        }
668
-                    }
669
-                    Event::Hide(wid) => borders.hide(wid),
670
-                    Event::Unhide(wid) => {
671
-                        if !borders.active_only || wid == borders.focused_wid {
672
-                            borders.unhide(wid);
673
-                        }
674
-                    }
675
-                    Event::FrontChange => {
676
-                        needs_resubscribe = true;
677
-                    }
678
-                    Event::SpaceChange => {
679
-                        needs_resubscribe = true;
893
+    unsafe {
894
+        let mut ctx = CFRunLoopTimerContext {
895
+            version: 0,
896
+            info: ptr::null_mut(),
897
+            retain: None,
898
+            release: None,
899
+            copy_description: None,
900
+        };
901
+        let timer = CFRunLoopTimerCreate(
902
+            ptr::null(),
903
+            CFAbsoluteTimeGetCurrent() + 0.05,
904
+            0.016,
905
+            0u64,
906
+            0i64,
907
+            timer_callback,
908
+            &mut ctx,
909
+        );
910
+        CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopDefaultMode);
911
+    }
912
+
913
+    unsafe { CFRunLoopRun() };
914
+
915
+    // Drop everything on the main thread (NSWindow.close in Drop).
916
+    MAIN_STATE.with(|cell| cell.borrow_mut().take());
917
+
918
+    SIGNAL_STOP_REQUESTED.store(true, Ordering::Relaxed);
919
+    let _ = signal_watcher.join();
920
+    drop(running);
921
+}
922
+
923
+struct MainState {
924
+    borders: BorderMap,
925
+    rx: mpsc::Receiver<Event>,
926
+    pending: HashMap<u32, std::time::Instant>,
927
+    batch_events: Vec<Event>,
928
+    batch_first_seen: Option<std::time::Instant>,
929
+}
930
+
931
+thread_local! {
932
+    static MAIN_STATE: std::cell::RefCell<Option<MainState>> = const { std::cell::RefCell::new(None) };
933
+}
934
+
935
+extern "C" fn timer_callback(_timer: *mut std::ffi::c_void, _info: *mut std::ffi::c_void) {
936
+    use std::time::{Duration, Instant};
937
+    use std::sync::atomic::AtomicUsize;
938
+    static TICK_COUNT: AtomicUsize = AtomicUsize::new(0);
939
+    let tick = TICK_COUNT.fetch_add(1, Ordering::Relaxed);
940
+    if tick == 0 {
941
+        debug!("[timer] first fire — main-thread event loop is alive");
942
+    } else if tick % 600 == 0 {
943
+        // every ~10s if interval is 16ms
944
+        debug!("[timer] tick {}", tick);
945
+    }
946
+    MAIN_STATE.with(|cell| {
947
+        let mut state_opt = cell.borrow_mut();
948
+        let s = match state_opt.as_mut() {
949
+            Some(s) => s,
950
+            None => return,
951
+        };
952
+        let mut received = 0usize;
953
+        loop {
954
+            match s.rx.try_recv() {
955
+                Ok(e) => {
956
+                    if s.batch_events.is_empty() {
957
+                        s.batch_first_seen = Some(Instant::now());
680958
                     }
959
+                    s.batch_events.push(e);
960
+                    received += 1;
681961
                 }
962
+                Err(mpsc::TryRecvError::Empty) => break,
963
+                Err(mpsc::TryRecvError::Disconnected) => break,
682964
             }
683
-
684
-            // Destroys
685
-            for wid in &destroyed {
686
-                borders.remove(*wid);
965
+        }
966
+        if received > 0 {
967
+            debug!(
968
+                "[timer] received {} new events; batch size now {}",
969
+                received,
970
+                s.batch_events.len()
971
+            );
972
+        }
973
+        // Process the accumulated batch after a 16ms quiet window
974
+        // (matches the old bg-thread behavior where it slept 16ms after
975
+        // the first event then drained). Events keep arriving, the batch
976
+        // grows; once 16ms passes without new events we flush.
977
+        let should_flush = s.batch_first_seen.is_some_and(|t| {
978
+            t.elapsed() >= Duration::from_millis(16) && received == 0
979
+        }) || s
980
+            .batch_first_seen
981
+            .is_some_and(|t| t.elapsed() >= Duration::from_millis(120));
982
+        if should_flush {
983
+            let events = std::mem::take(&mut s.batch_events);
984
+            s.batch_first_seen = None;
985
+            debug!("[timer] processing batch of {}", events.len());
986
+            process_event_batch(&mut s.borders, &mut s.pending, events);
987
+        } else {
988
+            // Even with no events, poll focus periodically so a missed
989
+            // FrontChange notification doesn't strand the active border.
990
+            // Cheap operation when focus hasn't changed.
991
+            s.borders.update_focus();
992
+            // Once per second, reconcile tracked overlays against
993
+            // current SLS state. Catches missed Close/Destroy events
994
+            // that would otherwise leave a dead border on screen.
995
+            if tick % 60 == 0 && tick > 0 {
996
+                let removed = s.borders.reconcile_tracked();
997
+                if removed {
998
+                    debug!("[timer] periodic reconcile removed stale overlays");
999
+                }
1000
+                // Cheap: re-applies just the CAShapeLayer frame/path
1001
+                // for every overlay. Recovers from layer state that
1002
+                // macOS resets during display sleep/wake without
1003
+                // touching the NSWindow frame.
1004
+                s.borders.refresh_all_layers();
6871005
             }
1006
+        }
1007
+    });
1008
+}
6881009
 
689
-            // Promote pending creates that have waited ≥100ms (tarmac positioning time)
690
-            let now = Instant::now();
691
-            let ready: Vec<u32> = pending
692
-                .iter()
693
-                .filter(|(wid, seen_at)| {
694
-                    !destroyed.contains(wid)
695
-                        && now.duration_since(**seen_at) >= Duration::from_millis(100)
696
-                })
697
-                .map(|(wid, _)| *wid)
698
-                .collect();
699
-            // Filter overlapping creates: if two windows overlap, keep smaller one
700
-            let mut bounds_map: Vec<(u32, CGRect)> = Vec::new();
701
-            for &wid in &ready {
702
-                unsafe {
703
-                    let mut b = CGRect::default();
704
-                    SLSGetWindowBounds(borders.main_cid, wid, &mut b);
705
-                    bounds_map.push((wid, b));
1010
+fn process_event_batch(
1011
+    borders: &mut BorderMap,
1012
+    pending: &mut HashMap<u32, std::time::Instant>,
1013
+    events: Vec<Event>,
1014
+) {
1015
+    use std::collections::HashSet;
1016
+    use std::time::{Duration, Instant};
1017
+
1018
+    let mut moved: HashSet<u32> = HashSet::new();
1019
+    let mut resized: HashSet<u32> = HashSet::new();
1020
+    let mut destroyed: HashSet<u32> = HashSet::new();
1021
+    let mut needs_resubscribe = false;
1022
+
1023
+    for event in events {
1024
+        match event {
1025
+            Event::Move(wid) => {
1026
+                if !borders.is_overlay(wid) {
1027
+                    moved.insert(wid);
7061028
                 }
7071029
             }
708
-
709
-            // If two new windows overlap closely, skip the larger one (container)
710
-            let mut skip: std::collections::HashSet<u32> = HashSet::new();
711
-            for i in 0..bounds_map.len() {
712
-                for j in (i + 1)..bounds_map.len() {
713
-                    let (wid_a, a) = &bounds_map[i];
714
-                    let (wid_b, b) = &bounds_map[j];
715
-                    // Check if centers are close (within 30px)
716
-                    let cx_a = a.origin.x + a.size.width / 2.0;
717
-                    let cy_a = a.origin.y + a.size.height / 2.0;
718
-                    let cx_b = b.origin.x + b.size.width / 2.0;
719
-                    let cy_b = b.origin.y + b.size.height / 2.0;
720
-                    if (cx_a - cx_b).abs() < 30.0 && (cy_a - cy_b).abs() < 30.0 {
721
-                        // Skip the larger one
722
-                        let area_a = window_area(*a);
723
-                        let area_b = window_area(*b);
724
-                        if area_a > area_b {
725
-                            skip.insert(*wid_a);
726
-                        } else {
727
-                            skip.insert(*wid_b);
728
-                        }
729
-                    }
1030
+            Event::Resize(wid) => {
1031
+                if !borders.is_overlay(wid) {
1032
+                    resized.insert(wid);
7301033
                 }
7311034
             }
732
-
733
-            for &wid in &ready {
734
-                pending.remove(&wid);
735
-                if !skip.contains(&wid) {
736
-                    borders.add_fresh(wid);
737
-                    if borders.active_only && wid != borders.focused_wid {
738
-                        borders.hide(wid);
739
-                    }
740
-                    needs_resubscribe = true;
1035
+            Event::Close(wid) | Event::Destroy(wid) => {
1036
+                if !borders.is_overlay(wid) {
1037
+                    debug!("[event] Close/Destroy target_wid={}", wid);
1038
+                    destroyed.insert(wid);
1039
+                    pending.remove(&wid);
7411040
                 }
7421041
             }
743
-
744
-            // Moves: reposition overlay (no destroy/create)
745
-            for wid in &moved {
746
-                if !resized.contains(wid) && !ready.contains(wid) {
747
-                    borders.reposition(*wid);
1042
+            Event::Create(wid) => {
1043
+                if !borders.is_overlay(wid) {
1044
+                    pending.entry(wid).or_insert_with(Instant::now);
1045
+                    borders.subscribe_target(wid);
7481046
                 }
7491047
             }
750
-
751
-            // Resizes: must recreate (can't reshape windows on Tahoe)
752
-            // Skip windows just created this batch — already at correct size
753
-            for wid in &resized {
754
-                if !ready.contains(wid) && borders.overlays.contains_key(wid) {
755
-                    borders.recreate(*wid);
756
-                    needs_resubscribe = true;
1048
+            Event::Hide(wid) => borders.hide(wid),
1049
+            Event::Unhide(wid) => {
1050
+                if !borders.is_overlay(wid) {
1051
+                    if !borders.overlays.contains_key(&wid) {
1052
+                        borders.add_fresh(wid);
1053
+                        borders.subscribe_target(wid);
1054
+                    }
1055
+                    if !borders.active_only || wid == borders.focused_wid {
1056
+                        borders.unhide(wid);
1057
+                    }
7571058
                 }
7581059
             }
759
-
760
-            // On space change, discover windows we haven't seen yet
761
-            if needs_resubscribe {
762
-                borders.discover_untracked();
1060
+            Event::FrontChange => {
1061
+                needs_resubscribe = true;
7631062
             }
1063
+            Event::SpaceChange => {
1064
+                needs_resubscribe = true;
1065
+            }
1066
+        }
1067
+    }
7641068
 
765
-            // Update focus (redraws borders in-place if changed)
766
-            borders.update_focus();
1069
+    for wid in &destroyed {
1070
+        borders.remove(*wid);
1071
+    }
7671072
 
768
-            // Re-subscribe ALL tracked windows (SLSRequestNotificationsForWindows replaces, not appends)
769
-            if needs_resubscribe || !destroyed.is_empty() {
770
-                borders.subscribe_all();
1073
+    let now = Instant::now();
1074
+    let ready: Vec<u32> = pending
1075
+        .iter()
1076
+        .filter(|(wid, seen_at)| {
1077
+            !destroyed.contains(wid) && now.duration_since(**seen_at) >= Duration::from_millis(100)
1078
+        })
1079
+        .map(|(wid, _)| *wid)
1080
+        .collect();
1081
+
1082
+    let mut bounds_map: Vec<(u32, CGRect)> = Vec::new();
1083
+    for &wid in &ready {
1084
+        unsafe {
1085
+            let mut b = CGRect::default();
1086
+            SLSGetWindowBounds(borders.main_cid, wid, &mut b);
1087
+            bounds_map.push((wid, b));
1088
+        }
1089
+    }
1090
+
1091
+    let mut skip: std::collections::HashSet<u32> = HashSet::new();
1092
+    for i in 0..bounds_map.len() {
1093
+        for j in (i + 1)..bounds_map.len() {
1094
+            let (wid_a, a) = &bounds_map[i];
1095
+            let (wid_b, b) = &bounds_map[j];
1096
+            if let Some(preference) = surface_preference(*a, *b) {
1097
+                match preference {
1098
+                    SurfacePreference::KeepExisting => {
1099
+                        skip.insert(*wid_b);
1100
+                    }
1101
+                    SurfacePreference::ReplaceExisting => {
1102
+                        skip.insert(*wid_a);
1103
+                    }
1104
+                }
7711105
             }
1106
+        }
1107
+    }
7721108
 
773
-            // After all processing, enforce active-only visibility
774
-            borders.enforce_active_only();
1109
+    for &wid in &ready {
1110
+        pending.remove(&wid);
1111
+        if !skip.contains(&wid) {
1112
+            borders.add_fresh(wid);
1113
+            if borders.active_only && wid != borders.focused_wid {
1114
+                borders.hide(wid);
1115
+            }
1116
+            needs_resubscribe = true;
7751117
         }
1118
+    }
7761119
 
777
-        // Clean up all overlays before exiting
778
-        borders.remove_all();
779
-    });
1120
+    for wid in &moved {
1121
+        if !resized.contains(wid) && !ready.contains(wid) && borders.sync_overlay(*wid) {
1122
+            needs_resubscribe = true;
1123
+        }
1124
+    }
7801125
 
781
-    unsafe { CFRunLoopRun() };
1126
+    for wid in &resized {
1127
+        if !ready.contains(wid)
1128
+            && borders.overlays.contains_key(wid)
1129
+            && borders.sync_overlay(*wid)
1130
+        {
1131
+            needs_resubscribe = true;
1132
+        }
1133
+    }
7821134
 
783
-    // SIGINT received — signal background thread to stop and wait
784
-    running.store(false, Ordering::Relaxed);
785
-    SIGNAL_STOP_REQUESTED.store(true, Ordering::Relaxed);
786
-    let _ = signal_watcher.join();
787
-    let _ = handle.join();
1135
+    if needs_resubscribe {
1136
+        borders.discover_untracked();
1137
+    }
1138
+
1139
+    needs_resubscribe |= borders.reconcile_tracked();
1140
+
1141
+    borders.update_focus();
1142
+
1143
+    if needs_resubscribe || !destroyed.is_empty() {
1144
+        borders.subscribe_all();
1145
+    }
1146
+
1147
+    borders.enforce_active_only();
1148
+}
1149
+
1150
+/// Re-log the screen layout when the display configuration changes
1151
+/// (monitor plug/unplug, resolution change). The callback also nudges
1152
+/// every tracked overlay to re-fetch its bounds so any cached cocoa Y
1153
+/// computed against the old primary height gets refreshed.
1154
+unsafe extern "C" fn display_reconfig_callback(
1155
+    display_id: u32,
1156
+    flags: u32,
1157
+    _user_info: *mut std::ffi::c_void,
1158
+) {
1159
+    debug!(display_id, flags, "[hotplug] CGDisplay reconfiguration");
1160
+    if let Some(mtm) = objc2::MainThreadMarker::new() {
1161
+        nswindow_overlay::log_screens(mtm);
1162
+    }
1163
+    MAIN_STATE.with(|cell| {
1164
+        if let Some(s) = cell.borrow_mut().as_mut() {
1165
+            s.borders.reconcile_all_force();
1166
+            s.borders.refresh_all_layers();
1167
+        }
1168
+    });
1169
+}
1170
+
1171
+fn register_display_hotplug_callback() {
1172
+    unsafe {
1173
+        let rc = CGDisplayRegisterReconfigurationCallback(
1174
+            Some(display_reconfig_callback),
1175
+            std::ptr::null_mut(),
1176
+        );
1177
+        debug!("[hotplug] register CGDisplayReconfiguration rc={}", rc);
1178
+    }
7881179
 }
7891180
 
7901181
 fn setup_event_port(cid: CGSConnectionID) {
@@ -829,7 +1220,7 @@ unsafe extern "C" fn drain_events(
8291220
     }
8301221
 }
8311222
 
832
-fn discover_windows(_cid: CGSConnectionID, own_pid: i32) -> Vec<u32> {
1223
+fn discover_windows(cid: CGSConnectionID, own_pid: i32) -> Vec<u32> {
8331224
     unsafe {
8341225
         let list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
8351226
         if list.is_null() {
@@ -865,6 +1256,10 @@ fn discover_windows(_cid: CGSConnectionID, own_pid: i32) -> Vec<u32> {
8651256
                 continue;
8661257
             }
8671258
 
1259
+            if !is_suitable_window(cid, wid) {
1260
+                continue;
1261
+            }
1262
+
8681263
             let mut layer: i32 = -1;
8691264
             if CFDictionaryGetValueIfPresent(dict, layer_key as CFTypeRef, &mut v) {
8701265
                 CFNumberGetValue(v, kCFNumberSInt32Type, &mut layer as *mut _ as *mut _);
@@ -884,106 +1279,6 @@ fn discover_windows(_cid: CGSConnectionID, own_pid: i32) -> Vec<u32> {
8841279
     }
8851280
 }
8861281
 
887
-/// Draw a border ring into an existing CGContext, clearing first.
888
-fn draw_border(
889
-    ctx: CGContextRef,
890
-    width: f64,
891
-    height: f64,
892
-    border_width: f64,
893
-    radius: f64,
894
-    color: (f64, f64, f64, f64),
895
-) {
896
-    unsafe {
897
-        let full = CGRect::new(0.0, 0.0, width, height);
898
-        CGContextClearRect(ctx, full);
899
-
900
-        let bw = border_width;
901
-        let stroke_rect = CGRect::new(bw / 2.0, bw / 2.0, width - bw, height - bw);
902
-        let max_r = (stroke_rect.size.width.min(stroke_rect.size.height) / 2.0).max(0.0);
903
-        let r = radius.min(max_r);
904
-
905
-        CGContextSetRGBStrokeColor(ctx, color.0, color.1, color.2, color.3);
906
-        CGContextSetLineWidth(ctx, bw);
907
-        let path = CGPathCreateWithRoundedRect(stroke_rect, r, r, ptr::null());
908
-        if !path.is_null() {
909
-            CGContextAddPath(ctx, path);
910
-            CGContextStrokePath(ctx);
911
-            CGPathRelease(path);
912
-        }
913
-        CGContextFlush(ctx);
914
-    }
915
-}
916
-
917
-fn create_overlay(
918
-    cid: CGSConnectionID,
919
-    target_wid: u32,
920
-    border_width: f64,
921
-    radius: f64,
922
-    color: (f64, f64, f64, f64),
923
-) -> Option<(CGSConnectionID, u32)> {
924
-    unsafe {
925
-        let mut bounds = CGRect::default();
926
-        let rc = SLSGetWindowBounds(cid, target_wid, &mut bounds);
927
-        if rc != kCGErrorSuccess {
928
-            debug!("[create_overlay] SLSGetWindowBounds failed for wid={target_wid} rc={rc}");
929
-            return None;
930
-        }
931
-        if bounds.size.width < 10.0 || bounds.size.height < 10.0 {
932
-            debug!(
933
-                "[create_overlay] wid={target_wid} too small: {}x{}",
934
-                bounds.size.width, bounds.size.height
935
-            );
936
-            return None;
937
-        }
938
-
939
-        let bw = border_width;
940
-        let ow = bounds.size.width + 2.0 * bw;
941
-        let oh = bounds.size.height + 2.0 * bw;
942
-        let ox = bounds.origin.x - bw;
943
-        let oy = bounds.origin.y - bw;
944
-
945
-        let frame = CGRect::new(0.0, 0.0, ow, oh);
946
-        let mut region: CFTypeRef = ptr::null();
947
-        CGSNewRegionWithRect(&frame, &mut region);
948
-        if region.is_null() {
949
-            debug!("[create_overlay] CGSNewRegionWithRect failed for wid={target_wid}");
950
-            return None;
951
-        }
952
-
953
-        let mut wid: u32 = 0;
954
-        SLSNewWindow(cid, 2, ox as f32, oy as f32, region, &mut wid);
955
-        CFRelease(region);
956
-        if wid == 0 {
957
-            debug!("[create_overlay] SLSNewWindow returned 0 for target={target_wid} cid={cid}");
958
-            return None;
959
-        }
960
-
961
-        debug!(
962
-            "[create_overlay] created overlay wid={wid} for target={target_wid} color=({:.2},{:.2},{:.2},{:.2})",
963
-            color.0, color.1, color.2, color.3
964
-        );
965
-
966
-        SLSSetWindowResolution(cid, wid, 2.0);
967
-        SLSSetWindowOpacity(cid, wid, false);
968
-        SLSSetWindowLevel(cid, wid, 0);
969
-        SLSOrderWindow(cid, wid, 1, target_wid);
970
-
971
-        // Draw border (point coordinates)
972
-        let ctx = SLWindowContextCreate(cid, wid, ptr::null());
973
-        if ctx.is_null() {
974
-            debug!("[create_overlay] SLWindowContextCreate returned null for overlay wid={wid}");
975
-            SLSReleaseWindow(cid, wid);
976
-            return None;
977
-        }
978
-
979
-        draw_border(ctx, ow, oh, bw, radius, color);
980
-        SLSFlushWindowContentRegion(cid, wid, ptr::null());
981
-        CGContextRelease(ctx);
982
-
983
-        Some((cid, wid))
984
-    }
985
-}
986
-
9871282
 fn list_windows() {
9881283
     let cid = unsafe { SLSMainConnectionID() };
9891284
     unsafe {
@@ -1033,7 +1328,10 @@ fn list_windows() {
10331328
 
10341329
 #[cfg(test)]
10351330
 mod tests {
1036
-    use super::{CGRect, intersection_area, is_same_window_surface};
1331
+    use super::{
1332
+        CGRect, SurfacePreference, WindowMetadata, intersection_area, is_same_window_surface,
1333
+        is_suitable_window_metadata, is_trackable_window, surface_preference,
1334
+    };
10371335
 
10381336
     #[test]
10391337
     fn same_surface_detects_contained_strip() {
@@ -1055,4 +1353,40 @@ mod tests {
10551353
         let b = CGRect::new(400.0, 400.0, 200.0, 200.0);
10561354
         assert_eq!(intersection_area(a, b), 0.0);
10571355
     }
1356
+
1357
+    #[test]
1358
+    fn same_surface_prefers_larger_bounds() {
1359
+        let strip = CGRect::new(114.0, 105.0, 1160.0, 140.0);
1360
+        let outer = CGRect::new(100.0, 100.0, 1200.0, 900.0);
1361
+        assert_eq!(
1362
+            surface_preference(strip, outer),
1363
+            Some(SurfacePreference::ReplaceExisting)
1364
+        );
1365
+    }
1366
+
1367
+    #[test]
1368
+    fn small_windows_remain_trackable() {
1369
+        let small = CGRect::new(100.0, 100.0, 12.0, 18.0);
1370
+        assert!(is_trackable_window(small, 4.0));
1371
+    }
1372
+
1373
+    #[test]
1374
+    fn suitable_window_metadata_matches_document_windows() {
1375
+        let metadata = WindowMetadata {
1376
+            parent_wid: 0,
1377
+            tags: super::WINDOW_TAG_DOCUMENT,
1378
+            attributes: super::WINDOW_ATTRIBUTE_REAL,
1379
+        };
1380
+        assert!(is_suitable_window_metadata(metadata));
1381
+    }
1382
+
1383
+    #[test]
1384
+    fn attached_windows_are_not_suitable_targets() {
1385
+        let metadata = WindowMetadata {
1386
+            parent_wid: 7,
1387
+            tags: super::WINDOW_TAG_DOCUMENT | super::WINDOW_TAG_ATTACHED,
1388
+            attributes: super::WINDOW_ATTRIBUTE_REAL,
1389
+        };
1390
+        assert!(!is_suitable_window_metadata(metadata));
1391
+    }
10581392
 }
src/nswindow_overlay.rsadded
@@ -0,0 +1,325 @@
1
+//! NSWindow-backed overlay border.
2
+//!
3
+//! Replaces the SLS-only window approach. The reason is screenshot
4
+//! exclusion on macOS Tahoe: `screencaptureui` enumerates windows via
5
+//! `_SLSCopyWindowsWithOptionsAndTagsAndSpaceOptions` +
6
+//! `_CGSGetWindowTags` and ignores the sharing-state of raw SLS-only
7
+//! windows. NSWindow.sharingType = .none is the only documented and
8
+//! verified-honored exclusion mechanism (verified empirically on Tahoe
9
+//! with `screencapture -l <wid>`: SLS overlays capture, NSWindows with
10
+//! `.none` sharingType return "could not create image from window").
11
+//!
12
+//! We use a CAShapeLayer for the rounded-rect border so updates stay
13
+//! declarative — no NSView subclassing required.
14
+
15
+use objc2::rc::Retained;
16
+use objc2::runtime::AnyObject;
17
+use objc2::{MainThreadMarker, MainThreadOnly, msg_send};
18
+use objc2_app_kit::{
19
+    NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSColor, NSScreen, NSWindow,
20
+    NSWindowCollectionBehavior, NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask,
21
+};
22
+use objc2_core_foundation::{CGPoint, CGRect, CGSize};
23
+use objc2_quartz_core::{CALayer, CAShapeLayer};
24
+use std::ptr;
25
+
26
+const NS_FLOATING_WINDOW_LEVEL: isize = 3;
27
+
28
+/// Top-left Y in CG global coordinates becomes bottom-left Y in Cocoa
29
+/// global coordinates by subtracting from the primary screen height.
30
+///
31
+/// We use the main CGDisplay's bounds rather than `NSScreen.screens`
32
+/// because NSScreen caches and only refreshes on certain notifications
33
+/// — when a monitor is plugged or unplugged, NSScreen.screens can
34
+/// return stale primary-height values, causing every cocoa Y on the
35
+/// new layout to be off by the difference. CGDisplayBounds reflects
36
+/// the current state immediately.
37
+fn primary_screen_height() -> f64 {
38
+    let main_id = objc2_core_graphics::CGMainDisplayID();
39
+    objc2_core_graphics::CGDisplayBounds(main_id).size.height
40
+}
41
+
42
+fn cg_to_cocoa_frame(cg: CGRect, _mtm: MainThreadMarker) -> CGRect {
43
+    let primary_height = primary_screen_height();
44
+    let cocoa_y = primary_height - cg.origin.y - cg.size.height;
45
+    CGRect::new(
46
+        CGPoint::new(cg.origin.x, cocoa_y),
47
+        CGSize::new(cg.size.width, cg.size.height),
48
+    )
49
+}
50
+
51
+/// Log all NSScreens and which one we'll treat as primary. Helps diagnose
52
+/// multi-monitor coordinate issues.
53
+pub fn log_screens(mtm: MainThreadMarker) {
54
+    let screens = NSScreen::screens(mtm);
55
+    let primary_h = primary_screen_height();
56
+    let cg_main_bounds = {
57
+        let id = objc2_core_graphics::CGMainDisplayID();
58
+        objc2_core_graphics::CGDisplayBounds(id)
59
+    };
60
+    tracing::debug!(
61
+        cg_primary_height = primary_h,
62
+        cg_main_x = cg_main_bounds.origin.x,
63
+        cg_main_y = cg_main_bounds.origin.y,
64
+        cg_main_w = cg_main_bounds.size.width,
65
+        cg_main_h = cg_main_bounds.size.height,
66
+        nsscreen_count = screens.count(),
67
+        "screen layout"
68
+    );
69
+    for i in 0..screens.count() {
70
+        let s = screens.objectAtIndex(i);
71
+        let f = s.frame();
72
+        tracing::debug!(
73
+            index = i,
74
+            cocoa_x = f.origin.x,
75
+            cocoa_y = f.origin.y,
76
+            w = f.size.width,
77
+            h = f.size.height,
78
+            "nsscreen"
79
+        );
80
+    }
81
+}
82
+
83
+/// Initialize NSApplication. Must be called once from the main thread.
84
+pub fn init_application() -> MainThreadMarker {
85
+    let mtm = MainThreadMarker::new().expect("init_application must run on the main thread");
86
+    let app = NSApplication::sharedApplication(mtm);
87
+    app.setActivationPolicy(NSApplicationActivationPolicy::Accessory);
88
+    mtm
89
+}
90
+
91
+/// One NSWindow + CAShapeLayer pair drawing a rounded-rect border.
92
+///
93
+/// `bounds_cg_*` fields are the TARGET window's CG bounds (origin
94
+/// top-left, Y-down) — same coordinate system the rest of ers uses.
95
+pub struct OverlayWindow {
96
+    window: Retained<NSWindow>,
97
+    border_layer: Retained<CAShapeLayer>,
98
+    pub bounds_cg_x: f64,
99
+    pub bounds_cg_y: f64,
100
+    pub bounds_cg_w: f64,
101
+    pub bounds_cg_h: f64,
102
+    pub border_width: f64,
103
+    pub radius: f64,
104
+    mtm: MainThreadMarker,
105
+}
106
+
107
+impl OverlayWindow {
108
+    /// Create an NSWindow border overlay around the given target bounds.
109
+    /// Coords are in CG space (origin top-left, Y-down).
110
+    pub fn new(
111
+        bounds_cg_x: f64,
112
+        bounds_cg_y: f64,
113
+        bounds_cg_w: f64,
114
+        bounds_cg_h: f64,
115
+        border_width: f64,
116
+        radius: f64,
117
+        color: (f64, f64, f64, f64),
118
+        mtm: MainThreadMarker,
119
+    ) -> Option<Self> {
120
+        let outer_cg = CGRect::new(
121
+            CGPoint::new(bounds_cg_x - border_width, bounds_cg_y - border_width),
122
+            CGSize::new(
123
+                bounds_cg_w + 2.0 * border_width,
124
+                bounds_cg_h + 2.0 * border_width,
125
+            ),
126
+        );
127
+        let cocoa_frame = cg_to_cocoa_frame(outer_cg, mtm);
128
+
129
+        let style = NSWindowStyleMask::Borderless;
130
+        let window: Retained<NSWindow> = unsafe {
131
+            msg_send![
132
+                NSWindow::alloc(mtm),
133
+                initWithContentRect: cocoa_frame,
134
+                styleMask: style,
135
+                backing: NSBackingStoreType::Buffered,
136
+                defer: false
137
+            ]
138
+        };
139
+        window.setOpaque(false);
140
+        window.setHasShadow(false);
141
+        window.setIgnoresMouseEvents(true);
142
+        window.setLevel(NS_FLOATING_WINDOW_LEVEL);
143
+        unsafe { window.setReleasedWhenClosed(false) };
144
+        window.setSharingType(NSWindowSharingType::None);
145
+        // Do NOT set CanJoinAllSpaces: that would draw the overlay on
146
+        // every macOS space simultaneously. tarmac's workspaces are
147
+        // not macOS spaces, but if the user has both, leaking onto
148
+        // every space looks like a "stuck border" bug.
149
+        window.setCollectionBehavior(
150
+            NSWindowCollectionBehavior::Stationary
151
+                | NSWindowCollectionBehavior::IgnoresCycle
152
+                | NSWindowCollectionBehavior::FullScreenAuxiliary,
153
+        );
154
+        // Clear background.
155
+        let clear = NSColor::clearColor();
156
+        window.setBackgroundColor(Some(&clear));
157
+
158
+        let content_view = window.contentView()?;
159
+        content_view.setWantsLayer(true);
160
+        let host_layer: Retained<CALayer> = unsafe {
161
+            let layer: Option<Retained<CALayer>> = msg_send![&*content_view, layer];
162
+            layer?
163
+        };
164
+
165
+        let border_layer = CAShapeLayer::new();
166
+        let path_rect = inset_for_stroke(outer_cg.size, border_width);
167
+        unsafe {
168
+            let path = objc2_core_graphics::CGPath::with_rounded_rect(
169
+                path_rect, radius, radius, ptr::null(),
170
+            );
171
+            let path_ref: *mut AnyObject =
172
+                objc2_core_foundation::CFRetained::as_ptr(&path).as_ptr() as *mut AnyObject;
173
+            let _: () = msg_send![&*border_layer, setPath: path_ref];
174
+
175
+            let _: () = msg_send![&*border_layer, setFillColor: ptr::null::<AnyObject>()];
176
+            let stroke = make_cgcolor(color, mtm);
177
+            let stroke_ref: *mut AnyObject =
178
+                objc2_core_foundation::CFRetained::as_ptr(&stroke).as_ptr() as *mut AnyObject;
179
+            let _: () = msg_send![&*border_layer, setStrokeColor: stroke_ref];
180
+            border_layer.setLineWidth(border_width);
181
+            border_layer.setFrame(CGRect::new(
182
+                CGPoint::new(0.0, 0.0),
183
+                CGSize::new(outer_cg.size.width, outer_cg.size.height),
184
+            ));
185
+            host_layer.addSublayer(&border_layer);
186
+        }
187
+
188
+        window.orderFrontRegardless();
189
+
190
+        Some(OverlayWindow {
191
+            window,
192
+            border_layer,
193
+            bounds_cg_x,
194
+            bounds_cg_y,
195
+            bounds_cg_w,
196
+            bounds_cg_h,
197
+            border_width,
198
+            radius,
199
+            mtm,
200
+        })
201
+    }
202
+
203
+    /// NSWindow's windowNumber, usable as a wid for tracking.
204
+    pub fn wid(&self) -> u32 {
205
+        self.window.windowNumber() as u32
206
+    }
207
+
208
+    pub fn set_bounds(&mut self, x: f64, y: f64, w: f64, h: f64) {
209
+        let outer_cg = CGRect::new(
210
+            CGPoint::new(x - self.border_width, y - self.border_width),
211
+            CGSize::new(w + 2.0 * self.border_width, h + 2.0 * self.border_width),
212
+        );
213
+        let cocoa_frame = cg_to_cocoa_frame(outer_cg, self.mtm);
214
+        self.window.setFrame_display(cocoa_frame, true);
215
+        let actual = self.window.frame();
216
+        let placed_correctly = (actual.origin.x - cocoa_frame.origin.x).abs() < 0.5
217
+            && (actual.origin.y - cocoa_frame.origin.y).abs() < 0.5;
218
+        if !placed_correctly {
219
+            tracing::warn!(
220
+                requested_x = cocoa_frame.origin.x,
221
+                requested_y = cocoa_frame.origin.y,
222
+                actual_x = actual.origin.x,
223
+                actual_y = actual.origin.y,
224
+                "NSWindow rejected setFrame placement"
225
+            );
226
+        }
227
+        // Update the border path to match new size.
228
+        unsafe {
229
+            let path = objc2_core_graphics::CGPath::with_rounded_rect(
230
+                inset_for_stroke(outer_cg.size, self.border_width),
231
+                self.radius,
232
+                self.radius,
233
+                ptr::null(),
234
+            );
235
+            let path_ref: *mut AnyObject =
236
+                objc2_core_foundation::CFRetained::as_ptr(&path).as_ptr() as *mut AnyObject;
237
+            let _: () = msg_send![&*self.border_layer, setPath: path_ref];
238
+            self.border_layer.setFrame(CGRect::new(
239
+                CGPoint::new(0.0, 0.0),
240
+                CGSize::new(outer_cg.size.width, outer_cg.size.height),
241
+            ));
242
+        }
243
+        self.bounds_cg_x = x;
244
+        self.bounds_cg_y = y;
245
+        self.bounds_cg_w = w;
246
+        self.bounds_cg_h = h;
247
+    }
248
+
249
+    /// Re-apply just the CAShapeLayer's frame and path to match the
250
+    /// current stored bounds. Cheap — no NSWindow setFrame. Useful when
251
+    /// macOS resets layer state during display sleep/wake but the
252
+    /// NSWindow's frame survives (in which case sync_overlay won't see
253
+    /// any CG bounds change and won't re-apply state on its own).
254
+    pub fn reapply_layer(&self) {
255
+        let outer_w = self.bounds_cg_w + 2.0 * self.border_width;
256
+        let outer_h = self.bounds_cg_h + 2.0 * self.border_width;
257
+        let outer_size = CGSize::new(outer_w, outer_h);
258
+        unsafe {
259
+            let path = objc2_core_graphics::CGPath::with_rounded_rect(
260
+                inset_for_stroke(outer_size, self.border_width),
261
+                self.radius,
262
+                self.radius,
263
+                ptr::null(),
264
+            );
265
+            let path_ref: *mut AnyObject =
266
+                objc2_core_foundation::CFRetained::as_ptr(&path).as_ptr() as *mut AnyObject;
267
+            let _: () = msg_send![&*self.border_layer, setPath: path_ref];
268
+            self.border_layer.setFrame(CGRect::new(
269
+                CGPoint::new(0.0, 0.0),
270
+                CGSize::new(outer_w, outer_h),
271
+            ));
272
+        }
273
+    }
274
+
275
+    pub fn set_color(&self, color: (f64, f64, f64, f64)) {
276
+        unsafe {
277
+            let stroke = make_cgcolor(color, self.mtm);
278
+            let stroke_ref: *mut AnyObject =
279
+                objc2_core_foundation::CFRetained::as_ptr(&stroke).as_ptr() as *mut AnyObject;
280
+            let _: () = msg_send![&*self.border_layer, setStrokeColor: stroke_ref];
281
+        }
282
+    }
283
+
284
+    pub fn order_above(&self, target_wid: u32) {
285
+        self.window
286
+            .orderWindow_relativeTo(NSWindowOrderingMode::Above, target_wid as isize);
287
+    }
288
+
289
+    pub fn order_out(&self) {
290
+        self.window.orderOut(None);
291
+    }
292
+}
293
+
294
+impl Drop for OverlayWindow {
295
+    fn drop(&mut self) {
296
+        // orderOut first so the visual disappears synchronously;
297
+        // close() afterward releases the window. Without orderOut a
298
+        // closed-but-still-onscreen window can briefly linger on
299
+        // Tahoe before Retained drops the last ref.
300
+        self.window.orderOut(None);
301
+        self.window.close();
302
+    }
303
+}
304
+
305
+fn inset_for_stroke(size: CGSize, border_width: f64) -> CGRect {
306
+    // CAShapeLayer strokes centered on the path. To get an exactly
307
+    // border_width-thick visible ring sitting inside the layer bounds,
308
+    // inset the path by half the line width and stroke at line_width
309
+    // = border_width.
310
+    let half = border_width / 2.0;
311
+    CGRect::new(
312
+        CGPoint::new(half, half),
313
+        CGSize::new(
314
+            (size.width - 2.0 * half).max(0.0),
315
+            (size.height - 2.0 * half).max(0.0),
316
+        ),
317
+    )
318
+}
319
+
320
+fn make_cgcolor(
321
+    rgba: (f64, f64, f64, f64),
322
+    _mtm: MainThreadMarker,
323
+) -> objc2_core_foundation::CFRetained<objc2_core_graphics::CGColor> {
324
+    objc2_core_graphics::CGColor::new_srgb(rgba.0, rgba.1, rgba.2, rgba.3)
325
+}
src/skylight.rsmodified
@@ -101,11 +101,22 @@ pub type CFNumberRef = *const c_void;
101101
 pub type CFMachPortRef = *const c_void;
102102
 pub type CFRunLoopSourceRef = *const c_void;
103103
 pub type CFRunLoopRef = *const c_void;
104
+pub type CFRunLoopTimerRef = *const c_void;
104105
 pub type CFAllocatorRef = *const c_void;
106
+
107
+#[repr(C)]
108
+pub struct CFRunLoopTimerContext {
109
+    pub version: i64,
110
+    pub info: *mut c_void,
111
+    pub retain: Option<extern "C" fn(*const c_void) -> *const c_void>,
112
+    pub release: Option<extern "C" fn(*const c_void)>,
113
+    pub copy_description: Option<extern "C" fn(*const c_void) -> CFStringRef>,
114
+}
105115
 pub type CGContextRef = *mut c_void;
106116
 pub type CGPathRef = *const c_void;
107117
 pub type CGMutablePathRef = *mut c_void;
108118
 pub type CGEventRef = *const c_void;
119
+pub type CGDisplayModeRef = *const c_void;
109120
 
110121
 // CF constants
111122
 pub const kCFNumberSInt32Type: i32 = 3;
@@ -174,6 +185,26 @@ unsafe extern "C" {
174185
         region: CFTypeRef,
175186
         wid_out: *mut u32,
176187
     ) -> CGError;
188
+    /// JankyBorders' `SLSNewWindowWithOpaqueShapeAndContext` — creates a
189
+    /// window with a custom hit-test shape and tag bits applied at
190
+    /// creation. Used so that screenshot-exclusion tag bit 9 lands on
191
+    /// the window before macOS Tahoe's compositor classifies it; setting
192
+    /// the bit post-creation is unreliable on Tahoe.
193
+    /// Reference: .refs/JankyBorders/src/misc/window.h:239
194
+    /// Reference: .refs/JankyBorders/src/misc/extern.h
195
+    pub fn SLSNewWindowWithOpaqueShapeAndContext(
196
+        cid: CGSConnectionID,
197
+        window_type: i32,
198
+        region: CFTypeRef,
199
+        opaque_shape: CFTypeRef,
200
+        options: i32,
201
+        tags: *mut u64,
202
+        x: f32,
203
+        y: f32,
204
+        tag_size: i32,
205
+        wid_out: *mut u32,
206
+        context: *mut std::ffi::c_void,
207
+    ) -> CGError;
177208
     pub fn SLSReleaseWindow(cid: CGSConnectionID, wid: u32) -> CGError;
178209
 
179210
     // Window properties
@@ -189,6 +220,12 @@ unsafe extern "C" {
189220
         tags: *const u64,
190221
         tag_size: i32,
191222
     ) -> CGError;
223
+    pub fn CGSGetWindowTags(
224
+        cid: CGSConnectionID,
225
+        wid: u32,
226
+        tags: *mut u64,
227
+        tag_size: i32,
228
+    ) -> CGError;
192229
     pub fn SLSSetWindowShape(
193230
         cid: CGSConnectionID,
194231
         wid: u32,
@@ -198,6 +235,22 @@ unsafe extern "C" {
198235
     ) -> CGError;
199236
     pub fn SLSSetWindowResolution(cid: CGSConnectionID, wid: u32, res: f64) -> CGError;
200237
     pub fn SLSSetWindowOpacity(cid: CGSConnectionID, wid: u32, is_opaque: bool) -> CGError;
238
+    /// SLS-level NSWindow.sharingType. Values: 0 = None (excluded from
239
+    /// screen capture / picker / recording — equivalent to
240
+    /// kCGWindowSharingNone), 1 = ReadOnly, 2 = ReadWrite.
241
+    pub fn SLSSetWindowSharingState(cid: CGSConnectionID, wid: u32, state: u32) -> CGError;
242
+    pub fn SLSGetWindowSharingState(
243
+        cid: CGSConnectionID,
244
+        wid: u32,
245
+        state_out: *mut u32,
246
+    ) -> CGError;
247
+    /// Mask of events the SLS window captures. Set to 0 to make the window
248
+    /// click-through (mouse events pass to the window beneath).
249
+    pub fn SLSSetWindowEventMask(cid: CGSConnectionID, wid: u32, mask: u32) -> CGError;
250
+    /// Hit-test/input shape. An empty region passes all mouse events
251
+    /// through to the window beneath. Equivalent to NSWindow's
252
+    /// `setIgnoresMouseEvents(true)` at the SLS layer.
253
+    pub fn SLSSetWindowEventShape(cid: CGSConnectionID, wid: u32, shape: CFTypeRef) -> CGError;
201254
     pub fn SLSSetWindowAlpha(cid: CGSConnectionID, wid: u32, alpha: f32) -> CGError;
202255
     pub fn SLSSetWindowBackgroundBlurRadius(cid: CGSConnectionID, wid: u32, radius: u32)
203256
     -> CGError;
@@ -304,6 +357,17 @@ pub const kCGNullWindowID: u32 = 0;
304357
 
305358
 unsafe extern "C" {
306359
     pub fn CGWindowListCopyWindowInfo(option: u32, relative_to: u32) -> CFArrayRef;
360
+    pub fn CGGetDisplaysWithPoint(
361
+        point: CGPoint,
362
+        max_displays: u32,
363
+        displays: *mut u32,
364
+        count: *mut u32,
365
+    ) -> CGError;
366
+    pub fn CGDisplayCopyDisplayMode(display: u32) -> CGDisplayModeRef;
367
+    pub fn CGDisplayModeGetWidth(mode: CGDisplayModeRef) -> usize;
368
+    pub fn CGDisplayModeGetHeight(mode: CGDisplayModeRef) -> usize;
369
+    pub fn CGDisplayModeGetPixelWidth(mode: CGDisplayModeRef) -> usize;
370
+    pub fn CGDisplayModeGetPixelHeight(mode: CGDisplayModeRef) -> usize;
307371
     pub fn CFDictionaryGetValueIfPresent(
308372
         dict: CFDictionaryRef,
309373
         key: CFTypeRef,
@@ -413,6 +477,18 @@ unsafe extern "C" {
413477
     pub fn CFRunLoopStop(rl: CFRunLoopRef);
414478
     pub fn CFRunLoopWakeUp(rl: CFRunLoopRef);
415479
 
480
+    pub fn CFRunLoopTimerCreate(
481
+        allocator: CFAllocatorRef,
482
+        fire_date: f64,
483
+        interval: f64,
484
+        flags: u64,
485
+        order: i64,
486
+        callout: extern "C" fn(*mut c_void, *mut c_void),
487
+        context: *mut CFRunLoopTimerContext,
488
+    ) -> CFRunLoopTimerRef;
489
+    pub fn CFRunLoopAddTimer(rl: CFRunLoopRef, timer: CFRunLoopTimerRef, mode: CFStringRef);
490
+    pub fn CFAbsoluteTimeGetCurrent() -> f64;
491
+
416492
     pub static kCFAllocatorDefault: CFAllocatorRef;
417493
     pub static kCFTypeDictionaryKeyCallBacks: c_void;
418494
     pub static kCFTypeDictionaryValueCallBacks: c_void;
@@ -428,6 +504,17 @@ unsafe extern "C" {
428504
     pub static mach_task_self_: u32;
429505
 }
430506
 
507
+// --- CGDisplay hotplug callback ---
508
+
509
+unsafe extern "C" {
510
+    pub fn CGDisplayRegisterReconfigurationCallback(
511
+        callback: Option<
512
+            unsafe extern "C" fn(display: u32, flags: u32, user_info: *mut std::ffi::c_void),
513
+        >,
514
+        user_info: *mut std::ffi::c_void,
515
+    ) -> i32;
516
+}
517
+
431518
 pub fn mach_task_self() -> u32 {
432519
     unsafe { mach_task_self_ }
433520
 }