Comparing changes

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

base: v0.1.2
compare: research/tahoe-screenshot-exclusion
Create pull request
Able to merge. These branches can be automatically merged.
48 commits 7 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.1.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/events.rsmodified
@@ -1,6 +1,8 @@
11
 //! SLS event registration and dispatch.
22
 
33
 use crate::skylight::*;
4
+use std::sync::OnceLock;
5
+use std::sync::atomic::{AtomicI32, Ordering};
46
 use std::sync::mpsc;
57
 
68
 pub const EVENT_WINDOW_CLOSE: u32 = 804;
@@ -27,21 +29,17 @@ pub enum Event {
2729
     FrontChange,
2830
 }
2931
 
30
-static mut TX: Option<mpsc::Sender<Event>> = None;
31
-static mut OWN_PID: i32 = 0;
32
+static TX: OnceLock<mpsc::Sender<Event>> = OnceLock::new();
33
+static OWN_PID: AtomicI32 = AtomicI32::new(0);
3234
 
3335
 pub fn init(tx: mpsc::Sender<Event>, own_pid: i32) {
34
-    unsafe {
35
-        TX = Some(tx);
36
-        OWN_PID = own_pid;
37
-    }
36
+    let _ = TX.set(tx);
37
+    OWN_PID.store(own_pid, Ordering::Relaxed);
3838
 }
3939
 
4040
 fn send(event: Event) {
41
-    unsafe {
42
-        if let Some(ref tx) = TX {
43
-            let _ = tx.send(event);
44
-        }
41
+    if let Some(tx) = TX.get() {
42
+        let _ = tx.send(event);
4543
     }
4644
 }
4745
 
@@ -51,7 +49,7 @@ fn is_own_window(cid: CGSConnectionID, wid: u32) -> bool {
5149
         SLSGetWindowOwner(cid, wid, &mut wid_cid);
5250
         let mut pid: i32 = 0;
5351
         SLSConnectionGetPID(wid_cid, &mut pid);
54
-        pid == OWN_PID
52
+        pid == OWN_PID.load(Ordering::Relaxed)
5553
     }
5654
 }
5755
 
