agent-device 0.21.6 → 0.21.8

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 (317) hide show
  1. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.6.apk → agent-device-android-ime-helper-0.21.8.apk} +0 -0
  2. package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.8.apk.sha256 +1 -0
  3. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.6.manifest.json → agent-device-android-ime-helper-0.21.8.manifest.json} +4 -4
  4. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.6.apk → agent-device-android-snapshot-helper-0.21.8.apk} +0 -0
  5. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.8.apk.sha256 +1 -0
  6. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.6.manifest.json → agent-device-android-snapshot-helper-0.21.8.manifest.json} +6 -6
  7. package/apple/macos-helper/Package.swift +16 -1
  8. package/apple/macos-helper/Sources/AgentDeviceMacOSDeviceHub/DeviceHubPose.swift +47 -0
  9. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubPose.swift +320 -0
  10. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +66 -13
  11. package/apple/macos-helper/Sources/AgentDeviceMacOSInput/MouseClickDelivery.swift +116 -0
  12. package/apple/macos-helper/Sources/AgentDeviceMacOSInput/MouseClickSchedule.swift +61 -0
  13. package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubPoseTests.swift +30 -0
  14. package/apple/macos-helper/Tests/AgentDeviceMacOSInputTests/MouseClickScheduleTests.swift +58 -0
  15. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +4 -0
  16. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +20 -0
  17. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerScrollViewportPolicy.swift +1 -2
  18. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +25 -12
  19. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +56 -1
  20. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +1 -1
  21. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +36 -108
  22. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Keyboard.swift +241 -0
  23. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +29 -0
  24. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +46 -0
  25. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+PrivateAXPresentation.swift +85 -26
  26. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +134 -117
  27. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotBackendCapabilities.swift +1 -3
  28. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +25 -4
  29. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +1 -1
  30. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +3 -0
  31. package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotCoordinateSpace.swift +197 -0
  32. package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotModels.swift +30 -4
  33. package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotPresentation.swift +0 -81
  34. package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotPresentationInvariant.swift +1 -9
  35. package/dist/src/absence-observation-errors.js +1 -1
  36. package/dist/src/adapter.js +1 -1
  37. package/dist/src/adb-executor.js +2 -2
  38. package/dist/src/adb-failure.js +1 -1
  39. package/dist/src/adb-transport.js +1 -1
  40. package/dist/src/adb.js +1 -1
  41. package/dist/src/agent-browser-lifecycle.js +1 -1
  42. package/dist/src/agent-browser-process-record.js +1 -1
  43. package/dist/src/agent-browser-provider.js +1 -1
  44. package/dist/src/agent-browser-tool.js +1 -1
  45. package/dist/src/agent-device-client.js +2 -2
  46. package/dist/src/ai-sdk.js +1 -1
  47. package/dist/src/alert-runtime.js +1 -1
  48. package/dist/src/android-adb.d.ts +29 -2
  49. package/dist/src/android-adb.js +1 -1
  50. package/dist/src/android-input-ownership.js +1 -1
  51. package/dist/src/android.js +1 -1
  52. package/dist/src/app-catalog.js +1 -1
  53. package/dist/src/app-deployment-resolution.js +1 -1
  54. package/dist/src/app-helpers.js +1 -1
  55. package/dist/src/app-inventory.js +1 -1
  56. package/dist/src/app-launch.js +2 -2
  57. package/dist/src/app-lifecycle.js +2 -2
  58. package/dist/src/app-lifecycle2.js +2 -2
  59. package/dist/src/app-log-files.js +1 -1
  60. package/dist/src/app-log-runtime2.js +3 -3
  61. package/dist/src/app-resolution.js +2 -2
  62. package/dist/src/app-state.js +1 -1
  63. package/dist/src/apple-runner-platform.js +1 -1
  64. package/dist/src/application-lifecycle-interaction.js +1 -1
  65. package/dist/src/archive.js +1 -1
  66. package/dist/src/artifact-download.js +1 -1
  67. package/dist/src/auth-session.js +1 -1
  68. package/dist/src/auth.js +1 -1
  69. package/dist/src/back-mode.js +1 -1
  70. package/dist/src/backend-snapshot-options.js +1 -1
  71. package/dist/src/boot-diagnostics.js +1 -1
  72. package/dist/src/capture-disclosure.js +1 -0
  73. package/dist/src/capture.js +1 -1
  74. package/dist/src/cli-config.js +1 -1
  75. package/dist/src/cli-help.js +18 -2
  76. package/dist/src/cli-output.js +1 -1
  77. package/dist/src/cli.js +4 -4
  78. package/dist/src/click-button.js +1 -1
  79. package/dist/src/client-metro.js +1 -1
  80. package/dist/src/client-types.d.ts +65 -3
  81. package/dist/src/clipboard-shell-response.js +1 -0
  82. package/dist/src/command-schema.js +1 -1
  83. package/dist/src/command-tools.js +4 -2
  84. package/dist/src/command.js +1 -1
  85. package/dist/src/config.js +1 -1
  86. package/dist/src/connection-runtime.js +1 -1
  87. package/dist/src/connection.js +1 -1
  88. package/dist/src/contracts.d.ts +1 -1
  89. package/dist/src/contracts.js +1 -1
  90. package/dist/src/daemon-client-lifecycle.js +1 -1
  91. package/dist/src/daemon.js +1 -1
  92. package/dist/src/debug-symbols-facade.js +1 -1
  93. package/dist/src/device-boot.js +1 -1
  94. package/dist/src/device-claim-inspection.js +1 -1
  95. package/dist/src/device-claim-rule.js +2 -1
  96. package/dist/src/device-input-state.d.ts +5 -5
  97. package/dist/src/device-inventory-context.js +1 -1
  98. package/dist/src/device-rotation.js +1 -1
  99. package/dist/src/device-selection-resolver.js +1 -1
  100. package/dist/src/device-session.js +1 -1
  101. package/dist/src/device-shell.js +1 -0
  102. package/dist/src/device.js +1 -1
  103. package/dist/src/device2.js +1 -1
  104. package/dist/src/devicectl.js +1 -0
  105. package/dist/src/diagnostics.js +1 -1
  106. package/dist/src/dispatch-resolve.js +1 -1
  107. package/dist/src/display-inventory.js +1 -0
  108. package/dist/src/doctor-facade.js +1 -1
  109. package/dist/src/doctor2.js +1 -1
  110. package/dist/src/durable-resource.js +1 -1
  111. package/dist/src/engine-eval-script.js +1 -1
  112. package/dist/src/error.js +1 -1
  113. package/dist/src/exec.js +1 -1
  114. package/dist/src/file.js +1 -1
  115. package/dist/src/find.js +1 -1
  116. package/dist/src/finders.js +1 -1
  117. package/dist/src/format.js +2 -1
  118. package/dist/src/generic.js +1 -1
  119. package/dist/src/gesture-admission.js +1 -1
  120. package/dist/src/gesture-plan.js +1 -1
  121. package/dist/src/gesture-runtime.js +1 -1
  122. package/dist/src/harmonyos.js +1 -1
  123. package/dist/src/hdc.js +1 -1
  124. package/dist/src/helper.js +2 -2
  125. package/dist/src/host.js +1 -0
  126. package/dist/src/human-control-contract.js +1 -1
  127. package/dist/src/human-control.js +1 -1
  128. package/dist/src/index.d.ts +1 -1
  129. package/dist/src/index.js +1 -1
  130. package/dist/src/input-actions.js +1 -1
  131. package/dist/src/input-actions2.js +1 -1
  132. package/dist/src/install-artifact-facade.js +1 -1
  133. package/dist/src/install-source-network-transport.js +1 -1
  134. package/dist/src/install-source2.js +1 -1
  135. package/dist/src/interaction-positionals.js +1 -1
  136. package/dist/src/interaction-ref-policy.js +1 -1
  137. package/dist/src/interaction.js +1 -1
  138. package/dist/src/interaction2.js +1 -1
  139. package/dist/src/interactor-operation-binding.js +1 -0
  140. package/dist/src/interactor-operation-catalog.js +1 -1
  141. package/dist/src/interactor.js +4 -5
  142. package/dist/src/interactor2.js +1 -1
  143. package/dist/src/interactors.js +1 -1
  144. package/dist/src/internal/bin.js +1 -1
  145. package/dist/src/internal/daemon.js +2 -2
  146. package/dist/src/internal/png-worker.d.ts +9 -1
  147. package/dist/src/internal/png-worker.js +1 -1
  148. package/dist/src/internal/run-script-http-child.js +1 -1
  149. package/dist/src/inventory.js +4 -4
  150. package/dist/src/inventory2.js +1 -1
  151. package/dist/src/inventory3.js +2 -2
  152. package/dist/src/inventory5.js +1 -1
  153. package/dist/src/ios-snapshot-acquisition.js +1 -1
  154. package/dist/src/ios-snapshot-runtime.js +1 -1
  155. package/dist/src/ios-target-activation.js +1 -0
  156. package/dist/src/lease-context.js +1 -1
  157. package/dist/src/lease-scope.js +1 -1
  158. package/dist/src/lease.js +1 -1
  159. package/dist/src/limrun-download-file.js +1 -0
  160. package/dist/src/limrun-runtime-dependencies.js +1 -1
  161. package/dist/src/limrun.d.ts +10 -4
  162. package/dist/src/linux.js +1 -1
  163. package/dist/src/local-interactor-operation-set.js +1 -1
  164. package/dist/src/location-coordinates.js +1 -1
  165. package/dist/src/macos-facade.js +1 -1
  166. package/dist/src/managed-device-scope.js +1 -1
  167. package/dist/src/mechanics.js +17 -16
  168. package/dist/src/mobile-snapshot-semantics.js +1 -1
  169. package/dist/src/multitouch-support.js +1 -1
  170. package/dist/src/observation.js +2 -2
  171. package/dist/src/output-common.js +5 -2
  172. package/dist/src/parameterized-recorded-fill.js +1 -1
  173. package/dist/src/perf-facade.js +2 -2
  174. package/dist/src/perf-runtime-operation-builder.js +1 -1
  175. package/dist/src/perf-runtime-plan.js +1 -1
  176. package/dist/src/perf.js +1 -1
  177. package/dist/src/physical-device-control.js +1 -1
  178. package/dist/src/platform-request-scope.js +1 -1
  179. package/dist/src/platform-runtime-android-adb-host.js +1 -1
  180. package/dist/src/platform-runtime-host.js +1 -1
  181. package/dist/src/platform-runtime-managed-owner.js +1 -1
  182. package/dist/src/platform-runtime-open-target.js +1 -1
  183. package/dist/src/platform-runtime-operation-host.js +4 -4
  184. package/dist/src/platform-runtime-runtime-hints.js +2 -2
  185. package/dist/src/platform-runtime-screen-recording-android-host.js +1 -1
  186. package/dist/src/platform-runtime-screen-recording-apple-runner-transport.js +1 -1
  187. package/dist/src/platform-runtime-screen-recording-apple-simulator-host.js +1 -1
  188. package/dist/src/platform-runtime-screen-recording-apple-transport.js +1 -1
  189. package/dist/src/platform-runtime-screen-recording-finalizer-host.js +1 -1
  190. package/dist/src/platform-runtime-screen-recording-harmony-host.js +1 -1
  191. package/dist/src/platform-runtime-screen-recording-process-host.js +1 -1
  192. package/dist/src/platform-runtime-unavailable.js +1 -1
  193. package/dist/src/platform-runtime.js +1 -1
  194. package/dist/src/platform-runtime2.js +1 -1
  195. package/dist/src/png-crop-bytes.js +1 -1
  196. package/dist/src/png-worker-client.js +1 -0
  197. package/dist/src/png.js +1 -1
  198. package/dist/src/prepare-kind.js +1 -1
  199. package/dist/src/provider-session-ownership.js +1 -1
  200. package/dist/src/provider-webdriver.js +1 -1
  201. package/dist/src/proxy.js +1 -1
  202. package/dist/src/quality-warnings.js +1 -1
  203. package/dist/src/react-native.js +1 -1
  204. package/dist/src/record-runtime.js +1 -1
  205. package/dist/src/record-trace.js +1 -1
  206. package/dist/src/rect-center.js +1 -1
  207. package/dist/src/register-builtins.js +1 -1
  208. package/dist/src/registry.js +35 -35
  209. package/dist/src/remote-config2.js +1 -1
  210. package/dist/src/replay-divergence.js +1 -1
  211. package/dist/src/replay.js +1 -1
  212. package/dist/src/reporting.js +1 -1
  213. package/dist/src/request-admission.js +1 -1
  214. package/dist/src/request-cancel.js +1 -1
  215. package/dist/src/request-runtime-binding.js +1 -1
  216. package/dist/src/result-serialization.js +1 -1
  217. package/dist/src/retry.js +1 -1
  218. package/dist/src/runner-cache-metadata.js +2 -2
  219. package/dist/src/runner-client.js +1 -1
  220. package/dist/src/runner-contract.js +3 -0
  221. package/dist/src/runner-dev-tools-security.js +1 -0
  222. package/dist/src/runner-device-readiness.js +1 -0
  223. package/dist/src/runner-disposal.js +1 -1
  224. package/dist/src/runner-operations-facade.js +1 -1
  225. package/dist/src/runner-presentation.js +1 -1
  226. package/dist/src/runner.js +1 -1
  227. package/dist/src/runtime-transport-hints.js +1 -1
  228. package/dist/src/runtime.js +3 -3
  229. package/dist/src/runtime10.js +1 -1
  230. package/dist/src/runtime2.js +4 -4
  231. package/dist/src/runtime3.js +1 -1
  232. package/dist/src/runtime4.js +5 -5
  233. package/dist/src/runtime5.js +1 -1
  234. package/dist/src/runtime6.js +1 -1
  235. package/dist/src/runtime7.js +1 -1
  236. package/dist/src/runtime8.js +1 -1
  237. package/dist/src/runtime9.js +1 -1
  238. package/dist/src/screen-recording-admission-ledger.js +1 -0
  239. package/dist/src/screen-recording-completion.js +1 -1
  240. package/dist/src/screen-recording-options.js +1 -1
  241. package/dist/src/screen-recording-resource-recovery.js +1 -1
  242. package/dist/src/screen-recording-transport.js +1 -0
  243. package/dist/src/screenshot-density.js +1 -1
  244. package/dist/src/screenshot-image.js +1 -0
  245. package/dist/src/screenshot-runtime.js +1 -1
  246. package/dist/src/screenshot.js +1 -1
  247. package/dist/src/screenshot3.js +1 -1
  248. package/dist/src/script.js +1 -1
  249. package/dist/src/scroll-command.js +1 -1
  250. package/dist/src/scroll-gesture.js +1 -1
  251. package/dist/src/sdk-batch-runner.js +2 -2
  252. package/dist/src/sdk-batch.js +2 -2
  253. package/dist/src/sdk-contracts.d.ts +76 -1
  254. package/dist/src/sdk-contracts.js +2 -2
  255. package/dist/src/sdk-device.js +1 -1
  256. package/dist/src/sdk-finders.d.ts +1 -1
  257. package/dist/src/sdk-finders.js +1 -1
  258. package/dist/src/sdk-io.js +1 -1
  259. package/dist/src/sdk-remote-config.js +1 -1
  260. package/dist/src/sdk-selectors.d.ts +29 -3
  261. package/dist/src/sdk-selectors.js +1 -1
  262. package/dist/src/selection.js +0 -0
  263. package/dist/src/selector-runtime.js +1 -1
  264. package/dist/src/selectors.js +1 -1
  265. package/dist/src/semantic-index.js +1 -1
  266. package/dist/src/server.js +1 -1
  267. package/dist/src/session-allocation.js +1 -1
  268. package/dist/src/session-observation.js +1 -1
  269. package/dist/src/session-routing.js +1 -1
  270. package/dist/src/session-runtime-admission.js +1 -1
  271. package/dist/src/session-snapshot.js +1 -1
  272. package/dist/src/session-store.js +3 -3
  273. package/dist/src/session2.js +4 -5
  274. package/dist/src/settings.js +1 -1
  275. package/dist/src/simctl-facade.js +1 -1
  276. package/dist/src/simctl.js +1 -1
  277. package/dist/src/simulator-boot.js +1 -1
  278. package/dist/src/simulator-facade.js +1 -1
  279. package/dist/src/simulator.js +1 -1
  280. package/dist/src/snapshot-capture-annotations.js +1 -1
  281. package/dist/src/snapshot-diagnostics.js +1 -1
  282. package/dist/src/snapshot-quality-backend-capabilities.js +1 -1
  283. package/dist/src/snapshot-runtime-binding.js +1 -1
  284. package/dist/src/snapshot-runtime.js +1 -1
  285. package/dist/src/snapshot-state.js +1 -1
  286. package/dist/src/snapshot.js +1 -1
  287. package/dist/src/snapshot2.js +1 -1
  288. package/dist/src/snapshot3.js +1 -1
  289. package/dist/src/snapshot4.js +1 -1
  290. package/dist/src/src.js +1 -1
  291. package/dist/src/src4.js +1 -1
  292. package/dist/src/src7.js +1 -1
  293. package/dist/src/src8.js +2 -2
  294. package/dist/src/stop-sequence.js +1 -1
  295. package/dist/src/string-enum.js +1 -1
  296. package/dist/src/takeover.js +1 -1
  297. package/dist/src/text-entry-focus.js +1 -1
  298. package/dist/src/tool-provider.js +1 -1
  299. package/dist/src/tool-provider2.js +1 -1
  300. package/dist/src/touch-runtime.js +1 -1
  301. package/dist/src/transport.js +1 -1
  302. package/dist/src/tree.js +1 -1
  303. package/dist/src/tv-remote.js +1 -1
  304. package/dist/src/type-text-runtime.js +1 -1
  305. package/dist/src/verified-file.js +1 -1
  306. package/dist/src/video.js +1 -1
  307. package/dist/src/viewport.js +1 -1
  308. package/dist/src/web.js +1 -1
  309. package/dist/src/web2.js +1 -1
  310. package/dist/src/webdriver-source.js +1 -1
  311. package/dist/src/window-state.js +1 -1
  312. package/package.json +4 -1
  313. package/server.json +2 -2
  314. package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.6.apk.sha256 +0 -1
  315. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.6.apk.sha256 +0 -1
  316. package/dist/src/session-target-evidence.js +0 -1
  317. package/dist/src/vars.js +0 -2
