gardesk/ers / d03506f

Browse files

Research: SLS-side screenshot exclusion attempts on Tahoe

Empirically disproved approaches for excluding ers overlays from
Tahoe's cmd+shift+4 + space picker. All of these apply *correctly*
to the overlay window — they just don't influence the picker.

Verified via post-create probes:
- SLSNewWindowWithOpaqueShapeAndContext baking tag bits (1<<1)|(1<<9)
at creation succeeds; readback shows tags=0x202.
- SLSSetWindowSharingState(0) succeeds; SLSGetWindowSharingState
reads back state=0.
- CGWindowListCopyWindowInfo for the overlay reports
kCGWindowSharingState=0 (the SLS-side state propagates through to
the CG window list, which SCWindow filters on).
- kCGWindowIsOnscreen is absent from the dictionary entry (CG
doesn&#39;t know the on-screen state of an SLS-only window without
an NSWindow wrapper).

Despite all of the above, the screenshot picker still hit-tests onto
the overlay and captures the border. This means Tahoe&#39;s picker uses
a query path that bypasses standard CGWindowSharingState and tag-bit
filtering for SLS-only windows.

Approaches still untested but likely dead-ends without docs:
SLSSetWindowType, SLSSetWindowFiltering, SLSSetWindowSubLevel.

Probe helper probe_cg_window_info added for ongoing investigation.
Branch is for research; main remains the working EventShape build.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
d03506f5706c45638490372395efa3ba0465e070
Parents
7c6e94f
Tree
b9dcdab

2 changed files

StatusFile+-
M src/main.rs 121 20
M src/skylight.rs 29 0
src/main.rsmodified
@@ -1087,6 +1087,68 @@ unsafe extern "C" fn drain_events(
10871087
     }
10881088
 }
10891089
 
