|
| 1 | +import CoreGraphics |
| 2 | +import Foundation |
| 3 | +import PeekabooAutomationKit |
| 4 | +import PeekabooBridge |
| 5 | +import PeekabooFoundation |
| 6 | + |
| 7 | +/// Windows of the receiving process that appeared after an action was dispatched. |
| 8 | +struct CreatedWindows: Encodable { |
| 9 | + let windows: [Window] |
| 10 | + /// exclusive when exactly one window appeared, ambiguous when several did, none when no window appeared. |
| 11 | + let attribution: String |
| 12 | + let note: String |
| 13 | +} |
| 14 | + |
| 15 | +/// Every window ID WindowServer knows, on screen or not; taken just before dispatch. |
| 16 | +func existingWindowIDs() -> Set<Int> { |
| 17 | + let list = CGWindowListCopyWindowInfo([.optionAll], kCGNullWindowID) as? [[String: Any]] ?? [] |
| 18 | + return Set(list.compactMap { ($0[kCGWindowNumber as String] as? NSNumber)?.intValue }) |
| 19 | +} |
| 20 | + |
| 21 | +/// Waits briefly for a new normal window of the process, then lists its new inventory windows. Never sends input. |
| 22 | +func createdWindows( |
| 23 | + _ client: PeekabooBridgeClient, process: ApplicationProcessIdentity, before: Set<Int>, waitingUpTo seconds: Double |
| 24 | +) async -> CreatedWindows? { |
| 25 | + let deadline = Date().addingTimeInterval(seconds) |
| 26 | + repeat { |
| 27 | + let list = CGWindowListCopyWindowInfo([.optionAll], kCGNullWindowID) as? [[String: Any]] ?? [] |
| 28 | + let appeared = list.contains { info in |
| 29 | + (info[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value == process.processIdentifier |
| 30 | + && (info[kCGWindowLayer as String] as? NSNumber)?.intValue == 0 |
| 31 | + && !before.contains((info[kCGWindowNumber as String] as? NSNumber)?.intValue ?? 0) |
| 32 | + } |
| 33 | + if appeared { |
| 34 | + // Windows open in quick succession, as with several documents; let them settle. |
| 35 | + try? await Task.sleep(nanoseconds: 250_000_000) |
| 36 | + break |
| 37 | + } |
| 38 | + try? await Task.sleep(nanoseconds: 125_000_000) |
| 39 | + } while Date() < deadline |
| 40 | + guard let inventory = try? await client.listWindowMutationInventory( |
| 41 | + target: .application("PID:\(process.processIdentifier)") |
| 42 | + ) else { return nil } |
| 43 | + let windows = inventory.items.filter { item in |
| 44 | + !before.contains(item.windowID) && item.mutationIdentity?.processIdentity == process |
| 45 | + }.map { Window($0, pid: process.processIdentifier) } |
| 46 | + switch windows.count { |
| 47 | + case 0: |
| 48 | + return CreatedWindows(windows: [], attribution: "none", note: "No new window of the app appeared within about \(Int(seconds.rounded())) second\(seconds < 1.5 ? "" : "s"). One may still open later, or the action may reuse an existing window; list desktop_windows before acting.") |
| 49 | + case 1: |
| 50 | + return CreatedWindows(windows: windows, attribution: "exclusive", note: "This is the only window of the app that appeared after the action was sent, so it is very likely the one the action opened. Another agent acting on the same app at the same moment can still open a window in this interval; if that matters, confirm its contents with desktop_inspect.") |
| 51 | + default: |
| 52 | + return CreatedWindows(windows: windows, attribution: "ambiguous", note: "Several windows of the app appeared after the action was sent. The action may have opened more than one, or something else opened the others at the same time; nothing here says which came from this action. Inspect them, or ask the user, before choosing one.") |
| 53 | + } |
| 54 | +} |
0 commit comments