@@ -0,0 +1 @@
1
+ d2661c9b0183854af3266f08aa084c1e6f5171be78c468333a8166b068482327 agent-device-android-ime-helper-0.21.8.apk
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "android-ime-helper",
3
- "version": "0.21.6",
4
- "assetName": "agent-device-android-ime-helper-0.21.6.apk",
5
- "sha256": "c705e3732af1307e0bf9f9ec058c898839ff695ffa9f3b9fcffe08ba63458487",
3
+ "version": "0.21.8",
4
+ "assetName": "agent-device-android-ime-helper-0.21.8.apk",
5
+ "sha256": "d2661c9b0183854af3266f08aa084c1e6f5171be78c468333a8166b068482327",
6
6
  "packageName": "com.callstack.agentdevice.imehelper",
7
- "versionCode": 21006,
7
+ "versionCode": 21008,
8
8
  "serviceComponent": "com.callstack.agentdevice.imehelper/.TestInputMethodService",
9
9
  "broadcastProtocol": "android-ime-helper-v1"
10
10
  }
@@ -0,0 +1 @@
1
+ 4a7a293f798f3c0349e23170d886b7defcd8eaad3f47b98c431185f033d7f800 agent-device-android-snapshot-helper-0.21.8.apk
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "android-snapshot-helper",
3
- "version": "0.21.6",
4
- "releaseTag": "v0.21.6",
5
- "assetName": "agent-device-android-snapshot-helper-0.21.6.apk",
3
+ "version": "0.21.8",
4
+ "releaseTag": "v0.21.8",
5
+ "assetName": "agent-device-android-snapshot-helper-0.21.8.apk",
6
6
  "apkUrl": null,
