@@ -56,38 +56,13 @@ impl BorderMap { |
| 56 | 56 | } |
| 57 | 57 | } |
| 58 | 58 | |
| 59 | | - /// Add border (event mode, fresh connection). |
| 59 | + /// Add border (event mode). Uses main_cid — fresh connections create |
| 60 | + /// invisible windows on Tahoe. |
| 60 | 61 | fn add_fresh(&mut self, target_wid: u32) { |
| 61 | 62 | if self.overlays.contains_key(&target_wid) { return; } |
| 62 | | - |
| 63 | | - // Filter: must be visible, owned by another process, not tiny |
| 64 | | - unsafe { |
| 65 | | - let mut shown = false; |
| 66 | | - SLSWindowIsOrderedIn(self.main_cid, target_wid, &mut shown); |
| 67 | | - if !shown { return; } |
| 68 | | - |
| 69 | | - let mut wid_cid: CGSConnectionID = 0; |
| 70 | | - SLSGetWindowOwner(self.main_cid, target_wid, &mut wid_cid); |
| 71 | | - let mut pid: i32 = 0; |
| 72 | | - SLSConnectionGetPID(wid_cid, &mut pid); |
| 73 | | - if pid == self.own_pid { return; } |
| 74 | | - |
| 75 | | - let mut bounds = CGRect::default(); |
| 76 | | - SLSGetWindowBounds(self.main_cid, target_wid, &mut bounds); |
| 77 | | - if bounds.size.width < 50.0 || bounds.size.height < 50.0 { return; } |
| 78 | | - } |
| 79 | | - |
| 80 | | - let fresh = unsafe { |
| 81 | | - let mut c: CGSConnectionID = 0; |
| 82 | | - SLSNewConnection(0, &mut c); |
| 83 | | - c |
| 84 | | - }; |
| 85 | | - if fresh == 0 { return; } |
| 86 | 63 | let color = self.color_for(target_wid); |
| 87 | | - if let Some((cid, wid)) = create_overlay(fresh, target_wid, self.border_width, color) { |
| 64 | + if let Some((cid, wid)) = create_overlay(self.main_cid, target_wid, self.border_width, color) { |
| 88 | 65 | self.overlays.insert(target_wid, Overlay { cid, wid }); |
| 89 | | - } else { |
| 90 | | - unsafe { SLSReleaseConnection(fresh); } |
| 91 | 66 | } |
| 92 | 67 | } |
| 93 | 68 | |
@@ -176,74 +151,116 @@ impl BorderMap { |
| 176 | 151 | } |
| 177 | 152 | } |
| 178 | 153 | |
| 179 | | - /// Detect focused window and update borders if focus changed. |
| 180 | | - fn update_focus(&mut self) { |
| 181 | | - let front = get_front_window(self.main_cid); |
| 182 | | - if front == 0 || front == self.focused_wid { return; } |
| 154 | + /// Redraw an existing overlay with a new color (no destroy/recreate). |
| 155 | + fn redraw(&self, target_wid: u32) { |
| 156 | + if let Some(overlay) = self.overlays.get(&target_wid) { |
| 157 | + unsafe { |
| 158 | + let mut bounds = CGRect::default(); |
| 159 | + if SLSGetWindowBounds(overlay.cid, target_wid, &mut bounds) != kCGErrorSuccess { |
| 160 | + return; |
| 161 | + } |
| 162 | + let bw = self.border_width; |
| 163 | + let ow = bounds.size.width + 2.0 * bw; |
| 164 | + let oh = bounds.size.height + 2.0 * bw; |
| 165 | + |
| 166 | + let ctx = SLWindowContextCreate(overlay.cid, overlay.wid, ptr::null()); |
| 167 | + if ctx.is_null() { return; } |
| 168 | + |
| 169 | + let full = CGRect::new(0.0, 0.0, ow, oh); |
| 170 | + CGContextClearRect(ctx, full); |
| 171 | + |
| 172 | + let color = self.color_for(target_wid); |
| 173 | + let stroke_rect = CGRect::new(bw / 2.0, bw / 2.0, ow - bw, oh - bw); |
| 174 | + let radius = 10.0_f64; |
| 175 | + let max_r = (stroke_rect.size.width.min(stroke_rect.size.height) / 2.0).max(0.0); |
| 176 | + let r = radius.min(max_r); |
| 177 | + |
| 178 | + CGContextSetRGBStrokeColor(ctx, color.0, color.1, color.2, color.3); |
| 179 | + CGContextSetLineWidth(ctx, bw); |
| 180 | + let path = CGPathCreateWithRoundedRect(stroke_rect, r, r, ptr::null()); |
| 181 | + if !path.is_null() { |
| 182 | + CGContextAddPath(ctx, path); |
| 183 | + CGContextStrokePath(ctx); |
| 184 | + CGPathRelease(path); |
| 185 | + } |
| 186 | + |
| 187 | + CGContextFlush(ctx); |
| 188 | + SLSFlushWindowContentRegion(overlay.cid, overlay.wid, ptr::null()); |
| 189 | + CGContextRelease(ctx); |
| 190 | + } |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + /// Detect focused window and update border colors if focus changed. |
| 195 | + /// Returns true if focus changed (callers should resubscribe). |
| 196 | + fn update_focus(&mut self) -> bool { |
| 197 | + let front = get_front_window(self.own_pid); |
| 198 | + if front == 0 || front == self.focused_wid { return false; } |
| 183 | 199 | |
| 184 | 200 | let old = self.focused_wid; |
| 185 | 201 | self.focused_wid = front; |
| 186 | | - let tracked = self.overlays.contains_key(&front); |
| 187 | | - eprintln!("[focus] {old} -> {front} (tracked={tracked})"); |
| 202 | + eprintln!("[focus] {} -> {} (tracked={})", old, front, self.overlays.contains_key(&front)); |
| 188 | 203 | |
| 189 | | - // Recreate both old and new focused borders with correct colors |
| 204 | + // Recreate overlays with new colors — re-obtaining a CGContext |
| 205 | + // for an existing window is unreliable on Tahoe |
| 190 | 206 | if self.overlays.contains_key(&old) { |
| 191 | 207 | self.recreate(old); |
| 192 | 208 | } |
| 193 | 209 | if self.overlays.contains_key(&front) { |
| 194 | 210 | self.recreate(front); |
| 195 | 211 | } |
| 212 | + true |
| 196 | 213 | } |
| 197 | 214 | } |
| 198 | 215 | |
| 199 | | -/// Get the front (focused) window ID using a fresh connection |
| 200 | | -/// (space queries poison the main cid). |
| 201 | | -fn get_front_window(_main_cid: CGSConnectionID) -> u32 { |
| 216 | +/// Get the front (focused) window ID using CGWindowListCopyWindowInfo. |
| 217 | +/// Avoids all SLS display/space queries which poison SLSNewWindow globally. |
| 218 | +fn get_front_window(own_pid: i32) -> u32 { |
| 202 | 219 | unsafe { |
| 203 | | - let mut cid: CGSConnectionID = 0; |
| 204 | | - SLSNewConnection(0, &mut cid); |
| 205 | | - if cid == 0 { return 0; } |
| 206 | | - let mut psn = ProcessSerialNumber { high: 0, low: 0 }; |
| 207 | | - _SLPSGetFrontProcess(&mut psn); |
| 208 | | - let mut target_cid: CGSConnectionID = 0; |
| 209 | | - SLSGetConnectionIDForPSN(cid, &mut psn, &mut target_cid); |
| 210 | | - |
| 211 | | - // Get active space |
| 212 | | - let uuid = SLSCopyActiveMenuBarDisplayIdentifier(cid); |
| 213 | | - if uuid.is_null() { return 0; } |
| 214 | | - let active_sid = SLSManagedDisplayGetCurrentSpace(cid, uuid); |
| 215 | | - CFRelease(uuid as CFTypeRef); |
| 216 | | - if active_sid == 0 { return 0; } |
| 217 | | - |
| 218 | | - let set_tags: u64 = 1; |
| 219 | | - let clear_tags: u64 = 0; |
| 220 | | - let space_list = cfarray_of_cfnumbers( |
| 221 | | - &active_sid as *const _ as *const _, |
| 222 | | - std::mem::size_of::<u64>(), |
| 223 | | - 1, |
| 224 | | - kCFNumberSInt64Type, |
| 225 | | - ); |
| 226 | | - |
| 227 | | - let window_list = SLSCopyWindowsWithOptionsAndTags( |
| 228 | | - cid, target_cid as u32, space_list, 0x2, &set_tags, &clear_tags, |
| 229 | | - ); |
| 220 | + let list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID); |
| 221 | + if list.is_null() { return 0; } |
| 230 | 222 | |
| 231 | | - let mut wid: u32 = 0; |
| 232 | | - if !window_list.is_null() { |
| 233 | | - let count = CFArrayGetCount(window_list); |
| 234 | | - if count > 0 { |
| 235 | | - // First window in the list is the frontmost |
| 236 | | - let mut v: CFTypeRef = ptr::null(); |
| 237 | | - let first = CFArrayGetValueAtIndex(window_list, 0); |
| 238 | | - if !first.is_null() { |
| 239 | | - CFNumberGetValue(first, kCFNumberSInt32Type, &mut wid as *mut _ as *mut _); |
| 240 | | - } |
| 223 | + let count = CFArrayGetCount(list); |
| 224 | + let wid_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowNumber\0".as_ptr(), kCFStringEncodingUTF8); |
| 225 | + let pid_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowOwnerPID\0".as_ptr(), kCFStringEncodingUTF8); |
| 226 | + let layer_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowLayer\0".as_ptr(), kCFStringEncodingUTF8); |
| 227 | + |
| 228 | + // CGWindowListCopyWindowInfo returns windows in front-to-back order. |
| 229 | + // First layer-0 window not owned by us is the focused window. |
| 230 | + let mut front_wid: u32 = 0; |
| 231 | + for i in 0..count { |
| 232 | + let dict = CFArrayGetValueAtIndex(list, i); |
| 233 | + if dict.is_null() { continue; } |
| 234 | + |
| 235 | + let mut v: CFTypeRef = ptr::null(); |
| 236 | + |
| 237 | + let mut layer: i32 = -1; |
| 238 | + if CFDictionaryGetValueIfPresent(dict, layer_key as CFTypeRef, &mut v) { |
| 239 | + CFNumberGetValue(v, kCFNumberSInt32Type, &mut layer as *mut _ as *mut _); |
| 240 | + } |
| 241 | + if layer != 0 { continue; } |
| 242 | + |
| 243 | + let mut pid: i32 = 0; |
| 244 | + if CFDictionaryGetValueIfPresent(dict, pid_key as CFTypeRef, &mut v) { |
| 245 | + CFNumberGetValue(v, kCFNumberSInt32Type, &mut pid as *mut _ as *mut _); |
| 246 | + } |
| 247 | + if pid == own_pid { continue; } |
| 248 | + |
| 249 | + let mut wid: u32 = 0; |
| 250 | + if CFDictionaryGetValueIfPresent(dict, wid_key as CFTypeRef, &mut v) { |
| 251 | + CFNumberGetValue(v, kCFNumberSInt32Type, &mut wid as *mut _ as *mut _); |
| 252 | + } |
| 253 | + if wid != 0 { |
| 254 | + front_wid = wid; |
| 255 | + break; |
| 241 | 256 | } |
| 242 | | - CFRelease(window_list); |
| 243 | 257 | } |
| 244 | | - CFRelease(space_list); |
| 245 | | - SLSReleaseConnection(cid); |
| 246 | | - wid |
| 258 | + |
| 259 | + CFRelease(wid_key as CFTypeRef); |
| 260 | + CFRelease(pid_key as CFTypeRef); |
| 261 | + CFRelease(layer_key as CFTypeRef); |
| 262 | + CFRelease(list); |
| 263 | + front_wid |
| 247 | 264 | } |
| 248 | 265 | } |
| 249 | 266 | |
@@ -290,7 +307,9 @@ fn main() { |
| 290 | 307 | |
| 291 | 308 | borders.subscribe_all(); |
| 292 | 309 | |
| 293 | | - borders.update_focus(); |
| 310 | + if borders.update_focus() { |
| 311 | + borders.subscribe_all(); |
| 312 | + } |
| 294 | 313 | |
| 295 | 314 | eprintln!("{} overlays tracked", borders.overlays.len()); |
| 296 | 315 | |
@@ -414,8 +433,10 @@ fn main() { |
| 414 | 433 | } |
| 415 | 434 | } |
| 416 | 435 | |
| 417 | | - // Update focus (detects front window, recolors if changed) |
| 418 | | - borders.update_focus(); |
| 436 | + // Update focus (detects front window, recreates borders if changed) |
| 437 | + if borders.update_focus() { |
| 438 | + needs_resubscribe = true; |
| 439 | + } |
| 419 | 440 | |
| 420 | 441 | // Re-subscribe ALL tracked windows (SLSRequestNotificationsForWindows replaces, not appends) |
| 421 | 442 | if needs_resubscribe || !destroyed.is_empty() { |
@@ -507,10 +528,13 @@ fn create_overlay( |
| 507 | 528 | ) -> Option<(CGSConnectionID, u32)> { |
| 508 | 529 | unsafe { |
| 509 | 530 | let mut bounds = CGRect::default(); |
| 510 | | - if SLSGetWindowBounds(cid, target_wid, &mut bounds) != kCGErrorSuccess { |
| 531 | + let rc = SLSGetWindowBounds(cid, target_wid, &mut bounds); |
| 532 | + if rc != kCGErrorSuccess { |
| 533 | + eprintln!("[create_overlay] SLSGetWindowBounds failed for wid={target_wid} rc={rc}"); |
| 511 | 534 | return None; |
| 512 | 535 | } |
| 513 | 536 | if bounds.size.width < 10.0 || bounds.size.height < 10.0 { |
| 537 | + eprintln!("[create_overlay] wid={target_wid} too small: {}x{}", bounds.size.width, bounds.size.height); |
| 514 | 538 | return None; |
| 515 | 539 | } |
| 516 | 540 | |
@@ -523,12 +547,21 @@ fn create_overlay( |
| 523 | 547 | let frame = CGRect::new(0.0, 0.0, ow, oh); |
| 524 | 548 | let mut region: CFTypeRef = ptr::null(); |
| 525 | 549 | CGSNewRegionWithRect(&frame, &mut region); |
| 526 | | - if region.is_null() { return None; } |
| 550 | + if region.is_null() { |
| 551 | + eprintln!("[create_overlay] CGSNewRegionWithRect failed for wid={target_wid}"); |
| 552 | + return None; |
| 553 | + } |
| 527 | 554 | |
| 528 | 555 | let mut wid: u32 = 0; |
| 529 | 556 | SLSNewWindow(cid, 2, ox as f32, oy as f32, region, &mut wid); |
| 530 | 557 | CFRelease(region); |
| 531 | | - if wid == 0 { return None; } |
| 558 | + if wid == 0 { |
| 559 | + eprintln!("[create_overlay] SLSNewWindow returned 0 for target={target_wid} cid={cid}"); |
| 560 | + return None; |
| 561 | + } |
| 562 | + |
| 563 | + eprintln!("[create_overlay] created overlay wid={wid} for target={target_wid} color=({:.2},{:.2},{:.2},{:.2})", |
| 564 | + color.0, color.1, color.2, color.3); |
| 532 | 565 | |
| 533 | 566 | SLSSetWindowResolution(cid, wid, 2.0); |
| 534 | 567 | SLSSetWindowOpacity(cid, wid, false); |
@@ -538,6 +571,7 @@ fn create_overlay( |
| 538 | 571 | // Draw border (point coordinates) |
| 539 | 572 | let ctx = SLWindowContextCreate(cid, wid, ptr::null()); |
| 540 | 573 | if ctx.is_null() { |
| 574 | + eprintln!("[create_overlay] SLWindowContextCreate returned null for overlay wid={wid}"); |
| 541 | 575 | SLSReleaseWindow(cid, wid); |
| 542 | 576 | return None; |
| 543 | 577 | } |