1090
+/// Look up an overlay window in CGWindowListCopyWindowInfo and dump the
1091
+/// keys that the screenshot picker / ScreenCaptureKit care about. Lets
1092
+/// us tell whether SLSSetWindowSharingState(0) propagates through to
1093
+/// the CG window list (the layer SCWindow filters on) or stops at SLS.
1094
+fn probe_cg_window_info(target_wid: u32) {
1095
+    unsafe {
1096
+        let list = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
1097
+        if list.is_null() {
1098
+            debug!("[probe_cg_window_info] wid={target_wid} list is null");
1099
+            return;
1100
+        }
1101
+        let count = CFArrayGetCount(list);
1102
+        let wid_key = cf_string_from_static(c"kCGWindowNumber");
1103
+        let sharing_key = cf_string_from_static(c"kCGWindowSharingState");
1104
+        let layer_key = cf_string_from_static(c"kCGWindowLayer");
1105
+        let alpha_key = cf_string_from_static(c"kCGWindowAlpha");
1106
+        let on_screen_key = cf_string_from_static(c"kCGWindowIsOnscreen");
1107
+        let mut found = false;
1108
+
1109
+        for i in 0..count {
1110
+            let dict = CFArrayGetValueAtIndex(list, i);
1111
+            if dict.is_null() {
1112
+                continue;
1113
+            }
1114
+            let mut v: CFTypeRef = ptr::null();
1115
+            let mut wid: u32 = 0;
1116
+            if CFDictionaryGetValueIfPresent(dict, wid_key as CFTypeRef, &mut v) {
1117
+                CFNumberGetValue(v, kCFNumberSInt32Type, &mut wid as *mut _ as *mut _);
1118
+            }
1119
+            if wid != target_wid {
1120
+                continue;
1121
+            }
1122
+
1123
+            let mut sharing: i32 = -1;
1124
+            if CFDictionaryGetValueIfPresent(dict, sharing_key as CFTypeRef, &mut v) {
1125
+                CFNumberGetValue(v, kCFNumberSInt32Type, &mut sharing as *mut _ as *mut _);
1126
+            }
1127
+            let mut layer: i32 = i32::MIN;
1128
+            if CFDictionaryGetValueIfPresent(dict, layer_key as CFTypeRef, &mut v) {
1129
+                CFNumberGetValue(v, kCFNumberSInt32Type, &mut layer as *mut _ as *mut _);
1130
+            }
1131
+            let mut alpha: f64 = -1.0;
1132
+            if CFDictionaryGetValueIfPresent(dict, alpha_key as CFTypeRef, &mut v) {
1133
+                CFNumberGetValue(v, 13 /* kCFNumberDoubleType */, &mut alpha as *mut _ as *mut _);
1134
+            }
1135
+            let on_screen_present =
1136
+                CFDictionaryGetValueIfPresent(dict, on_screen_key as CFTypeRef, &mut v);
1137
+
1138
+            debug!(
1139
+                "[probe_cg_window_info] wid={target_wid} cg_sharing={sharing} layer={layer} alpha={alpha:.3} on_screen_present={on_screen_present}"
1140
+            );
1141
+            found = true;
1142
+            break;
1143
+        }
1144
+
1145
+        if !found {
1146
+            debug!("[probe_cg_window_info] wid={target_wid} NOT FOUND in CGWindowList");
1147
+        }
1148
+        CFRelease(list as CFTypeRef);
1149
+    }
1150
+}
1151
+
10901152
 fn discover_windows(cid: CGSConnectionID, own_pid: i32) -> Vec<u32> {
10911153
     unsafe {
10921154
         let list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
@@ -1213,11 +1275,47 @@ fn create_overlay(
12131275
             return None;
12141276
         }
12151277
 
1278
+        // Empty hit-test shape: an SLS window with an empty opaque_shape
1279
+        // is click-through at the compositor level (no input region).
1280
+        let empty = CGRect::new(0.0, 0.0, 0.0, 0.0);
1281
+        let mut empty_region: CFTypeRef = ptr::null();
1282
+        if CGSNewRegionWithRect(&empty, &mut empty_region) != kCGErrorSuccess
1283
+            || empty_region.is_null()
1284
+        {
1285
+            debug!("[create_overlay] CGSNewRegionWithRect (empty) failed for wid={target_wid}");
1286
+            CFRelease(region);
1287
+            return None;
1288
+        }
1289
+
1290
+        // Create the overlay via SLSNewWindowWithOpaqueShapeAndContext
1291
+        // and bake tag bit 1 (click-through) and tag bit 9 (screenshot
1292
+        // exclusion) into the window at birth. Tahoe classifies windows
1293
+        // for capture/picker based on tags observed at creation time;
1294
+        // post-creation tag mutation lands too late and the picker keeps
1295
+        // including the overlay. Mirrors the JankyBorders unmanaged
1296
+        // create path (.refs/JankyBorders/src/misc/window.h:239).
1297
+        // options 13|(1<<18): documentation-window | ignores-cycle.
1298
+        let mut tags: u64 = (1u64 << 1) | (1u64 << 9);
12161299
         let mut wid: u32 = 0;
1217
-        SLSNewWindow(cid, 2, ox as f32, oy as f32, region, &mut wid);
1300
+        SLSNewWindowWithOpaqueShapeAndContext(
1301
+            cid,
1302
+            2,
1303
+            region,
1304
+            empty_region,
1305
+            13 | (1 << 18),
1306
+            &mut tags as *mut u64,
1307
+            ox as f32,
1308
+            oy as f32,
1309
+            64,
1310
+            &mut wid,
1311
+            ptr::null_mut(),
1312
+        );
12181313
         CFRelease(region);
1314
+        CFRelease(empty_region);
12191315
         if wid == 0 {
1220
-            debug!("[create_overlay] SLSNewWindow returned 0 for target={target_wid} cid={cid}");
1316
+            debug!(
1317
+                "[create_overlay] SLSNewWindowWithOpaqueShapeAndContext returned 0 for target={target_wid} cid={cid}"
1318
+            );
12211319
             return None;
12221320
         }
12231321
 
@@ -1226,6 +1324,27 @@ fn create_overlay(
12261324
             color.0, color.1, color.2, color.3
12271325
         );
12281326
 
1327
+        if let Some(metadata) = query_window_metadata(cid, wid) {
1328
+            debug!(
1329
+                "[create_overlay] post-create overlay wid={wid} tags={:#x} attributes={:#x} parent={}",
1330
+                metadata.tags, metadata.attributes, metadata.parent_wid
1331
+            );
1332
+        } else {
1333
+            debug!("[create_overlay] post-create wid={wid} metadata query failed");
1334
+        }
1335
+
1336
+        SLSSetWindowSharingState(cid, wid, 0);
1337
+        let mut sharing_state: u32 = u32::MAX;
1338
+        let rc = SLSGetWindowSharingState(cid, wid, &mut sharing_state);
1339
+        debug!("[create_overlay] sharing_state wid={wid} get_rc={rc} sls_state={sharing_state}");
1340
+
1341
+        // Probe what CGWindowListCopyWindowInfo (which the screenshot
1342
+        // picker / SCWindow use) reports for our overlay. If
1343
+        // kCGWindowSharingState comes back != 0 here, then SLS-side
1344
+        // sharing state is not propagated to the CG window list and
1345
+        // we'll need a different exclusion mechanism.
1346
+        probe_cg_window_info(wid);
1347
+
12291348
         SLSSetWindowResolution(cid, wid, scale);
12301349
         SLSSetWindowOpacity(cid, wid, false);
12311350
         SLSSetWindowLevel(cid, wid, 0);
@@ -1243,24 +1362,6 @@ fn create_overlay(
12431362
         SLSFlushWindowContentRegion(cid, wid, ptr::null());
12441363
         CGContextRelease(ctx);
12451364
 
1246
-        // Click-through. Setting an empty event/hit-test shape passes
1247
-        // mouse events through to the window beneath. We deliberately
1248
-        // avoid SLSSetWindowTags(kCGSIgnoreForEvents): even when set
1249
-        // before drawing on Tahoe, the tag-bit-1 flag breaks overlay
1250
-        // visibility during the rapid sync_overlay/recreate churn that
1251
-        // tiling produces. Empty event shape + zero event mask is the
1252
-        // only combination that gives both click-through AND persistent
1253
-        // borders on Tahoe.
1254
-        let empty = CGRect::new(0.0, 0.0, 0.0, 0.0);
1255
-        let mut empty_region: CFTypeRef = ptr::null();
1256
-        if CGSNewRegionWithRect(&empty, &mut empty_region) == kCGErrorSuccess
1257
-            && !empty_region.is_null()
1258
-        {
1259
-            SLSSetWindowEventShape(cid, wid, empty_region);
1260
-            CFRelease(empty_region);
1261
-        }
1262
-        SLSSetWindowEventMask(cid, wid, 0);
1263
-
12641365
         Some((cid, wid, bounds, scale))
12651366
     }
12661367
 }
src/skylight.rsmodified
@@ -175,6 +175,26 @@ unsafe extern "C" {
175175
         region: CFTypeRef,
176176
         wid_out: *mut u32,
177177
     ) -> CGError;
178
+    /// JankyBorders' `SLSNewWindowWithOpaqueShapeAndContext` — creates a
179
+    /// window with a custom hit-test shape and tag bits applied at
180
+    /// creation. Used so that screenshot-exclusion tag bit 9 lands on
181
+    /// the window before macOS Tahoe's compositor classifies it; setting
182
+    /// the bit post-creation is unreliable on Tahoe.
183
+    /// Reference: .refs/JankyBorders/src/misc/window.h:239
184
+    /// Reference: .refs/JankyBorders/src/misc/extern.h
185
+    pub fn SLSNewWindowWithOpaqueShapeAndContext(
186
+        cid: CGSConnectionID,
187
+        window_type: i32,
188
+        region: CFTypeRef,
189
+        opaque_shape: CFTypeRef,
190
+        options: i32,
191
+        tags: *mut u64,
192
+        x: f32,
193
+        y: f32,
194
+        tag_size: i32,
195
+        wid_out: *mut u32,
196
+        context: *mut std::ffi::c_void,
197
+    ) -> CGError;
178198
     pub fn SLSReleaseWindow(cid: CGSConnectionID, wid: u32) -> CGError;
179199
 
180200
     // Window properties
@@ -199,6 +219,15 @@ unsafe extern "C" {
199219
     ) -> CGError;
200220
     pub fn SLSSetWindowResolution(cid: CGSConnectionID, wid: u32, res: f64) -> CGError;
201221
     pub fn SLSSetWindowOpacity(cid: CGSConnectionID, wid: u32, is_opaque: bool) -> CGError;
222
+    /// SLS-level NSWindow.sharingType. Values: 0 = None (excluded from
223
+    /// screen capture / picker / recording — equivalent to
224
+    /// kCGWindowSharingNone), 1 = ReadOnly, 2 = ReadWrite.
225
+    pub fn SLSSetWindowSharingState(cid: CGSConnectionID, wid: u32, state: u32) -> CGError;
226
+    pub fn SLSGetWindowSharingState(
227
+        cid: CGSConnectionID,
228
+        wid: u32,
229
+        state_out: *mut u32,
230
+    ) -> CGError;
202231
     /// Mask of events the SLS window captures. Set to 0 to make the window
203232
     /// click-through (mouse events pass to the window beneath).
204233
     pub fn SLSSetWindowEventMask(cid: CGSConnectionID, wid: u32, mask: u32) -> CGError;