7
- "sha256": "a4c52106edd24230c2d7a89e955bd43be8115d39b5d0994f674cb4e93fe049e6",
8
- "checksumName": "agent-device-android-snapshot-helper-0.21.6.apk.sha256",
7
+ "sha256": "4a7a293f798f3c0349e23170d886b7defcd8eaad3f47b98c431185f033d7f800",
8
+ "checksumName": "agent-device-android-snapshot-helper-0.21.8.apk.sha256",
9
9
  "packageName": "com.callstack.agentdevice.snapshothelper",
10
- "versionCode": 21006,
10
+ "versionCode": 21008,
11
11
  "instrumentationRunner": "com.callstack.agentdevice.snapshothelper/.SnapshotInstrumentation",
12
12
  "minSdk": 23,
13
13
  "targetSdk": 36,
@@ -11,8 +11,23 @@ let package = Package(
11
11
  ),
12
12
  ],
13
13
  targets: [
14
+ .target(
15
+ name: "AgentDeviceMacOSInput"
16
+ ),
17
+ .target(
18
+ name: "AgentDeviceMacOSDeviceHub"
19
+ ),
14
20
  .executableTarget(
15
- name: "AgentDeviceMacOSHelper"
21
+ name: "AgentDeviceMacOSHelper",
22
+ dependencies: ["AgentDeviceMacOSInput", "AgentDeviceMacOSDeviceHub"]
23
+ ),
24
+ .testTarget(
25
+ name: "AgentDeviceMacOSInputTests",
26
+ dependencies: ["AgentDeviceMacOSInput"]
27
+ ),
28
+ .testTarget(
29
+ name: "AgentDeviceMacOSDeviceHubTests",
30
+ dependencies: ["AgentDeviceMacOSDeviceHub"]
16
31
  ),
17
32
  ]
