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.
- 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
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.8.apk.sha256 +1 -0
- 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
- 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
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.8.apk.sha256 +1 -0
- 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
- package/apple/macos-helper/Package.swift +16 -1
- package/apple/macos-helper/Sources/AgentDeviceMacOSDeviceHub/DeviceHubPose.swift +47 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubPose.swift +320 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +66 -13
- package/apple/macos-helper/Sources/AgentDeviceMacOSInput/MouseClickDelivery.swift +116 -0
- package/apple/macos-helper/Sources/AgentDeviceMacOSInput/MouseClickSchedule.swift +61 -0
- package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubPoseTests.swift +30 -0
- package/apple/macos-helper/Tests/AgentDeviceMacOSInputTests/MouseClickScheduleTests.swift +58 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +4 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +20 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerScrollViewportPolicy.swift +1 -2
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +25 -12
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +56 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +1 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +36 -108
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Keyboard.swift +241 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +29 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +46 -0
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+PrivateAXPresentation.swift +85 -26
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +134 -117
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotBackendCapabilities.swift +1 -3
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +25 -4
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +1 -1
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +3 -0
- package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotCoordinateSpace.swift +197 -0
- package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotModels.swift +30 -4
- package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotPresentation.swift +0 -81
- package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotPresentationInvariant.swift +1 -9
- package/dist/src/absence-observation-errors.js +1 -1
- package/dist/src/adapter.js +1 -1
- package/dist/src/adb-executor.js +2 -2
- package/dist/src/adb-failure.js +1 -1
- package/dist/src/adb-transport.js +1 -1
- package/dist/src/adb.js +1 -1
- package/dist/src/agent-browser-lifecycle.js +1 -1
- package/dist/src/agent-browser-process-record.js +1 -1
- package/dist/src/agent-browser-provider.js +1 -1
- package/dist/src/agent-browser-tool.js +1 -1
- package/dist/src/agent-device-client.js +2 -2
- package/dist/src/ai-sdk.js +1 -1
- package/dist/src/alert-runtime.js +1 -1
- package/dist/src/android-adb.d.ts +29 -2
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android-input-ownership.js +1 -1
- package/dist/src/android.js +1 -1
- package/dist/src/app-catalog.js +1 -1
- package/dist/src/app-deployment-resolution.js +1 -1
- package/dist/src/app-helpers.js +1 -1
- package/dist/src/app-inventory.js +1 -1
- package/dist/src/app-launch.js +2 -2
- package/dist/src/app-lifecycle.js +2 -2
- package/dist/src/app-lifecycle2.js +2 -2
- package/dist/src/app-log-files.js +1 -1
- package/dist/src/app-log-runtime2.js +3 -3
- package/dist/src/app-resolution.js +2 -2
- package/dist/src/app-state.js +1 -1
- package/dist/src/apple-runner-platform.js +1 -1
- package/dist/src/application-lifecycle-interaction.js +1 -1
- package/dist/src/archive.js +1 -1
- package/dist/src/artifact-download.js +1 -1
- package/dist/src/auth-session.js +1 -1
- package/dist/src/auth.js +1 -1
- package/dist/src/back-mode.js +1 -1
- package/dist/src/backend-snapshot-options.js +1 -1
- package/dist/src/boot-diagnostics.js +1 -1
- package/dist/src/capture-disclosure.js +1 -0
- package/dist/src/capture.js +1 -1
- package/dist/src/cli-config.js +1 -1
- package/dist/src/cli-help.js +18 -2
- package/dist/src/cli-output.js +1 -1
- package/dist/src/cli.js +4 -4
- package/dist/src/click-button.js +1 -1
- package/dist/src/client-metro.js +1 -1
- package/dist/src/client-types.d.ts +65 -3
- package/dist/src/clipboard-shell-response.js +1 -0
- package/dist/src/command-schema.js +1 -1
- package/dist/src/command-tools.js +4 -2
- package/dist/src/command.js +1 -1
- package/dist/src/config.js +1 -1
- package/dist/src/connection-runtime.js +1 -1
- package/dist/src/connection.js +1 -1
- package/dist/src/contracts.d.ts +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/daemon-client-lifecycle.js +1 -1
- package/dist/src/daemon.js +1 -1
- package/dist/src/debug-symbols-facade.js +1 -1
- package/dist/src/device-boot.js +1 -1
- package/dist/src/device-claim-inspection.js +1 -1
- package/dist/src/device-claim-rule.js +2 -1
- package/dist/src/device-input-state.d.ts +5 -5
- package/dist/src/device-inventory-context.js +1 -1
- package/dist/src/device-rotation.js +1 -1
- package/dist/src/device-selection-resolver.js +1 -1
- package/dist/src/device-session.js +1 -1
- package/dist/src/device-shell.js +1 -0
- package/dist/src/device.js +1 -1
- package/dist/src/device2.js +1 -1
- package/dist/src/devicectl.js +1 -0
- package/dist/src/diagnostics.js +1 -1
- package/dist/src/dispatch-resolve.js +1 -1
- package/dist/src/display-inventory.js +1 -0
- package/dist/src/doctor-facade.js +1 -1
- package/dist/src/doctor2.js +1 -1
- package/dist/src/durable-resource.js +1 -1
- package/dist/src/engine-eval-script.js +1 -1
- package/dist/src/error.js +1 -1
- package/dist/src/exec.js +1 -1
- package/dist/src/file.js +1 -1
- package/dist/src/find.js +1 -1
- package/dist/src/finders.js +1 -1
- package/dist/src/format.js +2 -1
- package/dist/src/generic.js +1 -1
- package/dist/src/gesture-admission.js +1 -1
- package/dist/src/gesture-plan.js +1 -1
- package/dist/src/gesture-runtime.js +1 -1
- package/dist/src/harmonyos.js +1 -1
- package/dist/src/hdc.js +1 -1
- package/dist/src/helper.js +2 -2
- package/dist/src/host.js +1 -0
- package/dist/src/human-control-contract.js +1 -1
- package/dist/src/human-control.js +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/input-actions.js +1 -1
- package/dist/src/input-actions2.js +1 -1
- package/dist/src/install-artifact-facade.js +1 -1
- package/dist/src/install-source-network-transport.js +1 -1
- package/dist/src/install-source2.js +1 -1
- package/dist/src/interaction-positionals.js +1 -1
- package/dist/src/interaction-ref-policy.js +1 -1
- package/dist/src/interaction.js +1 -1
- package/dist/src/interaction2.js +1 -1
- package/dist/src/interactor-operation-binding.js +1 -0
- package/dist/src/interactor-operation-catalog.js +1 -1
- package/dist/src/interactor.js +4 -5
- package/dist/src/interactor2.js +1 -1
- package/dist/src/interactors.js +1 -1
- package/dist/src/internal/bin.js +1 -1
- package/dist/src/internal/daemon.js +2 -2
- package/dist/src/internal/png-worker.d.ts +9 -1
- package/dist/src/internal/png-worker.js +1 -1
- package/dist/src/internal/run-script-http-child.js +1 -1
- package/dist/src/inventory.js +4 -4
- package/dist/src/inventory2.js +1 -1
- package/dist/src/inventory3.js +2 -2
- package/dist/src/inventory5.js +1 -1
- package/dist/src/ios-snapshot-acquisition.js +1 -1
- package/dist/src/ios-snapshot-runtime.js +1 -1
- package/dist/src/ios-target-activation.js +1 -0
- package/dist/src/lease-context.js +1 -1
- package/dist/src/lease-scope.js +1 -1
- package/dist/src/lease.js +1 -1
- package/dist/src/limrun-download-file.js +1 -0
- package/dist/src/limrun-runtime-dependencies.js +1 -1
- package/dist/src/limrun.d.ts +10 -4
- package/dist/src/linux.js +1 -1
- package/dist/src/local-interactor-operation-set.js +1 -1
- package/dist/src/location-coordinates.js +1 -1
- package/dist/src/macos-facade.js +1 -1
- package/dist/src/managed-device-scope.js +1 -1
- package/dist/src/mechanics.js +17 -16
- package/dist/src/mobile-snapshot-semantics.js +1 -1
- package/dist/src/multitouch-support.js +1 -1
- package/dist/src/observation.js +2 -2
- package/dist/src/output-common.js +5 -2
- package/dist/src/parameterized-recorded-fill.js +1 -1
- package/dist/src/perf-facade.js +2 -2
- package/dist/src/perf-runtime-operation-builder.js +1 -1
- package/dist/src/perf-runtime-plan.js +1 -1
- package/dist/src/perf.js +1 -1
- package/dist/src/physical-device-control.js +1 -1
- package/dist/src/platform-request-scope.js +1 -1
- package/dist/src/platform-runtime-android-adb-host.js +1 -1
- package/dist/src/platform-runtime-host.js +1 -1
- package/dist/src/platform-runtime-managed-owner.js +1 -1
- package/dist/src/platform-runtime-open-target.js +1 -1
- package/dist/src/platform-runtime-operation-host.js +4 -4
- package/dist/src/platform-runtime-runtime-hints.js +2 -2
- package/dist/src/platform-runtime-screen-recording-android-host.js +1 -1
- package/dist/src/platform-runtime-screen-recording-apple-runner-transport.js +1 -1
- package/dist/src/platform-runtime-screen-recording-apple-simulator-host.js +1 -1
- package/dist/src/platform-runtime-screen-recording-apple-transport.js +1 -1
- package/dist/src/platform-runtime-screen-recording-finalizer-host.js +1 -1
- package/dist/src/platform-runtime-screen-recording-harmony-host.js +1 -1
- package/dist/src/platform-runtime-screen-recording-process-host.js +1 -1
- package/dist/src/platform-runtime-unavailable.js +1 -1
- package/dist/src/platform-runtime.js +1 -1
- package/dist/src/platform-runtime2.js +1 -1
- package/dist/src/png-crop-bytes.js +1 -1
- package/dist/src/png-worker-client.js +1 -0
- package/dist/src/png.js +1 -1
- package/dist/src/prepare-kind.js +1 -1
- package/dist/src/provider-session-ownership.js +1 -1
- package/dist/src/provider-webdriver.js +1 -1
- package/dist/src/proxy.js +1 -1
- package/dist/src/quality-warnings.js +1 -1
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-runtime.js +1 -1
- package/dist/src/record-trace.js +1 -1
- package/dist/src/rect-center.js +1 -1
- package/dist/src/register-builtins.js +1 -1
- package/dist/src/registry.js +35 -35
- package/dist/src/remote-config2.js +1 -1
- package/dist/src/replay-divergence.js +1 -1
- package/dist/src/replay.js +1 -1
- package/dist/src/reporting.js +1 -1
- package/dist/src/request-admission.js +1 -1
- package/dist/src/request-cancel.js +1 -1
- package/dist/src/request-runtime-binding.js +1 -1
- package/dist/src/result-serialization.js +1 -1
- package/dist/src/retry.js +1 -1
- package/dist/src/runner-cache-metadata.js +2 -2
- package/dist/src/runner-client.js +1 -1
- package/dist/src/runner-contract.js +3 -0
- package/dist/src/runner-dev-tools-security.js +1 -0
- package/dist/src/runner-device-readiness.js +1 -0
- package/dist/src/runner-disposal.js +1 -1
- package/dist/src/runner-operations-facade.js +1 -1
- package/dist/src/runner-presentation.js +1 -1
- package/dist/src/runner.js +1 -1
- package/dist/src/runtime-transport-hints.js +1 -1
- package/dist/src/runtime.js +3 -3
- package/dist/src/runtime10.js +1 -1
- package/dist/src/runtime2.js +4 -4
- package/dist/src/runtime3.js +1 -1
- package/dist/src/runtime4.js +5 -5
- package/dist/src/runtime5.js +1 -1
- package/dist/src/runtime6.js +1 -1
- package/dist/src/runtime7.js +1 -1
- package/dist/src/runtime8.js +1 -1
- package/dist/src/runtime9.js +1 -1
- package/dist/src/screen-recording-admission-ledger.js +1 -0
- package/dist/src/screen-recording-completion.js +1 -1
- package/dist/src/screen-recording-options.js +1 -1
- package/dist/src/screen-recording-resource-recovery.js +1 -1
- package/dist/src/screen-recording-transport.js +1 -0
- package/dist/src/screenshot-density.js +1 -1
- package/dist/src/screenshot-image.js +1 -0
- package/dist/src/screenshot-runtime.js +1 -1
- package/dist/src/screenshot.js +1 -1
- package/dist/src/screenshot3.js +1 -1
- package/dist/src/script.js +1 -1
- package/dist/src/scroll-command.js +1 -1
- package/dist/src/scroll-gesture.js +1 -1
- package/dist/src/sdk-batch-runner.js +2 -2
- package/dist/src/sdk-batch.js +2 -2
- package/dist/src/sdk-contracts.d.ts +76 -1
- package/dist/src/sdk-contracts.js +2 -2
- package/dist/src/sdk-device.js +1 -1
- package/dist/src/sdk-finders.d.ts +1 -1
- package/dist/src/sdk-finders.js +1 -1
- package/dist/src/sdk-io.js +1 -1
- package/dist/src/sdk-remote-config.js +1 -1
- package/dist/src/sdk-selectors.d.ts +29 -3
- package/dist/src/sdk-selectors.js +1 -1
- package/dist/src/selection.js +0 -0
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.js +1 -1
- package/dist/src/semantic-index.js +1 -1
- package/dist/src/server.js +1 -1
- package/dist/src/session-allocation.js +1 -1
- package/dist/src/session-observation.js +1 -1
- package/dist/src/session-routing.js +1 -1
- package/dist/src/session-runtime-admission.js +1 -1
- package/dist/src/session-snapshot.js +1 -1
- package/dist/src/session-store.js +3 -3
- package/dist/src/session2.js +4 -5
- package/dist/src/settings.js +1 -1
- package/dist/src/simctl-facade.js +1 -1
- package/dist/src/simctl.js +1 -1
- package/dist/src/simulator-boot.js +1 -1
- package/dist/src/simulator-facade.js +1 -1
- package/dist/src/simulator.js +1 -1
- package/dist/src/snapshot-capture-annotations.js +1 -1
- package/dist/src/snapshot-diagnostics.js +1 -1
- package/dist/src/snapshot-quality-backend-capabilities.js +1 -1
- package/dist/src/snapshot-runtime-binding.js +1 -1
- package/dist/src/snapshot-runtime.js +1 -1
- package/dist/src/snapshot-state.js +1 -1
- package/dist/src/snapshot.js +1 -1
- package/dist/src/snapshot2.js +1 -1
- package/dist/src/snapshot3.js +1 -1
- package/dist/src/snapshot4.js +1 -1
- package/dist/src/src.js +1 -1
- package/dist/src/src4.js +1 -1
- package/dist/src/src7.js +1 -1
- package/dist/src/src8.js +2 -2
- package/dist/src/stop-sequence.js +1 -1
- package/dist/src/string-enum.js +1 -1
- package/dist/src/takeover.js +1 -1
- package/dist/src/text-entry-focus.js +1 -1
- package/dist/src/tool-provider.js +1 -1
- package/dist/src/tool-provider2.js +1 -1
- package/dist/src/touch-runtime.js +1 -1
- package/dist/src/transport.js +1 -1
- package/dist/src/tree.js +1 -1
- package/dist/src/tv-remote.js +1 -1
- package/dist/src/type-text-runtime.js +1 -1
- package/dist/src/verified-file.js +1 -1
- package/dist/src/video.js +1 -1
- package/dist/src/viewport.js +1 -1
- package/dist/src/web.js +1 -1
- package/dist/src/web2.js +1 -1
- package/dist/src/webdriver-source.js +1 -1
- package/dist/src/window-state.js +1 -1
- package/package.json +4 -1
- package/server.json +2 -2
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.6.apk.sha256 +0 -1
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.6.apk.sha256 +0 -1
- package/dist/src/session-target-evidence.js +0 -1
- package/dist/src/vars.js +0 -2
|
Binary file
|
|
@@ -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.
|
|
4
|
-
"assetName": "agent-device-android-ime-helper-0.21.
|
|
5
|
-
"sha256": "
|
|
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":
|
|
7
|
+
"versionCode": 21008,
|
|
8
8
|
"serviceComponent": "com.callstack.agentdevice.imehelper/.TestInputMethodService",
|
|
9
9
|
"broadcastProtocol": "android-ime-helper-v1"
|
|
10
10
|
}
|
|
Binary file
|
|
@@ -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.
|
|
4
|
-
"releaseTag": "v0.21.
|
|
5
|
-
"assetName": "agent-device-android-snapshot-helper-0.21.
|
|
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": "
|
|
8
|
-
"checksumName": "agent-device-android-snapshot-helper-0.21.
|
|
7
|
+
"sha256": "4a7a293f798f3c0349e23170d886b7defcd8eaad3f47b98c431185f033d7f800",
|
|
8
|
+
"checksumName": "agent-device-android-snapshot-helper-0.21.8.apk.sha256",
|
|
9
9
|
"packageName": "com.callstack.agentdevice.snapshothelper",
|
|
10
|
-
"versionCode":
|
|
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
|
-
|
|
384
|
-
|
|
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(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
+
}
|