@stim-cli/server 1.13.0 → 1.15.0

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.
Files changed (52) hide show
  1. package/README.md +1590 -42
  2. package/dist/protocol-MBMvJpvV.mjs +2715 -0
  3. package/dist/protocol.d.mts +2 -473
  4. package/dist/protocol.mjs +3 -2
  5. package/dist/protocol.schema.json +5121 -514
  6. package/dist/scrcpy/LICENSE +204 -0
  7. package/dist/scrcpy/NOTICE +22 -0
  8. package/dist/scrcpy/scrcpy-server +0 -0
  9. package/dist/stats-read.d.mts +1 -0
  10. package/dist/stats-read.mjs +46 -0
  11. package/dist/stim-frames/AndroidDeviceStream.swift +284 -0
  12. package/dist/stim-frames/DevToolsClient.swift +180 -0
  13. package/dist/stim-frames/DeviceFrameGeometry.swift +41 -0
  14. package/dist/stim-frames/DeviceFrameView.swift +167 -0
  15. package/dist/stim-frames/DuoCaptureSource.swift +132 -0
  16. package/dist/stim-frames/DuoDeviceProfile.swift +37 -0
  17. package/dist/stim-frames/DuoFrameRenderer.swift +111 -0
  18. package/dist/stim-frames/DuoHingeMonitor.swift +89 -0
  19. package/dist/stim-frames/DuoMesh.swift +100 -0
  20. package/dist/stim-frames/DuoModelView.swift +275 -0
  21. package/dist/stim-frames/DuoPoseClip.swift +140 -0
  22. package/dist/stim-frames/DuoScreenHitMesh.swift +200 -0
  23. package/dist/stim-frames/EmulatorFrameArtwork.swift +113 -0
  24. package/dist/stim-frames/EmulatorInput.swift +13 -2
  25. package/dist/stim-frames/EmulatorPosture.swift +2 -2
  26. package/dist/stim-frames/FrameArtwork.swift +37 -0
  27. package/dist/stim-frames/H264Decoder.swift +109 -0
  28. package/dist/stim-frames/InputMessages.swift +27 -6
  29. package/dist/stim-frames/KeyboardLayoutMap.swift +70 -0
  30. package/dist/stim-frames/MacosSource.swift +677 -0
  31. package/dist/stim-frames/OwnedAppWindowReader.swift +87 -0
  32. package/dist/stim-frames/OwnedAppWindows.swift +108 -0
  33. package/dist/stim-frames/PhoneSource.swift +315 -0
  34. package/dist/stim-frames/ProcessStream.swift +155 -0
  35. package/dist/stim-frames/ScrcpyProtocol.swift +167 -0
  36. package/dist/stim-frames/SimulatorDevelopmentOptions.swift +81 -0
  37. package/dist/stim-frames/SimulatorFrameArtwork.swift +118 -0
  38. package/dist/stim-frames/SimulatorHID.swift +70 -29
  39. package/dist/stim-frames/SimulatorHingeAngle.swift +56 -0
  40. package/dist/stim-frames/SimulatorPosture.swift +277 -0
  41. package/dist/stim-frames/SimulatorRotation.swift +28 -8
  42. package/dist/stim-frames/Siniulator-LICENSE.txt +21 -0
  43. package/dist/stim-frames/StoragePaths.swift +42 -0
  44. package/dist/stim-frames/VideoEncoder.swift +8 -6
  45. package/dist/stim-frames/WebPage.swift +301 -0
  46. package/dist/stim-frames/main.swift +651 -22
  47. package/dist/stim-host/Info.plist +28 -0
  48. package/dist/stim-host/stim-host.c +153 -0
  49. package/dist/stim-server-OiJI9xLU.mjs +13336 -0
  50. package/dist/stim-server.mjs +6 -3880
  51. package/package.json +3 -3
  52. package/dist/protocol-rC-5KT1O.mjs +0 -822
