@@ -16,7 +16,6 @@ use tracing::debug; |
| 16 | 16 | static SIGNAL_STOP_REQUESTED: AtomicBool = AtomicBool::new(false); |
| 17 | 17 | const MIN_TRACKED_WINDOW_SIZE: f64 = 4.0; |
| 18 | 18 | const GEOMETRY_EPSILON: f64 = 0.5; |
| 19 | | -const SCALE_EPSILON: f64 = 0.01; |
| 20 | 19 | const WINDOW_ATTRIBUTE_REAL: u64 = 1 << 1; |
| 21 | 20 | const WINDOW_TAG_DOCUMENT: u64 = 1 << 0; |
| 22 | 21 | const WINDOW_TAG_FLOATING: u64 = 1 << 1; |
@@ -189,59 +188,6 @@ unsafe extern "C" fn handle_sigint(_: libc::c_int) { |
| 189 | 188 | SIGNAL_STOP_REQUESTED.store(true, Ordering::Relaxed); |
| 190 | 189 | } |
| 191 | 190 | |
| 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 | 191 | /// Tracks overlays for target windows. |
| 246 | 192 | struct BorderMap { |
| 247 | 193 | overlays: HashMap<u32, Overlay>, |
@@ -379,11 +325,6 @@ impl BorderMap { |
| 379 | 325 | } |
| 380 | 326 | } |
| 381 | 327 | |
| 382 | | - fn remove_all(&mut self) { |
| 383 | | - // OverlayWindow's Drop closes the NSWindow. |
| 384 | | - self.overlays.clear(); |
| 385 | | - } |
| 386 | | - |
| 387 | 328 | fn remove(&mut self, target_wid: u32) { |
| 388 | 329 | if let Some(overlay) = self.overlays.remove(&target_wid) { |
| 389 | 330 | debug!( |
@@ -518,13 +459,6 @@ impl BorderMap { |
| 518 | 459 | } |
| 519 | 460 | } |
| 520 | 461 | |
| 521 | | - /// With NSWindow.setFrame_display we no longer need a destroy-and- |
| 522 | | - /// recreate path on resize. Kept as a thin alias so existing call |
| 523 | | - /// sites keep working. |
| 524 | | - fn recreate(&mut self, target_wid: u32) { |
| 525 | | - self.sync_overlay(target_wid); |
| 526 | | - } |
| 527 | | - |
| 528 | 462 | fn hide(&self, target_wid: u32) { |
| 529 | 463 | if let Some(o) = self.overlays.get(&target_wid) { |
| 530 | 464 | debug!("[hide] target={} overlay_wid={}", target_wid, o.wid()); |
@@ -578,9 +512,9 @@ impl BorderMap { |
| 578 | 512 | // to pass the add_fresh filter — retry on every poll until |
| 579 | 513 | // it sticks. |
| 580 | 514 | if !self.overlays.contains_key(&front) { |
| 581 | | - debug!("[focus-retry] front={} still untracked, retrying add_fresh", front); |
| 582 | 515 | self.add_fresh(front); |
| 583 | 516 | if self.overlays.contains_key(&front) { |
| 517 | + debug!("[focus-retry] front={} now tracked", front); |
| 584 | 518 | self.subscribe_target(front); |
| 585 | 519 | if self.active_only { |
| 586 | 520 | self.unhide(front); |
@@ -819,30 +753,12 @@ fn print_help() { |
| 819 | 753 | } |
| 820 | 754 | |
| 821 | 755 | fn main() { |
| 822 | | - // On the screenshot-exclusion research branch, default to file |
| 823 | | - // logging at debug level so we can diagnose the NSWindow refactor |
| 824 | | - // even when ers is spawned by tarmac (which inherits ers's stderr |
| 825 | | - // to wherever tarmac was launched, often invisibly). |
| 826 | | - let log_path = std::path::PathBuf::from("/tmp/ers-debug.log"); |
| 827 | | - let log_file = std::fs::OpenOptions::new() |
| 828 | | - .create(true) |
| 829 | | - .append(true) |
| 830 | | - .open(&log_path) |
| 831 | | - .ok(); |
| 832 | 756 | let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() |
| 833 | | - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("ers=debug")); |
| 834 | | - if let Some(file) = log_file { |
| 835 | | - tracing_subscriber::fmt() |
| 836 | | - .with_env_filter(env_filter) |
| 837 | | - .with_writer(std::sync::Mutex::new(file)) |
| 838 | | - .with_ansi(false) |
| 839 | | - .init(); |
| 840 | | - } else { |
| 757 | + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("ers=info")); |
| 841 | 758 | tracing_subscriber::fmt() |
| 842 | 759 | .with_env_filter(env_filter) |
| 843 | 760 | .with_writer(std::io::stderr) |
| 844 | 761 | .init(); |
| 845 | | - } |
| 846 | 762 | debug!("[main] ers starting, pid={}", std::process::id()); |
| 847 | 763 | |
| 848 | 764 | let args: Vec<String> = std::env::args().collect(); |
@@ -1304,68 +1220,6 @@ unsafe extern "C" fn drain_events( |
| 1304 | 1220 | } |
| 1305 | 1221 | } |
| 1306 | 1222 | |
| 1307 | | -/// Look up an overlay window in CGWindowListCopyWindowInfo and dump the |
| 1308 | | -/// keys that the screenshot picker / ScreenCaptureKit care about. Lets |
| 1309 | | -/// us tell whether SLSSetWindowSharingState(0) propagates through to |
| 1310 | | -/// the CG window list (the layer SCWindow filters on) or stops at SLS. |
| 1311 | | -fn probe_cg_window_info(target_wid: u32) { |
| 1312 | | - unsafe { |
| 1313 | | - let list = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID); |
| 1314 | | - if list.is_null() { |
| 1315 | | - debug!("[probe_cg_window_info] wid={target_wid} list is null"); |
| 1316 | | - return; |
| 1317 | | - } |
| 1318 | | - let count = CFArrayGetCount(list); |
| 1319 | | - let wid_key = cf_string_from_static(c"kCGWindowNumber"); |
| 1320 | | - let sharing_key = cf_string_from_static(c"kCGWindowSharingState"); |
| 1321 | | - let layer_key = cf_string_from_static(c"kCGWindowLayer"); |
| 1322 | | - let alpha_key = cf_string_from_static(c"kCGWindowAlpha"); |
| 1323 | | - let on_screen_key = cf_string_from_static(c"kCGWindowIsOnscreen"); |
| 1324 | | - let mut found = false; |
| 1325 | | - |
| 1326 | | - for i in 0..count { |
| 1327 | | - let dict = CFArrayGetValueAtIndex(list, i); |
| 1328 | | - if dict.is_null() { |
| 1329 | | - continue; |
| 1330 | | - } |
| 1331 | | - let mut v: CFTypeRef = ptr::null(); |
| 1332 | | - let mut wid: u32 = 0; |
| 1333 | | - if CFDictionaryGetValueIfPresent(dict, wid_key as CFTypeRef, &mut v) { |
| 1334 | | - CFNumberGetValue(v, kCFNumberSInt32Type, &mut wid as *mut _ as *mut _); |
| 1335 | | - } |
| 1336 | | - if wid != target_wid { |
| 1337 | | - continue; |
| 1338 | | - } |
| 1339 | | - |
| 1340 | | - let mut sharing: i32 = -1; |
| 1341 | | - if CFDictionaryGetValueIfPresent(dict, sharing_key as CFTypeRef, &mut v) { |
| 1342 | | - CFNumberGetValue(v, kCFNumberSInt32Type, &mut sharing as *mut _ as *mut _); |
| 1343 | | - } |
| 1344 | | - let mut layer: i32 = i32::MIN; |
| 1345 | | - if CFDictionaryGetValueIfPresent(dict, layer_key as CFTypeRef, &mut v) { |
| 1346 | | - CFNumberGetValue(v, kCFNumberSInt32Type, &mut layer as *mut _ as *mut _); |
| 1347 | | - } |
| 1348 | | - let mut alpha: f64 = -1.0; |
| 1349 | | - if CFDictionaryGetValueIfPresent(dict, alpha_key as CFTypeRef, &mut v) { |
| 1350 | | - CFNumberGetValue(v, 13 /* kCFNumberDoubleType */, &mut alpha as *mut _ as *mut _); |
| 1351 | | - } |
| 1352 | | - let on_screen_present = |
| 1353 | | - CFDictionaryGetValueIfPresent(dict, on_screen_key as CFTypeRef, &mut v); |
| 1354 | | - |
| 1355 | | - debug!( |
| 1356 | | - "[probe_cg_window_info] wid={target_wid} cg_sharing={sharing} layer={layer} alpha={alpha:.3} on_screen_present={on_screen_present}" |
| 1357 | | - ); |
| 1358 | | - found = true; |
| 1359 | | - break; |
| 1360 | | - } |
| 1361 | | - |
| 1362 | | - if !found { |
| 1363 | | - debug!("[probe_cg_window_info] wid={target_wid} NOT FOUND in CGWindowList"); |
| 1364 | | - } |
| 1365 | | - CFRelease(list as CFTypeRef); |
| 1366 | | - } |
| 1367 | | -} |
| 1368 | | - |
| 1369 | 1223 | fn discover_windows(cid: CGSConnectionID, own_pid: i32) -> Vec<u32> { |
| 1370 | 1224 | unsafe { |
| 1371 | 1225 | let list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID); |
@@ -1425,181 +1279,6 @@ fn discover_windows(cid: CGSConnectionID, own_pid: i32) -> Vec<u32> { |
| 1425 | 1279 | } |
| 1426 | 1280 | } |
| 1427 | 1281 | |
| 1428 | | -/// Draw a border ring into an existing CGContext, clearing first. |
| 1429 | | -fn draw_border( |
| 1430 | | - ctx: CGContextRef, |
| 1431 | | - width: f64, |
| 1432 | | - height: f64, |
| 1433 | | - border_width: f64, |
| 1434 | | - radius: f64, |
| 1435 | | - color: (f64, f64, f64, f64), |
| 1436 | | -) { |
| 1437 | | - unsafe { |
| 1438 | | - let full = CGRect::new(0.0, 0.0, width, height); |
| 1439 | | - CGContextClearRect(ctx, full); |
| 1440 | | - |
| 1441 | | - let bw = border_width; |
| 1442 | | - let stroke_rect = CGRect::new(bw / 2.0, bw / 2.0, width - bw, height - bw); |
| 1443 | | - let max_r = (stroke_rect.size.width.min(stroke_rect.size.height) / 2.0).max(0.0); |
| 1444 | | - let r = radius.min(max_r); |
| 1445 | | - |
| 1446 | | - CGContextSetRGBStrokeColor(ctx, color.0, color.1, color.2, color.3); |
| 1447 | | - CGContextSetLineWidth(ctx, bw); |
| 1448 | | - let path = CGPathCreateWithRoundedRect(stroke_rect, r, r, ptr::null()); |
| 1449 | | - if !path.is_null() { |
| 1450 | | - CGContextAddPath(ctx, path); |
| 1451 | | - CGContextStrokePath(ctx); |
| 1452 | | - CGPathRelease(path); |
| 1453 | | - } |
| 1454 | | - CGContextFlush(ctx); |
| 1455 | | - } |
| 1456 | | -} |
| 1457 | | - |
| 1458 | | -fn create_overlay( |
| 1459 | | - cid: CGSConnectionID, |
| 1460 | | - target_wid: u32, |
| 1461 | | - border_width: f64, |
| 1462 | | - radius: f64, |
| 1463 | | - color: (f64, f64, f64, f64), |
| 1464 | | -) -> Option<(CGSConnectionID, u32, CGRect, f64)> { |
| 1465 | | - unsafe { |
| 1466 | | - let mut bounds = CGRect::default(); |
| 1467 | | - let rc = SLSGetWindowBounds(cid, target_wid, &mut bounds); |
| 1468 | | - if rc != kCGErrorSuccess { |
| 1469 | | - debug!("[create_overlay] SLSGetWindowBounds failed for wid={target_wid} rc={rc}"); |
| 1470 | | - return None; |
| 1471 | | - } |
| 1472 | | - if !is_trackable_window(bounds, border_width) { |
| 1473 | | - debug!( |
| 1474 | | - "[create_overlay] wid={target_wid} too small: {}x{}", |
| 1475 | | - bounds.size.width, bounds.size.height |
| 1476 | | - ); |
| 1477 | | - return None; |
| 1478 | | - } |
| 1479 | | - |
| 1480 | | - let bw = border_width; |
| 1481 | | - let ow = bounds.size.width + 2.0 * bw; |
| 1482 | | - let oh = bounds.size.height + 2.0 * bw; |
| 1483 | | - let ox = bounds.origin.x - bw; |
| 1484 | | - let oy = bounds.origin.y - bw; |
| 1485 | | - let scale = display_scale_for_bounds(bounds); |
| 1486 | | - |
| 1487 | | - let frame = CGRect::new(0.0, 0.0, ow, oh); |
| 1488 | | - let mut region: CFTypeRef = ptr::null(); |
| 1489 | | - CGSNewRegionWithRect(&frame, &mut region); |
| 1490 | | - if region.is_null() { |
| 1491 | | - debug!("[create_overlay] CGSNewRegionWithRect failed for wid={target_wid}"); |
| 1492 | | - return None; |
| 1493 | | - } |
| 1494 | | - |
| 1495 | | - // Empty hit-test shape: an SLS window with an empty opaque_shape |
| 1496 | | - // is click-through at the compositor level (no input region). |
| 1497 | | - let empty = CGRect::new(0.0, 0.0, 0.0, 0.0); |
| 1498 | | - let mut empty_region: CFTypeRef = ptr::null(); |
| 1499 | | - if CGSNewRegionWithRect(&empty, &mut empty_region) != kCGErrorSuccess |
| 1500 | | - || empty_region.is_null() |
| 1501 | | - { |
| 1502 | | - debug!("[create_overlay] CGSNewRegionWithRect (empty) failed for wid={target_wid}"); |
| 1503 | | - CFRelease(region); |
| 1504 | | - return None; |
| 1505 | | - } |
| 1506 | | - |
| 1507 | | - // Create the overlay via SLSNewWindowWithOpaqueShapeAndContext |
| 1508 | | - // and bake tag bit 1 (click-through) and tag bit 9 (screenshot |
| 1509 | | - // exclusion) into the window at birth. Tahoe classifies windows |
| 1510 | | - // for capture/picker based on tags observed at creation time; |
| 1511 | | - // post-creation tag mutation lands too late and the picker keeps |
| 1512 | | - // including the overlay. Mirrors the JankyBorders unmanaged |
| 1513 | | - // create path (.refs/JankyBorders/src/misc/window.h:239). |
| 1514 | | - // options 13|(1<<18): documentation-window | ignores-cycle. |
| 1515 | | - let mut tags: u64 = (1u64 << 1) | (1u64 << 9); |
| 1516 | | - let mut wid: u32 = 0; |
| 1517 | | - SLSNewWindowWithOpaqueShapeAndContext( |
| 1518 | | - cid, |
| 1519 | | - 2, |
| 1520 | | - region, |
| 1521 | | - empty_region, |
| 1522 | | - 13 | (1 << 18), |
| 1523 | | - &mut tags as *mut u64, |
| 1524 | | - ox as f32, |
| 1525 | | - oy as f32, |
| 1526 | | - 64, |
| 1527 | | - &mut wid, |
| 1528 | | - ptr::null_mut(), |
| 1529 | | - ); |
| 1530 | | - CFRelease(region); |
| 1531 | | - CFRelease(empty_region); |
| 1532 | | - if wid == 0 { |
| 1533 | | - debug!( |
| 1534 | | - "[create_overlay] SLSNewWindowWithOpaqueShapeAndContext returned 0 for target={target_wid} cid={cid}" |
| 1535 | | - ); |
| 1536 | | - return None; |
| 1537 | | - } |
| 1538 | | - |
| 1539 | | - debug!( |
| 1540 | | - "[create_overlay] created overlay wid={wid} for target={target_wid} scale={scale:.2} color=({:.2},{:.2},{:.2},{:.2})", |
| 1541 | | - color.0, color.1, color.2, color.3 |
| 1542 | | - ); |
| 1543 | | - |
| 1544 | | - if let Some(metadata) = query_window_metadata(cid, wid) { |
| 1545 | | - debug!( |
| 1546 | | - "[create_overlay] post-create overlay wid={wid} tags={:#x} attributes={:#x} parent={}", |
| 1547 | | - metadata.tags, metadata.attributes, metadata.parent_wid |
| 1548 | | - ); |
| 1549 | | - } else { |
| 1550 | | - debug!("[create_overlay] post-create wid={wid} metadata query failed"); |
| 1551 | | - } |
| 1552 | | - |
| 1553 | | - SLSSetWindowSharingState(cid, wid, 0); |
| 1554 | | - let mut sharing_state: u32 = u32::MAX; |
| 1555 | | - let rc = SLSGetWindowSharingState(cid, wid, &mut sharing_state); |
| 1556 | | - debug!("[create_overlay] sharing_state wid={wid} get_rc={rc} sls_state={sharing_state}"); |
| 1557 | | - |
| 1558 | | - // Probe what CGWindowListCopyWindowInfo (which the screenshot |
| 1559 | | - // picker / SCWindow use) reports for our overlay. If |
| 1560 | | - // kCGWindowSharingState comes back != 0 here, then SLS-side |
| 1561 | | - // sharing state is not propagated to the CG window list and |
| 1562 | | - // we'll need a different exclusion mechanism. |
| 1563 | | - probe_cg_window_info(wid); |
| 1564 | | - |
| 1565 | | - SLSSetWindowResolution(cid, wid, scale); |
| 1566 | | - SLSSetWindowOpacity(cid, wid, false); |
| 1567 | | - SLSSetWindowLevel(cid, wid, 0); |
| 1568 | | - SLSOrderWindow(cid, wid, 1, target_wid); |
| 1569 | | - |
| 1570 | | - // Draw border (point coordinates) |
| 1571 | | - let ctx = SLWindowContextCreate(cid, wid, ptr::null()); |
| 1572 | | - if ctx.is_null() { |
| 1573 | | - debug!("[create_overlay] SLWindowContextCreate returned null for overlay wid={wid}"); |
| 1574 | | - SLSReleaseWindow(cid, wid); |
| 1575 | | - return None; |
| 1576 | | - } |
| 1577 | | - |
| 1578 | | - draw_border(ctx, ow, oh, bw, radius, color); |
| 1579 | | - SLSFlushWindowContentRegion(cid, wid, ptr::null()); |
| 1580 | | - CGContextRelease(ctx); |
| 1581 | | - |
| 1582 | | - // Post-creation tag mutation matching JankyBorders' pattern |
| 1583 | | - // at .refs/JankyBorders/src/misc/window.h:266-267. Verified |
| 1584 | | - // ineffective on Tahoe: tags set on windows owned by a |
| 1585 | | - // SLSNewConnection-created cid do NOT propagate to the global |
| 1586 | | - // server-side tag store, regardless of which cid issues the |
| 1587 | | - // SLSSetWindowTags call (tested both fresh and main cid). |
| 1588 | | - // The screencaptureui picker queries via _CGSGetWindowTags |
| 1589 | | - // from its own connection (otool confirmed) and reads 0x0 for |
| 1590 | | - // our overlays. Kept here aligned with JB so the diff is |
| 1591 | | - // legible; the actual fix requires creating overlays on the |
| 1592 | | - // process main cid (conflicts with the per-border fresh-cid |
| 1593 | | - // requirement in ers/CLAUDE.md) or backing them with NSWindow. |
| 1594 | | - let mut set_tags: u64 = (1u64 << 1) | (1u64 << 9); |
| 1595 | | - let mut clear_tags: u64 = 0; |
| 1596 | | - SLSSetWindowTags(cid, wid, &mut set_tags as *mut u64, 64); |
| 1597 | | - SLSClearWindowTags(cid, wid, &mut clear_tags as *mut u64, 64); |
| 1598 | | - |
| 1599 | | - Some((cid, wid, bounds, scale)) |
| 1600 | | - } |
| 1601 | | -} |
| 1602 | | - |
| 1603 | 1282 | fn list_windows() { |
| 1604 | 1283 | let cid = unsafe { SLSMainConnectionID() }; |
| 1605 | 1284 | unsafe { |