18
33
  )
@@ -0,0 +1,47 @@
1
+ import Foundation
2
+
3
+ /// The decisions the Device Hub pose press makes without a window server: which control a pose
4
+ /// names, which window belongs to a device, and which sidebar row identifies it. They live apart
5
+ /// from the helper executable so the tests can exercise them without an accessibility session.
6
+
7
+ /// Device Hub's three pose presets for a foldable simulator, named as the helper's `--pose`
8
+ /// argument spells them. Each maps to the accessibility description of its action-bar button.
9
+ public enum DeviceHubPose: String, CaseIterable, Sendable {
10
+ case closed
11
+ case book
12
+ case open
13
+
14
+ public init?(argument: String) {
15
+ self.init(rawValue: argument.lowercased())
16
+ }
17
+
18
+ /// The `AXDescription` Device Hub gives the button for this pose.
19
+ public var controlDescription: String {
20
+ switch self {
21
+ case .closed: return "Closed"
22
+ case .book: return "Book"
23
+ case .open: return "Open"
24
+ }
25
+ }
26
+ }
27
+
28
+ /// Device Hub is launched through a trampoline, so LaunchServices registers it with no process
29
+ /// identifier; the process table is matched on the executable path instead.
30
+ public let deviceHubExecutableSuffix = "/DeviceHub.app/Contents/MacOS/DeviceHub"
31
+
32
+ /// The separator Device Hub puts between the device name and the OS in a window title.
33
+ public let deviceHubWindowTitleSeparator = " – "
34
+
35
+ /// Device Hub titles a device window `<device name> – <OS> <version>`, so a window shows the
36
+ /// device when its title is the name or the name followed by that exact separator. A bare
37
+ /// space would also match a simulator whose name merely extends this one ("iPhone Duo Lab").
38
+ public func deviceHubWindowShows(deviceName: String, title: String?) -> Bool {
39
+ guard let title else { return false }
40
+ return title == deviceName || title.hasPrefix(deviceName + deviceHubWindowTitleSeparator)
41
+ }
42
+
43
+ /// The accessibility identifier of the sidebar row for one simulator. The UDID is the one device
44
+ /// identity Device Hub exposes that two simulators sharing a name cannot confuse.
45
+ public func deviceHubDeviceRowIdentifier(udid: String) -> String {
46
+ return "TableRow.Device.\(udid)"
47
+ }
@@ -0,0 +1,320 @@
1
+ import AgentDeviceMacOSDeviceHub
2
+ import AppKit
3
+ import ApplicationServices
4
+ import Darwin
5
+ import Foundation
6
+
7
+ /// The Xcode Device Hub application. Its device windows carry an action bar whose pose controls
8
+ /// are the only public seam onto the private channel that folds a simulator.
9
+ private let deviceHubBundleId = "com.apple.dt.Devices"
10
+
11
+ /// How long a reopen gets to restore a window, and a sidebar selection to switch the window's
12
+ /// device, before the press is refused.
13
+ private let deviceHubSettleDeadline: TimeInterval = 8
14
+ private let deviceHubPoll: TimeInterval = 0.25
15
+
16
+ struct DeviceHubPoseResponse: Encodable {
17
+ let pose: String
18
+ let control: String
19
+ let windowTitle: String
20
+ /** Whether Device Hub had to be asked to reopen a window before the device could be selected. */
21
+ let reopened: Bool
22
+ /** Whether the window was switched to the device through its sidebar row. */
23
+ let selected: Bool
24
+ }
25
+
26
+ func handleDeviceHub(arguments: [String]) throws -> any Encodable {
27
+ guard arguments.first == "pose" else {
28
+ throw HelperError.invalidArgs("device-hub requires pose")
29
+ }
30
+ let rest = Array(arguments.dropFirst())
31
+ guard let udid = helperOptionValue(arguments: rest, name: "--udid")?
32
+ .trimmingCharacters(in: .whitespacesAndNewlines),
33
+ !udid.isEmpty
34
+ else {
35
+ throw HelperError.invalidArgs("device-hub pose requires --udid <udid>")
36
+ }
37
+ guard let deviceName = helperOptionValue(arguments: rest, name: "--device-name")?
38
+ .trimmingCharacters(in: .whitespacesAndNewlines),
39
+ !deviceName.isEmpty
40
+ else {
41
+ throw HelperError.invalidArgs("device-hub pose requires --device-name <name>")
42
+ }
43
+ guard let pose = helperOptionValue(arguments: rest, name: "--pose").flatMap(DeviceHubPose.init(argument:))
44
+ else {
45
+ throw HelperError.invalidArgs("device-hub pose requires --pose <closed|book|open>")
46
+ }
47
+ let control = pose.controlDescription
48
+ guard AXIsProcessTrusted() else {
49
+ throw HelperError.commandFailed(
50
+ "fold needs Accessibility permission to press the Device Hub pose control",
51
+ details: ["reason": "accessibility-permission", "permission": "accessibility"]
52
+ )
53
+ }
54
+ guard let pid = deviceHubProcessIdentifier() else {
55
+ throw HelperError.commandFailed(
56
+ "Xcode Device Hub is not running",
57
+ details: ["reason": "device-hub-not-running", "bundleId": deviceHubBundleId]
58
+ )
59
+ }
60
+
61
+ let appElement = AXUIElementCreateApplication(pid)
62
+ let (window, reopened) = try deviceHubWindow(in: appElement, pid: pid, deviceName: deviceName)
63
+ let selected = try selectDevice(udid: udid, deviceName: deviceName, in: window, appElement: appElement)
64
+ let windowTitle = stringAttribute(window, attribute: kAXTitleAttribute as String) ?? ""
65
+ guard let button = awaitPoseButton(in: window, description: control) else {
66
+ throw HelperError.commandFailed(
67
+ "Device Hub shows no \(control) pose control for \(deviceName)",
68
+ details: [
69
+ "reason": "device-hub-pose-control-missing",
70
+ "windowTitle": windowTitle,
71
+ "control": control,
72
+ ]
73
+ )
74
+ }
75
+ let status = AXUIElementPerformAction(button, kAXPressAction as CFString)
76
+ guard status == .success else {
77
+ throw HelperError.commandFailed(
78
+ "Device Hub refused the \(control) pose press",
79
+ details: ["reason": "device-hub-press-failed", "status": "\(status.rawValue)"]
80
+ )
81
+ }
82
+ return SuccessEnvelope(
83
+ data: DeviceHubPoseResponse(
84
+ pose: pose.rawValue,
85
+ control: control,
86
+ windowTitle: windowTitle,
87
+ reopened: reopened,
88
+ selected: selected
89
+ )
90
+ )
91
+ }
92
+
93
+ /// Device Hub is launched through a trampoline, so LaunchServices registers it with no process
94
+ /// identifier (`NSRunningApplication.processIdentifier` is -1) and an accessibility element built
95
+ /// from that identifier is invalid. The process table is the only place its real pid appears.
96
+ private func deviceHubProcessIdentifier() -> pid_t? {
97
+ let executableSuffix = deviceHubExecutableSuffix
98
+ let byteCount = proc_listpids(UInt32(PROC_ALL_PIDS), 0, nil, 0)
99
+ guard byteCount > 0 else { return nil }
100
+ var pids = [pid_t](repeating: 0, count: Int(byteCount) / MemoryLayout<pid_t>.size + 64)
101
+ let filled = proc_listpids(
102
+ UInt32(PROC_ALL_PIDS), 0, &pids, Int32(pids.count * MemoryLayout<pid_t>.size)
103
+ )
104
+ guard filled > 0 else { return nil }
105
+ var path = [CChar](repeating: 0, count: 4096)
106
+ for pid in pids.prefix(Int(filled) / MemoryLayout<pid_t>.size) where pid > 0 {
107
+ guard proc_pidpath(pid, &path, UInt32(path.count)) > 0 else { continue }
108
+ if String(cString: path).hasSuffix(executableSuffix) {
109
+ return pid
110
+ }
111
+ }
112
+ return nil
113
+ }
114
+
115
+ /// A device window to drive: the one already showing this device when there is one, otherwise
116
+ /// any device window, since its sidebar can switch it to the device. A Device Hub with no window
117
+ /// at all — a simulator booted headlessly leaves it that way — is asked to reopen one.
118
+ private func deviceHubWindow(
119
+ in appElement: AXUIElement,
120
+ pid: pid_t,
121
+ deviceName: String
122
+ ) throws -> (window: AXUIElement, reopened: Bool) {
123
+ if let window = preferredWindow(in: appElement, deviceName: deviceName) {
124
+ return (window, false)
125
+ }
126
+ try sendReopenEvent(to: pid)
127
+ let deadline = Date().addingTimeInterval(deviceHubSettleDeadline)
128
+ while Date() < deadline {
129
+ Thread.sleep(forTimeInterval: deviceHubPoll)
130
+ if let window = preferredWindow(in: appElement, deviceName: deviceName) {
131
+ return (window, true)
132
+ }
133
+ }
134
+ throw HelperError.commandFailed(
135
+ "Device Hub shows no device window to drive",
136
+ details: ["reason": "device-hub-window-missing", "deviceName": deviceName]
137
+ )
138
+ }
139
+
140
+ private func preferredWindow(in appElement: AXUIElement, deviceName: String) -> AXUIElement? {
141
+ let candidates = windows(of: appElement)
142
+ return candidates.first { windowShows(deviceName: deviceName, $0) } ?? candidates.first
143
+ }
144
+
145
+ private func windowShows(deviceName: String, _ window: AXUIElement) -> Bool {
146
+ return deviceHubWindowShows(
147
+ deviceName: deviceName,
148
+ title: stringAttribute(window, attribute: kAXTitleAttribute as String)
149
+ )
150
+ }
151
+
152
+ /// `kAEReopenApplication` is what the Dock sends when an app with no open windows is clicked, and
153
+ /// it is the one event Device Hub answers by restoring the device window. Sent to the process
154
+ /// directly, because LaunchServices cannot address a trampolined app by bundle identifier.
155
+ private func sendReopenEvent(to pid: pid_t) throws {
156
+ let target = NSAppleEventDescriptor(processIdentifier: pid)
157
+ let event = NSAppleEventDescriptor(
158
+ eventClass: AEEventClass(kCoreEventClass),
159
+ eventID: AEEventID(kAEReopenApplication),
160
+ targetDescriptor: target,
161
+ returnID: AEReturnID(kAutoGenerateReturnID),
162
+ transactionID: AETransactionID(kAnyTransactionID)
163
+ )
164
+ do {
165
+ _ = try event.sendEvent(options: [.noReply], timeout: 5)
166
+ } catch {
167
+ let code = (error as NSError).code
168
+ throw HelperError.commandFailed(
169
+ "fold could not ask Device Hub to reopen its device window",
170
+ details: [
171
+ "reason": code == -1743 ? "automation-permission" : "device-hub-reopen-failed",
172
+ "error": String(describing: error),
173
+ ]
174
+ )
175
+ }
176
+ }
177
+
178
+ /// Switches the window to the device through its sidebar row, whose accessibility identifier is
179
+ /// `TableRow.Device.<udid>` — the one place Device Hub exposes a device identity that two
180
+ /// simulators sharing a name cannot confuse. A window already titled with the device still gets
181
+ /// the selection when the row is visible, because the title alone cannot tell such twins apart.
182
+ private func selectDevice(
183
+ udid: String,
184
+ deviceName: String,
185
+ in window: AXUIElement,
186
+ appElement: AXUIElement
187
+ ) throws -> Bool {
188
+ var shownSidebar = false
189
+ var row = deviceRow(udid: udid, in: window)
190
+ if row == nil, showSidebar(appElement: appElement) {
191
+ shownSidebar = true
192
+ let deadline = Date().addingTimeInterval(deviceHubSettleDeadline)
193
+ while row == nil, Date() < deadline {
194
+ Thread.sleep(forTimeInterval: deviceHubPoll)
195
+ row = deviceRow(udid: udid, in: window)
196
+ }
197
+ }
198
+ defer {
199
+ if shownSidebar { _ = pressMenuItem(appElement: appElement, menu: "View", item: "Hide Sidebar") }
200
+ }
201
+ guard let row else {
202
+ if windowShows(deviceName: deviceName, window) {
203
+ return false
204
+ }
205
+ throw HelperError.commandFailed(
206
+ "Device Hub lists no device \(udid) in its sidebar",
207
+ details: ["reason": "device-hub-device-missing", "udid": udid, "deviceName": deviceName]
208
+ )
209
+ }
210
+ let status = AXUIElementSetAttributeValue(row, kAXSelectedAttribute as CFString, kCFBooleanTrue)
211
+ guard status == .success else {
212
+ throw HelperError.commandFailed(
213
+ "Device Hub refused to select \(deviceName) in its sidebar",
214
+ details: ["reason": "device-hub-select-failed", "status": "\(status.rawValue)"]
215
+ )
216
+ }
217
+ let deadline = Date().addingTimeInterval(deviceHubSettleDeadline)
218
+ while !windowShows(deviceName: deviceName, window), Date() < deadline {
219
+ Thread.sleep(forTimeInterval: deviceHubPoll)
220
+ }
221
+ guard windowShows(deviceName: deviceName, window) else {
222
+ throw HelperError.commandFailed(
223
+ "Device Hub did not switch its window to \(deviceName)",
224
+ details: [
225
+ "reason": "device-hub-select-unconfirmed",
226
+ "windowTitle": stringAttribute(window, attribute: kAXTitleAttribute as String) ?? "",
227
+ ]
228
+ )
229
+ }
230
+ return true
231
+ }
232
+
233
+ private func deviceRow(udid: String, in window: AXUIElement) -> AXUIElement? {
234
+ guard let label = findElement(root: window, depth: 0, where: {
235
+ stringAttribute($0, attribute: "AXIdentifier") == deviceHubDeviceRowIdentifier(udid: udid)
236
+ }) else {
237
+ return nil
238
+ }
239
+ var current: AXUIElement? = label
240
+ while let element = current {
241
+ if stringAttribute(element, attribute: kAXRoleAttribute as String) == "AXRow" {
242
+ return element
243
+ }
244
+ current = elementAttribute(element, attribute: kAXParentAttribute as String)
245
+ }
246
+ return nil
247
+ }
248
+
249
+ private func showSidebar(appElement: AXUIElement) -> Bool {
250
+ return pressMenuItem(appElement: appElement, menu: "View", item: "Show Sidebar")
251
+ }
252
+
253
+ private func pressMenuItem(appElement: AXUIElement, menu: String, item: String) -> Bool {
254
+ guard let menuBar = elementAttribute(appElement, attribute: kAXMenuBarAttribute as String) else {
255
+ return false
256
+ }
257
+ for menuBarItem in children(of: menuBar)
258
+ where stringAttribute(menuBarItem, attribute: kAXTitleAttribute as String) == menu {
259
+ for submenu in children(of: menuBarItem) {
260
+ for menuItem in children(of: submenu)
261
+ where stringAttribute(menuItem, attribute: kAXTitleAttribute as String) == item {
262
+ return AXUIElementPerformAction(menuItem, kAXPressAction as CFString) == .success
263
+ }
264
+ }
265
+ }
266
+ return false
267
+ }
268
+
269
+ /// The action bar is rebuilt for the device the window shows, so right after a sidebar selection
270
+ /// the window already carries the new title while the pose controls are still being laid out.
271
+ /// The controls are therefore awaited, not looked up once.
272
+ private func awaitPoseButton(in window: AXUIElement, description: String) -> AXUIElement? {
273
+ let deadline = Date().addingTimeInterval(deviceHubSettleDeadline)
274
+ while true {
275
+ if let button = poseButton(in: window, description: description) {
276
+ return button
277
+ }
278
+ guard Date() < deadline else { return nil }
279
+ Thread.sleep(forTimeInterval: deviceHubPoll)
280
+ }
281
+ }
282
+
283
+ /// The pose controls sit in the window's action bar as `AXButton`s described by their preset
284
+ /// name; the simulated screen inside the same window is an iOS content group whose own buttons
285
+ /// carry app labels, never these three.
286
+ private func poseButton(in window: AXUIElement, description: String) -> AXUIElement? {
287
+ return findElement(root: window, depth: 0) {
288
+ stringAttribute($0, attribute: kAXRoleAttribute as String) == "AXButton"
289
+ && stringAttribute($0, attribute: kAXDescriptionAttribute as String) == description
290
+ }
291
+ }
292
+
293
+ private func findElement(
294
+ root: AXUIElement,
295
+ depth: Int,
296
+ where matches: (AXUIElement) -> Bool
297
+ ) -> AXUIElement? {
298
+ if depth > 14 {
299
+ return nil
300
+ }
301
+ for child in children(of: root) {
302
+ if matches(child) {
303
+ return child
304
+ }
305
+ if stringAttribute(child, attribute: kAXSubroleAttribute as String) == "iOSContentGroup" {
306
+ continue
307
+ }
308
+ if let nested = findElement(root: child, depth: depth + 1, where: matches) {
309
+ return nested
310
+ }
311
+ }
312
+ return nil
313
+ }
314
+
315
+ func helperOptionValue(arguments: [String], name: String) -> String? {
316
+ guard let index = arguments.firstIndex(of: name), arguments.indices.contains(index + 1) else {
317
+ return nil
318
+ }
319
+ return arguments[index + 1]
320
+ }
@@ -1,3 +1,4 @@
1
+ import AgentDeviceMacOSInput
1
2
  import AppKit