@@ -0,0 +1,677 @@
1
+ import AppKit
2
+ import CoreImage
3
+ import Darwin
4
+ import ScreenCaptureKit
5
+
6
+ final class MacosSource: NSObject, Source, SCStreamDelegate, SCStreamOutput {
7
+ struct OwnedApp: Decodable {
8
+ struct Process: Decodable {
9
+ let pid: Int32
10
+ let startedAtMicros: UInt64
11
+ }
12
+ let bundle: String
13
+ let bundleId: String
14
+ let executable: String
15
+ let app: Process
16
+ }
17
+
18
+ private typealias WindowLocationSetter = @convention(c) (CGEvent, CGPoint) -> Void
19
+ private static let windowLocation: WindowLocationSetter? = {
20
+ guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "CGEventSetWindowLocation") else { return nil }
21
+ return unsafeBitCast(symbol, to: WindowLocationSetter.self)
22
+ }()
23
+
24
+ private static let screenPermission =
25
+ ProcessInfo.processInfo.operatingSystemVersion.majorVersion >= 15
26
+ ? "Screen & System Audio Recording" : "Screen Recording"
27
+ private static let controlPermission =
28
+ ProcessInfo.processInfo.operatingSystemVersion.majorVersion >= 27
29
+ ? "Device Control and Data Access" : "Accessibility"
30
+
31
+ private let app: OwnedApp
32
+ private let pacer: Pacer
33
+ private let video = videoEncoder()
34
+ private let jpegGate = JpegGate()
35
+ private let keyboardLayouts = KeyboardLayoutCache()
36
+ private var window: SCWindow?
37
+ private var reported: (selection: OwnedAppWindows.Selection?, pinned: Bool)?
38
+ private var pinned: (window: UInt32, session: String)?
39
+ private var switching = false
40
+ private var followTimer: DispatchSourceTimer?
41
+ private let followQueue = DispatchQueue(label: "stim.frames.macos.follow")
42
+ private let inputQueue = DispatchQueue(label: "stim.frames.macos.input")
43
+ private var controlSession: String?
44
+ private var heldMouse:
45
+ (session: String, window: (windowID: CGWindowID, frame: CGRect), accessible: AXUIElement, position: CGPoint)?
46
+ private var lastInput: Task<Void, Never>?
47
+ private var stream: SCStream?
48
+ private var pixels: CVPixelBuffer?
49
+ private var ownershipTimer: DispatchSourceTimer?
50
+
51
+ init(app: OwnedApp) {
52
+ self.app = app
53
+ var render: (Config) -> Void = { _ in }
54
+ pacer = Pacer { render($0) }
55
+ super.init()
56
+ render = { [unowned self] config in self.render(config) }
57
+ }
58
+
59
+ func start() {
60
+ guard matches() else { fail("The owned macOS app process changed or exited.") }
61
+ guard CGPreflightScreenCaptureAccess() else {
62
+ if let host = ProcessInfo.processInfo.environment["STIM_CAPTURE_HOST"] {
63
+ fail(
64
+ "\(Self.screenPermission) access is unavailable for stim-frames. Allow \(host) in System Settings > Privacy & Security > \(Self.screenPermission) on this Mac, or run `stim-server service install` again on this Mac to show the request, then reconnect. The server never requests or resets permissions; status and logs remain available."
65
+ )
66
+ }
67
+ fail(
68
+ "\(Self.screenPermission) access is unavailable for stim-frames. Allow the existing capture host in System Settings > Privacy & Security > \(Self.screenPermission), then reconnect. Open Permissions in Stim Desktop on this Mac to set up access, then reconnect. The server never requests or resets permissions; status and logs remain available."
69
+ )
70
+ }
71
+ Task { [self] in
72
+ do {
73
+ var found: OwnedAppWindows.Selection?
74
+ for attempt in 1... {
75
+ do {
76
+ found = try self.currentWindows()
77
+ break
78
+ } catch {
79
+ if attempt == 4 { throw error }
80
+ try await Task.sleep(for: .milliseconds(250))
81
+ }
82
+ }
83
+ guard let selection = found else {
84
+ fail(
85
+ AXIsProcessTrusted()
86
+ ? "Open a window in the owned macOS app to view it. Stim does not capture the desktop or other apps."
87
+ : "Open one visible window in the owned macOS app to view it. Without \(Self.controlPermission) permission Stim cannot tell which app window is in front, so it views only an app with one window."
88
+ )
89
+ }
90
+ guard try await self.capture(selection) else { fail("The owned macOS app window closed before capture started.") }
91
+ let timer = DispatchSource.makeTimerSource(queue: self.pacer.queue)
92
+ timer.schedule(deadline: .now() + 1, repeating: 1)
93
+ timer.setEventHandler { [weak self] in
94
+ guard let self else { return }
95
+ if !self.matches() { fail("The owned macOS app process changed or exited.") }
96
+ }
97
+ self.ownershipTimer = timer
98
+ timer.resume()
99
+ let follow = DispatchSource.makeTimerSource(queue: self.followQueue)
100
+ follow.schedule(deadline: .now() + .milliseconds(250), repeating: .milliseconds(250))
101
+ follow.setEventHandler { [weak self] in self?.follow() }
102
+ self.followTimer = follow
103
+ follow.resume()
104
+ } catch { fail("Owned macOS window capture failed: \(error.localizedDescription)") }
105
+ }
106
+ }
107
+
108
+ private func capture(_ selection: OwnedAppWindows.Selection) async throws -> Bool {
109
+ guard let window = try await shareable(selection.current.id) else { return false }
110
+ let filter = SCContentFilter(desktopIndependentWindow: window)
111
+ let stream = SCStream(filter: filter, configuration: Self.configuration(filter), delegate: self)
112
+ try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: pacer.queue)
113
+ try await stream.startCapture()
114
+ followQueue.sync {
115
+ self.stream = stream
116
+ self.window = window
117
+ report(selection)
118
+ }
119
+ return true
120
+ }
121
+
122
+ private static func configuration(_ filter: SCContentFilter) -> SCStreamConfiguration {
123
+ let configuration = SCStreamConfiguration()
124
+ configuration.width = Int((filter.contentRect.width * CGFloat(filter.pointPixelScale)).rounded(.up))
125
+ configuration.height = Int((filter.contentRect.height * CGFloat(filter.pointPixelScale)).rounded(.up))
126
+ configuration.ignoreShadowsSingleWindow = true
127
+ configuration.minimumFrameInterval = CMTime(value: 1, timescale: 30)
128
+ configuration.queueDepth = 3
129
+ configuration.showsCursor = false
130
+ return configuration
131
+ }
132
+
133
+ private func shareable(_ id: CGWindowID) async throws -> SCWindow? {
134
+ let content = try await SCShareableContent.excludingDesktopWindows(true, onScreenWindowsOnly: true)
135
+ guard matches() else { fail("The owned macOS app process changed or exited.") }
136
+ return content.windows.first {
137
+ $0.windowID == id && $0.owningApplication?.processID == app.app.pid
138
+ && $0.owningApplication?.bundleIdentifier == app.bundleId && $0.windowLayer == 0
139
+ }
140
+ }
141
+
142
+ private func currentWindows() throws -> OwnedAppWindows.Selection? {
143
+ do { return try OwnedAppWindowReader.selection(pid: app.app.pid) } catch {
144
+ throw refusal("The owned app's current window inventory is unavailable.")
145
+ }
146
+ }
147
+
148
+ private func isOpen(_ id: CGWindowID) -> Bool {
149
+ OwnedAppWindowReader.screen(pid: app.app.pid)?.contains { $0.id == id } == true
150
+ }
151
+
152
+ private func applyPin(_ selection: OwnedAppWindows.Selection, screen: [OwnedAppWindows.Screen]?) -> OwnedAppWindows.Selection {
153
+ guard pinned != nil else { return selection }
154
+ guard let screen = screen ?? OwnedAppWindowReader.screen(pid: app.app.pid) else { return selection }
155
+ let result = OwnedAppWindows.pin(selection, to: pinned?.window, screen: screen)
156
+ if result.pinned == nil { pinned = nil }
157
+ return result.selection
158
+ }
159
+
160
+ private func follow() {
161
+ guard !switching, matches() else { return }
162
+ let found: OwnedAppWindows.Selection?
163
+ do { found = try currentWindows() } catch { return }
164
+ guard let found else {
165
+ pinned = nil
166
+ return report(nil)
167
+ }
168
+ let selection = applyPin(found, screen: nil)
169
+ guard let stream, let captured = window else {
170
+ switching = true
171
+ Task {
172
+ do {
173
+ _ = try await self.capture(selection)
174
+ } catch {
175
+ if self.isOpen(selection.current.id) { fail("Owned macOS window capture failed: \(error.localizedDescription)") }
176
+ }
177
+ self.followQueue.sync { self.switching = false }
178
+ }
179
+ return
180
+ }
181
+ guard selection.current.id != captured.windowID || selection.current.frame.size != captured.frame.size else {
182
+ return report(selection)
183
+ }
184
+ switching = true
185
+ Task {
186
+ var next: SCWindow?
187
+ do {
188
+ next = try await self.shareable(selection.current.id)
189
+ if let next {
190
+ let filter = SCContentFilter(desktopIndependentWindow: next)
191
+ try await stream.updateContentFilter(filter)
192
+ try await stream.updateConfiguration(Self.configuration(filter))
193
+ }
194
+ } catch {
195
+ next = nil
196
+ try? await stream.stopCapture()
197
+ self.followQueue.sync { if self.stream === stream { self.stream = nil } }
198
+ }
199
+ self.followQueue.sync {
200
+ self.switching = false
201
+ guard let next else { return }
202
+ self.window = next
203
+ self.report(selection)
204
+ }
205
+ if next != nil { self.keyframe() }
206
+ }
207
+ }
208
+
209
+ private func report(_ found: OwnedAppWindows.Selection?) {
210
+ let shown: (OwnedAppWindows.Window) -> OwnedAppWindows.Window = { .init(id: $0.id, title: $0.title, frame: $0.frame, accessible: nil) }
211
+ let selection = found.map { OwnedAppWindows.Selection(current: shown($0.current), windows: $0.windows.map(shown)) }
212
+ let state = (selection: selection, pinned: pinned != nil)
213
+ if let reported, reported.selection == state.selection, reported.pinned == state.pinned { return }
214
+ if reported?.selection == nil || selection == nil {
215
+ let reason =
216
+ AXIsProcessTrusted()
217
+ ? "The owned macOS app has no open window."
218
+ : "Without \(Self.controlPermission) permission Stim views only an owned app whose one window contains the others."
219
+ Output.notice(["stalled": selection == nil ? reason : NSNull()])
220
+ }
221
+ reported = state
222
+ let json: (OwnedAppWindows.Window) -> [String: Any] = {
223
+ [
224
+ "id": Int($0.id), "title": $0.title,
225
+ "frame": ["x": $0.frame.minX, "y": $0.frame.minY, "width": $0.frame.width, "height": $0.frame.height],
226
+ ]
227
+ }
228
+ Output.notice([
229
+ "macosWindows": [
230
+ "current": selection.map { json($0.current) as Any } ?? NSNull(), "windows": (selection?.windows ?? []).map(json),
231
+ "pinned": state.pinned,
232
+ ]
233
+ ])
234
+ }
235
+
236
+ func configure(_ config: Config) {
237
+ video.configure(enabled: config.video, maxEdge: config.maxEdge, fps: Int(config.fps), bitrate: config.bitrate)
238
+ pacer.queue.async {
239
+ self.pacer.config = config
240
+ self.pacer.changed()
241
+ }
242
+ }
243
+
244
+ func keyframe() {
245
+ video.requestKeyframe()
246
+ pacer.changed()
247
+ }
248
+
249
+ func recordKeyframe() {}
250
+
251
+ func stop() {
252
+ inputQueue.sync {
253
+ if let held = heldMouse { releaseMouse(held.session) }
254
+ controlSession = nil
255
+ }
256
+ }
257
+
258
+ func input(_ command: Command) {
259
+ inputQueue.async {
260
+ switch command {
261
+ case .control(let session, let enabled):
262
+ if enabled {
263
+ if let held = self.heldMouse { self.releaseMouse(held.session) }
264
+ self.controlSession = session
265
+ self.followQueue.async { if self.pinned?.session != session { self.pinned = nil } }
266
+ } else if self.controlSession == session {
267
+ self.releaseMouse(session)
268
+ self.controlSession = nil
269
+ self.unpin(session)
270
+ }
271
+ case .scoped(let session, let action):
272
+ let previous = self.lastInput
273
+ self.lastInput = Task {
274
+ await previous?.value
275
+ guard self.isActive(session) else { return }
276
+ do { try await self.apply(action, session: session) } catch is CancellationError {
277
+ } catch let changed as Dropped {
278
+ Output.notice(["inputError": changed.localizedDescription])
279
+ } catch {
280
+ if self.endControl(session) { Output.notice(["inputError": error.localizedDescription, "controlSession": session]) }
281
+ }
282
+ }
283
+ default: break
284
+ }
285
+ }
286
+ }
287
+
288
+ private func isActive(_ session: String) -> Bool {
289
+ inputQueue.sync { controlSession == session }
290
+ }
291
+
292
+ private func endControl(_ session: String) -> Bool {
293
+ inputQueue.sync {
294
+ guard controlSession == session else { return false }
295
+ releaseMouse(session)
296
+ controlSession = nil
297
+ unpin(session)
298
+ return true
299
+ }
300
+ }
301
+
302
+ /// A pin lasts only as long as the Control session that chose it.
303
+ private func unpin(_ session: String) {
304
+ followQueue.async { if self.pinned?.session == session { self.pinned = nil } }
305
+ }
306
+
307
+ private func releaseMouse(_ session: String) {
308
+ guard let held = heldMouse, held.session == session else { return }
309
+ heldMouse = nil
310
+ guard matches(), AXIsProcessTrusted(), CGPreflightPostEventAccess(),
311
+ let entries = CGWindowListCopyWindowInfo(.optionIncludingWindow, held.window.windowID) as? [[String: Any]],
312
+ entries.count == 1, let entry = entries.first,
313
+ entry[kCGWindowOwnerPID as String] as? Int == Int(app.app.pid),
314
+ entry[kCGWindowLayer as String] as? Int == 0,
315
+ let bounds = entry[kCGWindowBounds as String] as? [String: Any],
316
+ let frame = CGRect(dictionaryRepresentation: bounds as CFDictionary), frame == held.window.frame
317
+ else { return }
318
+ var focused: CFTypeRef?
319
+ guard
320
+ AXUIElementCopyAttributeValue(
321
+ AXUIElementCreateApplication(app.app.pid), kAXFocusedWindowAttribute as CFString, &focused) == .success,
322
+ let focused, CFEqual(focused, held.accessible),
323
+ let native = NSEvent.mouseEvent(
324
+ with: .leftMouseUp,
325
+ location: CGPoint(x: held.position.x - held.window.frame.minX, y: held.window.frame.maxY - held.position.y),
326
+ modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: Int(held.window.windowID),
327
+ context: nil, eventNumber: 0, clickCount: 1, pressure: 0), let event = native.cgEvent,
328
+ let setWindowLocation = Self.windowLocation,
329
+ matches()
330
+ else { return }
331
+ event.location = held.position
332
+ setWindowLocation(event, CGPoint(x: held.position.x - held.window.frame.minX, y: held.position.y - held.window.frame.minY))
333
+ event.setIntegerValueField(.mouseEventWindowUnderMousePointer, value: Int64(held.window.windowID))
334
+ event.setIntegerValueField(.mouseEventWindowUnderMousePointerThatCanHandleThisEvent, value: Int64(held.window.windowID))
335
+ event.postToPid(app.app.pid)
336
+ }
337
+
338
+ // NSRunningApplication(processIdentifier:) returns nil for 1-2 ms at a time on macOS 27 while the process runs.
339
+ private static func runningApplication(_ pid: pid_t) -> NSRunningApplication? {
340
+ for attempt in 0..<4 {
341
+ if attempt > 0 { usleep(5_000) }
342
+ if let running = NSRunningApplication(processIdentifier: pid) { return running }
343
+ }
344
+ return nil
345
+ }
346
+
347
+ private struct Dropped: LocalizedError {
348
+ var errorDescription: String? = "The owned app's window changed before input, so the input was not sent."
349
+ }
350
+
351
+ private func refusal(_ message: String) -> NSError {
352
+ NSError(domain: "StimFrames", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
353
+ }
354
+
355
+ private func inputWindow() throws -> ((windowID: CGWindowID, frame: CGRect), AXUIElement) {
356
+ guard matches(), let captured = followQueue.sync(execute: { window }) else {
357
+ throw refusal("The captured owned macOS app window is unavailable.")
358
+ }
359
+ guard AXIsProcessTrusted(), CGPreflightPostEventAccess() else {
360
+ let permission = Self.controlPermission
361
+ if let host = ProcessInfo.processInfo.environment["STIM_CAPTURE_HOST"] {
362
+ throw refusal(
363
+ "Control needs \(permission) permission. Allow \(host) in System Settings > Privacy & Security > \(permission) on this Mac, or run `stim-server service install` again on this Mac to show the request, then reconnect. The server never requests or resets permissions; viewing and logs remain available."
364
+ )
365
+ }
366
+ throw refusal(
367
+ "Control needs \(permission) permission for the capture host. Open Permissions in Stim Desktop on this Mac, or allow the host in System Settings > Privacy & Security > \(permission), then reconnect. The server never requests or resets permissions; viewing and logs remain available."
368
+ )
369
+ }
370
+ guard let accessible = OwnedAppWindowReader.accessible(pid: app.app.pid) else {
371
+ throw refusal("The owned app's accessible windows are unavailable.")
372
+ }
373
+ guard !accessible.modal else { throw refusal("Control needs accessible owned app windows without a modal dialog.") }
374
+ guard let screen = OwnedAppWindowReader.screen(pid: app.app.pid) else {
375
+ throw refusal("The owned app's current window inventory is unavailable.")
376
+ }
377
+ guard let found = OwnedAppWindows.select(screen: screen, accessible: accessible.windows), matches() else {
378
+ throw Dropped()
379
+ }
380
+ let selection = followQueue.sync { applyPin(found, screen: screen) }
381
+ guard selection.current.id == captured.windowID,
382
+ selection.current.frame.size == captured.frame.size, let index = selection.current.accessible
383
+ else { throw Dropped() }
384
+ return ((selection.current.id, selection.current.frame), accessible.elements[index])
385
+ }
386
+
387
+ private func attachedSheets(_ own: AXUIElement) -> [AXUIElement] {
388
+ var value: CFTypeRef?
389
+ guard AXUIElementCopyAttributeValue(own, kAXChildrenAttribute as CFString, &value) == .success,
390
+ let children = value as? [AXUIElement]
391
+ else { return [] }
392
+ var sheets: [AXUIElement] = []
393
+ for child in children {
394
+ var role: CFTypeRef?
395
+ if AXUIElementCopyAttributeValue(child, kAXRoleAttribute as CFString, &role) == .success,
396
+ role as? String == kAXSheetRole
397
+ {
398
+ sheets.append(child)
399
+ sheets.append(contentsOf: attachedSheets(child))
400
+ }
401
+ }
402
+ return sheets
403
+ }
404
+
405
+ private func isFocused(_ own: AXUIElement, application: AXUIElement) -> Bool {
406
+ var focused: CFTypeRef?
407
+ return AXUIElementCopyAttributeValue(application, kAXFocusedWindowAttribute as CFString, &focused) == .success
408
+ && focused.map { value in ([own] + attachedSheets(own)).contains { CFEqual(value, $0) } } == true
409
+ }
410
+
411
+ /// Pins capture and Control to the app's window `id`, raising it, or with nil follows the front window again.
412
+ private func select(_ id: UInt32?, session: String) throws {
413
+ guard let id else {
414
+ followQueue.sync { pinned = nil }
415
+ return
416
+ }
417
+ guard matches(), AXIsProcessTrusted(), let accessible = OwnedAppWindowReader.accessible(pid: app.app.pid),
418
+ let screen = OwnedAppWindowReader.screen(pid: app.app.pid)
419
+ else { throw Dropped(errorDescription: "The owned app's windows are unavailable, so the window was not chosen.") }
420
+ guard !accessible.modal else {
421
+ throw Dropped(errorDescription: "The owned app shows a modal dialog, so the window was not chosen.")
422
+ }
423
+ guard let selection = OwnedAppWindows.select(screen: screen, accessible: accessible.windows),
424
+ let index = selection.windows.first(where: { $0.id == id })?.accessible, matches()
425
+ else { throw Dropped() }
426
+ let element = accessible.elements[index]
427
+ _ = AXUIElementSetAttributeValue(element, kAXMainAttribute as CFString, kCFBooleanTrue)
428
+ _ = AXUIElementPerformAction(element, kAXRaiseAction as CFString)
429
+ guard isActive(session) else { throw CancellationError() }
430
+ followQueue.sync {
431
+ pinned = (id, session)
432
+ follow()
433
+ }
434
+ }
435
+
436
+ private func apply(_ command: Command, session: String) async throws {
437
+ if case .window(let id) = command { return try select(id, session: session) }
438
+ guard let setWindowLocation = Self.windowLocation else {
439
+ throw refusal("Native window input is unavailable on this macOS version. Viewing and logs remain available.")
440
+ }
441
+ let (window, own) = try inputWindow()
442
+ guard isActive(session) else { throw CancellationError() }
443
+ let application = AXUIElementCreateApplication(app.app.pid)
444
+ if !isFocused(own, application: application) {
445
+ guard let running = Self.runningApplication(app.app.pid)
446
+ else { throw refusal("The captured owned window could not be focused for Control.") }
447
+ Output.notice([
448
+ "controlActivated": "The captured window was not the app's key window, so Stim raised it and activated the app to deliver input."
449
+ ])
450
+ let raise = AXUIElementPerformAction(own, kAXRaiseAction as CFString)
451
+ let focus = AXUIElementSetAttributeValue(application, kAXFocusedWindowAttribute as CFString, own)
452
+ let activated = running.activate(options: [])
453
+ let deadline = ContinuousClock.now.advanced(by: .seconds(1))
454
+ while !(running.isActive && isFocused(own, application: application)) {
455
+ guard isActive(session), matches() else { throw CancellationError() }
456
+ guard activated, ContinuousClock.now < deadline else {
457
+ throw refusal(
458
+ "The captured owned window could not be focused for Control. (raise \(raise.rawValue), focus \(focus.rawValue), activate \(activated))"
459
+ )
460
+ }
461
+ try await Task.sleep(for: .milliseconds(10))
462
+ }
463
+ }
464
+ var focusedValue: CFTypeRef?
465
+ guard AXUIElementCopyAttributeValue(application, kAXFocusedWindowAttribute as CFString, &focusedValue) == .success,
466
+ let focusedValue, let focused = ([own] + attachedSheets(own)).first(where: { CFEqual(focusedValue, $0) }), matches()
467
+ else { throw refusal("The captured owned window lost focus before input.") }
468
+ var sheetWindow: (windowID: CGWindowID, frame: CGRect)?
469
+ if !CFEqual(focused, own) {
470
+ var positionValue: CFTypeRef?
471
+ var sizeValue: CFTypeRef?
472
+ var position = CGPoint.zero
473
+ var size = CGSize.zero
474
+ guard AXUIElementCopyAttributeValue(focused, kAXPositionAttribute as CFString, &positionValue) == .success,
475
+ AXUIElementCopyAttributeValue(focused, kAXSizeAttribute as CFString, &sizeValue) == .success,
476
+ let positionValue, let sizeValue, CFGetTypeID(positionValue) == AXValueGetTypeID(),
477
+ CFGetTypeID(sizeValue) == AXValueGetTypeID(),
478
+ AXValueGetValue(unsafeDowncast(positionValue, to: AXValue.self), .cgPoint, &position),
479
+ AXValueGetValue(unsafeDowncast(sizeValue, to: AXValue.self), .cgSize, &size),
480
+ let entries = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID)
481
+ as? [[String: Any]]
482
+ else { throw refusal("The focused owned sheet's window metadata is unavailable.") }
483
+ let frame = CGRect(origin: position, size: size)
484
+ guard
485
+ let entry = entries.first(where: {
486
+ guard $0[kCGWindowOwnerPID as String] as? Int == Int(app.app.pid),
487
+ $0[kCGWindowLayer as String] as? Int == 0,
488
+ let id = $0[kCGWindowNumber as String] as? UInt32, id != window.windowID,
489
+ let bounds = $0[kCGWindowBounds as String] as? [String: Any]
490
+ else { return false }
491
+ return CGRect(dictionaryRepresentation: bounds as CFDictionary) == frame
492
+ }), let id = entry[kCGWindowNumber as String] as? UInt32
493
+ else { throw refusal("The focused owned sheet does not match an on-screen app window.") }
494
+ sheetWindow = (id, frame)
495
+ }
496
+ let location: (CGPoint) -> CGPoint = {
497
+ CGPoint(x: window.frame.minX + $0.x * (window.frame.width - 1), y: window.frame.minY + $0.y * (window.frame.height - 1))
498
+ }
499
+ let pointerWindow: (CGPoint) -> (windowID: CGWindowID, frame: CGRect) = { point in
500
+ if let sheetWindow, sheetWindow.frame.contains(point) { return sheetWindow }
501
+ return window
502
+ }
503
+ let post: ([CGEvent], (windowID: CGWindowID, frame: CGRect)) throws -> Void = { events, target in
504
+ try self.inputQueue.sync {
505
+ guard self.controlSession == session else { throw CancellationError() }
506
+ var focusedValue: CFTypeRef?
507
+ guard self.matches(),
508
+ AXUIElementCopyAttributeValue(application, kAXFocusedWindowAttribute as CFString, &focusedValue) == .success,
509
+ let focusedValue, CFEqual(focusedValue, focused)
510
+ else { throw self.refusal("The captured owned window changed before input.") }
511
+ for event in events {
512
+ guard self.matches() else { throw self.refusal("The owned macOS app process changed before input.") }
513
+ event.setIntegerValueField(.mouseEventWindowUnderMousePointer, value: Int64(target.windowID))
514
+ event.setIntegerValueField(.mouseEventWindowUnderMousePointerThatCanHandleThisEvent, value: Int64(target.windowID))
515
+ // AppKit PID-targeted pointer events need CoreGraphics' private window-local annotation.
516
+ if [.leftMouseDown, .leftMouseUp, .leftMouseDragged, .scrollWheel].contains(event.type) {
517
+ setWindowLocation(event, CGPoint(x: event.location.x - target.frame.minX, y: event.location.y - target.frame.minY))
518
+ }
519
+ event.postToPid(self.app.app.pid)
520
+ switch event.type {
521
+ case .leftMouseDown:
522
+ self.heldMouse = (session, target, focused, event.location)
523
+ case .leftMouseDragged:
524
+ if self.heldMouse?.session == session { self.heldMouse?.position = event.location }
525
+ case .leftMouseUp:
526
+ if self.heldMouse?.session == session { self.heldMouse = nil }
527
+ default: break
528
+ }
529
+ }
530
+ }
531
+ }
532
+ let key: (CGKeyCode, CGEventFlags, String?) throws -> Void = { code, flags, text in
533
+ var events: [CGEvent] = []
534
+ for down in [true, false] {
535
+ guard let event = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: down) else {
536
+ throw self.refusal("The native keyboard event could not be created.")
537
+ }
538
+ event.flags = flags
539
+ if let text {
540
+ let utf16 = Array(text.utf16)
541
+ event.keyboardSetUnicodeString(stringLength: utf16.count, unicodeString: utf16)
542
+ }
543
+ events.append(event)
544
+ }
545
+ try post(events, window)
546
+ }
547
+ switch command {
548
+ case .touch(let phase, let point, _):
549
+ let type: NSEvent.EventType = phase == .down ? .leftMouseDown : phase == .up ? .leftMouseUp : .leftMouseDragged
550
+ let global = location(point)
551
+ let target = inputQueue.sync {
552
+ if phase != .down, let held = heldMouse, held.session == session { return held.window }
553
+ return pointerWindow(global)
554
+ }
555
+ guard
556
+ let native = NSEvent.mouseEvent(
557
+ with: type, location: CGPoint(x: global.x - target.frame.minX, y: target.frame.maxY - global.y),
558
+ modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: Int(target.windowID),
559
+ context: nil, eventNumber: 0, clickCount: 1, pressure: phase == .up ? 0 : 1), let event = native.cgEvent
560
+ else { throw refusal("The native mouse event could not be created.") }
561
+ event.location = global
562
+ try post([event], target)
563
+ case .scroll(let point, let delta):
564
+ let global = location(point)
565
+ let target = pointerWindow(global)
566
+ guard
567
+ let native = NSEvent.mouseEvent(
568
+ with: .mouseMoved, location: CGPoint(x: global.x - target.frame.minX, y: target.frame.maxY - global.y),
569
+ modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: Int(target.windowID),
570
+ context: nil, eventNumber: 0, clickCount: 0, pressure: 0), let event = native.cgEvent,
571
+ let wheel = CGEvent(
572
+ scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 2,
573
+ wheel1: Int32(delta.y.rounded()), wheel2: Int32(delta.x.rounded()), wheel3: 0)
574
+ else { throw refusal("The native scroll event could not be created.") }
575
+ event.type = .scrollWheel
576
+ for field: CGEventField in [
577
+ .scrollWheelEventDeltaAxis1, .scrollWheelEventDeltaAxis2,
578
+ .scrollWheelEventPointDeltaAxis1, .scrollWheelEventPointDeltaAxis2, .scrollWheelEventIsContinuous,
579
+ ] {
580
+ event.setIntegerValueField(field, value: wheel.getIntegerValueField(field))
581
+ }
582
+ for field: CGEventField in [.scrollWheelEventFixedPtDeltaAxis1, .scrollWheelEventFixedPtDeltaAxis2] {
583
+ event.setDoubleValueField(field, value: wheel.getDoubleValueField(field))
584
+ }
585
+ event.location = global
586
+ try post([event], target)
587
+ case .text(let text):
588
+ for character in text {
589
+ let special: [Character: CGKeyCode] = ["\n": 36, "\t": 48, "\u{0008}": 51]
590
+ try key(special[character] ?? 0, [], special[character] == nil ? String(character) : nil)
591
+ }
592
+ case .key(let name, let modifiers):
593
+ let codes: [String: CGKeyCode] = [
594
+ "escape": 53, "tab": 48, "return": 36, "backspace": 51,
595
+ "left": 123, "right": 124, "down": 125, "up": 126,
596
+ ]
597
+ let flags: [String: CGEventFlags] = [
598
+ "command": .maskCommand, "shift": .maskShift, "option": .maskAlternate, "control": .maskControl,
599
+ ]
600
+ guard modifiers.allSatisfy({ flags[$0] != nil }) else { throw refusal("Unsupported native key.") }
601
+ let code: CGKeyCode
602
+ if name.count == 1 {
603
+ guard let character = name.first, "abcdefghijklmnopqrstuvwxyz0123456789".contains(character) else {
604
+ throw refusal("Unsupported native key.")
605
+ }
606
+ let layout = try DispatchQueue.main.sync {
607
+ guard isActive(session), matches() else {
608
+ throw refusal("The Control session or owned app changed before reading its keyboard layout.")
609
+ }
610
+ return keyboardLayouts.current()
611
+ }
612
+ guard let layout else {
613
+ throw refusal(
614
+ "The Mac's keyboard layout could not be read, so native letter and digit shortcuts cannot be sent. Ordinary typing and navigation do not require it."
615
+ )
616
+ }
617
+ let command = modifiers.contains("command")
618
+ let control = modifiers.contains("control")
619
+ guard let resolved = layout.map.keyCode(for: character, command: command, control: control) else {
620
+ let layer = control ? (command ? " with Command and Control" : " with Control") : (command ? " with Command" : "")
621
+ throw refusal(
622
+ "The Mac's current keyboard layout (\(layout.id)) has no key that types \"\(character)\"\(layer), so the shortcut was not sent. Select an input source that has it and start Control again. Ordinary typing and navigation do not require it."
623
+ )
624
+ }
625
+ code = resolved
626
+ } else {
627
+ guard let navigation = codes[name] else { throw refusal("Unsupported native key.") }
628
+ code = navigation
629
+ }
630
+ try key(code, modifiers.reduce(CGEventFlags()) { $0.union(flags[$1]!) }, nil)
631
+ default: throw refusal("Unsupported native macOS input.")
632
+ }
633
+ }
634
+
635
+ func stream(_ stream: SCStream, didStopWithError error: Error) {
636
+ followQueue.asyncAfter(deadline: .now() + .milliseconds(500)) {
637
+ guard self.stream === stream else { return }
638
+ if let id = self.window?.windowID, self.isOpen(id) {
639
+ fail("Owned macOS window capture stopped: \(error.localizedDescription)")
640
+ }
641
+ self.stream = nil
642
+ }
643
+ }
644
+
645
+ func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
646
+ guard type == .screen, let buffer = sampleBuffer.imageBuffer else { return }
647
+ guard matches() else { fail("The owned macOS app process changed or exited.") }
648
+ pixels = buffer
649
+ pacer.changed()
650
+ }
651
+
652
+ private func render(_ config: Config) {
653
+ guard matches() else { fail("The owned macOS app process changed or exited.") }
654
+ guard let pixels else { return }
655
+ if config.video { video.encode(pixels, quarterTurns: 0, capturedAt: now()) }
656
+ if config.jpeg, jpegGate.admit(config, pacer: pacer),
657
+ let (data, width, height) = jpeg(CIImage(cvPixelBuffer: pixels), config: config)
658
+ {
659
+ Output.frame(jpeg: data, width: width, height: height)
660
+ }
661
+ }
662
+
663
+ private func matches() -> Bool {
664
+ var info = proc_bsdinfo()
665
+ guard app.app.pid > 0,
666
+ proc_pidinfo(app.app.pid, PROC_PIDTBSDINFO, 0, &info, Int32(MemoryLayout<proc_bsdinfo>.size))
667
+ == MemoryLayout<proc_bsdinfo>.size,
668
+ info.pbi_start_tvsec * 1_000_000 + info.pbi_start_tvusec == app.app.startedAtMicros,
669
+ let running = Self.runningApplication(app.app.pid),
670
+ running.bundleIdentifier == app.bundleId,
671
+ running.executableURL?.resolvingSymlinksInPath().path
672
+ == URL(fileURLWithPath: app.executable).resolvingSymlinksInPath().path,
673
+ running.bundleURL?.resolvingSymlinksInPath().path == URL(fileURLWithPath: app.bundle).resolvingSymlinksInPath().path
674
+ else { return false }
675
+ return true
676
+ }
677
+ }