@@ -64,7 +62,9 @@ unsafe extern "C" fn window_handler(
6462
     unsafe {
6563
         let wid = std::ptr::read_unaligned(data as *const u32);
6664
         let cid = context as isize as CGSConnectionID;
67
-        if wid == 0 || is_own_window(cid, wid) { return; }
65
+        if wid == 0 || is_own_window(cid, wid) {
66
+            return;
67
+        }
6868
 
6969
         match event {
7070
             EVENT_WINDOW_MOVE => send(Event::Move(wid)),
@@ -91,7 +91,9 @@ unsafe extern "C" fn spawn_handler(
9191
         let _sid = std::ptr::read_unaligned(data as *const u64);
9292
         let wid = std::ptr::read_unaligned(data.add(8) as *const u32);
9393
         let cid = context as isize as CGSConnectionID;
94
-        if wid == 0 || is_own_window(cid, wid) { return; }
94
+        if wid == 0 || is_own_window(cid, wid) {
95
+            return;
96
+        }
9597
 
9698
         match event {
9799
             EVENT_WINDOW_CREATE => send(Event::Create(wid)),
@@ -102,13 +104,19 @@ unsafe extern "C" fn spawn_handler(
102104
 }
103105
 
104106
 unsafe extern "C" fn space_handler(
105
-    _event: u32, _data: *const u8, _len: usize, _ctx: *mut std::ffi::c_void,
107
+    _event: u32,
108
+    _data: *const u8,
109
+    _len: usize,
110
+    _ctx: *mut std::ffi::c_void,
106111
 ) {
107112
     send(Event::SpaceChange);
108113
 }
109114
 
110115
 unsafe extern "C" fn front_handler(
111
-    _event: u32, _data: *const u8, _len: usize, _ctx: *mut std::ffi::c_void,
116
+    _event: u32,
117
+    _data: *const u8,
118
+    _len: usize,
119
+    _ctx: *mut std::ffi::c_void,
112120
 ) {
113121
     send(Event::FrontChange);
114122
 }
@@ -116,8 +124,14 @@ unsafe extern "C" fn front_handler(
116124
 pub fn register(cid: CGSConnectionID) {
117125
     let ctx = cid as isize as *mut std::ffi::c_void;
118126
     unsafe {
119
-        for &ev in &[EVENT_WINDOW_CLOSE, EVENT_WINDOW_MOVE, EVENT_WINDOW_RESIZE,
120
-                     EVENT_WINDOW_REORDER, EVENT_WINDOW_HIDE, EVENT_WINDOW_UNHIDE] {
127
+        for &ev in &[
128
+            EVENT_WINDOW_CLOSE,
129
+            EVENT_WINDOW_MOVE,
130
+            EVENT_WINDOW_RESIZE,
131
+            EVENT_WINDOW_REORDER,
132
+            EVENT_WINDOW_HIDE,
133
+            EVENT_WINDOW_UNHIDE,
134
+        ] {
121135
             SLSRegisterNotifyProc(window_handler as *const _, ev, ctx);
122136
         }
123137
         SLSRegisterNotifyProc(spawn_handler as *const _, EVENT_WINDOW_CREATE, ctx);
src/main.rsmodified
1616 lines changed — click to load
@@ -2,20 +2,190 @@
22
 
33
 mod events;
44
 mod skylight;
5
+mod nswindow_overlay;
56
 
67
 use events::Event;
78
 use skylight::*;
89
 use std::collections::HashMap;
910
 use std::ptr;
11
+use std::sync::Arc;
1012
 use std::sync::atomic::{AtomicBool, Ordering};
1113
 use std::sync::mpsc;
12
-use std::sync::Arc;
1314
 use tracing::debug;
1415
 
15
-/// Per-overlay state: the connection it was created on + its wid.
16
+static SIGNAL_STOP_REQUESTED: AtomicBool = AtomicBool::new(false);
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).
1632
 struct Overlay {
17
-    cid: CGSConnectionID,
18
-    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
+    }
52
+}
53
+
54
+fn window_area(bounds: CGRect) -> f64 {
55
+    bounds.size.width * bounds.size.height
56
+}
57
+
58
+fn intersection_area(a: CGRect, b: CGRect) -> f64 {
59
+    let left = a.origin.x.max(b.origin.x);
60
+    let top = a.origin.y.max(b.origin.y);
61
+    let right = (a.origin.x + a.size.width).min(b.origin.x + b.size.width);
62
+    let bottom = (a.origin.y + a.size.height).min(b.origin.y + b.size.height);
63
+    let width = (right - left).max(0.0);
64
+    let height = (bottom - top).max(0.0);
65
+    width * height
66
+}
67
+
68
+fn is_same_window_surface(a: CGRect, b: CGRect) -> bool {
69
+    let smaller = window_area(a).min(window_area(b));
70
+    smaller > 0.0 && intersection_area(a, b) / smaller >= 0.9
71
+}
72
+
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
+
183
+fn cf_string_from_static(name: &std::ffi::CStr) -> CFStringRef {
184
+    unsafe { CFStringCreateWithCString(ptr::null(), name.as_ptr().cast(), kCFStringEncodingUTF8) }
185
+}
186
+
187
+unsafe extern "C" fn handle_sigint(_: libc::c_int) {
188
+    SIGNAL_STOP_REQUESTED.store(true, Ordering::Relaxed);
19189
 }
20190
 
21191
 /// Tracks overlays for target windows.
@@ -29,132 +199,277 @@ struct BorderMap {
29199
     active_color: (f64, f64, f64, f64),
30200
     inactive_color: (f64, f64, f64, f64),
31201
     active_only: bool,
202
+    mtm: objc2::MainThreadMarker,
32203
 }
33204
 
34205
 impl BorderMap {
35
-    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 {
36212
         Self {
37213
             overlays: HashMap::new(),
214
+            mtm,
38215
             main_cid: cid,
39216
             own_pid,
40217
             border_width,
41218
             radius: 10.0,
42219
             focused_wid: 0,
43220
             active_color: (0.32, 0.58, 0.89, 1.0),   // #5294e2
44
-            inactive_color: (0.35, 0.35, 0.35, 0.8),  // dim gray
221
+            inactive_color: (0.35, 0.35, 0.35, 0.8), // dim gray
45222
             active_only: false,
46223
         }
47224
     }
48225
 
49226
     fn color_for(&self, target_wid: u32) -> (f64, f64, f64, f64) {
50
-        if target_wid == self.focused_wid { self.active_color } else { self.inactive_color }
227
+        if target_wid == self.focused_wid {
228
+            self.active_color
229
+        } else {
230
+            self.inactive_color
231
+        }
51232
     }
52233
 
53234
     fn is_overlay(&self, wid: u32) -> bool {
54
-        self.overlays.values().any(|o| o.wid == wid)
235
+        self.overlays.values().any(|o| o.wid() == wid)
55236
     }
56237
 
57
-    /// Add border (batch mode, uses main cid).
238
+    /// Add border using the standard filtering path.
58239
     fn add_batch(&mut self, target_wid: u32) {
59
-        if self.overlays.contains_key(&target_wid) { return; }
60
-        let color = self.color_for(target_wid);
61
-        if let Some((cid, wid)) = create_overlay(self.main_cid, target_wid, self.border_width, self.radius, color) {
62
-            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
+            }
63265
         }
266
+
267
+        Some(replacements)
64268
     }
65269
 
66270
     /// Add border (event mode). Uses main_cid — fresh connections create
67271
     /// invisible windows on Tahoe.
68272
     fn add_fresh(&mut self, target_wid: u32) {
69
-        if self.overlays.contains_key(&target_wid) { return; }
273
+        if self.overlays.contains_key(&target_wid) {
274
+            return;
275
+        }
70276
 
71277
         // Filter: must be visible, owned by another process, not tiny
72
-        unsafe {
278
+        let bounds = unsafe {
73279
             let mut shown = false;
74280
             SLSWindowIsOrderedIn(self.main_cid, target_wid, &mut shown);
75
-            if !shown { return; }
281
+            if !shown {
282
+                return;
283
+            }
76284
 
77285
             let mut wid_cid: CGSConnectionID = 0;
78286
             SLSGetWindowOwner(self.main_cid, target_wid, &mut wid_cid);
79287
             let mut pid: i32 = 0;
80288
             SLSConnectionGetPID(wid_cid, &mut pid);
81
-            if pid == self.own_pid { return; }
289
+            if pid == self.own_pid {
290
+                return;
291
+            }
292
+            if !is_suitable_window(self.main_cid, target_wid) {
293
+                return;
294
+            }
82295
 
83296
             let mut bounds = CGRect::default();
84297
             SLSGetWindowBounds(self.main_cid, target_wid, &mut bounds);
85
-            if bounds.size.width < 50.0 || bounds.size.height < 50.0 { return; }
298
+            if !is_trackable_window(bounds, self.border_width) {
299
+                return;
300
+            }
301
+            bounds
302
+        };
303
+
304
+        let Some(replacements) = self.surface_replacements(target_wid, bounds) else {
305
+            return;
306
+        };
307
+
308
+        for wid in replacements {
309
+            self.remove(wid);
86310
         }
87311
 
88312
         let color = self.color_for(target_wid);
89
-        if let Some((cid, wid)) = create_overlay(self.main_cid, target_wid, self.border_width, self.radius, color) {
90
-            self.overlays.insert(target_wid, Overlay { cid, 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,
318
+            self.border_width,
319
+            self.radius,
320
+            color,
321
+            self.mtm,
322
+        ) {
323
+            window.order_above(target_wid);
324
+            self.overlays.insert(target_wid, Overlay { window });
91325
         }
92326
     }
93327
 
94
-    fn remove_all(&mut self) {
95
-        let wids: Vec<u32> = self.overlays.keys().copied().collect();
96
-        for wid in wids {
97
-            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);
98339
         }
99340
     }
100341
 
101
-    fn remove(&mut self, target_wid: u32) {
102
-        if let Some(overlay) = self.overlays.remove(&target_wid) {
103
-            unsafe {
104
-                // Move off-screen first (most reliable hide on Tahoe)
105
-                let offscreen = CGPoint { x: -99999.0, y: -99999.0 };
106
-                SLSMoveWindow(overlay.cid, overlay.wid, &offscreen);
107
-                SLSSetWindowAlpha(overlay.cid, overlay.wid, 0.0);
108
-                SLSOrderWindow(overlay.cid, overlay.wid, 0, 0);
109
-                SLSReleaseWindow(overlay.cid, overlay.wid);
110
-                if overlay.cid != self.main_cid {
111
-                    SLSReleaseConnection(overlay.cid);
112
-                }
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;
113368
             }
114369
         }
115
-    }
116370
 
117
-    /// Move overlay to match target's current position (no recreate).
118
-    fn reposition(&self, target_wid: u32) {
119
-        if let Some(overlay) = self.overlays.get(&target_wid) {
120
-            unsafe {
121
-                let mut bounds = CGRect::default();
122
-                if SLSGetWindowBounds(overlay.cid, target_wid, &mut bounds) != kCGErrorSuccess {
123
-                    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);
124402
                 }
125
-                let bw = self.border_width;
126
-                let origin = CGPoint {
127
-                    x: bounds.origin.x - bw,
128
-                    y: bounds.origin.y - bw,
129
-                };
130
-                SLSMoveWindow(overlay.cid, overlay.wid, &origin);
131403
             }
132404
         }
405
+
406
+        false
133407
     }
134408
 
135
-    /// Recreate overlay at new size.
136
-    fn recreate(&mut self, target_wid: u32) {
137
-        if !self.overlays.contains_key(&target_wid) { return; }
138
-        self.remove(target_wid);
139
-        self.add_fresh(target_wid);
140
-        if self.active_only && target_wid != self.focused_wid {
141
-            self.hide(target_wid);
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);
415
+        }
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();
428
+        }
429
+    }
430
+
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();
442
+            unsafe {
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
+                }
458
+            }
142459
         }
143
-        self.subscribe_target(target_wid);
144460
     }
145461
 
146462
     fn hide(&self, target_wid: u32) {
147463
         if let Some(o) = self.overlays.get(&target_wid) {
148
-            unsafe { SLSOrderWindow(o.cid, o.wid, 0, 0); }
464
+            debug!("[hide] target={} overlay_wid={}", target_wid, o.wid());
465
+            o.window.order_out();
149466
         }
150467
     }
151468
 
152469
     fn unhide(&self, target_wid: u32) {
153470
         if let Some(o) = self.overlays.get(&target_wid) {
154
-            unsafe {
155
-                SLSSetWindowLevel(o.cid, o.wid, 25);
156
-                SLSOrderWindow(o.cid, o.wid, 1, 0);
157
-            }
471
+            debug!("[unhide] target={} overlay_wid={}", target_wid, o.wid());
472
+            o.window.order_above(target_wid);
158473
         }
159474
     }
160475
 
@@ -166,7 +481,9 @@ impl BorderMap {
166481
 
167482
     fn subscribe_all(&self) {
168483
         let target_wids: Vec<u32> = self.overlays.keys().copied().collect();
169
-        if target_wids.is_empty() { return; }
484
+        if target_wids.is_empty() {
485
+            return;
486
+        }
170487
         unsafe {
171488
             SLSRequestNotificationsForWindows(
172489
                 self.main_cid,
@@ -179,34 +496,63 @@ impl BorderMap {
179496
     /// Redraw an existing overlay with a new color (no destroy/recreate).
180497
     fn redraw(&self, target_wid: u32) {
181498
         if let Some(overlay) = self.overlays.get(&target_wid) {
182
-            unsafe {
183
-                let mut bounds = CGRect::default();
184
-                if SLSGetWindowBounds(overlay.cid, target_wid, &mut bounds) != kCGErrorSuccess {
185
-                    return;
186
-                }
187
-                let bw = self.border_width;
188
-                let ow = bounds.size.width + 2.0 * bw;
189
-                let oh = bounds.size.height + 2.0 * bw;
190
-
191
-                let ctx = SLWindowContextCreate(overlay.cid, overlay.wid, ptr::null());
192
-                if ctx.is_null() { return; }
193
-
194
-                let color = self.color_for(target_wid);
195
-                draw_border(ctx, ow, oh, bw, self.radius, color);
196
-                SLSFlushWindowContentRegion(overlay.cid, overlay.wid, ptr::null());
197
-                CGContextRelease(ctx);
198
-            }
499
+            overlay.window.set_color(self.color_for(target_wid));
199500
         }
200501
     }
201502
 
202503
     /// Detect focused window and update border colors if focus changed.
203504
     fn update_focus(&mut self) {
204505
         let front = get_front_window(self.own_pid);
205
-        if front == 0 || front == self.focused_wid { return; }
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
+            }
524
+            return;
525
+        }
206526
 
207527
         let old = self.focused_wid;
208528
         self.focused_wid = front;
209
-        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);
210556
 
211557
         if self.active_only {
212558
             self.hide(old);
@@ -216,17 +562,35 @@ impl BorderMap {
216562
         self.redraw(front);
217563
     }
218564
 
565
+    /// Discover on-screen windows and create borders for any untracked ones.
566
+    /// Called on space changes to pick up windows from workspaces we haven't visited.
567
+    fn discover_untracked(&mut self) {
568
+        let wids = discover_windows(self.main_cid, self.own_pid);
569
+        let mut added = false;
570
+        for wid in wids {
571
+            if !self.overlays.contains_key(&wid) {
572
+                self.add_fresh(wid);
573
+                if self.active_only && wid != self.focused_wid {
574
+                    self.hide(wid);
575
+                }
576
+                added = true;
577
+            }
578
+        }
579
+        if added {
580
+            self.subscribe_all();
581
+        }
582
+    }
583
+
219584
     /// In active-only mode, ensure only the focused overlay is visible.
220585
     fn enforce_active_only(&self) {
221
-        if !self.active_only { return; }
586
+        if !self.active_only {
587
+            return;
588
+        }
222589
         for (&target_wid, o) in &self.overlays {
223590
             if target_wid == self.focused_wid {
224
-                unsafe {
225
-                    SLSSetWindowLevel(o.cid, o.wid, 25);
226
-                    SLSOrderWindow(o.cid, o.wid, 1, 0);
227
-                }
591
+                o.window.order_above(target_wid);
228592
             } else {
229
-                unsafe { SLSOrderWindow(o.cid, o.wid, 0, 0); }
593
+                o.window.order_out();
230594
             }
231595
         }
232596
     }
@@ -245,22 +609,30 @@ fn get_front_window(own_pid: i32) -> u32 {
245609
         SLSGetConnectionIDForPSN(SLSMainConnectionID(), &mut psn, &mut front_cid);
246610
         let mut front_pid: i32 = 0;
247611
         SLSConnectionGetPID(front_cid, &mut front_pid);
248
-        if front_pid == 0 || front_pid == own_pid { return 0; }
612
+        if front_pid == 0 || front_pid == own_pid {
613
+            return 0;
614
+        }
249615
 
250616
         // Step 2: find the topmost layer-0 window belonging to that process
251617
         let list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
252
-        if list.is_null() { return 0; }
618
+        if list.is_null() {
619
+            return 0;
620
+        }
253621
 
254622
         let count = CFArrayGetCount(list);
255
-        let wid_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowNumber\0".as_ptr(), kCFStringEncodingUTF8);
256
-        let pid_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowOwnerPID\0".as_ptr(), kCFStringEncodingUTF8);
257
-        let layer_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowLayer\0".as_ptr(), kCFStringEncodingUTF8);
623
+        let wid_key = cf_string_from_static(c"kCGWindowNumber");
624
+        let pid_key = cf_string_from_static(c"kCGWindowOwnerPID");
625
+        let layer_key = cf_string_from_static(c"kCGWindowLayer");
258626
 
259627
         let mut front_wid: u32 = 0;
628
+        let mut front_bounds = CGRect::default();
629
+        let mut have_front_bounds = false;
260630
         let mut fallback_wid: u32 = 0;
261631
         for i in 0..count {
262632
             let dict = CFArrayGetValueAtIndex(list, i);
263
-            if dict.is_null() { continue; }
633
+            if dict.is_null() {
634
+                continue;
635
+            }
264636
 
265637
             let mut v: CFTypeRef = ptr::null();
266638
 
@@ -268,29 +640,62 @@ fn get_front_window(own_pid: i32) -> u32 {
268640
             if CFDictionaryGetValueIfPresent(dict, layer_key as CFTypeRef, &mut v) {
269641
                 CFNumberGetValue(v, kCFNumberSInt32Type, &mut layer as *mut _ as *mut _);
270642
             }
271
-            if layer != 0 { continue; }
643
+            if layer != 0 {
644
+                continue;
645
+            }
272646
 
273647
             let mut pid: i32 = 0;
274648
             if CFDictionaryGetValueIfPresent(dict, pid_key as CFTypeRef, &mut v) {
275649
                 CFNumberGetValue(v, kCFNumberSInt32Type, &mut pid as *mut _ as *mut _);
276650
             }
277
-            if pid == own_pid { continue; }
651
+            if pid == own_pid {
652
+                continue;
653
+            }
278654
 
279655
             let mut wid: u32 = 0;
280656
             if CFDictionaryGetValueIfPresent(dict, wid_key as CFTypeRef, &mut v) {
281657
                 CFNumberGetValue(v, kCFNumberSInt32Type, &mut wid as *mut _ as *mut _);
282658
             }
283
-            if wid == 0 { continue; }
659
+            if wid == 0 {
660
+                continue;
661
+            }
662
+
663
+            if !is_suitable_window(SLSMainConnectionID(), wid) {
664
+                continue;
665
+            }
284666
 
285667
             // Track first non-self window as fallback (z-order based)
286668
             if fallback_wid == 0 {
287669
                 fallback_wid = wid;
288670
             }
289671
 
290
-            // Prefer a window from the front process
672
+            // Prefer a window from the front process. If another layer-0 surface
673
+            // from that app nearly fully contains the current one, treat the
674
+            // larger surface as the real window. Firefox can surface a tab-strip
675
+            // child ahead of the outer window after a tile.
291676
             if pid == front_pid {
292
-                front_wid = wid;
293
-                break;
677
+                let mut bounds = CGRect::default();
678
+                if SLSGetWindowBounds(SLSMainConnectionID(), wid, &mut bounds) != kCGErrorSuccess {
679
+                    if front_wid == 0 {
680
+                        front_wid = wid;
681
+                    }
682
+                    continue;
683
+                }
684
+
685
+                if front_wid == 0 {
686
+                    front_wid = wid;
687
+                    front_bounds = bounds;
688
+                    have_front_bounds = true;
689
+                    continue;
690
+                }
691
+
692
+                if have_front_bounds
693
+                    && is_same_window_surface(front_bounds, bounds)
694
+                    && window_area(bounds) > window_area(front_bounds)
695
+                {
696
+                    front_wid = wid;
697
+                    front_bounds = bounds;
698
+                }
294699
             }
295700
         }
296701
 
@@ -311,13 +716,17 @@ fn get_front_window(own_pid: i32) -> u32 {
311716
 /// Parse hex color string (#RRGGBB or #RRGGBBAA) to (r, g, b, a) floats.
312717
 fn parse_color(s: &str) -> Option<(f64, f64, f64, f64)> {
313718
     let hex = s.strip_prefix('#').unwrap_or(s);
314
-    if hex.len() != 6 && hex.len() != 8 { return None; }
719
+    if hex.len() != 6 && hex.len() != 8 {
720
+        return None;
721
+    }
315722
     let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f64 / 255.0;
316723
     let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f64 / 255.0;
317724
     let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f64 / 255.0;
318725
     let a = if hex.len() == 8 {
319726
         u8::from_str_radix(&hex[6..8], 16).ok()? as f64 / 255.0
320
-    } else { 1.0 };
727
+    } else {
728
+        1.0
729
+    };
321730
     Some((r, g, b, a))
322731
 }
323732
 
@@ -344,10 +753,13 @@ fn print_help() {
344753
 }
345754
 
346755
 fn main() {
756
+    let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
757
+        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("ers=info"));
347758
     tracing_subscriber::fmt()
348
-        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
759
+        .with_env_filter(env_filter)
349760
         .with_writer(std::io::stderr)
350761
         .init();
762
+    debug!("[main] ers starting, pid={}", std::process::id());
351763
 
352764
     let args: Vec<String> = std::env::args().collect();
353765
 
@@ -379,6 +791,13 @@ fn main() {
379791
 
380792
     let active_only = args.iter().any(|s| s == "--active-only");
381793
 
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
+
382801
     let cid = unsafe { SLSMainConnectionID() };
383802
     let own_pid = unsafe {
384803
         let mut pid: i32 = 0;
@@ -393,7 +812,7 @@ fn main() {
393812
     setup_event_port(cid);
394813
 
395814
     // Discover and create borders
396
-    let mut borders = BorderMap::new(cid, own_pid, border_width);
815
+    let mut borders = BorderMap::new(cid, own_pid, border_width, mtm);
397816
     borders.radius = radius;
398817
     borders.active_color = active_color;
399818
     borders.inactive_color = inactive_color;
@@ -414,7 +833,9 @@ fn main() {
414833
 
415834
     if borders.active_only {
416835
         let focused = borders.focused_wid;
417
-        let to_hide: Vec<u32> = borders.overlays.keys()
836
+        let to_hide: Vec<u32> = borders
837
+            .overlays
838
+            .keys()
418839
             .filter(|&&wid| wid != focused)
419840
             .copied()
420841
             .collect();
@@ -425,187 +846,354 @@ fn main() {
425846
 
426847
     debug!("{} overlays tracked", borders.overlays.len());
427848
 
428
-    // SIGINT flag — background thread checks this to clean up
849
+    SIGNAL_STOP_REQUESTED.store(false, Ordering::Relaxed);
850
+
851
+    // Background watcher translates the signal-safe atomic into a normal
852
+    // CoreFoundation shutdown request on a Rust thread.
429853
     let running = Arc::new(AtomicBool::new(true));
854
+    let signal_watcher = std::thread::spawn(|| {
855
+        use std::time::Duration;
856
+
857
+        while !SIGNAL_STOP_REQUESTED.load(Ordering::Relaxed) {
858
+            std::thread::sleep(Duration::from_millis(10));
859
+        }
860
+
861
+        unsafe {
862
+            let run_loop = CFRunLoopGetMain();
863
+            CFRunLoopStop(run_loop);
864
+            CFRunLoopWakeUp(run_loop);
865
+        }
866
+    });
867
+
430868
     unsafe {
431
-        libc::signal(libc::SIGINT, {
432
-            unsafe extern "C" fn handler(_: libc::c_int) {
433
-                unsafe {
434
-                    CFRunLoopStop(CFRunLoopGetMain());
435
-                }
436
-            }
437
-            handler as *const () as libc::sighandler_t
438
-        });
869
+        libc::signal(
870
+            libc::SIGINT,
871
+            handle_sigint as *const () as libc::sighandler_t,
872
+        );
873
+        libc::signal(
874
+            libc::SIGTERM,
875
+            handle_sigint as *const () as libc::sighandler_t,
876
+        );
439877
     }
440878
 
441
-    // Process events on background thread with coalescing
442
-    let running_bg = Arc::clone(&running);
443
-    let handle = std::thread::spawn(move || {
444
-        use std::collections::HashSet;
445
-        use std::time::Duration;
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
+    });
446892
 
447
-        // Persist across batches: windows we know about but haven't bordered yet
448
-        let mut pending: HashSet<u32> = HashSet::new();
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
+    }
449912
 
450
-        while running_bg.load(Ordering::Relaxed) {
451
-            let first = match rx.recv_timeout(Duration::from_millis(100)) {
452
-                Ok(e) => e,
453
-                Err(mpsc::RecvTimeoutError::Timeout) => continue,
454
-                Err(mpsc::RecvTimeoutError::Disconnected) => break,
455
-            };
913
+    unsafe { CFRunLoopRun() };
456914
 
457
-            std::thread::sleep(std::time::Duration::from_millis(16));
915
+    // Drop everything on the main thread (NSWindow.close in Drop).
916
+    MAIN_STATE.with(|cell| cell.borrow_mut().take());
458917
 
459
-            let mut events = vec![first];
460
-            while let Ok(e) = rx.try_recv() {
461
-                events.push(e);
462
-            }
918
+    SIGNAL_STOP_REQUESTED.store(true, Ordering::Relaxed);
919
+    let _ = signal_watcher.join();
920
+    drop(running);
921
+}
463922
 
464
-            let mut moved: HashSet<u32> = HashSet::new();
465
-            let mut resized: HashSet<u32> = HashSet::new();
466
-            let mut destroyed: HashSet<u32> = HashSet::new();
467
-            let mut needs_resubscribe = false;
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
+}
468930
 
469
-            for event in events {
470
-                match event {
471
-                    Event::Move(wid) => {
472
-                        if !borders.is_overlay(wid) {
473
-                            moved.insert(wid);
474
-                        }
475
-                    }
476
-                    Event::Resize(wid) => {
477
-                        if !borders.is_overlay(wid) {
478
-                            resized.insert(wid);
479
-                        }
480
-                    }
481
-                    Event::Close(wid) | Event::Destroy(wid) => {
482
-                        if !borders.is_overlay(wid) {
483
-                            destroyed.insert(wid);
484
-                            pending.remove(&wid);
485
-                        }
486
-                    }
487
-                    Event::Create(wid) => {
488
-                        if !borders.is_overlay(wid) {
489
-                            pending.insert(wid);
490
-                            borders.subscribe_target(wid);
491
-                        }
492
-                    }
493
-                    Event::Hide(wid) => borders.hide(wid),
494
-                    Event::Unhide(wid) => {
495
-                        if !borders.active_only || wid == borders.focused_wid {
496
-                            borders.unhide(wid);
497
-                        }
498
-                    }
499
-                    Event::FrontChange => {
500
-                        needs_resubscribe = true;
501
-                    }
502
-                    Event::SpaceChange => {
503
-                        needs_resubscribe = true;
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());
504958
                     }
959
+                    s.batch_events.push(e);
960
+                    received += 1;
505961
                 }
962
+                Err(mpsc::TryRecvError::Empty) => break,
963
+                Err(mpsc::TryRecvError::Disconnected) => break,
506964
             }
507
-
508
-            // Destroys
509
-            for wid in &destroyed {
510
-                borders.remove(*wid);
511
-            }
512
-
513
-            // Promote ALL pending creates that weren't destroyed
514
-            // (the 150ms debounce is enough for tarmac to position them)
515
-            let ready: Vec<u32> = pending.iter()
516
-                .filter(|wid| !destroyed.contains(wid))
517
-                .copied()
518
-                .collect();
519
-            // Filter overlapping creates: if two windows overlap, keep smaller one
520
-            let mut bounds_map: Vec<(u32, CGRect)> = Vec::new();
521
-            for &wid in &ready {
522
-                unsafe {
523
-                    let mut b = CGRect::default();
524
-                    SLSGetWindowBounds(borders.main_cid, wid, &mut b);
525
-                    bounds_map.push((wid, b));
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");
526999
                 }
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();
5271005
             }
1006
+        }
1007
+    });
1008
+}
5281009
 
529
-            // If two new windows overlap closely, skip the larger one (container)
530
-            let mut skip: std::collections::HashSet<u32> = HashSet::new();
531
-            for i in 0..bounds_map.len() {
532
-                for j in (i+1)..bounds_map.len() {
533
-                    let (wid_a, a) = &bounds_map[i];
534
-                    let (wid_b, b) = &bounds_map[j];
535
-                    // Check if centers are close (within 30px)
536
-                    let cx_a = a.origin.x + a.size.width / 2.0;
537
-                    let cy_a = a.origin.y + a.size.height / 2.0;
538
-                    let cx_b = b.origin.x + b.size.width / 2.0;
539
-                    let cy_b = b.origin.y + b.size.height / 2.0;
540
-                    if (cx_a - cx_b).abs() < 30.0 && (cy_a - cy_b).abs() < 30.0 {
541
-                        // Skip the larger one
542
-                        let area_a = a.size.width * a.size.height;
543
-                        let area_b = b.size.width * b.size.height;
544
-                        if area_a > area_b {
545
-                            skip.insert(*wid_a);
546
-                        } else {
547
-                            skip.insert(*wid_b);
548
-                        }
549
-                    }
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);
5501028
                 }
5511029
             }
552
-
553
-            for &wid in &ready {
554
-                pending.remove(&wid);
555
-                if !skip.contains(&wid) {
556
-                    borders.add_fresh(wid);
557
-                    if borders.active_only && wid != borders.focused_wid {
558
-                        borders.hide(wid);
559
-                    }
560
-                    needs_resubscribe = true;
1030
+            Event::Resize(wid) => {
1031
+                if !borders.is_overlay(wid) {
1032
+                    resized.insert(wid);
5611033
                 }
5621034
             }
563
-
564
-            // Moves: reposition overlay (no destroy/create)
565
-            for wid in &moved {
566
-                if !resized.contains(wid) && !ready.contains(wid) {
567
-                    borders.reposition(*wid);
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);
5681040
                 }
5691041
             }
570
-
571
-            // Resizes: must recreate (can't reshape windows on Tahoe)
572
-            // Skip windows just created this batch — already at correct size
573
-            for wid in &resized {
574
-                if !ready.contains(wid) && borders.overlays.contains_key(wid) {
575
-                    borders.recreate(*wid);
576
-                    needs_resubscribe = true;
1042
+            Event::Create(wid) => {
1043
+                if !borders.is_overlay(wid) {
1044
+                    pending.entry(wid).or_insert_with(Instant::now);
1045
+                    borders.subscribe_target(wid);
1046
+                }
1047
+            }
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
+                    }
5771058
                 }
5781059
             }
1060
+            Event::FrontChange => {
1061
+                needs_resubscribe = true;
1062
+            }
1063
+            Event::SpaceChange => {
1064
+                needs_resubscribe = true;
1065
+            }
1066
+        }
1067
+    }
5791068
 
580
-            // Update focus (redraws borders in-place if changed)
581
-            borders.update_focus();
1069
+    for wid in &destroyed {
1070
+        borders.remove(*wid);
1071
+    }
5821072
 
583
-            // Re-subscribe ALL tracked windows (SLSRequestNotificationsForWindows replaces, not appends)
584
-            if needs_resubscribe || !destroyed.is_empty() {
585
-                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
+                }
5861105
             }
1106
+        }
1107
+    }
5871108
 
588
-            // After all processing, enforce active-only visibility
589
-            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;
5901117
         }
1118
+    }
5911119
 