2
3
  import ApplicationServices
3
4
  import CoreGraphics
@@ -63,6 +64,10 @@ struct ReadResponse: Encodable {
63
64
  struct PressResponse: Encodable {
64
65
  let x: Double
65
66
  let y: Double
67
+ /// Hold actually posted; a short request is raised to the deliverable floor.
68
+ let holdMs: Int
69
+ let clicks: Int
70
+ let doubleClick: Bool
66
71
  let bundleId: String?
67
72
  let surface: String?
68
73
  }
@@ -122,6 +127,8 @@ struct AgentDeviceMacOSHelper {
122
127
  return try handleScreenshot(arguments: Array(arguments.dropFirst()))
123
128
  case "audio-probe":
124
129
  return try handleAudioProbe(arguments: Array(arguments.dropFirst()))
130
+ case "device-hub":
131
+ return try handleDeviceHub(arguments: Array(arguments.dropFirst()))
125
132
  default:
126
133
  throw HelperError.invalidArgs("unknown command: \(command)")
127
134
  }
@@ -378,10 +385,48 @@ struct AgentDeviceMacOSHelper {
378
385
  throw HelperError.invalidArgs("press requires --x <number> --y <number>")
379
386
  }
380
387
 
388
+ let holdMs = try validatedPressInt(
389
+ optionValue(arguments: arguments, name: "--hold-ms"),
390
+ name: "--hold-ms",
391
+ minimum: 0,
392
+ default: 0
393
+ )
394
+ let clicks = try validatedPressInt(
395
+ optionValue(arguments: arguments, name: "--clicks"),
396
+ name: "--clicks",
397
+ minimum: 1,
398
+ default: 1
399
+ )
400
+ let intervalMs = try validatedPressInt(
401
+ optionValue(arguments: arguments, name: "--interval-ms"),
402
+ name: "--interval-ms",
403
+ minimum: 0,
404
+ default: 120
405
+ )
406
+
407
+ let doubleClick = arguments.contains("--double-click")
381
408
  let bundleId = try optionValue(arguments: arguments, name: "--bundle-id").map(validatedBundleId)
382
409
  let surface = optionValue(arguments: arguments, name: "--surface")
383
- try pressAtPosition(bundleId: bundleId, surface: surface, x: x, y: y)
384
- return SuccessEnvelope(data: PressResponse(x: x, y: y, bundleId: bundleId, surface: surface))
410
+ let request = MouseClickRequest(
411
+ x: x,
412
+ y: y,
413
+ holdMs: holdMs,
414
+ clicks: clicks,
415
+ doubleClick: doubleClick,
416
+ intervalMs: intervalMs
417
+ )
418
+ try pressAtPosition(request)
419
+ return SuccessEnvelope(
420
+ data: PressResponse(
421
+ x: x,
422
+ y: y,
423
+ holdMs: mouseClickHoldMs(requestedMs: holdMs),
424
+ clicks: clicks,
425
+ doubleClick: doubleClick,
426
+ bundleId: bundleId,
427
+ surface: surface
428
+ )
429
+ )
385
430
  }
