@@ -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 | fn add_fresh(&mut self, target_wid: u32) { | 61 | fn add_fresh(&mut self, target_wid: u32) { |
| 61 | if self.overlays.contains_key(&target_wid) { return; } | 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 | let color = self.color_for(target_wid); | 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 | self.overlays.insert(target_wid, Overlay { cid, wid }); | 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. | 154 | + /// Redraw an existing overlay with a new color (no destroy/recreate). |
| 180 | - fn update_focus(&mut self) { | 155 | + fn redraw(&self, target_wid: u32) { |
| 181 | - let front = get_front_window(self.main_cid); | 156 | + if let Some(overlay) = self.overlays.get(&target_wid) { |
| 182 | - if front == 0 || front == self.focused_wid { return; } | 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 | let old = self.focused_wid; | 200 | let old = self.focused_wid; |
| 185 | self.focused_wid = front; | 201 | self.focused_wid = front; |
| 186 | - let tracked = self.overlays.contains_key(&front); | 202 | + eprintln!("[focus] {} -> {} (tracked={})", old, front, self.overlays.contains_key(&front)); |
| 187 | - eprintln!("[focus] {old} -> {front} (tracked={tracked})"); | | |
| 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 | if self.overlays.contains_key(&old) { | 206 | if self.overlays.contains_key(&old) { |
| 191 | self.recreate(old); | 207 | self.recreate(old); |
| 192 | } | 208 | } |
| 193 | if self.overlays.contains_key(&front) { | 209 | if self.overlays.contains_key(&front) { |
| 194 | self.recreate(front); | 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 | 216 | +/// Get the front (focused) window ID using CGWindowListCopyWindowInfo. |
| 200 | -/// (space queries poison the main cid). | 217 | +/// Avoids all SLS display/space queries which poison SLSNewWindow globally. |
| 201 | -fn get_front_window(_main_cid: CGSConnectionID) -> u32 { | 218 | +fn get_front_window(own_pid: i32) -> u32 { |
| 202 | unsafe { | 219 | unsafe { |
| 203 | - let mut cid: CGSConnectionID = 0; | 220 | + let list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID); |
| 204 | - SLSNewConnection(0, &mut cid); | 221 | + if list.is_null() { return 0; } |
| 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 | - ); | | |
| 230 | | 222 | |
| 231 | - let mut wid: u32 = 0; | 223 | + let count = CFArrayGetCount(list); |
| 232 | - if !window_list.is_null() { | 224 | + let wid_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowNumber\0".as_ptr(), kCFStringEncodingUTF8); |
| 233 | - let count = CFArrayGetCount(window_list); | 225 | + let pid_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowOwnerPID\0".as_ptr(), kCFStringEncodingUTF8); |
| 234 | - if count > 0 { | 226 | + let layer_key = CFStringCreateWithCString(ptr::null(), b"kCGWindowLayer\0".as_ptr(), kCFStringEncodingUTF8); |
| 235 | - // First window in the list is the frontmost | 227 | + |
| 236 | - let mut v: CFTypeRef = ptr::null(); | 228 | + // CGWindowListCopyWindowInfo returns windows in front-to-back order. |
| 237 | - let first = CFArrayGetValueAtIndex(window_list, 0); | 229 | + // First layer-0 window not owned by us is the focused window. |
| 238 | - if !first.is_null() { | 230 | + let mut front_wid: u32 = 0; |
| 239 | - CFNumberGetValue(first, kCFNumberSInt32Type, &mut wid as *mut _ as *mut _); | 231 | + for i in 0..count { |
| 240 | - } | 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); | 258 | + |
| 245 | - SLSReleaseConnection(cid); | 259 | + CFRelease(wid_key as CFTypeRef); |
| 246 | - wid | 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 | borders.subscribe_all(); | 308 | borders.subscribe_all(); |
| 292 | | 309 | |
| 293 | - borders.update_focus(); | 310 | + if borders.update_focus() { |
| | 311 | + borders.subscribe_all(); |
| | 312 | + } |
| 294 | | 313 | |
| 295 | eprintln!("{} overlays tracked", borders.overlays.len()); | 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) | 436 | + // Update focus (detects front window, recreates borders if changed) |
| 418 | - borders.update_focus(); | 437 | + if borders.update_focus() { |
| | 438 | + needs_resubscribe = true; |
| | 439 | + } |
| 419 | | 440 | |
| 420 | // Re-subscribe ALL tracked windows (SLSRequestNotificationsForWindows replaces, not appends) | 441 | // Re-subscribe ALL tracked windows (SLSRequestNotificationsForWindows replaces, not appends) |
| 421 | if needs_resubscribe || !destroyed.is_empty() { | 442 | if needs_resubscribe || !destroyed.is_empty() { |
@@ -507,10 +528,13 @@ fn create_overlay( |
| 507 | ) -> Option<(CGSConnectionID, u32)> { | 528 | ) -> Option<(CGSConnectionID, u32)> { |
| 508 | unsafe { | 529 | unsafe { |
| 509 | let mut bounds = CGRect::default(); | 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 | return None; | 534 | return None; |
| 512 | } | 535 | } |
| 513 | if bounds.size.width < 10.0 || bounds.size.height < 10.0 { | 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 | return None; | 538 | return None; |
| 515 | } | 539 | } |
| 516 | | 540 | |
@@ -523,12 +547,21 @@ fn create_overlay( |
| 523 | let frame = CGRect::new(0.0, 0.0, ow, oh); | 547 | let frame = CGRect::new(0.0, 0.0, ow, oh); |
| 524 | let mut region: CFTypeRef = ptr::null(); | 548 | let mut region: CFTypeRef = ptr::null(); |
| 525 | CGSNewRegionWithRect(&frame, &mut region); | 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 | let mut wid: u32 = 0; | 555 | let mut wid: u32 = 0; |
| 529 | SLSNewWindow(cid, 2, ox as f32, oy as f32, region, &mut wid); | 556 | SLSNewWindow(cid, 2, ox as f32, oy as f32, region, &mut wid); |
| 530 | CFRelease(region); | 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 | SLSSetWindowResolution(cid, wid, 2.0); | 566 | SLSSetWindowResolution(cid, wid, 2.0); |
| 534 | SLSSetWindowOpacity(cid, wid, false); | 567 | SLSSetWindowOpacity(cid, wid, false); |
@@ -538,6 +571,7 @@ fn create_overlay( |
| 538 | // Draw border (point coordinates) | 571 | // Draw border (point coordinates) |
| 539 | let ctx = SLWindowContextCreate(cid, wid, ptr::null()); | 572 | let ctx = SLWindowContextCreate(cid, wid, ptr::null()); |
| 540 | if ctx.is_null() { | 573 | if ctx.is_null() { |
| | 574 | + eprintln!("[create_overlay] SLWindowContextCreate returned null for overlay wid={wid}"); |
| 541 | SLSReleaseWindow(cid, wid); | 575 | SLSReleaseWindow(cid, wid); |
| 542 | return None; | 576 | return None; |
| 543 | } | 577 | } |