zig-mobile-runner 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +484 -0
- package/CONTRIBUTING.md +42 -0
- package/FEATURES.md +112 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/SECURITY.md +34 -0
- package/build.zig +38 -0
- package/build.zig.zon +7 -0
- package/clients/README.md +144 -0
- package/clients/go/README.md +24 -0
- package/clients/go/examples/fake-session/main.go +93 -0
- package/clients/go/go.mod +3 -0
- package/clients/go/zmr/client.go +432 -0
- package/clients/kotlin/README.md +35 -0
- package/clients/kotlin/build.gradle.kts +35 -0
- package/clients/kotlin/settings.gradle.kts +15 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
- package/clients/python/README.md +29 -0
- package/clients/python/examples/fake_session.py +48 -0
- package/clients/python/pyproject.toml +13 -0
- package/clients/python/zmr_client.py +202 -0
- package/clients/rust/Cargo.lock +107 -0
- package/clients/rust/Cargo.toml +10 -0
- package/clients/rust/README.md +19 -0
- package/clients/rust/examples/fake_session.rs +70 -0
- package/clients/rust/src/lib.rs +461 -0
- package/clients/swift/Package.swift +16 -0
- package/clients/swift/README.md +36 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
- package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
- package/clients/typescript/README.md +34 -0
- package/clients/typescript/examples/fake-session.mjs +36 -0
- package/clients/typescript/index.d.ts +144 -0
- package/clients/typescript/index.mjs +192 -0
- package/clients/typescript/package.json +8 -0
- package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
- package/docs/adr/0002-app-local-zmr-contract.md +39 -0
- package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
- package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
- package/docs/adr/README.md +12 -0
- package/docs/ai-agents.md +156 -0
- package/docs/app-integration.md +316 -0
- package/docs/benchmarking.md +275 -0
- package/docs/client-installation.md +141 -0
- package/docs/clients.md +98 -0
- package/docs/config.md +175 -0
- package/docs/demo.md +259 -0
- package/docs/dsl.md +57 -0
- package/docs/install.md +233 -0
- package/docs/market-positioning.md +70 -0
- package/docs/npm.md +359 -0
- package/docs/protocol-fixtures/README.md +8 -0
- package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
- package/docs/protocol-versioning.md +65 -0
- package/docs/protocol.md +560 -0
- package/docs/publication.md +77 -0
- package/docs/release-audit.md +99 -0
- package/docs/release-candidate.md +111 -0
- package/docs/release-evidence.md +188 -0
- package/docs/release-notes-template.md +58 -0
- package/docs/roadmap.md +334 -0
- package/docs/scenario-authoring.md +88 -0
- package/docs/shipping.md +170 -0
- package/docs/trace-privacy.md +88 -0
- package/docs/troubleshooting.md +256 -0
- package/examples/android-app-auth-probe.json +89 -0
- package/examples/android-app-error-state.json +13 -0
- package/examples/android-app-login-smoke.json +192 -0
- package/examples/android-app-onboarding.json +12 -0
- package/examples/android-app-referral-deep-link.json +12 -0
- package/examples/android-shim-smoke.json +19 -0
- package/examples/demo-failure.json +12 -0
- package/examples/demo-fake.json +14 -0
- package/examples/ios-dev-client-open-link.json +26 -0
- package/examples/ios-dev-client-route-snapshot.json +24 -0
- package/examples/ios-shim-smoke.json +23 -0
- package/examples/ios-smoke.json +9 -0
- package/go.work +3 -0
- package/npm/agents.mjs +183 -0
- package/npm/app-config.mjs +95 -0
- package/npm/build-zmr.mjs +21 -0
- package/npm/commands.mjs +104 -0
- package/npm/generated-files.mjs +50 -0
- package/npm/index.mjs +75 -0
- package/npm/init-app.mjs +80 -0
- package/npm/package-scripts.mjs +72 -0
- package/npm/postinstall.mjs +21 -0
- package/npm/scaffold.mjs +179 -0
- package/npm/scenarios.mjs +93 -0
- package/npm/setup.mjs +69 -0
- package/npm/wizard.mjs +117 -0
- package/npm/zmr.mjs +23 -0
- package/package.json +114 -0
- package/prebuilds/darwin-arm64/zmr +0 -0
- package/prebuilds/darwin-x64/zmr +0 -0
- package/prebuilds/linux-arm64/zmr +0 -0
- package/prebuilds/linux-x64/zmr +0 -0
- package/schemas/README.md +26 -0
- package/schemas/action-result.schema.json +27 -0
- package/schemas/capabilities-output.schema.json +98 -0
- package/schemas/devices-output.schema.json +25 -0
- package/schemas/doctor-output.schema.json +51 -0
- package/schemas/explain-output.schema.json +51 -0
- package/schemas/import-output.schema.json +23 -0
- package/schemas/init-output.schema.json +71 -0
- package/schemas/json-rpc.schema.json +55 -0
- package/schemas/release-manifest.schema.json +43 -0
- package/schemas/release-readiness-output.schema.json +127 -0
- package/schemas/run-output.schema.json +43 -0
- package/schemas/scenario.schema.json +128 -0
- package/schemas/schemas-output.schema.json +26 -0
- package/schemas/semantic-snapshot.schema.json +116 -0
- package/schemas/snapshot.schema.json +60 -0
- package/schemas/trace-event.schema.json +14 -0
- package/schemas/trace-manifest.schema.json +59 -0
- package/schemas/validate-output.schema.json +42 -0
- package/schemas/version-output.schema.json +23 -0
- package/schemas/zmr-config.schema.json +75 -0
- package/scripts/android-emulator.sh +126 -0
- package/scripts/assert-ios-physical-ready.sh +213 -0
- package/scripts/benchmark-command.sh +307 -0
- package/scripts/benchmark.sh +359 -0
- package/scripts/benchmark_gate.py +117 -0
- package/scripts/benchmark_result_row.py +88 -0
- package/scripts/compare-benchmarks.py +288 -0
- package/scripts/create-android-demo-app.sh +342 -0
- package/scripts/create-ios-demo-app.sh +261 -0
- package/scripts/demo-android-real.sh +232 -0
- package/scripts/demo-ios-real.sh +270 -0
- package/scripts/demo.sh +464 -0
- package/scripts/device-matrix.sh +338 -0
- package/scripts/ensure-ios-shim-target.rb +237 -0
- package/scripts/install-android-shim.sh +281 -0
- package/scripts/install-ios-shim.sh +589 -0
- package/scripts/pilot-gate.sh +560 -0
- package/scripts/release-readiness.py +838 -0
- package/scripts/release-readiness.sh +91 -0
- package/scripts/run-android-pilot.sh +561 -0
- package/scripts/run-ios-pilot.sh +509 -0
- package/shims/android/README.md +21 -0
- package/shims/android/ZMRShimInstrumentedTest.java +152 -0
- package/shims/android/protocol.md +18 -0
- package/shims/ios/README.md +50 -0
- package/shims/ios/ZMRShim.swift +110 -0
- package/shims/ios/ZMRShimUITestCase.swift +475 -0
- package/shims/ios/protocol.md +74 -0
- package/skills/zmr-mobile-testing/SKILL.md +127 -0
- package/src/android.zig +344 -0
- package/src/android_device_info.zig +99 -0
- package/src/android_emulator.zig +154 -0
- package/src/android_screen_recording.zig +112 -0
- package/src/android_shell.zig +112 -0
- package/src/bundle.zig +124 -0
- package/src/bundle_redaction.zig +272 -0
- package/src/bundle_tar.zig +123 -0
- package/src/cli_devices.zig +97 -0
- package/src/cli_doctor.zig +114 -0
- package/src/cli_import.zig +70 -0
- package/src/cli_info.zig +39 -0
- package/src/cli_init.zig +72 -0
- package/src/cli_output.zig +467 -0
- package/src/cli_run.zig +259 -0
- package/src/cli_serve.zig +287 -0
- package/src/cli_trace.zig +111 -0
- package/src/cli_validate.zig +41 -0
- package/src/command.zig +211 -0
- package/src/config.zig +305 -0
- package/src/config_diagnostics.zig +212 -0
- package/src/config_paths.zig +49 -0
- package/src/device_registry.zig +37 -0
- package/src/doctor.zig +412 -0
- package/src/doctor_hints.zig +52 -0
- package/src/errors.zig +55 -0
- package/src/fake_device.zig +163 -0
- package/src/health.zig +28 -0
- package/src/importer.zig +343 -0
- package/src/importer_json.zig +100 -0
- package/src/importer_model.zig +103 -0
- package/src/ios.zig +399 -0
- package/src/ios_devices.zig +219 -0
- package/src/ios_lifecycle.zig +72 -0
- package/src/ios_shim.zig +242 -0
- package/src/ios_snapshot.zig +20 -0
- package/src/json_fields.zig +80 -0
- package/src/json_rpc.zig +150 -0
- package/src/json_rpc_methods.zig +318 -0
- package/src/json_rpc_observation.zig +31 -0
- package/src/json_rpc_params.zig +52 -0
- package/src/json_rpc_protocol.zig +110 -0
- package/src/json_rpc_trace.zig +73 -0
- package/src/main.zig +135 -0
- package/src/mcp.zig +234 -0
- package/src/mcp_protocol.zig +64 -0
- package/src/mcp_trace.zig +83 -0
- package/src/report.zig +346 -0
- package/src/report_html.zig +63 -0
- package/src/report_values.zig +27 -0
- package/src/run_options.zig +152 -0
- package/src/runner.zig +280 -0
- package/src/runner_actions.zig +109 -0
- package/src/runner_config.zig +6 -0
- package/src/runner_diagnostics.zig +268 -0
- package/src/runner_events.zig +170 -0
- package/src/runner_native.zig +88 -0
- package/src/runner_waits.zig +300 -0
- package/src/scaffold.zig +472 -0
- package/src/scenario.zig +346 -0
- package/src/scenario_fields.zig +50 -0
- package/src/schema_registry.zig +53 -0
- package/src/selector.zig +84 -0
- package/src/semantic.zig +171 -0
- package/src/trace.zig +315 -0
- package/src/trace_json.zig +340 -0
- package/src/trace_summary.zig +218 -0
- package/src/trace_summary_diagnostic.zig +202 -0
- package/src/types.zig +120 -0
- package/src/uiautomator.zig +164 -0
- package/src/validation.zig +187 -0
- package/src/version.zig +22 -0
- package/viewer/app.js +373 -0
- package/viewer/index.html +126 -0
- package/viewer/parser.js +233 -0
- package/viewer/styles.css +585 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# iOS Shim Protocol
|
|
2
|
+
|
|
3
|
+
The iOS shim protocol is internal and may change before `v1.0.0`.
|
|
4
|
+
|
|
5
|
+
Commands are newline-delimited JSON objects:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{"cmd":"snapshot"}
|
|
9
|
+
{"cmd":"tap","selector":"text=Continue","x":20,"y":40}
|
|
10
|
+
{"cmd":"type","selector":"identifier=email","text":"hello"}
|
|
11
|
+
{"cmd":"eraseText","selector":"identifier=email","maxChars":20}
|
|
12
|
+
{"cmd":"hideKeyboard"}
|
|
13
|
+
{"cmd":"swipe","x1":300,"y1":900,"x2":300,"y2":300,"durationMs":250}
|
|
14
|
+
{"cmd":"pressBack"}
|
|
15
|
+
{"cmd":"settle","durationMs":1000}
|
|
16
|
+
{"cmd":"appState"}
|
|
17
|
+
{"cmd":"acceptSystemAlert","text":"Open"}
|
|
18
|
+
{"cmd":"query","selector":"textContains=Continue"}
|
|
19
|
+
{"cmd":"screenshot"}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Selector-addressed `tap`, `type`, and `eraseText` support these internal
|
|
23
|
+
selector strings:
|
|
24
|
+
|
|
25
|
+
- `text=<exact>` and `textContains=<substring>`
|
|
26
|
+
- `label=<exact>` and `labelContains=<substring>`
|
|
27
|
+
- `identifier=<exact>` and `identifierContains=<substring>`
|
|
28
|
+
- `resourceId=<exact>` and `resourceIdContains=<substring>`
|
|
29
|
+
- `id=<stable snapshot id>`
|
|
30
|
+
- `value=<exact>` and `valueContains=<substring>`
|
|
31
|
+
- `type=<XCUIElementType...>`
|
|
32
|
+
|
|
33
|
+
`query` is the fast path used by Zig waits/assertions for single-field selectors
|
|
34
|
+
that XCTest can evaluate natively. It returns:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{"status":"ok","exists":true,"hittable":true}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`screenshot` returns a PNG payload encoded for the local shim transport:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{"status":"ok","format":"png","base64":"..."}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Snapshot responses return bounded XCTest element data in a shape Zig can map
|
|
47
|
+
into `UiNode`. The shim captures common interactive and readable element
|
|
48
|
+
families and caps the response at 256 nodes so large application trees do not
|
|
49
|
+
turn every snapshot into a full hierarchy crawl:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"status": "ok",
|
|
54
|
+
"nodes": [
|
|
55
|
+
{
|
|
56
|
+
"id": "button-continue",
|
|
57
|
+
"type": "XCUIElementTypeButton",
|
|
58
|
+
"label": "Continue",
|
|
59
|
+
"value": "Continue",
|
|
60
|
+
"identifier": "continue_button",
|
|
61
|
+
"bounds": { "x": 10, "y": 20, "width": 100, "height": 44 },
|
|
62
|
+
"enabled": true,
|
|
63
|
+
"visible": true,
|
|
64
|
+
"selected": false
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Errors use a stable envelope:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{"status":"error","code":"selector.timeout","message":"selector did not match"}
|
|
74
|
+
```
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zmr-mobile-testing
|
|
3
|
+
description: Use when testing mobile apps with Zig Mobile Runner, integrating app-local .zmr setup, driving Android or iOS simulator scenarios, using JSON-RPC or MCP agent sessions, exporting traces, or comparing mobile runner benchmarks.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ZMR Mobile Testing
|
|
7
|
+
|
|
8
|
+
Use ZMR as the typed control plane for mobile app testing. Keep model reasoning
|
|
9
|
+
outside the runner; use ZMR for device discovery, observations, actions, waits,
|
|
10
|
+
assertions, traces, and diagnostics.
|
|
11
|
+
|
|
12
|
+
## Start From App-Local State
|
|
13
|
+
|
|
14
|
+
1. Look for `.zmr/config.json` in the app checkout.
|
|
15
|
+
2. If it is missing, scaffold it:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx zmr-wizard --app-id com.example.mobiletest --package-json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
3. Run setup diagnostics before touching a device:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
zmr doctor --json --config .zmr/config.json
|
|
25
|
+
zmr validate --json .zmr/android-smoke.json
|
|
26
|
+
zmr validate --json .zmr/ios-smoke.json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Use `zmr doctor --strict --json` for CI-style gates.
|
|
30
|
+
|
|
31
|
+
## Agent Session Pattern
|
|
32
|
+
|
|
33
|
+
Prefer JSON-RPC over stdio for interactive agent work:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
zmr serve --transport stdio --config .zmr/config.json --trace-dir traces/zmr-agent
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Call methods in this order:
|
|
40
|
+
|
|
41
|
+
1. `runner.capabilities`
|
|
42
|
+
2. `session.create`
|
|
43
|
+
3. `observe.semanticSnapshot` for planning, or `observe.snapshot` for raw adapter data
|
|
44
|
+
4. one typed action, wait, or assertion
|
|
45
|
+
5. `observe.semanticSnapshot`
|
|
46
|
+
6. `trace.events` while the session is active
|
|
47
|
+
7. `trace.export` with redaction enabled
|
|
48
|
+
8. `session.close`
|
|
49
|
+
|
|
50
|
+
Do not scrape terminal output when CLI JSON, snapshots, action results, or trace
|
|
51
|
+
events contain the same information.
|
|
52
|
+
|
|
53
|
+
For MCP-capable agents, start:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
zmr mcp --config .zmr/config.json --trace-dir traces/zmr-agent
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Use the `semantic_snapshot`, `tap`, `type`, `wait_visible`, `trace_events`, and
|
|
60
|
+
`trace_export` tools. Prefer `semantic_snapshot` because it normalizes Android
|
|
61
|
+
and iOS hierarchy classes into roles, selectors, bounds, and recommended
|
|
62
|
+
actions.
|
|
63
|
+
|
|
64
|
+
## Scenario Pattern
|
|
65
|
+
|
|
66
|
+
For repeatable tests, edit `.zmr/*.json` scenarios and run:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
zmr validate --json .zmr/<scenario>.json
|
|
70
|
+
zmr run .zmr/<scenario>.json --json --trace-dir traces/zmr-<scenario>
|
|
71
|
+
zmr explain --json traces/zmr-<scenario>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Prefer stable selectors: resource id or accessibility identifier first,
|
|
75
|
+
content description/accessibility label second, exact text third, textContains
|
|
76
|
+
only when copy varies, coordinates last.
|
|
77
|
+
|
|
78
|
+
Use `waitAny` for valid branches and `whenVisible` for optional screens. Keep
|
|
79
|
+
credentials, private app terms, and private traces out of public docs and
|
|
80
|
+
examples.
|
|
81
|
+
|
|
82
|
+
## Trace Handling
|
|
83
|
+
|
|
84
|
+
When a run fails, inspect `zmr explain --json`, `events.jsonl`, the final
|
|
85
|
+
snapshot, and the trace viewer report from `zmr report`.
|
|
86
|
+
|
|
87
|
+
Before sharing:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
zmr export traces/zmr-<scenario> --out traces/zmr-<scenario>-redacted.zmrtrace --redact
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Add `--omit-screenshots` if visual artifacts may contain sensitive data.
|
|
94
|
+
|
|
95
|
+
## Release And Claim Guard
|
|
96
|
+
|
|
97
|
+
Before reporting that ZMR is ready for a release, production use, or a market
|
|
98
|
+
comparison, ask the runner to evaluate evidence instead of inferring from test
|
|
99
|
+
passes:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
zmr-release-readiness --json \
|
|
103
|
+
--evidence traces/release-candidate/<run>/evidence.jsonl \
|
|
104
|
+
--target dev-preview
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For production or market claims, include app-local pilot and benchmark evidence
|
|
108
|
+
with additional `--evidence` arguments. Read `satisfied` for proven requirements
|
|
109
|
+
and `blocked`, `missing`, `insufficient`, `failed`, and `planned` for remaining
|
|
110
|
+
work. Use `recommendedWording` as the release summary and respect
|
|
111
|
+
`claimLimitations`; do not infer stronger claims from `passed` alone or upgrade
|
|
112
|
+
a dev-preview result into a production-stable or competitive claim. When
|
|
113
|
+
blocked, execute `nextSteps[].commands` in order and use `nextSteps[].covers`
|
|
114
|
+
to understand which blocked requirements each step resolves.
|
|
115
|
+
|
|
116
|
+
## Benchmarks
|
|
117
|
+
|
|
118
|
+
Use ZMR repeated runs:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
zmr-benchmark --zmr .zmr/android-smoke.json --device emulator-5554 --runs 20 --trace-root traces/zmr-android-reliability --min-pass-rate 100 --max-failures 0
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Use `zmr-benchmark-command` for any app-local baseline command and
|
|
125
|
+
`zmr-compare-benchmarks` for reports. Only claim performance wins from
|
|
126
|
+
equivalent app paths, same device state, repeated runs, and trace-backed
|
|
127
|
+
failure diagnostics.
|
package/src/android.zig
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const command = @import("command.zig");
|
|
3
|
+
const android_device_info = @import("android_device_info.zig");
|
|
4
|
+
const android_shell = @import("android_shell.zig");
|
|
5
|
+
const android_screen_recording = @import("android_screen_recording.zig");
|
|
6
|
+
const ios_shim = @import("ios_shim.zig");
|
|
7
|
+
const trace = @import("trace.zig");
|
|
8
|
+
const types = @import("types.zig");
|
|
9
|
+
const uiautomator = @import("uiautomator.zig");
|
|
10
|
+
|
|
11
|
+
const default_max_output = 32 * 1024 * 1024;
|
|
12
|
+
const default_adb_timeout_ms = 15_000;
|
|
13
|
+
const install_adb_timeout_ms = 120_000;
|
|
14
|
+
const shim_timeout_ms = 5_000;
|
|
15
|
+
const open_link_attempts = 3;
|
|
16
|
+
const open_link_retry_delay_ms = 500;
|
|
17
|
+
|
|
18
|
+
pub const AndroidDevice = struct {
|
|
19
|
+
allocator: std.mem.Allocator,
|
|
20
|
+
adb_path: []const u8 = "adb",
|
|
21
|
+
serial: ?[]const u8 = null,
|
|
22
|
+
app_id: []const u8,
|
|
23
|
+
shim_path: ?[]const u8 = null,
|
|
24
|
+
|
|
25
|
+
pub fn init(
|
|
26
|
+
allocator: std.mem.Allocator,
|
|
27
|
+
adb_path: []const u8,
|
|
28
|
+
serial: ?[]const u8,
|
|
29
|
+
app_id: []const u8,
|
|
30
|
+
) !AndroidDevice {
|
|
31
|
+
return try initWithShim(allocator, adb_path, serial, app_id, null);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub fn initWithShim(
|
|
35
|
+
allocator: std.mem.Allocator,
|
|
36
|
+
adb_path: []const u8,
|
|
37
|
+
serial: ?[]const u8,
|
|
38
|
+
app_id: []const u8,
|
|
39
|
+
shim_path: ?[]const u8,
|
|
40
|
+
) !AndroidDevice {
|
|
41
|
+
return .{
|
|
42
|
+
.allocator = allocator,
|
|
43
|
+
.adb_path = try allocator.dupe(u8, adb_path),
|
|
44
|
+
.serial = try types.dupeOptional(allocator, serial),
|
|
45
|
+
.app_id = try allocator.dupe(u8, app_id),
|
|
46
|
+
.shim_path = try types.dupeOptional(allocator, shim_path),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub fn deinit(self: *AndroidDevice) void {
|
|
51
|
+
self.allocator.free(self.adb_path);
|
|
52
|
+
if (self.serial) |value| self.allocator.free(value);
|
|
53
|
+
self.allocator.free(self.app_id);
|
|
54
|
+
if (self.shim_path) |value| self.allocator.free(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub fn listDevices(self: *AndroidDevice) ![]types.DeviceInfo {
|
|
58
|
+
return try android_device_info.listDevices(self.allocator, self.adb_path);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub fn install(self: *AndroidDevice, apk_path: []const u8) !void {
|
|
62
|
+
const result = try self.runAdbWithTimeout(&.{ "install", "-r", apk_path }, default_max_output, install_adb_timeout_ms);
|
|
63
|
+
defer result.deinit(self.allocator);
|
|
64
|
+
try result.ensureSuccess();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pub fn launch(self: *AndroidDevice) !void {
|
|
68
|
+
const result = try self.runAdb(&.{ "shell", "monkey", "-p", self.app_id, "-c", "android.intent.category.LAUNCHER", "1" }, default_max_output);
|
|
69
|
+
defer result.deinit(self.allocator);
|
|
70
|
+
try result.ensureSuccess();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
pub fn stop(self: *AndroidDevice) !void {
|
|
74
|
+
const result = try self.runAdb(&.{ "shell", "am", "force-stop", self.app_id }, default_max_output);
|
|
75
|
+
defer result.deinit(self.allocator);
|
|
76
|
+
try result.ensureSuccess();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pub fn clearState(self: *AndroidDevice) !void {
|
|
80
|
+
const result = try self.runAdb(&.{ "shell", "pm", "clear", self.app_id }, default_max_output);
|
|
81
|
+
defer result.deinit(self.allocator);
|
|
82
|
+
try result.ensureSuccess();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
pub fn openLink(self: *AndroidDevice, url: []const u8) !void {
|
|
86
|
+
var args = try android_shell.openLinkIntent(self.allocator, url, self.app_id);
|
|
87
|
+
defer args.deinit();
|
|
88
|
+
var attempt: usize = 0;
|
|
89
|
+
while (attempt < open_link_attempts) : (attempt += 1) {
|
|
90
|
+
const result = try self.runAdb(args.items(), default_max_output);
|
|
91
|
+
defer result.deinit(self.allocator);
|
|
92
|
+
try result.ensureSuccess();
|
|
93
|
+
|
|
94
|
+
if (self.isAppForeground() catch false) return;
|
|
95
|
+
if (attempt + 1 < open_link_attempts) {
|
|
96
|
+
std.Thread.sleep(open_link_retry_delay_ms * std.time.ns_per_ms);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return error.AppDidNotOpen;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
pub fn tap(self: *AndroidDevice, x: i32, y: i32) !void {
|
|
103
|
+
if (self.shim_path != null) return try self.runShimAction(.{ .kind = .tap, .x = x, .y = y });
|
|
104
|
+
var args = try android_shell.tap(self.allocator, x, y);
|
|
105
|
+
defer args.deinit();
|
|
106
|
+
const result = try self.runAdb(args.items(), default_max_output);
|
|
107
|
+
defer result.deinit(self.allocator);
|
|
108
|
+
try result.ensureSuccess();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
pub fn typeText(self: *AndroidDevice, text: []const u8) !void {
|
|
112
|
+
if (self.shim_path != null) return try self.runShimAction(.{ .kind = .type_text, .text = text });
|
|
113
|
+
var args = try android_shell.typeText(self.allocator, text);
|
|
114
|
+
defer args.deinit();
|
|
115
|
+
const result = try self.runAdb(args.items(), default_max_output);
|
|
116
|
+
defer result.deinit(self.allocator);
|
|
117
|
+
try result.ensureSuccess();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
pub fn eraseText(self: *AndroidDevice, max_chars: u32) !void {
|
|
121
|
+
if (self.shim_path != null) return try self.runShimAction(.{ .kind = .erase_text, .max_chars = max_chars });
|
|
122
|
+
var args = try android_shell.eraseText(self.allocator, max_chars);
|
|
123
|
+
defer args.deinit();
|
|
124
|
+
const result = try self.runAdb(args.items(), default_max_output);
|
|
125
|
+
defer result.deinit(self.allocator);
|
|
126
|
+
try result.ensureSuccess();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pub fn hideKeyboard(self: *AndroidDevice) !void {
|
|
130
|
+
if (self.shim_path != null) return try self.runShimAction(.{ .kind = .hide_keyboard });
|
|
131
|
+
var args = try android_shell.pressBack(self.allocator);
|
|
132
|
+
defer args.deinit();
|
|
133
|
+
const result = try self.runAdb(args.items(), default_max_output);
|
|
134
|
+
defer result.deinit(self.allocator);
|
|
135
|
+
try result.ensureSuccess();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pub fn swipe(self: *AndroidDevice, x1: i32, y1: i32, x2: i32, y2: i32, duration_ms: u32) !void {
|
|
139
|
+
if (self.shim_path != null) return try self.runShimAction(.{ .kind = .swipe, .x1 = x1, .y1 = y1, .x2 = x2, .y2 = y2, .duration_ms = duration_ms });
|
|
140
|
+
var args = try android_shell.swipe(self.allocator, x1, y1, x2, y2, duration_ms);
|
|
141
|
+
defer args.deinit();
|
|
142
|
+
const result = try self.runAdb(args.items(), default_max_output);
|
|
143
|
+
defer result.deinit(self.allocator);
|
|
144
|
+
try result.ensureSuccess();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
pub fn pressBack(self: *AndroidDevice) !void {
|
|
148
|
+
if (self.shim_path != null) return try self.runShimAction(.{ .kind = .press_back });
|
|
149
|
+
var args = try android_shell.pressBack(self.allocator);
|
|
150
|
+
defer args.deinit();
|
|
151
|
+
const result = try self.runAdb(args.items(), default_max_output);
|
|
152
|
+
defer result.deinit(self.allocator);
|
|
153
|
+
try result.ensureSuccess();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
pub fn startScreenRecording(self: *AndroidDevice, remote_path: []const u8) !AndroidScreenRecording {
|
|
157
|
+
return try android_screen_recording.start(self.allocator, self.adb_path, self.serial, remote_path);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
pub fn settle(self: *AndroidDevice, timeout_ms: u64) !void {
|
|
161
|
+
if (self.shim_path != null) {
|
|
162
|
+
return try self.runShimAction(.{
|
|
163
|
+
.kind = .settle,
|
|
164
|
+
.duration_ms = @as(u32, @intCast(@min(timeout_ms, std.math.maxInt(u32)))),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
std.Thread.sleep(timeout_ms * std.time.ns_per_ms);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
pub fn snapshot(self: *AndroidDevice, writer: ?*trace.TraceWriter) !types.ObservationSnapshot {
|
|
171
|
+
const id = if (writer) |tw| try tw.nextSnapshotId() else try std.fmt.allocPrint(self.allocator, "snapshot-{d}", .{std.time.milliTimestamp()});
|
|
172
|
+
errdefer self.allocator.free(id);
|
|
173
|
+
|
|
174
|
+
const xml = if (self.shim_path == null) try self.dumpHierarchy() else null;
|
|
175
|
+
defer if (xml) |value| self.allocator.free(value);
|
|
176
|
+
const nodes = if (self.shim_path) |_|
|
|
177
|
+
try self.snapshotNodesFromShim()
|
|
178
|
+
else
|
|
179
|
+
try uiautomator.parseHierarchy(self.allocator, xml.?);
|
|
180
|
+
errdefer {
|
|
181
|
+
for (nodes) |node| node.deinit(self.allocator);
|
|
182
|
+
self.allocator.free(nodes);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
var screenshot_artifact: ?[]const u8 = null;
|
|
186
|
+
errdefer if (screenshot_artifact) |path| self.allocator.free(path);
|
|
187
|
+
var tree_artifact: ?[]const u8 = null;
|
|
188
|
+
errdefer if (tree_artifact) |path| self.allocator.free(path);
|
|
189
|
+
|
|
190
|
+
if (writer) |tw| {
|
|
191
|
+
if (tw.capture.capture_screenshots) {
|
|
192
|
+
const screenshot = self.captureScreenshot() catch null;
|
|
193
|
+
if (screenshot) |bytes| {
|
|
194
|
+
defer self.allocator.free(bytes);
|
|
195
|
+
const file_name = try std.fmt.allocPrint(self.allocator, "{s}.png", .{id});
|
|
196
|
+
defer self.allocator.free(file_name);
|
|
197
|
+
screenshot_artifact = try tw.writeArtifact(file_name, bytes);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (tw.capture.capture_hierarchy and xml != null) {
|
|
201
|
+
const tree_name = try std.fmt.allocPrint(self.allocator, "{s}.xml", .{id});
|
|
202
|
+
defer self.allocator.free(tree_name);
|
|
203
|
+
tree_artifact = try tw.writeArtifact(tree_name, xml.?);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const active = try self.activeWindow();
|
|
208
|
+
errdefer active.deinit(self.allocator);
|
|
209
|
+
|
|
210
|
+
const screen = self.viewport() catch types.Viewport{};
|
|
211
|
+
const display_density_dpi = self.displayDensityDpi() catch null;
|
|
212
|
+
const capture_logs = if (writer) |tw| tw.capture.capture_logs else true;
|
|
213
|
+
const logs = if (capture_logs) self.logDelta() catch null else null;
|
|
214
|
+
errdefer if (logs) |value| self.allocator.free(value);
|
|
215
|
+
|
|
216
|
+
return .{
|
|
217
|
+
.id = id,
|
|
218
|
+
.timestamp_ms = std.time.milliTimestamp(),
|
|
219
|
+
.viewport = screen,
|
|
220
|
+
.display_density_dpi = display_density_dpi,
|
|
221
|
+
.active_package = active.package,
|
|
222
|
+
.active_activity = active.activity,
|
|
223
|
+
.screenshot_artifact = screenshot_artifact,
|
|
224
|
+
.tree_artifact = tree_artifact,
|
|
225
|
+
.focused_node_id = null,
|
|
226
|
+
.log_delta = logs,
|
|
227
|
+
.nodes = nodes,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn dumpHierarchy(self: *AndroidDevice) ![]u8 {
|
|
232
|
+
const result = try self.runAdb(&.{ "exec-out", "uiautomator", "dump", "/dev/tty" }, default_max_output);
|
|
233
|
+
defer result.deinit(self.allocator);
|
|
234
|
+
try result.ensureSuccess();
|
|
235
|
+
return try self.allocator.dupe(u8, result.stdout);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fn captureScreenshot(self: *AndroidDevice) ![]u8 {
|
|
239
|
+
const result = try self.runAdb(&.{ "exec-out", "screencap", "-p" }, default_max_output);
|
|
240
|
+
defer result.deinit(self.allocator);
|
|
241
|
+
try result.ensureSuccess();
|
|
242
|
+
return try self.allocator.dupe(u8, result.stdout);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
fn activeWindow(self: *AndroidDevice) !ActiveWindow {
|
|
246
|
+
const result = try self.runAdb(&.{ "shell", "dumpsys", "window" }, default_max_output);
|
|
247
|
+
defer result.deinit(self.allocator);
|
|
248
|
+
try result.ensureSuccess();
|
|
249
|
+
return try android_device_info.parseActiveWindow(self.allocator, result.stdout);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fn isAppForeground(self: *AndroidDevice) !bool {
|
|
253
|
+
const active = try self.activeWindow();
|
|
254
|
+
defer active.deinit(self.allocator);
|
|
255
|
+
const package = active.package orelse return false;
|
|
256
|
+
return std.mem.eql(u8, package, self.app_id);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fn viewport(self: *AndroidDevice) !types.Viewport {
|
|
260
|
+
const result = try self.runAdb(&.{ "shell", "wm", "size" }, 4096);
|
|
261
|
+
defer result.deinit(self.allocator);
|
|
262
|
+
try result.ensureSuccess();
|
|
263
|
+
return android_device_info.parseViewport(result.stdout) catch types.Viewport{};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fn displayDensityDpi(self: *AndroidDevice) !?u32 {
|
|
267
|
+
const result = try self.runAdb(&.{ "shell", "wm", "density" }, 4096);
|
|
268
|
+
defer result.deinit(self.allocator);
|
|
269
|
+
try result.ensureSuccess();
|
|
270
|
+
return android_device_info.parseDisplayDensityDpi(result.stdout);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn logDelta(self: *AndroidDevice) !?[]const u8 {
|
|
274
|
+
const result = try self.runAdb(&.{ "logcat", "-d", "-t", "80" }, 1024 * 1024);
|
|
275
|
+
defer result.deinit(self.allocator);
|
|
276
|
+
if (result.term != .Exited or result.term.Exited != 0) return null;
|
|
277
|
+
return try self.allocator.dupe(u8, result.stdout);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
fn snapshotNodesFromShim(self: *AndroidDevice) ![]types.UiNode {
|
|
281
|
+
const response = try self.runShim(.{ .kind = .snapshot });
|
|
282
|
+
defer self.allocator.free(response);
|
|
283
|
+
return try ios_shim.parseSnapshotNodes(self.allocator, response);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
fn runShimAction(self: *AndroidDevice, shim_command: ios_shim.Command) !void {
|
|
287
|
+
const response = try self.runShim(shim_command);
|
|
288
|
+
defer self.allocator.free(response);
|
|
289
|
+
try ios_shim.parseOkResponse(response);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fn runShim(self: *AndroidDevice, shim_command: ios_shim.Command) ![]u8 {
|
|
293
|
+
const path = self.shim_path orelse return error.AndroidShimRequired;
|
|
294
|
+
|
|
295
|
+
var input = std.ArrayList(u8).empty;
|
|
296
|
+
defer input.deinit(self.allocator);
|
|
297
|
+
try ios_shim.writeCommandJson(input.writer(self.allocator), shim_command);
|
|
298
|
+
|
|
299
|
+
const result = try command.runWithInputTimeout(self.allocator, &.{path}, input.items, 4 * 1024 * 1024, shim_timeout_ms);
|
|
300
|
+
defer result.deinit(self.allocator);
|
|
301
|
+
try result.ensureSuccess();
|
|
302
|
+
return try self.allocator.dupe(u8, result.stdout);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
fn runAdb(self: *AndroidDevice, extra: []const []const u8, max_output_bytes: usize) !command.ExecResult {
|
|
306
|
+
return try self.runAdbWithTimeout(extra, max_output_bytes, default_adb_timeout_ms);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fn runAdbWithTimeout(self: *AndroidDevice, extra: []const []const u8, max_output_bytes: usize, timeout_ms: u64) !command.ExecResult {
|
|
310
|
+
var argv = std.ArrayList([]const u8).empty;
|
|
311
|
+
defer argv.deinit(self.allocator);
|
|
312
|
+
try self.appendAdbBase(&argv);
|
|
313
|
+
try argv.appendSlice(self.allocator, extra);
|
|
314
|
+
return try command.runWithTimeout(self.allocator, argv.items, max_output_bytes, timeout_ms);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
fn appendAdbBase(self: *AndroidDevice, argv: *std.ArrayList([]const u8)) !void {
|
|
318
|
+
try argv.append(self.allocator, self.adb_path);
|
|
319
|
+
if (self.serial) |serial| {
|
|
320
|
+
try argv.append(self.allocator, "-s");
|
|
321
|
+
try argv.append(self.allocator, serial);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
pub const AndroidScreenRecording = android_screen_recording.AndroidScreenRecording;
|
|
327
|
+
|
|
328
|
+
pub fn listDevices(allocator: std.mem.Allocator, adb_path: []const u8) ![]types.DeviceInfo {
|
|
329
|
+
return try android_device_info.listDevices(allocator, adb_path);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
pub const ActiveWindow = android_device_info.ActiveWindow;
|
|
333
|
+
|
|
334
|
+
pub fn parseActiveWindow(allocator: std.mem.Allocator, dumpsys: []const u8) !ActiveWindow {
|
|
335
|
+
return try android_device_info.parseActiveWindow(allocator, dumpsys);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
pub fn parseViewport(output: []const u8) !types.Viewport {
|
|
339
|
+
return try android_device_info.parseViewport(output);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
pub fn parseDisplayDensityDpi(output: []const u8) ?u32 {
|
|
343
|
+
return android_device_info.parseDisplayDensityDpi(output);
|
|
344
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const command = @import("command.zig");
|
|
3
|
+
const types = @import("types.zig");
|
|
4
|
+
|
|
5
|
+
const default_adb_timeout_ms = 15_000;
|
|
6
|
+
|
|
7
|
+
pub fn listDevices(allocator: std.mem.Allocator, adb_path: []const u8) ![]types.DeviceInfo {
|
|
8
|
+
const result = try command.runWithTimeout(allocator, &.{ adb_path, "devices" }, 1024 * 1024, default_adb_timeout_ms);
|
|
9
|
+
defer result.deinit(allocator);
|
|
10
|
+
try result.ensureSuccess();
|
|
11
|
+
|
|
12
|
+
var devices = std.ArrayList(types.DeviceInfo).empty;
|
|
13
|
+
errdefer {
|
|
14
|
+
for (devices.items) |device| device.deinit(allocator);
|
|
15
|
+
devices.deinit(allocator);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var lines = std.mem.splitScalar(u8, result.stdout, '\n');
|
|
19
|
+
_ = lines.next();
|
|
20
|
+
while (lines.next()) |raw_line| {
|
|
21
|
+
const line = std.mem.trim(u8, raw_line, " \t\r\n");
|
|
22
|
+
if (line.len == 0) continue;
|
|
23
|
+
var parts = std.mem.tokenizeAny(u8, line, " \t");
|
|
24
|
+
const serial = parts.next() orelse continue;
|
|
25
|
+
const state = parts.next() orelse continue;
|
|
26
|
+
try devices.append(allocator, .{
|
|
27
|
+
.serial = try allocator.dupe(u8, serial),
|
|
28
|
+
.state = try allocator.dupe(u8, state),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return try devices.toOwnedSlice(allocator);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pub const ActiveWindow = struct {
|
|
36
|
+
package: ?[]const u8 = null,
|
|
37
|
+
activity: ?[]const u8 = null,
|
|
38
|
+
|
|
39
|
+
pub fn deinit(self: ActiveWindow, allocator: std.mem.Allocator) void {
|
|
40
|
+
if (self.package) |value| allocator.free(value);
|
|
41
|
+
if (self.activity) |value| allocator.free(value);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
pub fn parseActiveWindow(allocator: std.mem.Allocator, dumpsys: []const u8) !ActiveWindow {
|
|
46
|
+
const markers = [_][]const u8{ "mCurrentFocus=", "mFocusedApp=", "topResumedActivity=" };
|
|
47
|
+
for (markers) |marker| {
|
|
48
|
+
if (std.mem.indexOf(u8, dumpsys, marker)) |pos| {
|
|
49
|
+
const line_end = std.mem.indexOfScalarPos(u8, dumpsys, pos, '\n') orelse dumpsys.len;
|
|
50
|
+
const line = dumpsys[pos..line_end];
|
|
51
|
+
if (parsePackageActivity(allocator, line)) |active| return active else |_| continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return .{};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn parsePackageActivity(allocator: std.mem.Allocator, line: []const u8) !ActiveWindow {
|
|
58
|
+
const slash = std.mem.indexOfScalar(u8, line, '/') orelse return error.NoActivity;
|
|
59
|
+
var pkg_start = slash;
|
|
60
|
+
while (pkg_start > 0) : (pkg_start -= 1) {
|
|
61
|
+
const ch = line[pkg_start - 1];
|
|
62
|
+
if (!(std.ascii.isAlphanumeric(ch) or ch == '_' or ch == '.')) break;
|
|
63
|
+
}
|
|
64
|
+
var activity_end = slash + 1;
|
|
65
|
+
while (activity_end < line.len) : (activity_end += 1) {
|
|
66
|
+
const ch = line[activity_end];
|
|
67
|
+
if (!(std.ascii.isAlphanumeric(ch) or ch == '_' or ch == '.' or ch == '$')) break;
|
|
68
|
+
}
|
|
69
|
+
if (pkg_start >= slash or activity_end <= slash + 1) return error.NoActivity;
|
|
70
|
+
return .{
|
|
71
|
+
.package = try allocator.dupe(u8, line[pkg_start..slash]),
|
|
72
|
+
.activity = try allocator.dupe(u8, line[slash + 1 .. activity_end]),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pub fn parseViewport(output: []const u8) !types.Viewport {
|
|
77
|
+
const marker = "Physical size:";
|
|
78
|
+
const start = std.mem.indexOf(u8, output, marker) orelse return error.NoViewport;
|
|
79
|
+
const after = std.mem.trim(u8, output[start + marker.len ..], " \t\r\n");
|
|
80
|
+
const x = std.mem.indexOfScalar(u8, after, 'x') orelse return error.NoViewport;
|
|
81
|
+
var end: usize = x + 1;
|
|
82
|
+
while (end < after.len and std.ascii.isDigit(after[end])) : (end += 1) {}
|
|
83
|
+
return .{
|
|
84
|
+
.width = try std.fmt.parseInt(u32, std.mem.trim(u8, after[0..x], " \t"), 10),
|
|
85
|
+
.height = try std.fmt.parseInt(u32, after[x + 1 .. end], 10),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub fn parseDisplayDensityDpi(output: []const u8) ?u32 {
|
|
90
|
+
var lines = std.mem.splitScalar(u8, output, '\n');
|
|
91
|
+
while (lines.next()) |raw_line| {
|
|
92
|
+
const line = std.mem.trim(u8, raw_line, " \t\r\n");
|
|
93
|
+
const prefix = "Physical density:";
|
|
94
|
+
if (!std.mem.startsWith(u8, line, prefix)) continue;
|
|
95
|
+
const value = std.mem.trim(u8, line[prefix.len..], " \t\r\n");
|
|
96
|
+
return std.fmt.parseInt(u32, value, 10) catch null;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|