@lattices/cli 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,313 @@
1
+ import XCTest
2
+ import CoreGraphics
3
+ import AppKit
4
+
5
+ /// Experiments: can we programmatically create a new stage by joining windows?
6
+ ///
7
+ /// Stage Manager groups windows into stages. When a user drags a window from
8
+ /// the strip onto the center stage, it joins that stage. Can we replicate this
9
+ /// via AX, CGS, or simulated events?
10
+ final class StageJoinTests: XCTestCase {
11
+
12
+ // MARK: - Helpers
13
+
14
+ struct LiveWindow {
15
+ let wid: UInt32
16
+ let app: String
17
+ let pid: Int32
18
+ let title: String
19
+ let bounds: CGRect
20
+ let isOnScreen: Bool
21
+ }
22
+
23
+ /// Get all real app windows (layer 0, >= 50x50)
24
+ func getRealWindows() -> [LiveWindow] {
25
+ guard let list = CGWindowListCopyWindowInfo(
26
+ [.optionAll, .excludeDesktopElements],
27
+ kCGNullWindowID
28
+ ) as? [[String: Any]] else { return [] }
29
+
30
+ let skip: Set<String> = [
31
+ "Window Server", "Dock", "Control Center", "SystemUIServer",
32
+ "Notification Center", "Spotlight", "WindowManager",
33
+ "Lattices",
34
+ ]
35
+
36
+ return list.compactMap { info in
37
+ guard let wid = info[kCGWindowNumber as String] as? UInt32,
38
+ let owner = info[kCGWindowOwnerName as String] as? String,
39
+ let pid = info[kCGWindowOwnerPID as String] as? Int32,
40
+ let boundsDict = info[kCGWindowBounds as String] as? NSDictionary
41
+ else { return nil }
42
+
43
+ var rect = CGRect.zero
44
+ guard CGRectMakeWithDictionaryRepresentation(boundsDict, &rect) else { return nil }
45
+ let title = info[kCGWindowName as String] as? String ?? ""
46
+ let layer = info[kCGWindowLayer as String] as? Int ?? 0
47
+ let isOnScreen = info[kCGWindowIsOnscreen as String] as? Bool ?? false
48
+
49
+ guard layer == 0, rect.width >= 50, rect.height >= 50 else { return nil }
50
+ guard !skip.contains(owner) else { return nil }
51
+
52
+ return LiveWindow(wid: wid, app: owner, pid: pid, title: title,
53
+ bounds: rect, isOnScreen: isOnScreen)
54
+ }
55
+ }
56
+
57
+ /// Get AX window elements for a PID
58
+ func getAXWindows(pid: Int32) -> [AXUIElement] {
59
+ let app = AXUIElementCreateApplication(pid)
60
+ var ref: CFTypeRef?
61
+ guard AXUIElementCopyAttributeValue(app, kAXWindowsAttribute as CFString, &ref) == .success,
62
+ let windows = ref as? [AXUIElement] else { return [] }
63
+ return windows
64
+ }
65
+
66
+ /// Move an AX window to a position
67
+ func moveAXWindow(_ axWin: AXUIElement, to point: CGPoint) -> Bool {
68
+ var p = point
69
+ let posValue = AXValueCreate(.cgPoint, &p)!
70
+ return AXUIElementSetAttributeValue(axWin, kAXPositionAttribute as CFString, posValue) == .success
71
+ }
72
+
73
+ /// Resize an AX window
74
+ func resizeAXWindow(_ axWin: AXUIElement, to size: CGSize) -> Bool {
75
+ var s = size
76
+ let sizeValue = AXValueCreate(.cgSize, &s)!
77
+ return AXUIElementSetAttributeValue(axWin, kAXSizeAttribute as CFString, sizeValue) == .success
78
+ }
79
+
80
+ /// Raise an AX window
81
+ func raiseAXWindow(_ axWin: AXUIElement) -> Bool {
82
+ AXUIElementPerformAction(axWin, kAXRaiseAction as CFString) == .success
83
+ }
84
+
85
+ /// Snapshot which windows are onscreen (active stage)
86
+ func activeStageWids() -> Set<UInt32> {
87
+ Set(getRealWindows().filter { $0.isOnScreen && $0.bounds.width > 250 }.map(\.wid))
88
+ }
89
+
90
+ /// Print stage state
91
+ func printStageState(label: String) {
92
+ let windows = getRealWindows()
93
+ let active = windows.filter { $0.isOnScreen && $0.bounds.width > 250 }
94
+ let strip = windows.filter { $0.isOnScreen && $0.bounds.width < 250 && $0.bounds.origin.x < 220 }
95
+ print("\n[\(label)]")
96
+ print(" Active stage: \(active.map { "\($0.app)(\($0.wid))" }.joined(separator: ", "))")
97
+ print(" Strip: \(Set(strip.map(\.app)).sorted().joined(separator: ", "))")
98
+ }
99
+
100
+ // MARK: - Approach 1: Activate two apps rapidly
101
+
102
+ func testJoinByActivation() throws {
103
+ let smEnabled = UserDefaults(suiteName: "com.apple.WindowManager")?.bool(forKey: "GloballyEnabled") ?? false
104
+ try XCTSkipUnless(smEnabled, "Stage Manager is OFF")
105
+
106
+ let windows = getRealWindows()
107
+
108
+ // Find two different apps that are NOT in the current stage
109
+ let offscreenApps = Dictionary(grouping: windows.filter { !$0.isOnScreen && $0.bounds.width > 250 },
110
+ by: \.app)
111
+ let candidates = offscreenApps.keys.sorted()
112
+ guard candidates.count >= 2 else {
113
+ print("Need at least 2 offscreen apps, found: \(candidates)")
114
+ return
115
+ }
116
+
117
+ let appA = candidates[0]
118
+ let appB = candidates[1]
119
+ let winA = offscreenApps[appA]!.first!
120
+ let winB = offscreenApps[appB]!.first!
121
+
122
+ print("Attempting to join \(appA) and \(appB) by rapid activation")
123
+ printStageState(label: "BEFORE")
124
+
125
+ // Activate app A
126
+ let nsAppA = NSRunningApplication(processIdentifier: winA.pid)
127
+ nsAppA?.activate()
128
+ Thread.sleep(forTimeInterval: 0.3)
129
+
130
+ printStageState(label: "After activating \(appA)")
131
+
132
+ // Now immediately activate app B — does SM merge them?
133
+ let nsAppB = NSRunningApplication(processIdentifier: winB.pid)
134
+ nsAppB?.activate()
135
+ Thread.sleep(forTimeInterval: 0.5)
136
+
137
+ printStageState(label: "After activating \(appB)")
138
+
139
+ // Check: are both apps in the active stage?
140
+ let finalActive = getRealWindows().filter { $0.isOnScreen && $0.bounds.width > 250 }
141
+ let activeApps = Set(finalActive.map(\.app))
142
+ let joined = activeApps.contains(appA) && activeApps.contains(appB)
143
+ print("\nResult: both apps in active stage? \(joined)")
144
+ print("Active apps: \(activeApps.sorted())")
145
+ }
146
+
147
+ // MARK: - Approach 2: Move offscreen window into active stage area via AX
148
+
149
+ func testJoinByAXMove() throws {
150
+ let smEnabled = UserDefaults(suiteName: "com.apple.WindowManager")?.bool(forKey: "GloballyEnabled") ?? false
151
+ try XCTSkipUnless(smEnabled, "Stage Manager is OFF")
152
+
153
+ let windows = getRealWindows()
154
+
155
+ // Find the active stage center
156
+ let activeWindows = windows.filter { $0.isOnScreen && $0.bounds.width > 250 }
157
+ guard let anchor = activeWindows.first else {
158
+ print("No active stage window found")
159
+ return
160
+ }
161
+
162
+ // Find an offscreen app to pull in
163
+ let offscreen = windows.filter { !$0.isOnScreen && $0.bounds.width > 250 && $0.app != anchor.app }
164
+ guard let target = offscreen.first else {
165
+ print("No offscreen window to test with")
166
+ return
167
+ }
168
+
169
+ print("Attempting to join \(target.app)[\(target.wid)] into \(anchor.app)'s stage via AX move")
170
+ printStageState(label: "BEFORE")
171
+
172
+ // Get AX window for target
173
+ let axWindows = getAXWindows(pid: target.pid)
174
+ guard let axWin = axWindows.first else {
175
+ print("Could not get AX window for \(target.app)")
176
+ return
177
+ }
178
+
179
+ // Move it right next to the anchor window
180
+ let destPoint = CGPoint(x: anchor.bounds.origin.x + 50, y: anchor.bounds.origin.y + 50)
181
+ let moved = moveAXWindow(axWin, to: destPoint)
182
+ print("AX move result: \(moved)")
183
+
184
+ // Raise it
185
+ let raised = raiseAXWindow(axWin)
186
+ print("AX raise result: \(raised)")
187
+
188
+ Thread.sleep(forTimeInterval: 0.5)
189
+ printStageState(label: "After AX move + raise")
190
+
191
+ // Did it join the stage?
192
+ let finalActive = getRealWindows().filter { $0.isOnScreen && $0.bounds.width > 250 }
193
+ let activeApps = Set(finalActive.map(\.app))
194
+ let joined = activeApps.contains(target.app) && activeApps.contains(anchor.app)
195
+ print("\nResult: \(target.app) joined \(anchor.app)'s stage? \(joined)")
196
+ print("Active apps: \(activeApps.sorted())")
197
+ }
198
+
199
+ // MARK: - Approach 3: AX move + activate target app (without switching stage)
200
+
201
+ func testJoinByMoveAndActivate() throws {
202
+ let smEnabled = UserDefaults(suiteName: "com.apple.WindowManager")?.bool(forKey: "GloballyEnabled") ?? false
203
+ try XCTSkipUnless(smEnabled, "Stage Manager is OFF")
204
+
205
+ let windows = getRealWindows()
206
+ let activeWindows = windows.filter { $0.isOnScreen && $0.bounds.width > 250 }
207
+ guard let anchor = activeWindows.first else { return }
208
+
209
+ let offscreen = windows.filter { !$0.isOnScreen && $0.bounds.width > 250 && $0.app != anchor.app }
210
+ guard let target = offscreen.first else { return }
211
+
212
+ print("Attempting: move \(target.app) via AX, then activate it")
213
+ printStageState(label: "BEFORE")
214
+
215
+ // Step 1: Move target window into center area via AX
216
+ let axWindows = getAXWindows(pid: target.pid)
217
+ guard let axWin = axWindows.first else { return }
218
+
219
+ let dest = CGPoint(x: anchor.bounds.midX, y: anchor.bounds.midY)
220
+ _ = moveAXWindow(axWin, to: dest)
221
+ _ = resizeAXWindow(axWin, to: CGSize(width: 800, height: 600))
222
+
223
+ Thread.sleep(forTimeInterval: 0.2)
224
+
225
+ // Step 2: Activate anchor app first (keep it in stage)
226
+ NSRunningApplication(processIdentifier: anchor.pid)?.activate()
227
+ Thread.sleep(forTimeInterval: 0.1)
228
+
229
+ // Step 3: Raise the target window (without activate, to avoid stage switch)
230
+ _ = raiseAXWindow(axWin)
231
+
232
+ Thread.sleep(forTimeInterval: 0.5)
233
+ printStageState(label: "After move + raise (no activate)")
234
+
235
+ // Step 4: Now try activating both
236
+ NSRunningApplication(processIdentifier: anchor.pid)?.activate()
237
+ Thread.sleep(forTimeInterval: 0.1)
238
+ // Use AX to set focused on target window
239
+ AXUIElementSetAttributeValue(axWin, kAXFocusedAttribute as CFString, kCFBooleanTrue)
240
+ AXUIElementSetAttributeValue(axWin, kAXMainAttribute as CFString, kCFBooleanTrue)
241
+
242
+ Thread.sleep(forTimeInterval: 0.5)
243
+ printStageState(label: "After setting focus+main on target")
244
+
245
+ let finalActive = getRealWindows().filter { $0.isOnScreen && $0.bounds.width > 250 }
246
+ let activeApps = Set(finalActive.map(\.app))
247
+ let joined = activeApps.contains(target.app) && activeApps.contains(anchor.app)
248
+ print("\nResult: joined? \(joined) — active apps: \(activeApps.sorted())")
249
+ }
250
+
251
+ // MARK: - Approach 4: CGS space manipulation — put both windows on same space
252
+
253
+ func testJoinBySameSpace() throws {
254
+ let smEnabled = UserDefaults(suiteName: "com.apple.WindowManager")?.bool(forKey: "GloballyEnabled") ?? false
255
+ try XCTSkipUnless(smEnabled, "Stage Manager is OFF")
256
+
257
+ // Load CGS functions
258
+ typealias CGSConnectionID = UInt32
259
+ typealias CGSMainConnectionIDFunc = @convention(c) () -> CGSConnectionID
260
+ typealias CGSAddWindowsToSpacesFunc = @convention(c) (CGSConnectionID, CFArray, CFArray) -> Void
261
+ typealias CGSGetActiveSpaceFunc = @convention(c) (CGSConnectionID) -> Int
262
+
263
+ guard let handle = dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight", RTLD_LAZY),
264
+ let mainConnSym = dlsym(handle, "CGSMainConnectionID"),
265
+ let addToSpacesSym = dlsym(handle, "CGSAddWindowsToSpaces"),
266
+ let activeSpaceSym = dlsym(handle, "CGSGetActiveSpace")
267
+ else {
268
+ print("Could not load SkyLight functions")
269
+ return
270
+ }
271
+
272
+ let CGSMainConnectionID = unsafeBitCast(mainConnSym, to: CGSMainConnectionIDFunc.self)
273
+ let CGSAddWindowsToSpaces = unsafeBitCast(addToSpacesSym, to: CGSAddWindowsToSpacesFunc.self)
274
+ let CGSGetActiveSpace = unsafeBitCast(activeSpaceSym, to: CGSGetActiveSpaceFunc.self)
275
+
276
+ let cid = CGSMainConnectionID()
277
+ let activeSpace = CGSGetActiveSpace(cid)
278
+ print("Connection: \(cid), Active space: \(activeSpace)")
279
+
280
+ let windows = getRealWindows()
281
+ let activeWindows = windows.filter { $0.isOnScreen && $0.bounds.width > 250 }
282
+ guard let anchor = activeWindows.first else { return }
283
+
284
+ let offscreen = windows.filter { !$0.isOnScreen && $0.bounds.width > 250 && $0.app != anchor.app }
285
+ guard let target = offscreen.first else { return }
286
+
287
+ print("Attempting: add \(target.app)[\(target.wid)] to space \(activeSpace) via CGSAddWindowsToSpaces")
288
+ printStageState(label: "BEFORE")
289
+
290
+ // Add target window to active space
291
+ let windowIDs = [target.wid] as CFArray
292
+ let spaceIDs = [activeSpace] as CFArray
293
+ CGSAddWindowsToSpaces(cid, windowIDs, spaceIDs)
294
+
295
+ Thread.sleep(forTimeInterval: 0.5)
296
+ printStageState(label: "After CGSAddWindowsToSpaces")
297
+
298
+ // Also try raising it via AX
299
+ let axWindows = getAXWindows(pid: target.pid)
300
+ if let axWin = axWindows.first {
301
+ _ = raiseAXWindow(axWin)
302
+ Thread.sleep(forTimeInterval: 0.3)
303
+ printStageState(label: "After raise")
304
+ }
305
+
306
+ let finalActive = getRealWindows().filter { $0.isOnScreen && $0.bounds.width > 250 }
307
+ let activeApps = Set(finalActive.map(\.app))
308
+ let joined = activeApps.contains(target.app) && activeApps.contains(anchor.app)
309
+ print("\nResult: joined? \(joined) — active apps: \(activeApps.sorted())")
310
+
311
+ dlclose(handle)
312
+ }
313
+ }
@@ -0,0 +1,280 @@
1
+ import XCTest
2
+ import CoreGraphics
3
+ import AppKit
4
+
5
+ final class StageManagerTests: XCTestCase {
6
+
7
+ // MARK: - Detection
8
+
9
+ func testStageManagerEnabled() {
10
+ let defaults = UserDefaults(suiteName: "com.apple.WindowManager")
11
+ let enabled = defaults?.bool(forKey: "GloballyEnabled") ?? false
12
+ print("Stage Manager enabled: \(enabled)")
13
+
14
+ // Also read all known keys
15
+ let keys = [
16
+ "GloballyEnabled",
17
+ "GloballyEnabledEver",
18
+ "AutoHide",
19
+ "AppWindowGroupingBehavior",
20
+ "HideDesktop",
21
+ "StageManagerHideWidgets",
22
+ "EnableStandardClickToShowDesktop",
23
+ "EnableTiledWindowMargins",
24
+ "EnableTilingByEdgeDrag",
25
+ "EnableTopTilingByEdgeDrag",
26
+ "StandardHideDesktopIcons",
27
+ "StandardHideWidgets",
28
+ ]
29
+
30
+ print("\n=== com.apple.WindowManager preferences ===")
31
+ for key in keys {
32
+ let val = defaults?.object(forKey: key)
33
+ print(" \(key): \(val ?? "nil" as Any)")
34
+ }
35
+
36
+ // Not asserting true/false — just verifying we can read the domain
37
+ XCTAssertNotNil(defaults, "Should be able to open com.apple.WindowManager domain")
38
+ }
39
+
40
+ // MARK: - Window Classification
41
+
42
+ func testClassifyWindows() {
43
+ let smEnabled = UserDefaults(suiteName: "com.apple.WindowManager")?.bool(forKey: "GloballyEnabled") ?? false
44
+
45
+ guard smEnabled else {
46
+ print("Stage Manager is OFF — skipping window classification")
47
+ return
48
+ }
49
+
50
+ guard let windowList = CGWindowListCopyWindowInfo(
51
+ [.optionAll, .excludeDesktopElements],
52
+ kCGNullWindowID
53
+ ) as? [[String: Any]] else {
54
+ XCTFail("Could not get window list")
55
+ return
56
+ }
57
+
58
+ var activeStage: [(wid: UInt32, app: String, title: String, bounds: CGRect)] = []
59
+ var stripThumbnails: [(wid: UInt32, app: String, title: String, bounds: CGRect)] = []
60
+ var hiddenStage: [(wid: UInt32, app: String, title: String, bounds: CGRect)] = []
61
+ var gestureOverlays: [(wid: UInt32, bounds: CGRect)] = []
62
+ var appIcons: [(wid: UInt32, bounds: CGRect)] = []
63
+
64
+ let mainScreen = NSScreen.main!
65
+ let stripMaxX: CGFloat = 220 // strip occupies roughly left 220px
66
+ let thumbnailMaxSize: CGFloat = 250
67
+
68
+ for info in windowList {
69
+ guard let wid = info[kCGWindowNumber as String] as? UInt32,
70
+ let owner = info[kCGWindowOwnerName as String] as? String,
71
+ let boundsDict = info[kCGWindowBounds as String] as? NSDictionary
72
+ else { continue }
73
+
74
+ var rect = CGRect.zero
75
+ guard CGRectMakeWithDictionaryRepresentation(boundsDict, &rect) else { continue }
76
+
77
+ let title = info[kCGWindowName as String] as? String ?? ""
78
+ let layer = info[kCGWindowLayer as String] as? Int ?? 0
79
+ let isOnScreen = info[kCGWindowIsOnscreen as String] as? Bool ?? false
80
+
81
+ guard layer == 0 else { continue }
82
+ guard rect.width >= 10, rect.height >= 10 else { continue }
83
+
84
+ // WindowManager process windows (GBOs and app icons)
85
+ if owner == "WindowManager" {
86
+ if title == "Gesture Blocking Overlay" || title.isEmpty {
87
+ if rect.width <= 80 && rect.height <= 80 {
88
+ appIcons.append((wid: wid, bounds: rect))
89
+ } else {
90
+ gestureOverlays.append((wid: wid, bounds: rect))
91
+ }
92
+ }
93
+ continue
94
+ }
95
+
96
+ // Skip non-app processes
97
+ let skipOwners: Set<String> = [
98
+ "Window Server", "Dock", "Control Center", "SystemUIServer",
99
+ "Notification Center", "Spotlight",
100
+ ]
101
+ if skipOwners.contains(owner) { continue }
102
+
103
+ if !isOnScreen {
104
+ // Hidden in another stage
105
+ hiddenStage.append((wid: wid, app: owner, title: title, bounds: rect))
106
+ } else if rect.width < thumbnailMaxSize && rect.height < thumbnailMaxSize
107
+ && rect.origin.x < stripMaxX {
108
+ // Strip thumbnail
109
+ stripThumbnails.append((wid: wid, app: owner, title: title, bounds: rect))
110
+ } else if rect.width >= 50 && rect.height >= 50 {
111
+ // Active stage window
112
+ activeStage.append((wid: wid, app: owner, title: title, bounds: rect))
113
+ }
114
+ }
115
+
116
+ print("\n=== Stage Manager Window Classification ===")
117
+ print("Screen: \(Int(mainScreen.frame.width))x\(Int(mainScreen.frame.height))")
118
+
119
+ print("\n--- Active Stage (\(activeStage.count) windows) ---")
120
+ for w in activeStage {
121
+ print(" [\(w.wid)] \(w.app) — \"\(w.title)\"")
122
+ print(" bounds: \(Int(w.bounds.origin.x)),\(Int(w.bounds.origin.y)) \(Int(w.bounds.width))x\(Int(w.bounds.height))")
123
+ }
124
+
125
+ print("\n--- Strip Thumbnails (\(stripThumbnails.count) windows) ---")
126
+ for w in stripThumbnails {
127
+ print(" [\(w.wid)] \(w.app) — \"\(w.title)\"")
128
+ print(" bounds: \(Int(w.bounds.origin.x)),\(Int(w.bounds.origin.y)) \(Int(w.bounds.width))x\(Int(w.bounds.height))")
129
+ }
130
+
131
+ print("\n--- Hidden in Other Stages (\(hiddenStage.count) windows) ---")
132
+ for w in hiddenStage {
133
+ print(" [\(w.wid)] \(w.app) — \"\(w.title)\"")
134
+ print(" bounds: \(Int(w.bounds.origin.x)),\(Int(w.bounds.origin.y)) \(Int(w.bounds.width))x\(Int(w.bounds.height))")
135
+ }
136
+
137
+ print("\n--- Gesture Blocking Overlays (\(gestureOverlays.count)) ---")
138
+ for g in gestureOverlays {
139
+ print(" [\(g.wid)] bounds: \(Int(g.bounds.origin.x)),\(Int(g.bounds.origin.y)) \(Int(g.bounds.width))x\(Int(g.bounds.height))")
140
+ }
141
+
142
+ print("\n--- App Icons in Strip (\(appIcons.count)) ---")
143
+ for a in appIcons {
144
+ print(" [\(a.wid)] bounds: \(Int(a.bounds.origin.x)),\(Int(a.bounds.origin.y)) \(Int(a.bounds.width))x\(Int(a.bounds.height))")
145
+ }
146
+
147
+ // Try to correlate GBOs to strip thumbnails by proximity
148
+ print("\n--- GBO ↔ Thumbnail Correlation ---")
149
+ for gbo in gestureOverlays {
150
+ let gboCenter = CGPoint(x: gbo.bounds.midX, y: gbo.bounds.midY)
151
+ var closest: (wid: UInt32, app: String, dist: CGFloat) = (0, "", .greatestFiniteMagnitude)
152
+ for thumb in stripThumbnails {
153
+ let thumbCenter = CGPoint(x: thumb.bounds.midX, y: thumb.bounds.midY)
154
+ let dist = hypot(gboCenter.x - thumbCenter.x, gboCenter.y - thumbCenter.y)
155
+ if dist < closest.dist {
156
+ closest = (thumb.wid, thumb.app, dist)
157
+ }
158
+ }
159
+ if closest.dist < 300 {
160
+ print(" GBO [\(gbo.wid)] → Thumbnail [\(closest.wid)] \(closest.app) (dist: \(Int(closest.dist))px)")
161
+ } else {
162
+ print(" GBO [\(gbo.wid)] → no match (closest: \(Int(closest.dist))px)")
163
+ }
164
+ }
165
+ }
166
+
167
+ // MARK: - Stage Grouping Heuristic
168
+
169
+ func testInferStageGroups() {
170
+ let smEnabled = UserDefaults(suiteName: "com.apple.WindowManager")?.bool(forKey: "GloballyEnabled") ?? false
171
+
172
+ guard smEnabled else {
173
+ print("Stage Manager is OFF — skipping stage grouping")
174
+ return
175
+ }
176
+
177
+ guard let windowList = CGWindowListCopyWindowInfo(
178
+ [.optionAll, .excludeDesktopElements],
179
+ kCGNullWindowID
180
+ ) as? [[String: Any]] else {
181
+ XCTFail("Could not get window list")
182
+ return
183
+ }
184
+
185
+ // Stage Manager groups windows together. When "All at Once" is selected,
186
+ // all windows from one app move together. We can infer groups by looking
187
+ // at which offscreen windows share similar thumbnail strip positions.
188
+
189
+ struct WinInfo {
190
+ let wid: UInt32
191
+ let app: String
192
+ let title: String
193
+ let bounds: CGRect
194
+ let isOnScreen: Bool
195
+ let pid: Int32
196
+ }
197
+
198
+ var appWindows: [String: [WinInfo]] = [:]
199
+ let skipOwners: Set<String> = [
200
+ "Window Server", "Dock", "Control Center", "SystemUIServer",
201
+ "Notification Center", "Spotlight", "WindowManager",
202
+ ]
203
+
204
+ for info in windowList {
205
+ guard let wid = info[kCGWindowNumber as String] as? UInt32,
206
+ let owner = info[kCGWindowOwnerName as String] as? String,
207
+ let pid = info[kCGWindowOwnerPID as String] as? Int32,
208
+ let boundsDict = info[kCGWindowBounds as String] as? NSDictionary
209
+ else { continue }
210
+
211
+ var rect = CGRect.zero
212
+ guard CGRectMakeWithDictionaryRepresentation(boundsDict, &rect) else { continue }
213
+ let title = info[kCGWindowName as String] as? String ?? ""
214
+ let layer = info[kCGWindowLayer as String] as? Int ?? 0
215
+ let isOnScreen = info[kCGWindowIsOnscreen as String] as? Bool ?? false
216
+
217
+ guard layer == 0, rect.width >= 50, rect.height >= 50 else { continue }
218
+ guard !skipOwners.contains(owner) else { continue }
219
+
220
+ let w = WinInfo(wid: wid, app: owner, title: title, bounds: rect,
221
+ isOnScreen: isOnScreen, pid: pid)
222
+ appWindows[owner, default: []].append(w)
223
+ }
224
+
225
+ // Classify into stages
226
+ var currentStageApps: Set<String> = []
227
+ var otherStageApps: Set<String> = []
228
+
229
+ for (app, wins) in appWindows {
230
+ let hasOnScreen = wins.contains { $0.isOnScreen && $0.bounds.width > 250 }
231
+ if hasOnScreen {
232
+ currentStageApps.insert(app)
233
+ } else {
234
+ otherStageApps.insert(app)
235
+ }
236
+ }
237
+
238
+ print("\n=== Inferred Stage Groups ===")
239
+ print("\n🟢 Current Stage:")
240
+ for app in currentStageApps.sorted() {
241
+ let wins = appWindows[app]!.filter { $0.isOnScreen }
242
+ print(" \(app) (\(wins.count) window\(wins.count == 1 ? "" : "s"))")
243
+ for w in wins {
244
+ print(" [\(w.wid)] \"\(w.title)\" — \(Int(w.bounds.width))x\(Int(w.bounds.height))")
245
+ }
246
+ }
247
+
248
+ print("\n🔵 Other Stages:")
249
+ for app in otherStageApps.sorted() {
250
+ let wins = appWindows[app]!
251
+ let onScreen = wins.filter { $0.isOnScreen }
252
+ let offScreen = wins.filter { !$0.isOnScreen }
253
+ print(" \(app) (\(offScreen.count) hidden, \(onScreen.count) thumbnail)")
254
+ for w in offScreen.prefix(3) {
255
+ print(" [\(w.wid)] \"\(w.title)\" — \(Int(w.bounds.width))x\(Int(w.bounds.height))")
256
+ }
257
+ if offScreen.count > 3 { print(" ... and \(offScreen.count - 3) more") }
258
+ }
259
+ }
260
+
261
+ // MARK: - Preferences Change Detection
262
+
263
+ func testStageManagerPrefsObservation() {
264
+ // Test that we can observe preference changes via polling
265
+ let defaults = UserDefaults(suiteName: "com.apple.WindowManager")
266
+ let initial = defaults?.bool(forKey: "GloballyEnabled") ?? false
267
+ print("Initial Stage Manager state: \(initial)")
268
+
269
+ // Quick re-read to verify consistency
270
+ let reread = defaults?.bool(forKey: "GloballyEnabled") ?? false
271
+ XCTAssertEqual(initial, reread, "Preference should be stable across reads")
272
+
273
+ // Read AppWindowGroupingBehavior
274
+ let grouping = defaults?.integer(forKey: "AppWindowGroupingBehavior") ?? -1
275
+ print("AppWindowGroupingBehavior: \(grouping) (\(grouping == 0 ? "All at Once" : grouping == 1 ? "One at a Time" : "unknown"))")
276
+
277
+ let autoHide = defaults?.bool(forKey: "AutoHide") ?? false
278
+ print("AutoHide (strip): \(autoHide)")
279
+ }
280
+ }