592
-        // Clean up all overlays before exiting
593
-        borders.remove_all();
594
-    });
1120
+    for wid in &moved {
1121
+        if !resized.contains(wid) && !ready.contains(wid) && borders.sync_overlay(*wid) {
1122
+            needs_resubscribe = true;
1123
+        }
1124
+    }
5951125
 
596
-    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
+    }
1134
+
1135
+    if needs_resubscribe {
1136
+        borders.discover_untracked();
1137
+    }
1138
+
1139
+    needs_resubscribe |= borders.reconcile_tracked();
1140
+
1141
+    borders.update_focus();
5971142
 
598
-    // SIGINT received — signal background thread to stop and wait
599
-    running.store(false, Ordering::Relaxed);
600
-    let _ = handle.join();
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
+    }
6011179
 }
6021180
 
6031181
 fn setup_event_port(cid: CGSConnectionID) {
6041182
     unsafe {
6051183
         let mut port: u32 = 0;
606
-        if SLSGetEventPort(cid, &mut port) != kCGErrorSuccess { return; }
607
-        let cf_port = CFMachPortCreateWithPort(ptr::null(), port, drain_events as *const _, ptr::null(), false);
608
-        if cf_port.is_null() { return; }
1184
+        if SLSGetEventPort(cid, &mut port) != kCGErrorSuccess {
1185
+            return;
1186
+        }
1187
+        let cf_port = CFMachPortCreateWithPort(
1188
+            ptr::null(),
1189
+            port,
1190
+            drain_events as *const _,
1191
+            ptr::null(),
1192
+            false,
1193
+        );
1194
+        if cf_port.is_null() {
1195
+            return;
1196
+        }
6091197
         _CFMachPortSetOptions(cf_port, 0x40);
6101198
         let source = CFMachPortCreateRunLoopSource(ptr::null(), cf_port, 0);
6111199
         if !source.is_null() {
@@ -616,7 +1204,12 @@ fn setup_event_port(cid: CGSConnectionID) {
6161204
     }
6171205
 }
6181206
 
619
-unsafe extern "C" fn drain_events(_: CFMachPortRef, _: *mut std::ffi::c_void, _: i64, _: *mut std::ffi::c_void) {
1207
+unsafe extern "C" fn drain_events(
1208
+    _: CFMachPortRef,
1209
+    _: *mut std::ffi::c_void,
1210
+    _: i64,
1211
+    _: *mut std::ffi::c_void,
1212
+) {
6201213
     unsafe {
6211214
         let cid = SLSMainConnectionID();
6221215
         let mut ev = SLEventCreateNextEvent(cid);
@@ -627,39 +1220,53 @@ unsafe extern "C" fn drain_events(_: CFMachPortRef, _: *mut std::ffi::c_void, _:
6271220
     }
6281221
 }
6291222
 
630
-fn discover_windows(_cid: CGSConnectionID, own_pid: i32) -> Vec<u32> {
1223
+fn discover_windows(cid: CGSConnectionID, own_pid: i32) -> Vec<u32> {
6311224
     unsafe {
6321225
         let list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
633
-        if list.is_null() { return vec![]; }
1226
+        if list.is_null() {
1227
+            return vec![];
1228
+        }
6341229
 
6351230
         let count = CFArrayGetCount(list);
636
-        let wid_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowNumber\0".as_ptr(), kCFStringEncodingUTF8);
637
-        let pid_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowOwnerPID\0".as_ptr(), kCFStringEncodingUTF8);
638
-        let layer_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowLayer\0".as_ptr(), kCFStringEncodingUTF8);
1231
+        let wid_key = cf_string_from_static(c"kCGWindowNumber");
1232
+        let pid_key = cf_string_from_static(c"kCGWindowOwnerPID");
1233
+        let layer_key = cf_string_from_static(c"kCGWindowLayer");
6391234
 
6401235
         let mut wids = Vec::new();
6411236
         for i in 0..count {
6421237
             let dict = CFArrayGetValueAtIndex(list, i);
643
-            if dict.is_null() { continue; }
1238
+            if dict.is_null() {
1239
+                continue;
1240
+            }
6441241
 
6451242
             let mut v: CFTypeRef = ptr::null();
6461243
             let mut wid: u32 = 0;
6471244
             if CFDictionaryGetValueIfPresent(dict, wid_key as CFTypeRef, &mut v) {
6481245
                 CFNumberGetValue(v, kCFNumberSInt32Type, &mut wid as *mut _ as *mut _);
6491246
             }
650
-            if wid == 0 { continue; }
1247
+            if wid == 0 {
1248
+                continue;
1249
+            }
6511250
 
6521251
             let mut pid: i32 = 0;
6531252
             if CFDictionaryGetValueIfPresent(dict, pid_key as CFTypeRef, &mut v) {
6541253
                 CFNumberGetValue(v, kCFNumberSInt32Type, &mut pid as *mut _ as *mut _);
6551254
             }
656
-            if pid == own_pid { continue; }
1255
+            if pid == own_pid {
1256
+                continue;
1257
+            }
1258
+
1259
+            if !is_suitable_window(cid, wid) {
1260
+                continue;
1261
+            }
6571262
 
6581263
             let mut layer: i32 = -1;
6591264
             if CFDictionaryGetValueIfPresent(dict, layer_key as CFTypeRef, &mut v) {
6601265
                 CFNumberGetValue(v, kCFNumberSInt32Type, &mut layer as *mut _ as *mut _);
6611266
             }
662
-            if layer != 0 { continue; }
1267
+            if layer != 0 {
1268
+                continue;
1269
+            }
6631270
 
6641271
             wids.push(wid);
6651272
         }
@@ -672,114 +1279,26 @@ fn discover_windows(_cid: CGSConnectionID, own_pid: i32) -> Vec<u32> {
6721279
     }
6731280
 }
6741281
 
675
-/// Draw a border ring into an existing CGContext, clearing first.
676
-fn draw_border(
677
-    ctx: CGContextRef,
678
-    width: f64,
679
-    height: f64,
680
-    border_width: f64,
681
-    radius: f64,
682
-    color: (f64, f64, f64, f64),
683
-) {
684
-    unsafe {
685
-        let full = CGRect::new(0.0, 0.0, width, height);
686
-        CGContextClearRect(ctx, full);
687
-
688
-        let bw = border_width;
689
-        let stroke_rect = CGRect::new(bw / 2.0, bw / 2.0, width - bw, height - bw);
690
-        let max_r = (stroke_rect.size.width.min(stroke_rect.size.height) / 2.0).max(0.0);
691
-        let r = radius.min(max_r);
692
-
693
-        CGContextSetRGBStrokeColor(ctx, color.0, color.1, color.2, color.3);
694
-        CGContextSetLineWidth(ctx, bw);
695
-        let path = CGPathCreateWithRoundedRect(stroke_rect, r, r, ptr::null());
696
-        if !path.is_null() {
697
-            CGContextAddPath(ctx, path);
698
-            CGContextStrokePath(ctx);
699
-            CGPathRelease(path);
700
-        }
701
-        CGContextFlush(ctx);
702
-    }
703
-}
704
-
705
-fn create_overlay(
706
-    cid: CGSConnectionID,
707
-    target_wid: u32,
708
-    border_width: f64,
709
-    radius: f64,
710
-    color: (f64, f64, f64, f64),
711
-) -> Option<(CGSConnectionID, u32)> {
712
-    unsafe {
713
-        let mut bounds = CGRect::default();
714
-        let rc = SLSGetWindowBounds(cid, target_wid, &mut bounds);
715
-        if rc != kCGErrorSuccess {
716
-            debug!("[create_overlay] SLSGetWindowBounds failed for wid={target_wid} rc={rc}");
717
-            return None;
718
-        }
719
-        if bounds.size.width < 10.0 || bounds.size.height < 10.0 {
720
-            debug!("[create_overlay] wid={target_wid} too small: {}x{}", bounds.size.width, bounds.size.height);
721
-            return None;
722
-        }
723
-
724
-        let bw = border_width;
725
-        let ow = bounds.size.width + 2.0 * bw;
726
-        let oh = bounds.size.height + 2.0 * bw;
727
-        let ox = bounds.origin.x - bw;
728
-        let oy = bounds.origin.y - bw;
729
-
730
-        let frame = CGRect::new(0.0, 0.0, ow, oh);
731
-        let mut region: CFTypeRef = ptr::null();
732
-        CGSNewRegionWithRect(&frame, &mut region);
733
-        if region.is_null() {
734
-            debug!("[create_overlay] CGSNewRegionWithRect failed for wid={target_wid}");
735
-            return None;
736
-        }
737
-
738
-        let mut wid: u32 = 0;
739
-        SLSNewWindow(cid, 2, ox as f32, oy as f32, region, &mut wid);
740
-        CFRelease(region);
741
-        if wid == 0 {
742
-            debug!("[create_overlay] SLSNewWindow returned 0 for target={target_wid} cid={cid}");
743
-            return None;
744
-        }
745
-
746
-        debug!("[create_overlay] created overlay wid={wid} for target={target_wid} color=({:.2},{:.2},{:.2},{:.2})",
747
-            color.0, color.1, color.2, color.3);
748
-
749
-        SLSSetWindowResolution(cid, wid, 2.0);
750
-        SLSSetWindowOpacity(cid, wid, false);
751
-        SLSSetWindowLevel(cid, wid, 25);
752
-        SLSOrderWindow(cid, wid, 1, 0);
753
-
754
-        // Draw border (point coordinates)
755
-        let ctx = SLWindowContextCreate(cid, wid, ptr::null());
756
-        if ctx.is_null() {
757
-            debug!("[create_overlay] SLWindowContextCreate returned null for overlay wid={wid}");
758
-            SLSReleaseWindow(cid, wid);
759
-            return None;
760
-        }
761
-
762
-        draw_border(ctx, ow, oh, bw, radius, color);
763
-        SLSFlushWindowContentRegion(cid, wid, ptr::null());
764
-        CGContextRelease(ctx);
765
-
766
-        Some((cid, wid))
767
-    }
768
-}
769
-
7701282
 fn list_windows() {
7711283
     let cid = unsafe { SLSMainConnectionID() };
7721284
     unsafe {
7731285
         let list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
774
-        if list.is_null() { return; }
1286
+        if list.is_null() {
1287
+            return;
1288
+        }
7751289
         let count = CFArrayGetCount(list);
776
-        let wid_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowNumber\0".as_ptr(), kCFStringEncodingUTF8);
777
-        let layer_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowLayer\0".as_ptr(), kCFStringEncodingUTF8);
1290
+        let wid_key = cf_string_from_static(c"kCGWindowNumber");
1291
+        let layer_key = cf_string_from_static(c"kCGWindowLayer");
7781292
 
779
-        eprintln!("{:>6}  {:>8}  {:>8}  {:>6}  {:>6}", "wid", "x", "y", "w", "h");
1293
+        eprintln!(
1294
+            "{:>6}  {:>8}  {:>8}  {:>6}  {:>6}",
1295
+            "wid", "x", "y", "w", "h"
1296
+        );
7801297
         for i in 0..count {
7811298
             let dict = CFArrayGetValueAtIndex(list, i);
782
-            if dict.is_null() { continue; }
1299
+            if dict.is_null() {
1300
+                continue;
1301
+            }
7831302
 
7841303
             let mut v: CFTypeRef = ptr::null();
7851304
             let mut wid: u32 = 0;
@@ -790,12 +1309,16 @@ fn list_windows() {
7901309
             if CFDictionaryGetValueIfPresent(dict, layer_key as CFTypeRef, &mut v) {
7911310
                 CFNumberGetValue(v, kCFNumberSInt32Type, &mut layer as *mut _ as *mut _);
7921311
             }
793
-            if layer != 0 || wid == 0 { continue; }
1312
+            if layer != 0 || wid == 0 {
1313
+                continue;
1314
+            }
7941315
 
7951316
             let mut bounds = CGRect::default();
7961317
             SLSGetWindowBounds(cid, wid, &mut bounds);
797
-            eprintln!("{wid:>6}  {:>8.0}  {:>8.0}  {:>6.0}  {:>6.0}",
798
-                bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height);
1318
+            eprintln!(
1319
+                "{wid:>6}  {:>8.0}  {:>8.0}  {:>6.0}  {:>6.0}",
1320
+                bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height
1321
+            );
7991322
         }
8001323
         CFRelease(wid_key as CFTypeRef);
8011324
         CFRelease(layer_key as CFTypeRef);
@@ -803,3 +1326,67 @@ fn list_windows() {
8031326
     }
8041327
 }
8051328
 
1329
+#[cfg(test)]
1330
+mod tests {
1331
+    use super::{
1332
+        CGRect, SurfacePreference, WindowMetadata, intersection_area, is_same_window_surface,
1333
+        is_suitable_window_metadata, is_trackable_window, surface_preference,
1334
+    };
1335
+
1336
+    #[test]
1337
+    fn same_surface_detects_contained_strip() {
1338
+        let outer = CGRect::new(100.0, 100.0, 1200.0, 900.0);
1339
+        let strip = CGRect::new(114.0, 105.0, 1160.0, 140.0);
1340
+        assert!(is_same_window_surface(outer, strip));
1341
+    }
1342
+
1343
+    #[test]
1344
+    fn different_windows_are_not_treated_as_one_surface() {
1345
+        let a = CGRect::new(100.0, 100.0, 1200.0, 900.0);
1346
+        let b = CGRect::new(300.0, 300.0, 1160.0, 140.0);
1347
+        assert!(!is_same_window_surface(a, b));
1348
+    }
1349
+
1350
+    #[test]
1351
+    fn intersection_area_is_zero_without_overlap() {
1352
+        let a = CGRect::new(100.0, 100.0, 200.0, 200.0);
1353
+        let b = CGRect::new(400.0, 400.0, 200.0, 200.0);
1354
+        assert_eq!(intersection_area(a, b), 0.0);
1355
+    }
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
+    }
1392
+}
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;
@@ -146,21 +157,9 @@ unsafe extern "C" {
146157
         out_cid: *mut CGSConnectionID,
147158
     ) -> CGError;
148159
     pub fn SLSConnectionGetPID(cid: CGSConnectionID, pid: *mut i32) -> CGError;
149
-    pub fn SLSGetWindowBounds(
150
-        cid: CGSConnectionID,
151
-        wid: u32,
152
-        frame: *mut CGRect,
153
-    ) -> CGError;
154
-    pub fn SLSWindowIsOrderedIn(
155
-        cid: CGSConnectionID,
156
-        wid: u32,
157
-        shown: *mut bool,
158
-    ) -> CGError;
159
-    pub fn SLSGetWindowLevel(
160
-        cid: CGSConnectionID,
161
-        wid: u32,
162
-        level_out: *mut i64,
163
-    ) -> CGError;
160
+    pub fn SLSGetWindowBounds(cid: CGSConnectionID, wid: u32, frame: *mut CGRect) -> CGError;
161
+    pub fn SLSWindowIsOrderedIn(cid: CGSConnectionID, wid: u32, shown: *mut bool) -> CGError;
162
+    pub fn SLSGetWindowLevel(cid: CGSConnectionID, wid: u32, level_out: *mut i64) -> CGError;
164163
 
165164
     // Window iterator queries
166165
     pub fn SLSWindowQueryWindows(
@@ -186,6 +185,26 @@ unsafe extern "C" {
186185
         region: CFTypeRef,
187186
         wid_out: *mut u32,
188187
     ) -> 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;
189208
     pub fn SLSReleaseWindow(cid: CGSConnectionID, wid: u32) -> CGError;
190209
 
191210
     // Window properties
@@ -201,6 +220,12 @@ unsafe extern "C" {
201220
         tags: *const u64,
202221
         tag_size: i32,
203222
     ) -> CGError;
223
+    pub fn CGSGetWindowTags(
224
+        cid: CGSConnectionID,
225
+        wid: u32,
226
+        tags: *mut u64,
227
+        tag_size: i32,
228
+    ) -> CGError;
204229
     pub fn SLSSetWindowShape(
205230
         cid: CGSConnectionID,
206231
         wid: u32,
@@ -208,48 +233,33 @@ unsafe extern "C" {
208233
         y_offset: f32,
209234
         shape: CFTypeRef,
210235
     ) -> CGError;
211
-    pub fn SLSSetWindowResolution(
212
-        cid: CGSConnectionID,
213
-        wid: u32,
214
-        res: f64,
215
-    ) -> CGError;
216
-    pub fn SLSSetWindowOpacity(
217
-        cid: CGSConnectionID,
218
-        wid: u32,
219
-        is_opaque: bool,
220
-    ) -> CGError;
221
-    pub fn SLSSetWindowAlpha(
222
-        cid: CGSConnectionID,
223
-        wid: u32,
224
-        alpha: f32,
225
-    ) -> CGError;
226
-    pub fn SLSSetWindowBackgroundBlurRadius(
227
-        cid: CGSConnectionID,
228
-        wid: u32,
229
-        radius: u32,
230
-    ) -> CGError;
231
-    pub fn SLSSetWindowLevel(
236
+    pub fn SLSSetWindowResolution(cid: CGSConnectionID, wid: u32, res: f64) -> CGError;
237
+    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(
232243
         cid: CGSConnectionID,
233244
         wid: u32,
234
-        level: i32,
235
-    ) -> CGError;
236
-    pub fn SLSOrderWindow(
237
-        cid: CGSConnectionID,
238
-        wid: u32,
239
-        mode: i32,
240
-        relative_to: u32,
241
-    ) -> CGError;
242
-    pub fn SLSMoveWindow(
243
-        cid: CGSConnectionID,
244
-        wid: u32,
245
-        point: *const CGPoint,
245
+        state_out: *mut u32,
246246
     ) -> 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;
254
+    pub fn SLSSetWindowAlpha(cid: CGSConnectionID, wid: u32, alpha: f32) -> CGError;
255
+    pub fn SLSSetWindowBackgroundBlurRadius(cid: CGSConnectionID, wid: u32, radius: u32)
256
+    -> CGError;
257
+    pub fn SLSSetWindowLevel(cid: CGSConnectionID, wid: u32, level: i32) -> CGError;
258
+    pub fn SLSOrderWindow(cid: CGSConnectionID, wid: u32, mode: i32, relative_to: u32) -> CGError;
259
+    pub fn SLSMoveWindow(cid: CGSConnectionID, wid: u32, point: *const CGPoint) -> CGError;
247260
 
248261
     // Shadow
249
-    pub fn SLSWindowSetShadowProperties(
250
-        wid: u32,
251
-        properties: CFDictionaryRef,
252
-    ) -> CGError;
262
+    pub fn SLSWindowSetShadowProperties(wid: u32, properties: CFDictionaryRef) -> CGError;
253263
 
254264
     // Drawing context
255265
     pub fn SLWindowContextCreate(
@@ -260,11 +270,7 @@ unsafe extern "C" {
260270
 
261271
     // Transactions
262272
     pub fn SLSTransactionCreate(cid: CGSConnectionID) -> CFTypeRef;
263
-    pub fn SLSTransactionSetWindowLevel(
264
-        transaction: CFTypeRef,
265
-        wid: u32,
266
-        level: i32,
267
-    ) -> CGError;
273
+    pub fn SLSTransactionSetWindowLevel(transaction: CFTypeRef, wid: u32, level: i32) -> CGError;
268274
     pub fn SLSTransactionMoveWindowWithGroup(
269275
         transaction: CFTypeRef,
270276
         wid: u32,
@@ -276,11 +282,7 @@ unsafe extern "C" {
276282
         order: i32,
277283
         rel_wid: u32,
278284
     ) -> CGError;
279
-    pub fn SLSTransactionSetWindowAlpha(
280
-        transaction: CFTypeRef,
281
-        wid: u32,
282
-        alpha: f32,
283
-    ) -> CGError;
285
+    pub fn SLSTransactionSetWindowAlpha(transaction: CFTypeRef, wid: u32, alpha: f32) -> CGError;
284286
     pub fn SLSTransactionSetWindowTransform(
285287
         transaction: CFTypeRef,
286288
         wid: u32,
@@ -314,14 +316,8 @@ unsafe extern "C" {
314316
     ) -> CFArrayRef;
315317
     pub fn SLSCopyManagedDisplays(cid: CGSConnectionID) -> CFArrayRef;
316318
     pub fn SLSCopyManagedDisplaySpaces(cid: CGSConnectionID) -> CFArrayRef;
317
-    pub fn SLSCopyManagedDisplayForWindow(
318
-        cid: CGSConnectionID,
319
-        wid: u32,
320
-    ) -> CFStringRef;
321
-    pub fn SLSManagedDisplayGetCurrentSpace(
322
-        cid: CGSConnectionID,
323
-        uuid: CFStringRef,
324
-    ) -> u64;
319
+    pub fn SLSCopyManagedDisplayForWindow(cid: CGSConnectionID, wid: u32) -> CFStringRef;
320
+    pub fn SLSManagedDisplayGetCurrentSpace(cid: CGSConnectionID, uuid: CFStringRef) -> u64;
325321
     pub fn SLSCopyActiveMenuBarDisplayIdentifier(cid: CGSConnectionID) -> CFStringRef;
326322
     pub fn SLSMoveWindowsToManagedSpace(
327323
         cid: CGSConnectionID,
@@ -361,6 +357,17 @@ pub const kCGNullWindowID: u32 = 0;
361357
 
362358
 unsafe extern "C" {
363359
     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;
364371
     pub fn CFDictionaryGetValueIfPresent(
365372
         dict: CFDictionaryRef,
366373
         key: CFTypeRef,
@@ -378,20 +385,8 @@ pub const kCFStringEncodingUTF8: u32 = 0x0800_0100;
378385
 // --- CoreGraphics drawing ---
379386
 
380387
 unsafe extern "C" {
381
-    pub fn CGContextSetRGBStrokeColor(
382
-        ctx: CGContextRef,
383
-        r: f64,
384
-        g: f64,
385
-        b: f64,
386
-        a: f64,
387
-    );
388
-    pub fn CGContextSetRGBFillColor(
389
-        ctx: CGContextRef,
390
-        r: f64,
391
-        g: f64,
392
-        b: f64,
393
-        a: f64,
394
-    );
388
+    pub fn CGContextSetRGBStrokeColor(ctx: CGContextRef, r: f64, g: f64, b: f64, a: f64);
389
+    pub fn CGContextSetRGBFillColor(ctx: CGContextRef, r: f64, g: f64, b: f64, a: f64);
395390
     pub fn CGContextSetLineWidth(ctx: CGContextRef, width: f64);
396391
     pub fn CGContextClearRect(ctx: CGContextRef, rect: CGRect);
397392
     pub fn CGContextEOFillPath(ctx: CGContextRef);
@@ -419,11 +414,7 @@ unsafe extern "C" {
419414
         rx: f64,
420415
         ry: f64,
421416
     );
422
-    pub fn CGPathAddRect(
423
-        path: CGMutablePathRef,
424
-        transform: *const CGAffineTransform,
425
-        rect: CGRect,
426
-    );
417
+    pub fn CGPathAddRect(path: CGMutablePathRef, transform: *const CGAffineTransform, rect: CGRect);
427418
     pub fn CGPathAddPath(
428419
         path: CGMutablePathRef,
429420
         transform: *const CGAffineTransform,
@@ -452,11 +443,7 @@ unsafe extern "C" {
452443
         the_type: i32,
453444
         value_ptr: *const c_void,
454445
     ) -> CFNumberRef;
455
-    pub fn CFNumberGetValue(
456
-        number: CFNumberRef,
457
-        the_type: i32,
458
-        value_ptr: *mut c_void,
459
-    ) -> bool;
446
+    pub fn CFNumberGetValue(number: CFNumberRef, the_type: i32, value_ptr: *mut c_void) -> bool;
460447
     pub fn CFNumberGetType(number: CFNumberRef) -> i32;
461448
 
462449
     pub fn CFDictionaryCreate(
@@ -488,6 +475,19 @@ unsafe extern "C" {
488475
     pub fn CFRunLoopAddSource(rl: CFRunLoopRef, source: CFRunLoopSourceRef, mode: CFStringRef);
489476
     pub fn CFRunLoopRun();
490477
     pub fn CFRunLoopStop(rl: CFRunLoopRef);
478
+    pub fn CFRunLoopWakeUp(rl: CFRunLoopRef);
479
+
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;
491491
 
492492
     pub static kCFAllocatorDefault: CFAllocatorRef;
493493
     pub static kCFTypeDictionaryKeyCallBacks: c_void;
@@ -504,13 +504,29 @@ unsafe extern "C" {
504504
     pub static mach_task_self_: u32;
505505
 }
506506
 
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
+
507518
 pub fn mach_task_self() -> u32 {
508519
     unsafe { mach_task_self_ }
509520
 }
510521
 
511522
 // --- Helper: create CFArray of CFNumbers ---
512523
 
513
-pub unsafe fn cfarray_of_cfnumbers(values: *const c_void, size: usize, count: i32, num_type: i32) -> CFArrayRef {
524
+pub unsafe fn cfarray_of_cfnumbers(
525
+    values: *const c_void,
526
+    size: usize,
527
+    count: i32,
528
+    num_type: i32,
529
+) -> CFArrayRef {
514530
     unsafe {
515531
         let mut temp: Vec<CFNumberRef> = Vec::with_capacity(count as usize);
516532
         for i in 0..count {
@@ -521,7 +537,7 @@ pub unsafe fn cfarray_of_cfnumbers(values: *const c_void, size: usize, count: i3
521537
             std::ptr::null(),
522538
             temp.as_ptr() as *const CFTypeRef,
523539
             count as i64,
524
-            &kCFTypeArrayCallBacks as *const _ as *const c_void,
540
+            &kCFTypeArrayCallBacks as *const _,
525541
         );
526542
         for n in &temp {
527543
             CFRelease(*n);