Rust · 49449 bytes Raw Blame History
1 //! ers — window border renderer
2
3 mod events;
4 mod skylight;
5 mod nswindow_overlay;
6
7 use events::Event;
8 use skylight::*;
9 use std::collections::HashMap;
10 use std::ptr;
11 use std::sync::Arc;
12 use std::sync::atomic::{AtomicBool, Ordering};
13 use std::sync::mpsc;
14 use tracing::debug;
15
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 SCALE_EPSILON: f64 = 0.01;
20 const WINDOW_ATTRIBUTE_REAL: u64 = 1 << 1;
21 const WINDOW_TAG_DOCUMENT: u64 = 1 << 0;
22 const WINDOW_TAG_FLOATING: u64 = 1 << 1;
23 const WINDOW_TAG_ATTACHED: u64 = 1 << 7;
24 const WINDOW_TAG_IGNORES_CYCLE: u64 = 1 << 18;
25 const WINDOW_TAG_MODAL: u64 = 1 << 31;
26 const WINDOW_TAG_REAL_SURFACE: u64 = 1 << 58;
27
28 /// Per-overlay state: an NSWindow drawing the rounded-rect border via
29 /// CAShapeLayer. Replaces the old SLS-only overlay window — see
30 /// nswindow_overlay.rs for the rationale (screencaptureui on Tahoe
31 /// only honors NSWindow.sharingType, not SLS sharing-state nor tag
32 /// bits, for raw SLS-only windows).
33 struct Overlay {
34 window: nswindow_overlay::OverlayWindow,
35 }
36
37 impl Overlay {
38 fn wid(&self) -> u32 {
39 self.window.wid()
40 }
41 fn bounds(&self) -> CGRect {
42 CGRect {
43 origin: CGPoint {
44 x: self.window.bounds_cg_x,
45 y: self.window.bounds_cg_y,
46 },
47 size: CGSize {
48 width: self.window.bounds_cg_w,
49 height: self.window.bounds_cg_h,
50 },
51 }
52 }
53 }
54
55 fn window_area(bounds: CGRect) -> f64 {
56 bounds.size.width * bounds.size.height
57 }
58
59 fn intersection_area(a: CGRect, b: CGRect) -> f64 {
60 let left = a.origin.x.max(b.origin.x);
61 let top = a.origin.y.max(b.origin.y);
62 let right = (a.origin.x + a.size.width).min(b.origin.x + b.size.width);
63 let bottom = (a.origin.y + a.size.height).min(b.origin.y + b.size.height);
64 let width = (right - left).max(0.0);
65 let height = (bottom - top).max(0.0);
66 width * height
67 }
68
69 fn is_same_window_surface(a: CGRect, b: CGRect) -> bool {
70 let smaller = window_area(a).min(window_area(b));
71 smaller > 0.0 && intersection_area(a, b) / smaller >= 0.9
72 }
73
74 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
75 enum SurfacePreference {
76 KeepExisting,
77 ReplaceExisting,
78 }
79
80 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
81 struct WindowMetadata {
82 parent_wid: u32,
83 tags: u64,
84 attributes: u64,
85 }
86
87 fn surface_preference(existing: CGRect, candidate: CGRect) -> Option<SurfacePreference> {
88 if !is_same_window_surface(existing, candidate) {
89 return None;
90 }
91
92 if window_area(candidate) > window_area(existing) {
93 Some(SurfacePreference::ReplaceExisting)
94 } else {
95 Some(SurfacePreference::KeepExisting)
96 }
97 }
98
99 fn minimum_trackable_dimension(border_width: f64) -> f64 {
100 border_width.max(MIN_TRACKED_WINDOW_SIZE)
101 }
102
103 fn is_trackable_window(bounds: CGRect, border_width: f64) -> bool {
104 let min_dimension = minimum_trackable_dimension(border_width);
105 bounds.size.width >= min_dimension && bounds.size.height >= min_dimension
106 }
107
108 fn origin_changed(a: CGRect, b: CGRect) -> bool {
109 (a.origin.x - b.origin.x).abs() > GEOMETRY_EPSILON
110 || (a.origin.y - b.origin.y).abs() > GEOMETRY_EPSILON
111 }
112
113 fn size_changed(a: CGRect, b: CGRect) -> bool {
114 (a.size.width - b.size.width).abs() > GEOMETRY_EPSILON
115 || (a.size.height - b.size.height).abs() > GEOMETRY_EPSILON
116 }
117
118 fn is_suitable_window_metadata(metadata: WindowMetadata) -> bool {
119 metadata.parent_wid == 0
120 && ((metadata.attributes & WINDOW_ATTRIBUTE_REAL) != 0
121 || (metadata.tags & WINDOW_TAG_REAL_SURFACE) != 0)
122 && (metadata.tags & WINDOW_TAG_ATTACHED) == 0
123 && (metadata.tags & WINDOW_TAG_IGNORES_CYCLE) == 0
124 && ((metadata.tags & WINDOW_TAG_DOCUMENT) != 0
125 || ((metadata.tags & WINDOW_TAG_FLOATING) != 0
126 && (metadata.tags & WINDOW_TAG_MODAL) != 0))
127 }
128
129 fn query_window_metadata(cid: CGSConnectionID, wid: u32) -> Option<WindowMetadata> {
130 unsafe {
131 let window_ref = cfarray_of_cfnumbers(
132 (&wid as *const u32).cast(),
133 std::mem::size_of::<u32>(),
134 1,
135 kCFNumberSInt32Type,
136 );
137 if window_ref.is_null() {
138 return None;
139 }
140
141 let query = SLSWindowQueryWindows(cid, window_ref, 0x0);
142 CFRelease(window_ref);
143 if query.is_null() {
144 return None;
145 }
146
147 let iterator = SLSWindowQueryResultCopyWindows(query);
148 CFRelease(query);
149 if iterator.is_null() {
150 return None;
151 }
152
153 let metadata = if SLSWindowIteratorAdvance(iterator) {
154 Some(WindowMetadata {
155 parent_wid: SLSWindowIteratorGetParentID(iterator),
156 tags: SLSWindowIteratorGetTags(iterator),
157 attributes: SLSWindowIteratorGetAttributes(iterator),
158 })
159 } else {
160 None
161 };
162
163 CFRelease(iterator);
164 metadata
165 }
166 }
167
168 fn is_suitable_window(cid: CGSConnectionID, wid: u32) -> bool {
169 match query_window_metadata(cid, wid) {
170 Some(metadata) => {
171 let suitable = is_suitable_window_metadata(metadata);
172 if !suitable {
173 debug!(
174 "[window_filter] rejecting wid={} parent={} tags={:#x} attributes={:#x}",
175 wid, metadata.parent_wid, metadata.tags, metadata.attributes
176 );
177 }
178 suitable
179 }
180 None => false,
181 }
182 }
183
184 fn cf_string_from_static(name: &std::ffi::CStr) -> CFStringRef {
185 unsafe { CFStringCreateWithCString(ptr::null(), name.as_ptr().cast(), kCFStringEncodingUTF8) }
186 }
187
188 unsafe extern "C" fn handle_sigint(_: libc::c_int) {
189 SIGNAL_STOP_REQUESTED.store(true, Ordering::Relaxed);
190 }
191
192 fn display_scale_for_bounds(bounds: CGRect) -> f64 {
193 let point = CGPoint {
194 x: bounds.origin.x + bounds.size.width / 2.0,
195 y: bounds.origin.y + bounds.size.height / 2.0,
196 };
197
198 unsafe {
199 let mut display_id = 0u32;
200 let mut count = 0u32;
201 if CGGetDisplaysWithPoint(point, 1, &mut display_id, &mut count) != kCGErrorSuccess
202 || count == 0
203 {
204 return 2.0;
205 }
206
207 let mode = CGDisplayCopyDisplayMode(display_id);
208 if mode.is_null() {
209 return 2.0;
210 }
211
212 let width = CGDisplayModeGetWidth(mode) as f64;
213 let height = CGDisplayModeGetHeight(mode) as f64;
214 let pixel_width = CGDisplayModeGetPixelWidth(mode) as f64;
215 let pixel_height = CGDisplayModeGetPixelHeight(mode) as f64;
216 CFRelease(mode as CFTypeRef);
217
218 let scale_x = if width > 0.0 {
219 pixel_width / width
220 } else {
221 0.0
222 };
223 let scale_y = if height > 0.0 {
224 pixel_height / height
225 } else {
226 0.0
227 };
228
229 let scale = match (scale_x.is_finite(), scale_y.is_finite()) {
230 (true, true) if scale_x >= 1.0 && scale_y >= 1.0 => (scale_x + scale_y) / 2.0,
231 (true, _) if scale_x >= 1.0 => scale_x,
232 (_, true) if scale_y >= 1.0 => scale_y,
233 _ => 2.0,
234 };
235
236 debug!(
237 "[display_scale] display={} point=({:.1},{:.1}) scale={:.2}",
238 display_id, point.x, point.y, scale
239 );
240
241 scale
242 }
243 }
244
245 /// Tracks overlays for target windows.
246 struct BorderMap {
247 overlays: HashMap<u32, Overlay>,
248 main_cid: CGSConnectionID,
249 own_pid: i32,
250 border_width: f64,
251 radius: f64,
252 focused_wid: u32,
253 active_color: (f64, f64, f64, f64),
254 inactive_color: (f64, f64, f64, f64),
255 active_only: bool,
256 mtm: objc2::MainThreadMarker,
257 }
258
259 impl BorderMap {
260 fn new(
261 cid: CGSConnectionID,
262 own_pid: i32,
263 border_width: f64,
264 mtm: objc2::MainThreadMarker,
265 ) -> Self {
266 Self {
267 overlays: HashMap::new(),
268 mtm,
269 main_cid: cid,
270 own_pid,
271 border_width,
272 radius: 10.0,
273 focused_wid: 0,
274 active_color: (0.32, 0.58, 0.89, 1.0), // #5294e2
275 inactive_color: (0.35, 0.35, 0.35, 0.8), // dim gray
276 active_only: false,
277 }
278 }
279
280 fn color_for(&self, target_wid: u32) -> (f64, f64, f64, f64) {
281 if target_wid == self.focused_wid {
282 self.active_color
283 } else {
284 self.inactive_color
285 }
286 }
287
288 fn is_overlay(&self, wid: u32) -> bool {
289 self.overlays.values().any(|o| o.wid() == wid)
290 }
291
292 /// Add border using the standard filtering path.
293 fn add_batch(&mut self, target_wid: u32) {
294 self.add_fresh(target_wid);
295 }
296
297 fn surface_replacements(&self, target_wid: u32, bounds: CGRect) -> Option<Vec<u32>> {
298 let mut replacements = Vec::new();
299
300 for &existing_wid in self.overlays.keys() {
301 if existing_wid == target_wid {
302 continue;
303 }
304
305 unsafe {
306 let mut existing_bounds = CGRect::default();
307 if SLSGetWindowBounds(self.main_cid, existing_wid, &mut existing_bounds)
308 != kCGErrorSuccess
309 {
310 continue;
311 }
312
313 match surface_preference(existing_bounds, bounds) {
314 Some(SurfacePreference::KeepExisting) => return None,
315 Some(SurfacePreference::ReplaceExisting) => replacements.push(existing_wid),
316 None => {}
317 }
318 }
319 }
320
321 Some(replacements)
322 }
323
324 /// Add border (event mode). Uses main_cid — fresh connections create
325 /// invisible windows on Tahoe.
326 fn add_fresh(&mut self, target_wid: u32) {
327 if self.overlays.contains_key(&target_wid) {
328 return;
329 }
330
331 // Filter: must be visible, owned by another process, not tiny
332 let bounds = unsafe {
333 let mut shown = false;
334 SLSWindowIsOrderedIn(self.main_cid, target_wid, &mut shown);
335 if !shown {
336 return;
337 }
338
339 let mut wid_cid: CGSConnectionID = 0;
340 SLSGetWindowOwner(self.main_cid, target_wid, &mut wid_cid);
341 let mut pid: i32 = 0;
342 SLSConnectionGetPID(wid_cid, &mut pid);
343 if pid == self.own_pid {
344 return;
345 }
346 if !is_suitable_window(self.main_cid, target_wid) {
347 return;
348 }
349
350 let mut bounds = CGRect::default();
351 SLSGetWindowBounds(self.main_cid, target_wid, &mut bounds);
352 if !is_trackable_window(bounds, self.border_width) {
353 return;
354 }
355 bounds
356 };
357
358 let Some(replacements) = self.surface_replacements(target_wid, bounds) else {
359 return;
360 };
361
362 for wid in replacements {
363 self.remove(wid);
364 }
365
366 let color = self.color_for(target_wid);
367 if let Some(window) = nswindow_overlay::OverlayWindow::new(
368 bounds.origin.x,
369 bounds.origin.y,
370 bounds.size.width,
371 bounds.size.height,
372 self.border_width,
373 self.radius,
374 color,
375 self.mtm,
376 ) {
377 window.order_above(target_wid);
378 self.overlays.insert(target_wid, Overlay { window });
379 }
380 }
381
382 fn remove_all(&mut self) {
383 // OverlayWindow's Drop closes the NSWindow.
384 self.overlays.clear();
385 }
386
387 fn remove(&mut self, target_wid: u32) {
388 // OverlayWindow's Drop closes the NSWindow.
389 self.overlays.remove(&target_wid);
390 }
391
392 /// Reconcile a tracked overlay against its target window.
393 fn sync_overlay(&mut self, target_wid: u32) -> bool {
394 if !self.overlays.contains_key(&target_wid) {
395 return false;
396 }
397
398 let mut bounds = CGRect::default();
399 unsafe {
400 if SLSGetWindowBounds(self.main_cid, target_wid, &mut bounds) != kCGErrorSuccess {
401 return false;
402 }
403
404 if !is_suitable_window(self.main_cid, target_wid) {
405 self.remove(target_wid);
406 return true;
407 }
408
409 if !is_trackable_window(bounds, self.border_width) {
410 self.remove(target_wid);
411 return true;
412 }
413 }
414
415 if let Some(overlay) = self.overlays.get_mut(&target_wid) {
416 let prev = overlay.bounds();
417 if size_changed(prev, bounds) || origin_changed(prev, bounds) {
418 debug!(
419 "[sync_overlay] target={} geometry ({:.1},{:.1},{:.1},{:.1}) -> ({:.1},{:.1},{:.1},{:.1})",
420 target_wid,
421 prev.origin.x,
422 prev.origin.y,
423 prev.size.width,
424 prev.size.height,
425 bounds.origin.x,
426 bounds.origin.y,
427 bounds.size.width,
428 bounds.size.height
429 );
430 overlay.window.set_bounds(
431 bounds.origin.x,
432 bounds.origin.y,
433 bounds.size.width,
434 bounds.size.height,
435 );
436 overlay.window.order_above(target_wid);
437 }
438 }
439
440 false
441 }
442
443 fn reconcile_tracked(&mut self) -> bool {
444 let tracked: Vec<u32> = self.overlays.keys().copied().collect();
445 let mut changed = false;
446
447 for wid in tracked {
448 changed |= self.sync_overlay(wid);
449 }
450
451 changed
452 }
453
454 /// With NSWindow.setFrame_display we no longer need a destroy-and-
455 /// recreate path on resize. Kept as a thin alias so existing call
456 /// sites keep working.
457 fn recreate(&mut self, target_wid: u32) {
458 self.sync_overlay(target_wid);
459 }
460
461 fn hide(&self, target_wid: u32) {
462 if let Some(o) = self.overlays.get(&target_wid) {
463 o.window.order_out();
464 }
465 }
466
467 fn unhide(&self, target_wid: u32) {
468 if let Some(o) = self.overlays.get(&target_wid) {
469 o.window.order_above(target_wid);
470 }
471 }
472
473 fn subscribe_target(&self, target_wid: u32) {
474 unsafe {
475 SLSRequestNotificationsForWindows(self.main_cid, &target_wid, 1);
476 }
477 }
478
479 fn subscribe_all(&self) {
480 let target_wids: Vec<u32> = self.overlays.keys().copied().collect();
481 if target_wids.is_empty() {
482 return;
483 }
484 unsafe {
485 SLSRequestNotificationsForWindows(
486 self.main_cid,
487 target_wids.as_ptr(),
488 target_wids.len() as i32,
489 );
490 }
491 }
492
493 /// Redraw an existing overlay with a new color (no destroy/recreate).
494 fn redraw(&self, target_wid: u32) {
495 if let Some(overlay) = self.overlays.get(&target_wid) {
496 overlay.window.set_color(self.color_for(target_wid));
497 }
498 }
499
500 /// Detect focused window and update border colors if focus changed.
501 fn update_focus(&mut self) {
502 let front = get_front_window(self.own_pid);
503 if front == 0 || front == self.focused_wid {
504 return;
505 }
506
507 let old = self.focused_wid;
508 self.focused_wid = front;
509 debug!("[focus] {} -> {}", old, front);
510
511 // Pull both overlays' positions to the targets' current SLS bounds
512 // before un/hiding. AX-driven moves during a stack cycle frequently
513 // don't fire SLS WINDOW_MOVE notifications, so a stored overlay
514 // can be at stale coordinates. SLSGetWindowBounds (inside
515 // sync_overlay) is real-time and doesn't wait for a notification.
516 self.sync_overlay(old);
517 self.sync_overlay(front);
518
519 if self.active_only {
520 self.hide(old);
521 self.unhide(front);
522 }
523 self.redraw(old);
524 self.redraw(front);
525 }
526
527 /// Discover on-screen windows and create borders for any untracked ones.
528 /// Called on space changes to pick up windows from workspaces we haven't visited.
529 fn discover_untracked(&mut self) {
530 let wids = discover_windows(self.main_cid, self.own_pid);
531 let mut added = false;
532 for wid in wids {
533 if !self.overlays.contains_key(&wid) {
534 self.add_fresh(wid);
535 if self.active_only && wid != self.focused_wid {
536 self.hide(wid);
537 }
538 added = true;
539 }
540 }
541 if added {
542 self.subscribe_all();
543 }
544 }
545
546 /// In active-only mode, ensure only the focused overlay is visible.
547 fn enforce_active_only(&self) {
548 if !self.active_only {
549 return;
550 }
551 for (&target_wid, o) in &self.overlays {
552 if target_wid == self.focused_wid {
553 o.window.order_above(target_wid);
554 } else {
555 o.window.order_out();
556 }
557 }
558 }
559 }
560
561 /// Get the front (focused) window ID.
562 /// Uses _SLPSGetFrontProcess to find the active app, then CGWindowListCopyWindowInfo
563 /// to find its topmost layer-0 window. This works with tiling WMs where focus
564 /// changes don't alter z-order.
565 fn get_front_window(own_pid: i32) -> u32 {
566 unsafe {
567 // Step 1: get the front (active) process PID
568 let mut psn = ProcessSerialNumber { high: 0, low: 0 };
569 _SLPSGetFrontProcess(&mut psn);
570 let mut front_cid: CGSConnectionID = 0;
571 SLSGetConnectionIDForPSN(SLSMainConnectionID(), &mut psn, &mut front_cid);
572 let mut front_pid: i32 = 0;
573 SLSConnectionGetPID(front_cid, &mut front_pid);
574 if front_pid == 0 || front_pid == own_pid {
575 return 0;
576 }
577
578 // Step 2: find the topmost layer-0 window belonging to that process
579 let list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
580 if list.is_null() {
581 return 0;
582 }
583
584 let count = CFArrayGetCount(list);
585 let wid_key = cf_string_from_static(c"kCGWindowNumber");
586 let pid_key = cf_string_from_static(c"kCGWindowOwnerPID");
587 let layer_key = cf_string_from_static(c"kCGWindowLayer");
588
589 let mut front_wid: u32 = 0;
590 let mut front_bounds = CGRect::default();
591 let mut have_front_bounds = false;
592 let mut fallback_wid: u32 = 0;
593 for i in 0..count {
594 let dict = CFArrayGetValueAtIndex(list, i);
595 if dict.is_null() {
596 continue;
597 }
598
599 let mut v: CFTypeRef = ptr::null();
600
601 let mut layer: i32 = -1;
602 if CFDictionaryGetValueIfPresent(dict, layer_key as CFTypeRef, &mut v) {
603 CFNumberGetValue(v, kCFNumberSInt32Type, &mut layer as *mut _ as *mut _);
604 }
605 if layer != 0 {
606 continue;
607 }
608
609 let mut pid: i32 = 0;
610 if CFDictionaryGetValueIfPresent(dict, pid_key as CFTypeRef, &mut v) {
611 CFNumberGetValue(v, kCFNumberSInt32Type, &mut pid as *mut _ as *mut _);
612 }
613 if pid == own_pid {
614 continue;
615 }
616
617 let mut wid: u32 = 0;
618 if CFDictionaryGetValueIfPresent(dict, wid_key as CFTypeRef, &mut v) {
619 CFNumberGetValue(v, kCFNumberSInt32Type, &mut wid as *mut _ as *mut _);
620 }
621 if wid == 0 {
622 continue;
623 }
624
625 if !is_suitable_window(SLSMainConnectionID(), wid) {
626 continue;
627 }
628
629 // Track first non-self window as fallback (z-order based)
630 if fallback_wid == 0 {
631 fallback_wid = wid;
632 }
633
634 // Prefer a window from the front process. If another layer-0 surface
635 // from that app nearly fully contains the current one, treat the
636 // larger surface as the real window. Firefox can surface a tab-strip
637 // child ahead of the outer window after a tile.
638 if pid == front_pid {
639 let mut bounds = CGRect::default();
640 if SLSGetWindowBounds(SLSMainConnectionID(), wid, &mut bounds) != kCGErrorSuccess {
641 if front_wid == 0 {
642 front_wid = wid;
643 }
644 continue;
645 }
646
647 if front_wid == 0 {
648 front_wid = wid;
649 front_bounds = bounds;
650 have_front_bounds = true;
651 continue;
652 }
653
654 if have_front_bounds
655 && is_same_window_surface(front_bounds, bounds)
656 && window_area(bounds) > window_area(front_bounds)
657 {
658 front_wid = wid;
659 front_bounds = bounds;
660 }
661 }
662 }
663
664 // Fall back to z-order if front process has no visible windows
665 // (e.g., switched to a workspace where the front app has no windows)
666 if front_wid == 0 {
667 front_wid = fallback_wid;
668 }
669
670 CFRelease(wid_key as CFTypeRef);
671 CFRelease(pid_key as CFTypeRef);
672 CFRelease(layer_key as CFTypeRef);
673 CFRelease(list);
674 front_wid
675 }
676 }
677
678 /// Parse hex color string (#RRGGBB or #RRGGBBAA) to (r, g, b, a) floats.
679 fn parse_color(s: &str) -> Option<(f64, f64, f64, f64)> {
680 let hex = s.strip_prefix('#').unwrap_or(s);
681 if hex.len() != 6 && hex.len() != 8 {
682 return None;
683 }
684 let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f64 / 255.0;
685 let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f64 / 255.0;
686 let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f64 / 255.0;
687 let a = if hex.len() == 8 {
688 u8::from_str_radix(&hex[6..8], 16).ok()? as f64 / 255.0
689 } else {
690 1.0
691 };
692 Some((r, g, b, a))
693 }
694
695 fn flag_value<'a>(args: &'a [String], flags: &[&str]) -> Option<&'a str> {
696 args.iter()
697 .position(|s| flags.iter().any(|f| s == f))
698 .and_then(|i| args.get(i + 1))
699 .map(|s| s.as_str())
700 }
701
702 fn print_help() {
703 eprintln!("ers — window border renderer for tarmac");
704 eprintln!();
705 eprintln!("USAGE: ers [OPTIONS] [WINDOW_ID]");
706 eprintln!();
707 eprintln!("OPTIONS:");
708 eprintln!(" -w, --width <PX> Border width in pixels (default: 4.0)");
709 eprintln!(" -r, --radius <PX> Corner radius (default: 10.0)");
710 eprintln!(" -c, --color <HEX> Active border color (default: #5294e2)");
711 eprintln!(" -i, --inactive <HEX> Inactive border color (default: #59595980)");
712 eprintln!(" --active-only Only show border on focused window");
713 eprintln!(" --list List on-screen windows and exit");
714 eprintln!(" -h, --help Show this help");
715 }
716
717 fn main() {
718 tracing_subscriber::fmt()
719 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
720 .with_writer(std::io::stderr)
721 .init();
722
723 let args: Vec<String> = std::env::args().collect();
724
725 if args.iter().any(|s| s == "--help" || s == "-h") {
726 print_help();
727 return;
728 }
729
730 if args.get(1).is_some_and(|s| s == "--list") {
731 list_windows();
732 return;
733 }
734
735 let border_width: f64 = flag_value(&args, &["--width", "-w"])
736 .and_then(|v| v.parse().ok())
737 .unwrap_or(4.0);
738
739 let radius: f64 = flag_value(&args, &["--radius", "-r"])
740 .and_then(|v| v.parse().ok())
741 .unwrap_or(10.0);
742
743 let active_color = flag_value(&args, &["--color", "-c"])
744 .and_then(parse_color)
745 .unwrap_or((0.32, 0.58, 0.89, 1.0));
746
747 let inactive_color = flag_value(&args, &["--inactive", "-i"])
748 .and_then(parse_color)
749 .unwrap_or((0.35, 0.35, 0.35, 0.8));
750
751 let active_only = args.iter().any(|s| s == "--active-only");
752
753 // Initialize NSApplication on the main thread before we touch any
754 // AppKit APIs. NSWindow operations (used by nswindow_overlay) all
755 // require a main-thread context.
756 let mtm = nswindow_overlay::init_application();
757
758 let cid = unsafe { SLSMainConnectionID() };
759 let own_pid = unsafe {
760 let mut pid: i32 = 0;
761 pid_for_task(mach_task_self(), &mut pid);
762 pid
763 };
764
765 // Event channel
766 let (tx, rx) = mpsc::channel();
767 events::init(tx, own_pid);
768 events::register(cid);
769 setup_event_port(cid);
770
771 // Discover and create borders
772 let mut borders = BorderMap::new(cid, own_pid, border_width, mtm);
773 borders.radius = radius;
774 borders.active_color = active_color;
775 borders.inactive_color = inactive_color;
776 borders.active_only = active_only;
777
778 if let Some(target) = args.get(1).and_then(|s| s.parse::<u32>().ok()) {
779 borders.add_batch(target);
780 } else {
781 let wids = discover_windows(cid, own_pid);
782 for &wid in &wids {
783 borders.add_batch(wid);
784 }
785 }
786
787 borders.subscribe_all();
788
789 borders.update_focus();
790
791 if borders.active_only {
792 let focused = borders.focused_wid;
793 let to_hide: Vec<u32> = borders
794 .overlays
795 .keys()
796 .filter(|&&wid| wid != focused)
797 .copied()
798 .collect();
799 for wid in to_hide {
800 borders.hide(wid);
801 }
802 }
803
804 debug!("{} overlays tracked", borders.overlays.len());
805
806 SIGNAL_STOP_REQUESTED.store(false, Ordering::Relaxed);
807
808 // Background watcher translates the signal-safe atomic into a normal
809 // CoreFoundation shutdown request on a Rust thread.
810 let running = Arc::new(AtomicBool::new(true));
811 let signal_watcher = std::thread::spawn(|| {
812 use std::time::Duration;
813
814 while !SIGNAL_STOP_REQUESTED.load(Ordering::Relaxed) {
815 std::thread::sleep(Duration::from_millis(10));
816 }
817
818 unsafe {
819 let run_loop = CFRunLoopGetMain();
820 CFRunLoopStop(run_loop);
821 CFRunLoopWakeUp(run_loop);
822 }
823 });
824
825 unsafe {
826 libc::signal(
827 libc::SIGINT,
828 handle_sigint as *const () as libc::sighandler_t,
829 );
830 libc::signal(
831 libc::SIGTERM,
832 handle_sigint as *const () as libc::sighandler_t,
833 );
834 }
835
836 // Process events on the main thread via a CFRunLoopTimer.
837 // BorderMap holds Retained<NSWindow> handles, which are
838 // !Send/!Sync — AppKit calls must originate from the main thread.
839 // Stash state in thread_local for the C callback to access.
840 MAIN_STATE.with(|cell| {
841 *cell.borrow_mut() = Some(MainState {
842 borders,
843 rx,
844 pending: HashMap::new(),
845 batch_events: Vec::new(),
846 batch_first_seen: None,
847 });
848 });
849
850 unsafe {
851 let mut ctx = CFRunLoopTimerContext {
852 version: 0,
853 info: ptr::null_mut(),
854 retain: None,
855 release: None,
856 copy_description: None,
857 };
858 let timer = CFRunLoopTimerCreate(
859 ptr::null(),
860 CFAbsoluteTimeGetCurrent() + 0.05,
861 0.016,
862 0,
863 0,
864 timer_callback,
865 &mut ctx,
866 );
867 CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopDefaultMode);
868 }
869
870 unsafe { CFRunLoopRun() };
871
872 // Drop everything on the main thread (NSWindow.close in Drop).
873 MAIN_STATE.with(|cell| cell.borrow_mut().take());
874
875 SIGNAL_STOP_REQUESTED.store(true, Ordering::Relaxed);
876 let _ = signal_watcher.join();
877 drop(running);
878 }
879
880 struct MainState {
881 borders: BorderMap,
882 rx: mpsc::Receiver<Event>,
883 pending: HashMap<u32, std::time::Instant>,
884 batch_events: Vec<Event>,
885 batch_first_seen: Option<std::time::Instant>,
886 }
887
888 thread_local! {
889 static MAIN_STATE: std::cell::RefCell<Option<MainState>> = const { std::cell::RefCell::new(None) };
890 }
891
892 extern "C" fn timer_callback(_timer: *mut std::ffi::c_void, _info: *mut std::ffi::c_void) {
893 use std::time::{Duration, Instant};
894 MAIN_STATE.with(|cell| {
895 let mut state_opt = cell.borrow_mut();
896 let s = match state_opt.as_mut() {
897 Some(s) => s,
898 None => return,
899 };
900 loop {
901 match s.rx.try_recv() {
902 Ok(e) => {
903 if s.batch_events.is_empty() {
904 s.batch_first_seen = Some(Instant::now());
905 }
906 s.batch_events.push(e);
907 }
908 Err(mpsc::TryRecvError::Empty) => break,
909 Err(mpsc::TryRecvError::Disconnected) => break,
910 }
911 }
912 if let Some(first_seen) = s.batch_first_seen
913 && first_seen.elapsed() >= Duration::from_millis(100)
914 {
915 let events = std::mem::take(&mut s.batch_events);
916 s.batch_first_seen = None;
917 process_event_batch(&mut s.borders, &mut s.pending, events);
918 }
919 });
920 }
921
922 fn process_event_batch(
923 borders: &mut BorderMap,
924 pending: &mut HashMap<u32, std::time::Instant>,
925 events: Vec<Event>,
926 ) {
927 use std::collections::HashSet;
928 use std::time::{Duration, Instant};
929
930 let mut moved: HashSet<u32> = HashSet::new();
931 let mut resized: HashSet<u32> = HashSet::new();
932 let mut destroyed: HashSet<u32> = HashSet::new();
933 let mut needs_resubscribe = false;
934
935 for event in events {
936 match event {
937 Event::Move(wid) => {
938 if !borders.is_overlay(wid) {
939 moved.insert(wid);
940 }
941 }
942 Event::Resize(wid) => {
943 if !borders.is_overlay(wid) {
944 resized.insert(wid);
945 }
946 }
947 Event::Close(wid) | Event::Destroy(wid) => {
948 if !borders.is_overlay(wid) {
949 destroyed.insert(wid);
950 pending.remove(&wid);
951 }
952 }
953 Event::Create(wid) => {
954 if !borders.is_overlay(wid) {
955 pending.entry(wid).or_insert_with(Instant::now);
956 borders.subscribe_target(wid);
957 }
958 }
959 Event::Hide(wid) => borders.hide(wid),
960 Event::Unhide(wid) => {
961 if !borders.active_only || wid == borders.focused_wid {
962 borders.unhide(wid);
963 }
964 }
965 Event::FrontChange => {
966 needs_resubscribe = true;
967 }
968 Event::SpaceChange => {
969 needs_resubscribe = true;
970 }
971 }
972 }
973
974 for wid in &destroyed {
975 borders.remove(*wid);
976 }
977
978 let now = Instant::now();
979 let ready: Vec<u32> = pending
980 .iter()
981 .filter(|(wid, seen_at)| {
982 !destroyed.contains(wid) && now.duration_since(**seen_at) >= Duration::from_millis(100)
983 })
984 .map(|(wid, _)| *wid)
985 .collect();
986
987 let mut bounds_map: Vec<(u32, CGRect)> = Vec::new();
988 for &wid in &ready {
989 unsafe {
990 let mut b = CGRect::default();
991 SLSGetWindowBounds(borders.main_cid, wid, &mut b);
992 bounds_map.push((wid, b));
993 }
994 }
995
996 let mut skip: std::collections::HashSet<u32> = HashSet::new();
997 for i in 0..bounds_map.len() {
998 for j in (i + 1)..bounds_map.len() {
999 let (wid_a, a) = &bounds_map[i];
1000 let (wid_b, b) = &bounds_map[j];
1001 if let Some(preference) = surface_preference(*a, *b) {
1002 match preference {
1003 SurfacePreference::KeepExisting => {
1004 skip.insert(*wid_b);
1005 }
1006 SurfacePreference::ReplaceExisting => {
1007 skip.insert(*wid_a);
1008 }
1009 }
1010 }
1011 }
1012 }
1013
1014 for &wid in &ready {
1015 pending.remove(&wid);
1016 if !skip.contains(&wid) {
1017 borders.add_fresh(wid);
1018 if borders.active_only && wid != borders.focused_wid {
1019 borders.hide(wid);
1020 }
1021 needs_resubscribe = true;
1022 }
1023 }
1024
1025 for wid in &moved {
1026 if !resized.contains(wid) && !ready.contains(wid) && borders.sync_overlay(*wid) {
1027 needs_resubscribe = true;
1028 }
1029 }
1030
1031 for wid in &resized {
1032 if !ready.contains(wid)
1033 && borders.overlays.contains_key(wid)
1034 && borders.sync_overlay(*wid)
1035 {
1036 needs_resubscribe = true;
1037 }
1038 }
1039
1040 if needs_resubscribe {
1041 borders.discover_untracked();
1042 }
1043
1044 needs_resubscribe |= borders.reconcile_tracked();
1045
1046 borders.update_focus();
1047
1048 if needs_resubscribe || !destroyed.is_empty() {
1049 borders.subscribe_all();
1050 }
1051
1052 borders.enforce_active_only();
1053 }
1054
1055 fn setup_event_port(cid: CGSConnectionID) {
1056 unsafe {
1057 let mut port: u32 = 0;
1058 if SLSGetEventPort(cid, &mut port) != kCGErrorSuccess {
1059 return;
1060 }
1061 let cf_port = CFMachPortCreateWithPort(
1062 ptr::null(),
1063 port,
1064 drain_events as *const _,
1065 ptr::null(),
1066 false,
1067 );
1068 if cf_port.is_null() {
1069 return;
1070 }
1071 _CFMachPortSetOptions(cf_port, 0x40);
1072 let source = CFMachPortCreateRunLoopSource(ptr::null(), cf_port, 0);
1073 if !source.is_null() {
1074 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
1075 CFRelease(source);
1076 }
1077 CFRelease(cf_port);
1078 }
1079 }
1080
1081 unsafe extern "C" fn drain_events(
1082 _: CFMachPortRef,
1083 _: *mut std::ffi::c_void,
1084 _: i64,
1085 _: *mut std::ffi::c_void,
1086 ) {
1087 unsafe {
1088 let cid = SLSMainConnectionID();
1089 let mut ev = SLEventCreateNextEvent(cid);
1090 while !ev.is_null() {
1091 CFRelease(ev as CFTypeRef);
1092 ev = SLEventCreateNextEvent(cid);
1093 }
1094 }
1095 }
1096
1097 /// Look up an overlay window in CGWindowListCopyWindowInfo and dump the
1098 /// keys that the screenshot picker / ScreenCaptureKit care about. Lets
1099 /// us tell whether SLSSetWindowSharingState(0) propagates through to
1100 /// the CG window list (the layer SCWindow filters on) or stops at SLS.
1101 fn probe_cg_window_info(target_wid: u32) {
1102 unsafe {
1103 let list = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
1104 if list.is_null() {
1105 debug!("[probe_cg_window_info] wid={target_wid} list is null");
1106 return;
1107 }
1108 let count = CFArrayGetCount(list);
1109 let wid_key = cf_string_from_static(c"kCGWindowNumber");
1110 let sharing_key = cf_string_from_static(c"kCGWindowSharingState");
1111 let layer_key = cf_string_from_static(c"kCGWindowLayer");
1112 let alpha_key = cf_string_from_static(c"kCGWindowAlpha");
1113 let on_screen_key = cf_string_from_static(c"kCGWindowIsOnscreen");
1114 let mut found = false;
1115
1116 for i in 0..count {
1117 let dict = CFArrayGetValueAtIndex(list, i);
1118 if dict.is_null() {
1119 continue;
1120 }
1121 let mut v: CFTypeRef = ptr::null();
1122 let mut wid: u32 = 0;
1123 if CFDictionaryGetValueIfPresent(dict, wid_key as CFTypeRef, &mut v) {
1124 CFNumberGetValue(v, kCFNumberSInt32Type, &mut wid as *mut _ as *mut _);
1125 }
1126 if wid != target_wid {
1127 continue;
1128 }
1129
1130 let mut sharing: i32 = -1;
1131 if CFDictionaryGetValueIfPresent(dict, sharing_key as CFTypeRef, &mut v) {
1132 CFNumberGetValue(v, kCFNumberSInt32Type, &mut sharing as *mut _ as *mut _);
1133 }
1134 let mut layer: i32 = i32::MIN;
1135 if CFDictionaryGetValueIfPresent(dict, layer_key as CFTypeRef, &mut v) {
1136 CFNumberGetValue(v, kCFNumberSInt32Type, &mut layer as *mut _ as *mut _);
1137 }
1138 let mut alpha: f64 = -1.0;
1139 if CFDictionaryGetValueIfPresent(dict, alpha_key as CFTypeRef, &mut v) {
1140 CFNumberGetValue(v, 13 /* kCFNumberDoubleType */, &mut alpha as *mut _ as *mut _);
1141 }
1142 let on_screen_present =
1143 CFDictionaryGetValueIfPresent(dict, on_screen_key as CFTypeRef, &mut v);
1144
1145 debug!(
1146 "[probe_cg_window_info] wid={target_wid} cg_sharing={sharing} layer={layer} alpha={alpha:.3} on_screen_present={on_screen_present}"
1147 );
1148 found = true;
1149 break;
1150 }
1151
1152 if !found {
1153 debug!("[probe_cg_window_info] wid={target_wid} NOT FOUND in CGWindowList");
1154 }
1155 CFRelease(list as CFTypeRef);
1156 }
1157 }
1158
1159 fn discover_windows(cid: CGSConnectionID, own_pid: i32) -> Vec<u32> {
1160 unsafe {
1161 let list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
1162 if list.is_null() {
1163 return vec![];
1164 }
1165
1166 let count = CFArrayGetCount(list);
1167 let wid_key = cf_string_from_static(c"kCGWindowNumber");
1168 let pid_key = cf_string_from_static(c"kCGWindowOwnerPID");
1169 let layer_key = cf_string_from_static(c"kCGWindowLayer");
1170
1171 let mut wids = Vec::new();
1172 for i in 0..count {
1173 let dict = CFArrayGetValueAtIndex(list, i);
1174 if dict.is_null() {
1175 continue;
1176 }
1177
1178 let mut v: CFTypeRef = ptr::null();
1179 let mut wid: u32 = 0;
1180 if CFDictionaryGetValueIfPresent(dict, wid_key as CFTypeRef, &mut v) {
1181 CFNumberGetValue(v, kCFNumberSInt32Type, &mut wid as *mut _ as *mut _);
1182 }
1183 if wid == 0 {
1184 continue;
1185 }
1186
1187 let mut pid: i32 = 0;
1188 if CFDictionaryGetValueIfPresent(dict, pid_key as CFTypeRef, &mut v) {
1189 CFNumberGetValue(v, kCFNumberSInt32Type, &mut pid as *mut _ as *mut _);
1190 }
1191 if pid == own_pid {
1192 continue;
1193 }
1194
1195 if !is_suitable_window(cid, wid) {
1196 continue;
1197 }
1198
1199 let mut layer: i32 = -1;
1200 if CFDictionaryGetValueIfPresent(dict, layer_key as CFTypeRef, &mut v) {
1201 CFNumberGetValue(v, kCFNumberSInt32Type, &mut layer as *mut _ as *mut _);
1202 }
1203 if layer != 0 {
1204 continue;
1205 }
1206
1207 wids.push(wid);
1208 }
1209
1210 CFRelease(wid_key as CFTypeRef);
1211 CFRelease(pid_key as CFTypeRef);
1212 CFRelease(layer_key as CFTypeRef);
1213 CFRelease(list);
1214 wids
1215 }
1216 }
1217
1218 /// Draw a border ring into an existing CGContext, clearing first.
1219 fn draw_border(
1220 ctx: CGContextRef,
1221 width: f64,
1222 height: f64,
1223 border_width: f64,
1224 radius: f64,
1225 color: (f64, f64, f64, f64),
1226 ) {
1227 unsafe {
1228 let full = CGRect::new(0.0, 0.0, width, height);
1229 CGContextClearRect(ctx, full);
1230
1231 let bw = border_width;
1232 let stroke_rect = CGRect::new(bw / 2.0, bw / 2.0, width - bw, height - bw);
1233 let max_r = (stroke_rect.size.width.min(stroke_rect.size.height) / 2.0).max(0.0);
1234 let r = radius.min(max_r);
1235
1236 CGContextSetRGBStrokeColor(ctx, color.0, color.1, color.2, color.3);
1237 CGContextSetLineWidth(ctx, bw);
1238 let path = CGPathCreateWithRoundedRect(stroke_rect, r, r, ptr::null());
1239 if !path.is_null() {
1240 CGContextAddPath(ctx, path);
1241 CGContextStrokePath(ctx);
1242 CGPathRelease(path);
1243 }
1244 CGContextFlush(ctx);
1245 }
1246 }
1247
1248 fn create_overlay(
1249 cid: CGSConnectionID,
1250 target_wid: u32,
1251 border_width: f64,
1252 radius: f64,
1253 color: (f64, f64, f64, f64),
1254 ) -> Option<(CGSConnectionID, u32, CGRect, f64)> {
1255 unsafe {
1256 let mut bounds = CGRect::default();
1257 let rc = SLSGetWindowBounds(cid, target_wid, &mut bounds);
1258 if rc != kCGErrorSuccess {
1259 debug!("[create_overlay] SLSGetWindowBounds failed for wid={target_wid} rc={rc}");
1260 return None;
1261 }
1262 if !is_trackable_window(bounds, border_width) {
1263 debug!(
1264 "[create_overlay] wid={target_wid} too small: {}x{}",
1265 bounds.size.width, bounds.size.height
1266 );
1267 return None;
1268 }
1269
1270 let bw = border_width;
1271 let ow = bounds.size.width + 2.0 * bw;
1272 let oh = bounds.size.height + 2.0 * bw;
1273 let ox = bounds.origin.x - bw;
1274 let oy = bounds.origin.y - bw;
1275 let scale = display_scale_for_bounds(bounds);
1276
1277 let frame = CGRect::new(0.0, 0.0, ow, oh);
1278 let mut region: CFTypeRef = ptr::null();
1279 CGSNewRegionWithRect(&frame, &mut region);
1280 if region.is_null() {
1281 debug!("[create_overlay] CGSNewRegionWithRect failed for wid={target_wid}");
1282 return None;
1283 }
1284
1285 // Empty hit-test shape: an SLS window with an empty opaque_shape
1286 // is click-through at the compositor level (no input region).
1287 let empty = CGRect::new(0.0, 0.0, 0.0, 0.0);
1288 let mut empty_region: CFTypeRef = ptr::null();
1289 if CGSNewRegionWithRect(&empty, &mut empty_region) != kCGErrorSuccess
1290 || empty_region.is_null()
1291 {
1292 debug!("[create_overlay] CGSNewRegionWithRect (empty) failed for wid={target_wid}");
1293 CFRelease(region);
1294 return None;
1295 }
1296
1297 // Create the overlay via SLSNewWindowWithOpaqueShapeAndContext
1298 // and bake tag bit 1 (click-through) and tag bit 9 (screenshot
1299 // exclusion) into the window at birth. Tahoe classifies windows
1300 // for capture/picker based on tags observed at creation time;
1301 // post-creation tag mutation lands too late and the picker keeps
1302 // including the overlay. Mirrors the JankyBorders unmanaged
1303 // create path (.refs/JankyBorders/src/misc/window.h:239).
1304 // options 13|(1<<18): documentation-window | ignores-cycle.
1305 let mut tags: u64 = (1u64 << 1) | (1u64 << 9);
1306 let mut wid: u32 = 0;
1307 SLSNewWindowWithOpaqueShapeAndContext(
1308 cid,
1309 2,
1310 region,
1311 empty_region,
1312 13 | (1 << 18),
1313 &mut tags as *mut u64,
1314 ox as f32,
1315 oy as f32,
1316 64,
1317 &mut wid,
1318 ptr::null_mut(),
1319 );
1320 CFRelease(region);
1321 CFRelease(empty_region);
1322 if wid == 0 {
1323 debug!(
1324 "[create_overlay] SLSNewWindowWithOpaqueShapeAndContext returned 0 for target={target_wid} cid={cid}"
1325 );
1326 return None;
1327 }
1328
1329 debug!(
1330 "[create_overlay] created overlay wid={wid} for target={target_wid} scale={scale:.2} color=({:.2},{:.2},{:.2},{:.2})",
1331 color.0, color.1, color.2, color.3
1332 );
1333
1334 if let Some(metadata) = query_window_metadata(cid, wid) {
1335 debug!(
1336 "[create_overlay] post-create overlay wid={wid} tags={:#x} attributes={:#x} parent={}",
1337 metadata.tags, metadata.attributes, metadata.parent_wid
1338 );
1339 } else {
1340 debug!("[create_overlay] post-create wid={wid} metadata query failed");
1341 }
1342
1343 SLSSetWindowSharingState(cid, wid, 0);
1344 let mut sharing_state: u32 = u32::MAX;
1345 let rc = SLSGetWindowSharingState(cid, wid, &mut sharing_state);
1346 debug!("[create_overlay] sharing_state wid={wid} get_rc={rc} sls_state={sharing_state}");
1347
1348 // Probe what CGWindowListCopyWindowInfo (which the screenshot
1349 // picker / SCWindow use) reports for our overlay. If
1350 // kCGWindowSharingState comes back != 0 here, then SLS-side
1351 // sharing state is not propagated to the CG window list and
1352 // we'll need a different exclusion mechanism.
1353 probe_cg_window_info(wid);
1354
1355 SLSSetWindowResolution(cid, wid, scale);
1356 SLSSetWindowOpacity(cid, wid, false);
1357 SLSSetWindowLevel(cid, wid, 0);
1358 SLSOrderWindow(cid, wid, 1, target_wid);
1359
1360 // Draw border (point coordinates)
1361 let ctx = SLWindowContextCreate(cid, wid, ptr::null());
1362 if ctx.is_null() {
1363 debug!("[create_overlay] SLWindowContextCreate returned null for overlay wid={wid}");
1364 SLSReleaseWindow(cid, wid);
1365 return None;
1366 }
1367
1368 draw_border(ctx, ow, oh, bw, radius, color);
1369 SLSFlushWindowContentRegion(cid, wid, ptr::null());
1370 CGContextRelease(ctx);
1371
1372 // Post-creation tag mutation matching JankyBorders' pattern
1373 // at .refs/JankyBorders/src/misc/window.h:266-267. Verified
1374 // ineffective on Tahoe: tags set on windows owned by a
1375 // SLSNewConnection-created cid do NOT propagate to the global
1376 // server-side tag store, regardless of which cid issues the
1377 // SLSSetWindowTags call (tested both fresh and main cid).
1378 // The screencaptureui picker queries via _CGSGetWindowTags
1379 // from its own connection (otool confirmed) and reads 0x0 for
1380 // our overlays. Kept here aligned with JB so the diff is
1381 // legible; the actual fix requires creating overlays on the
1382 // process main cid (conflicts with the per-border fresh-cid
1383 // requirement in ers/CLAUDE.md) or backing them with NSWindow.
1384 let mut set_tags: u64 = (1u64 << 1) | (1u64 << 9);
1385 let mut clear_tags: u64 = 0;
1386 SLSSetWindowTags(cid, wid, &mut set_tags as *mut u64, 64);
1387 SLSClearWindowTags(cid, wid, &mut clear_tags as *mut u64, 64);
1388
1389 Some((cid, wid, bounds, scale))
1390 }
1391 }
1392
1393 fn list_windows() {
1394 let cid = unsafe { SLSMainConnectionID() };
1395 unsafe {
1396 let list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
1397 if list.is_null() {
1398 return;
1399 }
1400 let count = CFArrayGetCount(list);
1401 let wid_key = cf_string_from_static(c"kCGWindowNumber");
1402 let layer_key = cf_string_from_static(c"kCGWindowLayer");
1403
1404 eprintln!(
1405 "{:>6} {:>8} {:>8} {:>6} {:>6}",
1406 "wid", "x", "y", "w", "h"
1407 );
1408 for i in 0..count {
1409 let dict = CFArrayGetValueAtIndex(list, i);
1410 if dict.is_null() {
1411 continue;
1412 }
1413
1414 let mut v: CFTypeRef = ptr::null();
1415 let mut wid: u32 = 0;
1416 let mut layer: i32 = -1;
1417 if CFDictionaryGetValueIfPresent(dict, wid_key as CFTypeRef, &mut v) {
1418 CFNumberGetValue(v, kCFNumberSInt32Type, &mut wid as *mut _ as *mut _);
1419 }
1420 if CFDictionaryGetValueIfPresent(dict, layer_key as CFTypeRef, &mut v) {
1421 CFNumberGetValue(v, kCFNumberSInt32Type, &mut layer as *mut _ as *mut _);
1422 }
1423 if layer != 0 || wid == 0 {
1424 continue;
1425 }
1426
1427 let mut bounds = CGRect::default();
1428 SLSGetWindowBounds(cid, wid, &mut bounds);
1429 eprintln!(
1430 "{wid:>6} {:>8.0} {:>8.0} {:>6.0} {:>6.0}",
1431 bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height
1432 );
1433 }
1434 CFRelease(wid_key as CFTypeRef);
1435 CFRelease(layer_key as CFTypeRef);
1436 CFRelease(list);
1437 }
1438 }
1439
1440 #[cfg(test)]
1441 mod tests {
1442 use super::{
1443 CGRect, SurfacePreference, WindowMetadata, intersection_area, is_same_window_surface,
1444 is_suitable_window_metadata, is_trackable_window, surface_preference,
1445 };
1446
1447 #[test]
1448 fn same_surface_detects_contained_strip() {
1449 let outer = CGRect::new(100.0, 100.0, 1200.0, 900.0);
1450 let strip = CGRect::new(114.0, 105.0, 1160.0, 140.0);
1451 assert!(is_same_window_surface(outer, strip));
1452 }
1453
1454 #[test]
1455 fn different_windows_are_not_treated_as_one_surface() {
1456 let a = CGRect::new(100.0, 100.0, 1200.0, 900.0);
1457 let b = CGRect::new(300.0, 300.0, 1160.0, 140.0);
1458 assert!(!is_same_window_surface(a, b));
1459 }
1460
1461 #[test]
1462 fn intersection_area_is_zero_without_overlap() {
1463 let a = CGRect::new(100.0, 100.0, 200.0, 200.0);
1464 let b = CGRect::new(400.0, 400.0, 200.0, 200.0);
1465 assert_eq!(intersection_area(a, b), 0.0);
1466 }
1467
1468 #[test]
1469 fn same_surface_prefers_larger_bounds() {
1470 let strip = CGRect::new(114.0, 105.0, 1160.0, 140.0);
1471 let outer = CGRect::new(100.0, 100.0, 1200.0, 900.0);
1472 assert_eq!(
1473 surface_preference(strip, outer),
1474 Some(SurfacePreference::ReplaceExisting)
1475 );
1476 }
1477
1478 #[test]
1479 fn small_windows_remain_trackable() {
1480 let small = CGRect::new(100.0, 100.0, 12.0, 18.0);
1481 assert!(is_trackable_window(small, 4.0));
1482 }
1483
1484 #[test]
1485 fn suitable_window_metadata_matches_document_windows() {
1486 let metadata = WindowMetadata {
1487 parent_wid: 0,
1488 tags: super::WINDOW_TAG_DOCUMENT,
1489 attributes: super::WINDOW_ATTRIBUTE_REAL,
1490 };
1491 assert!(is_suitable_window_metadata(metadata));
1492 }
1493
1494 #[test]
1495 fn attached_windows_are_not_suitable_targets() {
1496 let metadata = WindowMetadata {
1497 parent_wid: 7,
1498 tags: super::WINDOW_TAG_DOCUMENT | super::WINDOW_TAG_ATTACHED,
1499 attributes: super::WINDOW_ATTRIBUTE_REAL,
1500 };
1501 assert!(!is_suitable_window_metadata(metadata));
1502 }
1503 }
1504