386
431
 
387
432
  static func handleScreenshot(arguments: [String]) throws -> any Encodable {
@@ -434,6 +479,21 @@ private func intOption(arguments: [String], name: String) -> Int? {
434
479
  return Int(value)
435
480
  }
436
481
 
482
+ private func validatedPressInt(
483
+ _ raw: String?,
484
+ name: String,
485
+ minimum: Int,
486
+ default fallback: Int
487
+ ) throws -> Int {
488
+ guard let raw else {
489
+ return fallback
490
+ }
491
+ guard let value = Int(raw), value >= minimum else {
492
+ throw HelperError.invalidArgs("press \(name) must be an integer of at least \(minimum)")
493
+ }
494
+ return value
495
+ }
496
+
437
497
  private func readTextAtPosition(bundleId: String?, surface: String?, x: Double, y: Double) throws -> String {
438
498
  let targetApp: NSRunningApplication?
439
499
  if surface == "frontmost-app" || (surface == nil && bundleId != nil) {
@@ -481,19 +541,12 @@ private func readTextAtPosition(bundleId: String?, surface: String?, x: Double,
481
541
  throw HelperError.commandFailed("read did not resolve text")
482
542
  }
483
543
 
484
- private func pressAtPosition(bundleId: String?, surface: String?, x: Double, y: Double) throws {
485
- _ = bundleId
486
- _ = surface
487
- let point = CGPoint(x: x, y: y)
488
- guard let move = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: point, mouseButton: .left),
489
- let down = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: point, mouseButton: .left),
490
- let up = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: point, mouseButton: .left)
491
- else {
544
+ private func pressAtPosition(_ request: MouseClickRequest) throws {
545
+ do {
546
+ try postMouseClick(request)
547
+ } catch MouseClickDeliveryError.eventCreationFailed {
492
548
  throw HelperError.commandFailed("press action failed", details: ["reason": "event_creation_failed"])
493
549
  }
494
- move.post(tap: .cghidEventTap)
495
- down.post(tap: .cghidEventTap)
496
- up.post(tap: .cghidEventTap)
497
550
  }
498
551
 
499
552
  private func captureSurfaceScreenshot(surface: String?, outPath: String, fullscreen: Bool) throws {
@@ -0,0 +1,116 @@
1
+ import CoreGraphics
2
+ import Foundation
3
+
4
+ public struct MouseClickRequest: Equatable, Sendable {
5
+ public let x: Double
6
+ public let y: Double
7
+ /// How long the button stays down. Zero asks for the default hold.
8
+ public let holdMs: Int
9
+ /// Independent presses, each at click state 1.
10
+ public let clicks: Int
11
+ /// Post every press as a double-click pair with a rising click state.
12
+ public let doubleClick: Bool
13
+ public let intervalMs: Int
14
+
15
+ public init(
16
+ x: Double,
17
+ y: Double,
18
+ holdMs: Int = 0,
19
+ clicks: Int = 1,
20
+ doubleClick: Bool = false,
21
+ intervalMs: Int = 120
22
+ ) {
23
+ self.x = x
24
+ self.y = y
25
+ self.holdMs = holdMs
26
+ self.clicks = clicks
27
+ self.doubleClick = doubleClick
28
+ self.intervalMs = intervalMs
29
+ }
30
+ }
31
+
32
+ public enum MouseClickDeliveryError: Error, Equatable {
33
+ case eventCreationFailed
34
+ }
35
+
36
+ /// The button the helper currently holds down, kept where a signal handler can reach it.
37
+ /// A helper killed between a mouse-down and its mouse-up would otherwise leave the system's
38
+ /// primary button stuck down for whatever the user touches next. The host stops the helper
39
+ /// with SIGTERM before SIGKILL (`runMacOsHelper` in `helper.ts`) so that this handler runs
40
+ /// on a deadline and on a cancelled request, not only on a signal sent by hand.
41
+ nonisolated(unsafe) private var heldMouseButton: (point: CGPoint, clickState: Int)?
42
+
43
+ private func releaseHeldMouseButton() {
44
+ guard let held = heldMouseButton else { return }
45
+ heldMouseButton = nil
46
+ let up = CGEvent(
47
+ mouseEventSource: nil,
48
+ mouseType: .leftMouseUp,
49
+ mouseCursorPosition: held.point,
50
+ mouseButton: .left
51
+ )
52
+ up?.setIntegerValueField(.mouseEventClickState, value: Int64(held.clickState))
53
+ up?.post(tap: .cghidEventTap)
54
+ }
55
+
56
+ private func installMouseReleaseOnTermination() {
57
+ for terminationSignal in [SIGTERM, SIGINT, SIGHUP] {
58
+ signal(terminationSignal) { received in
59
+ releaseHeldMouseButton()
60
+ _exit(128 + received)
61
+ }
62
+ }
63
+ }
64
+
65
+ /// Posts a click the way a trackpad would: one motion to the point, then each press held
66
+ /// long enough for the app to accept the release. A termination signal that lands inside a
67
+ /// hold releases the button before the process exits.
68
+ public func postMouseClick(_ request: MouseClickRequest) throws {
69
+ installMouseReleaseOnTermination()
70
+ let point = CGPoint(x: request.x, y: request.y)
71
+ let hold = mouseClickHoldMs(requestedMs: request.holdMs)
72
+ let presses = mouseClickPresses(
73
+ clicks: request.clicks,
74
+ doubleClick: request.doubleClick,
75
+ intervalMs: request.intervalMs
76
+ )
77
+
78
+ guard let move = CGEvent(
79
+ mouseEventSource: nil,
80
+ mouseType: .mouseMoved,
81
+ mouseCursorPosition: point,
82
+ mouseButton: .left
83
+ ) else {
84
+ throw MouseClickDeliveryError.eventCreationFailed
85
+ }
86
+ move.post(tap: .cghidEventTap)
87
+
88
+ for press in presses {
89
+ if press.delayBeforeMs > 0 {
90
+ usleep(UInt32(press.delayBeforeMs) * 1000)
91
+ }
92
+ guard let down = CGEvent(
93
+ mouseEventSource: nil,
94
+ mouseType: .leftMouseDown,
95
+ mouseCursorPosition: point,
96
+ mouseButton: .left
97
+ ), let up = CGEvent(
98
+ mouseEventSource: nil,
99
+ mouseType: .leftMouseUp,
100
+ mouseCursorPosition: point,
101
+ mouseButton: .left
102
+ ) else {
103
+ throw MouseClickDeliveryError.eventCreationFailed
104
+ }
105
+ down.setIntegerValueField(.mouseEventClickState, value: Int64(press.clickState))
106
+ up.setIntegerValueField(.mouseEventClickState, value: Int64(press.clickState))
107
+ heldMouseButton = (point, press.clickState)
108
+ down.post(tap: .cghidEventTap)
109
+ usleep(UInt32(hold) * 1000)
110
+ // The record clears only after the up is posted: a signal that lands between the two
111
+ // would otherwise find nothing to release and exit with the button still down. A signal
112
+ // that lands after the post releases a button already up, which is harmless.
113
+ up.post(tap: .cghidEventTap)
114
+ heldMouseButton = nil
115
+ }
116
+ }