Rust · 28275 bytes Raw Blame History
1 //! ers — window border renderer
2
3 mod events;
4 mod skylight;
5
6 use events::Event;
7 use skylight::*;
8 use std::collections::HashMap;
9 use std::ptr;
10 use std::sync::atomic::{AtomicBool, Ordering};
11 use std::sync::mpsc;
12 use std::sync::Arc;
13 use tracing::debug;
14
15 /// Per-overlay state: the connection it was created on + its wid.
16 struct Overlay {
17 cid: CGSConnectionID,
18 wid: u32,
19 }
20
21 /// Tracks overlays for target windows.
22 struct BorderMap {
23 overlays: HashMap<u32, Overlay>,
24 main_cid: CGSConnectionID,
25 own_pid: i32,
26 border_width: f64,
27 radius: f64,
28 focused_wid: u32,
29 active_color: (f64, f64, f64, f64),
30 inactive_color: (f64, f64, f64, f64),
31 active_only: bool,
32 }
33
34 impl BorderMap {
35 fn new(cid: CGSConnectionID, own_pid: i32, border_width: f64) -> Self {
36 Self {
37 overlays: HashMap::new(),
38 main_cid: cid,
39 own_pid,
40 border_width,
41 radius: 10.0,
42 focused_wid: 0,
43 active_color: (0.32, 0.58, 0.89, 1.0), // #5294e2
44 inactive_color: (0.35, 0.35, 0.35, 0.8), // dim gray
45 active_only: false,
46 }
47 }
48
49 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 }
51 }
52
53 fn is_overlay(&self, wid: u32) -> bool {
54 self.overlays.values().any(|o| o.wid == wid)
55 }
56
57 /// Add border (batch mode, uses main cid).
58 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 });
63 }
64 }
65
66 /// Add border (event mode). Uses main_cid — fresh connections create
67 /// invisible windows on Tahoe.
68 fn add_fresh(&mut self, target_wid: u32) {
69 if self.overlays.contains_key(&target_wid) { return; }
70
71 // Filter: must be visible, owned by another process, not tiny
72 unsafe {
73 let mut shown = false;
74 SLSWindowIsOrderedIn(self.main_cid, target_wid, &mut shown);
75 if !shown { return; }
76
77 let mut wid_cid: CGSConnectionID = 0;
78 SLSGetWindowOwner(self.main_cid, target_wid, &mut wid_cid);
79 let mut pid: i32 = 0;
80 SLSConnectionGetPID(wid_cid, &mut pid);
81 if pid == self.own_pid { return; }
82
83 let mut bounds = CGRect::default();
84 SLSGetWindowBounds(self.main_cid, target_wid, &mut bounds);
85 if bounds.size.width < 50.0 || bounds.size.height < 50.0 { return; }
86 }
87
88 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 });
91 }
92 }
93
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);
98 }
99 }
100
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 }
113 }
114 }
115 }
116
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;
124 }
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);
131 }
132 }
133 }
134
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);
142 }
143 self.subscribe_target(target_wid);
144 }
145
146 fn hide(&self, target_wid: u32) {
147 if let Some(o) = self.overlays.get(&target_wid) {
148 unsafe { SLSOrderWindow(o.cid, o.wid, 0, 0); }
149 }
150 }
151
152 fn unhide(&self, target_wid: u32) {
153 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 }
158 }
159 }
160
161 fn subscribe_target(&self, target_wid: u32) {
162 unsafe {
163 SLSRequestNotificationsForWindows(self.main_cid, &target_wid, 1);
164 }
165 }
166
167 fn subscribe_all(&self) {
168 let target_wids: Vec<u32> = self.overlays.keys().copied().collect();
169 if target_wids.is_empty() { return; }
170 unsafe {
171 SLSRequestNotificationsForWindows(
172 self.main_cid,
173 target_wids.as_ptr(),
174 target_wids.len() as i32,
175 );
176 }
177 }
178
179 /// Redraw an existing overlay with a new color (no destroy/recreate).
180 fn redraw(&self, target_wid: u32) {
181 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 }
199 }
200 }
201
202 /// Detect focused window and update border colors if focus changed.
203 fn update_focus(&mut self) {
204 let front = get_front_window(self.own_pid);
205 if front == 0 || front == self.focused_wid { return; }
206
207 let old = self.focused_wid;
208 self.focused_wid = front;
209 debug!("[focus] {} -> {}", old, front);
210
211 if self.active_only {
212 self.hide(old);
213 self.unhide(front);
214 }
215 self.redraw(old);
216 self.redraw(front);
217 }
218
219 /// In active-only mode, ensure only the focused overlay is visible.
220 fn enforce_active_only(&self) {
221 if !self.active_only { return; }
222 for (&target_wid, o) in &self.overlays {
223 if target_wid == self.focused_wid {
224 unsafe {
225 SLSSetWindowLevel(o.cid, o.wid, 25);
226 SLSOrderWindow(o.cid, o.wid, 1, 0);
227 }
228 } else {
229 unsafe { SLSOrderWindow(o.cid, o.wid, 0, 0); }
230 }
231 }
232 }
233 }
234
235 /// Get the front (focused) window ID using CGWindowListCopyWindowInfo.
236 /// Avoids all SLS display/space queries which poison SLSNewWindow globally.
237 fn get_front_window(own_pid: i32) -> u32 {
238 unsafe {
239 let list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
240 if list.is_null() { return 0; }
241
242 let count = CFArrayGetCount(list);
243 let wid_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowNumber\0".as_ptr(), kCFStringEncodingUTF8);
244 let pid_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowOwnerPID\0".as_ptr(), kCFStringEncodingUTF8);
245 let layer_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowLayer\0".as_ptr(), kCFStringEncodingUTF8);
246
247 // CGWindowListCopyWindowInfo returns windows in front-to-back order.
248 // First layer-0 window not owned by us is the focused window.
249 let mut front_wid: u32 = 0;
250 for i in 0..count {
251 let dict = CFArrayGetValueAtIndex(list, i);
252 if dict.is_null() { continue; }
253
254 let mut v: CFTypeRef = ptr::null();
255
256 let mut layer: i32 = -1;
257 if CFDictionaryGetValueIfPresent(dict, layer_key as CFTypeRef, &mut v) {
258 CFNumberGetValue(v, kCFNumberSInt32Type, &mut layer as *mut _ as *mut _);
259 }
260 if layer != 0 { continue; }
261
262 let mut pid: i32 = 0;
263 if CFDictionaryGetValueIfPresent(dict, pid_key as CFTypeRef, &mut v) {
264 CFNumberGetValue(v, kCFNumberSInt32Type, &mut pid as *mut _ as *mut _);
265 }
266 if pid == own_pid { continue; }
267
268 let mut wid: u32 = 0;
269 if CFDictionaryGetValueIfPresent(dict, wid_key as CFTypeRef, &mut v) {
270 CFNumberGetValue(v, kCFNumberSInt32Type, &mut wid as *mut _ as *mut _);
271 }
272 if wid != 0 {
273 front_wid = wid;
274 break;
275 }
276 }
277
278 CFRelease(wid_key as CFTypeRef);
279 CFRelease(pid_key as CFTypeRef);
280 CFRelease(layer_key as CFTypeRef);
281 CFRelease(list);
282 front_wid
283 }
284 }
285
286 /// Parse hex color string (#RRGGBB or #RRGGBBAA) to (r, g, b, a) floats.
287 fn parse_color(s: &str) -> Option<(f64, f64, f64, f64)> {
288 let hex = s.strip_prefix('#').unwrap_or(s);
289 if hex.len() != 6 && hex.len() != 8 { return None; }
290 let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f64 / 255.0;
291 let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f64 / 255.0;
292 let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f64 / 255.0;
293 let a = if hex.len() == 8 {
294 u8::from_str_radix(&hex[6..8], 16).ok()? as f64 / 255.0
295 } else { 1.0 };
296 Some((r, g, b, a))
297 }
298
299 fn flag_value<'a>(args: &'a [String], flags: &[&str]) -> Option<&'a str> {
300 args.iter()
301 .position(|s| flags.iter().any(|f| s == f))
302 .and_then(|i| args.get(i + 1))
303 .map(|s| s.as_str())
304 }
305
306 fn print_help() {
307 eprintln!("ers — window border renderer for tarmac");
308 eprintln!();
309 eprintln!("USAGE: ers [OPTIONS] [WINDOW_ID]");
310 eprintln!();
311 eprintln!("OPTIONS:");
312 eprintln!(" -w, --width <PX> Border width in pixels (default: 4.0)");
313 eprintln!(" -r, --radius <PX> Corner radius (default: 10.0)");
314 eprintln!(" -c, --color <HEX> Active border color (default: #5294e2)");
315 eprintln!(" -i, --inactive <HEX> Inactive border color (default: #59595980)");
316 eprintln!(" --active-only Only show border on focused window");
317 eprintln!(" --list List on-screen windows and exit");
318 eprintln!(" -h, --help Show this help");
319 }
320
321 fn main() {
322 tracing_subscriber::fmt()
323 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
324 .with_writer(std::io::stderr)
325 .init();
326
327 let args: Vec<String> = std::env::args().collect();
328
329 if args.iter().any(|s| s == "--help" || s == "-h") {
330 print_help();
331 return;
332 }
333
334 if args.get(1).is_some_and(|s| s == "--list") {
335 list_windows();
336 return;
337 }
338
339 let border_width: f64 = flag_value(&args, &["--width", "-w"])
340 .and_then(|v| v.parse().ok())
341 .unwrap_or(4.0);
342
343 let radius: f64 = flag_value(&args, &["--radius", "-r"])
344 .and_then(|v| v.parse().ok())
345 .unwrap_or(10.0);
346
347 let active_color = flag_value(&args, &["--color", "-c"])
348 .and_then(parse_color)
349 .unwrap_or((0.32, 0.58, 0.89, 1.0));
350
351 let inactive_color = flag_value(&args, &["--inactive", "-i"])
352 .and_then(parse_color)
353 .unwrap_or((0.35, 0.35, 0.35, 0.8));
354
355 let active_only = args.iter().any(|s| s == "--active-only");
356
357 let cid = unsafe { SLSMainConnectionID() };
358 let own_pid = unsafe {
359 let mut pid: i32 = 0;
360 pid_for_task(mach_task_self(), &mut pid);
361 pid
362 };
363
364 // Event channel
365 let (tx, rx) = mpsc::channel();
366 events::init(tx, own_pid);
367 events::register(cid);
368 setup_event_port(cid);
369
370 // Discover and create borders
371 let mut borders = BorderMap::new(cid, own_pid, border_width);
372 borders.radius = radius;
373 borders.active_color = active_color;
374 borders.inactive_color = inactive_color;
375 borders.active_only = active_only;
376
377 if let Some(target) = args.get(1).and_then(|s| s.parse::<u32>().ok()) {
378 borders.add_batch(target);
379 } else {
380 let wids = discover_windows(cid, own_pid);
381 for &wid in &wids {
382 borders.add_batch(wid);
383 }
384 }
385
386 borders.subscribe_all();
387
388 borders.update_focus();
389
390 if borders.active_only {
391 let focused = borders.focused_wid;
392 let to_hide: Vec<u32> = borders.overlays.keys()
393 .filter(|&&wid| wid != focused)
394 .copied()
395 .collect();
396 for wid in to_hide {
397 borders.hide(wid);
398 }
399 }
400
401 debug!("{} overlays tracked", borders.overlays.len());
402
403 // SIGINT flag — background thread checks this to clean up
404 let running = Arc::new(AtomicBool::new(true));
405 unsafe {
406 libc::signal(libc::SIGINT, {
407 unsafe extern "C" fn handler(_: libc::c_int) {
408 unsafe {
409 CFRunLoopStop(CFRunLoopGetMain());
410 }
411 }
412 handler as *const () as libc::sighandler_t
413 });
414 }
415
416 // Process events on background thread with coalescing
417 let running_bg = Arc::clone(&running);
418 let handle = std::thread::spawn(move || {
419 use std::collections::HashSet;
420 use std::time::Duration;
421
422 // Persist across batches: windows we know about but haven't bordered yet
423 let mut pending: HashSet<u32> = HashSet::new();
424
425 while running_bg.load(Ordering::Relaxed) {
426 let first = match rx.recv_timeout(Duration::from_millis(100)) {
427 Ok(e) => e,
428 Err(mpsc::RecvTimeoutError::Timeout) => continue,
429 Err(mpsc::RecvTimeoutError::Disconnected) => break,
430 };
431
432 std::thread::sleep(std::time::Duration::from_millis(16));
433
434 let mut events = vec![first];
435 while let Ok(e) = rx.try_recv() {
436 events.push(e);
437 }
438
439 let mut moved: HashSet<u32> = HashSet::new();
440 let mut resized: HashSet<u32> = HashSet::new();
441 let mut destroyed: HashSet<u32> = HashSet::new();
442 let mut needs_resubscribe = false;
443
444 for event in events {
445 match event {
446 Event::Move(wid) => {
447 if !borders.is_overlay(wid) {
448 moved.insert(wid);
449 }
450 }
451 Event::Resize(wid) => {
452 if !borders.is_overlay(wid) {
453 resized.insert(wid);
454 }
455 }
456 Event::Close(wid) | Event::Destroy(wid) => {
457 if !borders.is_overlay(wid) {
458 destroyed.insert(wid);
459 pending.remove(&wid);
460 }
461 }
462 Event::Create(wid) => {
463 if !borders.is_overlay(wid) {
464 pending.insert(wid);
465 borders.subscribe_target(wid);
466 }
467 }
468 Event::Hide(wid) => borders.hide(wid),
469 Event::Unhide(wid) => {
470 if !borders.active_only || wid == borders.focused_wid {
471 borders.unhide(wid);
472 }
473 }
474 Event::FrontChange => {
475 needs_resubscribe = true;
476 }
477 Event::SpaceChange => {
478 needs_resubscribe = true;
479 }
480 }
481 }
482
483 // Destroys
484 for wid in &destroyed {
485 borders.remove(*wid);
486 }
487
488 // Promote ALL pending creates that weren't destroyed
489 // (the 150ms debounce is enough for tarmac to position them)
490 let ready: Vec<u32> = pending.iter()
491 .filter(|wid| !destroyed.contains(wid))
492 .copied()
493 .collect();
494 // Filter overlapping creates: if two windows overlap, keep smaller one
495 let mut bounds_map: Vec<(u32, CGRect)> = Vec::new();
496 for &wid in &ready {
497 unsafe {
498 let mut b = CGRect::default();
499 SLSGetWindowBounds(borders.main_cid, wid, &mut b);
500 bounds_map.push((wid, b));
501 }
502 }
503
504 // If two new windows overlap closely, skip the larger one (container)
505 let mut skip: std::collections::HashSet<u32> = HashSet::new();
506 for i in 0..bounds_map.len() {
507 for j in (i+1)..bounds_map.len() {
508 let (wid_a, a) = &bounds_map[i];
509 let (wid_b, b) = &bounds_map[j];
510 // Check if centers are close (within 30px)
511 let cx_a = a.origin.x + a.size.width / 2.0;
512 let cy_a = a.origin.y + a.size.height / 2.0;
513 let cx_b = b.origin.x + b.size.width / 2.0;
514 let cy_b = b.origin.y + b.size.height / 2.0;
515 if (cx_a - cx_b).abs() < 30.0 && (cy_a - cy_b).abs() < 30.0 {
516 // Skip the larger one
517 let area_a = a.size.width * a.size.height;
518 let area_b = b.size.width * b.size.height;
519 if area_a > area_b {
520 skip.insert(*wid_a);
521 } else {
522 skip.insert(*wid_b);
523 }
524 }
525 }
526 }
527
528 for &wid in &ready {
529 pending.remove(&wid);
530 if !skip.contains(&wid) {
531 borders.add_fresh(wid);
532 if borders.active_only && wid != borders.focused_wid {
533 borders.hide(wid);
534 }
535 needs_resubscribe = true;
536 }
537 }
538
539 // Moves: reposition overlay (no destroy/create)
540 for wid in &moved {
541 if !resized.contains(wid) && !ready.contains(wid) {
542 borders.reposition(*wid);
543 }
544 }
545
546 // Resizes: must recreate (can't reshape windows on Tahoe)
547 // Skip windows just created this batch — already at correct size
548 for wid in &resized {
549 if !ready.contains(wid) && borders.overlays.contains_key(wid) {
550 borders.recreate(*wid);
551 needs_resubscribe = true;
552 }
553 }
554
555 // Update focus (redraws borders in-place if changed)
556 borders.update_focus();
557
558 // Re-subscribe ALL tracked windows (SLSRequestNotificationsForWindows replaces, not appends)
559 if needs_resubscribe || !destroyed.is_empty() {
560 borders.subscribe_all();
561 }
562
563 // After all processing, enforce active-only visibility
564 borders.enforce_active_only();
565 }
566
567 // Clean up all overlays before exiting
568 borders.remove_all();
569 });
570
571 unsafe { CFRunLoopRun() };
572
573 // SIGINT received — signal background thread to stop and wait
574 running.store(false, Ordering::Relaxed);
575 let _ = handle.join();
576 }
577
578 fn setup_event_port(cid: CGSConnectionID) {
579 unsafe {
580 let mut port: u32 = 0;
581 if SLSGetEventPort(cid, &mut port) != kCGErrorSuccess { return; }
582 let cf_port = CFMachPortCreateWithPort(ptr::null(), port, drain_events as *const _, ptr::null(), false);
583 if cf_port.is_null() { return; }
584 _CFMachPortSetOptions(cf_port, 0x40);
585 let source = CFMachPortCreateRunLoopSource(ptr::null(), cf_port, 0);
586 if !source.is_null() {
587 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
588 CFRelease(source);
589 }
590 CFRelease(cf_port);
591 }
592 }
593
594 unsafe extern "C" fn drain_events(_: CFMachPortRef, _: *mut std::ffi::c_void, _: i64, _: *mut std::ffi::c_void) {
595 unsafe {
596 let cid = SLSMainConnectionID();
597 let mut ev = SLEventCreateNextEvent(cid);
598 while !ev.is_null() {
599 CFRelease(ev as CFTypeRef);
600 ev = SLEventCreateNextEvent(cid);
601 }
602 }
603 }
604
605 fn discover_windows(_cid: CGSConnectionID, own_pid: i32) -> Vec<u32> {
606 unsafe {
607 let list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
608 if list.is_null() { return vec![]; }
609
610 let count = CFArrayGetCount(list);
611 let wid_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowNumber\0".as_ptr(), kCFStringEncodingUTF8);
612 let pid_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowOwnerPID\0".as_ptr(), kCFStringEncodingUTF8);
613 let layer_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowLayer\0".as_ptr(), kCFStringEncodingUTF8);
614
615 let mut wids = Vec::new();
616 for i in 0..count {
617 let dict = CFArrayGetValueAtIndex(list, i);
618 if dict.is_null() { continue; }
619
620 let mut v: CFTypeRef = ptr::null();
621 let mut wid: u32 = 0;
622 if CFDictionaryGetValueIfPresent(dict, wid_key as CFTypeRef, &mut v) {
623 CFNumberGetValue(v, kCFNumberSInt32Type, &mut wid as *mut _ as *mut _);
624 }
625 if wid == 0 { continue; }
626
627 let mut pid: i32 = 0;
628 if CFDictionaryGetValueIfPresent(dict, pid_key as CFTypeRef, &mut v) {
629 CFNumberGetValue(v, kCFNumberSInt32Type, &mut pid as *mut _ as *mut _);
630 }
631 if pid == own_pid { continue; }
632
633 let mut layer: i32 = -1;
634 if CFDictionaryGetValueIfPresent(dict, layer_key as CFTypeRef, &mut v) {
635 CFNumberGetValue(v, kCFNumberSInt32Type, &mut layer as *mut _ as *mut _);
636 }
637 if layer != 0 { continue; }
638
639 wids.push(wid);
640 }
641
642 CFRelease(wid_key as CFTypeRef);
643 CFRelease(pid_key as CFTypeRef);
644 CFRelease(layer_key as CFTypeRef);
645 CFRelease(list);
646 wids
647 }
648 }
649
650 /// Draw a border ring into an existing CGContext, clearing first.
651 fn draw_border(
652 ctx: CGContextRef,
653 width: f64,
654 height: f64,
655 border_width: f64,
656 radius: f64,
657 color: (f64, f64, f64, f64),
658 ) {
659 unsafe {
660 let full = CGRect::new(0.0, 0.0, width, height);
661 CGContextClearRect(ctx, full);
662
663 let bw = border_width;
664 let stroke_rect = CGRect::new(bw / 2.0, bw / 2.0, width - bw, height - bw);
665 let max_r = (stroke_rect.size.width.min(stroke_rect.size.height) / 2.0).max(0.0);
666 let r = radius.min(max_r);
667
668 CGContextSetRGBStrokeColor(ctx, color.0, color.1, color.2, color.3);
669 CGContextSetLineWidth(ctx, bw);
670 let path = CGPathCreateWithRoundedRect(stroke_rect, r, r, ptr::null());
671 if !path.is_null() {
672 CGContextAddPath(ctx, path);
673 CGContextStrokePath(ctx);
674 CGPathRelease(path);
675 }
676 CGContextFlush(ctx);
677 }
678 }
679
680 fn create_overlay(
681 cid: CGSConnectionID,
682 target_wid: u32,
683 border_width: f64,
684 radius: f64,
685 color: (f64, f64, f64, f64),
686 ) -> Option<(CGSConnectionID, u32)> {
687 unsafe {
688 let mut bounds = CGRect::default();
689 let rc = SLSGetWindowBounds(cid, target_wid, &mut bounds);
690 if rc != kCGErrorSuccess {
691 debug!("[create_overlay] SLSGetWindowBounds failed for wid={target_wid} rc={rc}");
692 return None;
693 }
694 if bounds.size.width < 10.0 || bounds.size.height < 10.0 {
695 debug!("[create_overlay] wid={target_wid} too small: {}x{}", bounds.size.width, bounds.size.height);
696 return None;
697 }
698
699 let bw = border_width;
700 let ow = bounds.size.width + 2.0 * bw;
701 let oh = bounds.size.height + 2.0 * bw;
702 let ox = bounds.origin.x - bw;
703 let oy = bounds.origin.y - bw;
704
705 let frame = CGRect::new(0.0, 0.0, ow, oh);
706 let mut region: CFTypeRef = ptr::null();
707 CGSNewRegionWithRect(&frame, &mut region);
708 if region.is_null() {
709 debug!("[create_overlay] CGSNewRegionWithRect failed for wid={target_wid}");
710 return None;
711 }
712
713 let mut wid: u32 = 0;
714 SLSNewWindow(cid, 2, ox as f32, oy as f32, region, &mut wid);
715 CFRelease(region);
716 if wid == 0 {
717 debug!("[create_overlay] SLSNewWindow returned 0 for target={target_wid} cid={cid}");
718 return None;
719 }
720
721 debug!("[create_overlay] created overlay wid={wid} for target={target_wid} color=({:.2},{:.2},{:.2},{:.2})",
722 color.0, color.1, color.2, color.3);
723
724 SLSSetWindowResolution(cid, wid, 2.0);
725 SLSSetWindowOpacity(cid, wid, false);
726 SLSSetWindowLevel(cid, wid, 25);
727 SLSOrderWindow(cid, wid, 1, 0);
728
729 // Draw border (point coordinates)
730 let ctx = SLWindowContextCreate(cid, wid, ptr::null());
731 if ctx.is_null() {
732 debug!("[create_overlay] SLWindowContextCreate returned null for overlay wid={wid}");
733 SLSReleaseWindow(cid, wid);
734 return None;
735 }
736
737 draw_border(ctx, ow, oh, bw, radius, color);
738 SLSFlushWindowContentRegion(cid, wid, ptr::null());
739 CGContextRelease(ctx);
740
741 Some((cid, wid))
742 }
743 }
744
745 fn list_windows() {
746 let cid = unsafe { SLSMainConnectionID() };
747 unsafe {
748 let list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
749 if list.is_null() { return; }
750 let count = CFArrayGetCount(list);
751 let wid_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowNumber\0".as_ptr(), kCFStringEncodingUTF8);
752 let layer_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowLayer\0".as_ptr(), kCFStringEncodingUTF8);
753
754 eprintln!("{:>6} {:>8} {:>8} {:>6} {:>6}", "wid", "x", "y", "w", "h");
755 for i in 0..count {
756 let dict = CFArrayGetValueAtIndex(list, i);
757 if dict.is_null() { continue; }
758
759 let mut v: CFTypeRef = ptr::null();
760 let mut wid: u32 = 0;
761 let mut layer: i32 = -1;
762 if CFDictionaryGetValueIfPresent(dict, wid_key as CFTypeRef, &mut v) {
763 CFNumberGetValue(v, kCFNumberSInt32Type, &mut wid as *mut _ as *mut _);
764 }
765 if CFDictionaryGetValueIfPresent(dict, layer_key as CFTypeRef, &mut v) {
766 CFNumberGetValue(v, kCFNumberSInt32Type, &mut layer as *mut _ as *mut _);
767 }
768 if layer != 0 || wid == 0 { continue; }
769
770 let mut bounds = CGRect::default();
771 SLSGetWindowBounds(cid, wid, &mut bounds);
772 eprintln!("{wid:>6} {:>8.0} {:>8.0} {:>6.0} {:>6.0}",
773 bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height);
774 }
775 CFRelease(wid_key as CFTypeRef);
776 CFRelease(layer_key as CFTypeRef);
777 CFRelease(list);
778 }
779 }
780
781