androidctl 0.1.0__py3-none-any.whl

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 (187) hide show
  1. androidctl/__init__.py +5 -0
  2. androidctl/__main__.py +4 -0
  3. androidctl/_version.py +1 -0
  4. androidctl/app.py +73 -0
  5. androidctl/cli_options.py +27 -0
  6. androidctl/command_payloads.py +264 -0
  7. androidctl/command_views.py +157 -0
  8. androidctl/commands/__init__.py +1 -0
  9. androidctl/commands/actions.py +236 -0
  10. androidctl/commands/adb_wireless.py +157 -0
  11. androidctl/commands/close.py +30 -0
  12. androidctl/commands/connect.py +69 -0
  13. androidctl/commands/execute.py +179 -0
  14. androidctl/commands/list_apps.py +26 -0
  15. androidctl/commands/observe.py +26 -0
  16. androidctl/commands/open.py +41 -0
  17. androidctl/commands/plumbing.py +58 -0
  18. androidctl/commands/run_pipeline.py +307 -0
  19. androidctl/commands/screenshot.py +29 -0
  20. androidctl/commands/setup.py +301 -0
  21. androidctl/commands/wait.py +60 -0
  22. androidctl/daemon/__init__.py +1 -0
  23. androidctl/daemon/client.py +348 -0
  24. androidctl/daemon/discovery.py +190 -0
  25. androidctl/daemon/launcher.py +26 -0
  26. androidctl/daemon/owner.py +349 -0
  27. androidctl/errors/__init__.py +1 -0
  28. androidctl/errors/mapping.py +149 -0
  29. androidctl/errors/models.py +16 -0
  30. androidctl/exit_codes.py +8 -0
  31. androidctl/output.py +147 -0
  32. androidctl/parsing/__init__.py +1 -0
  33. androidctl/parsing/duration.py +17 -0
  34. androidctl/parsing/open_target.py +51 -0
  35. androidctl/parsing/refs.py +12 -0
  36. androidctl/parsing/screen_id.py +10 -0
  37. androidctl/parsing/wait.py +70 -0
  38. androidctl/renderers/__init__.py +110 -0
  39. androidctl/renderers/_paths.py +109 -0
  40. androidctl/renderers/xml.py +234 -0
  41. androidctl/renderers/xml_projection.py +732 -0
  42. androidctl/resources/__init__.py +1 -0
  43. androidctl/resources/androidctl-agent-0.1.0-release.apk +0 -0
  44. androidctl/setup/__init__.py +1 -0
  45. androidctl/setup/accessibility.py +159 -0
  46. androidctl/setup/adb.py +586 -0
  47. androidctl/setup/apk_resource.py +29 -0
  48. androidctl/setup/pairing.py +70 -0
  49. androidctl/setup/verify.py +175 -0
  50. androidctl/workspace/__init__.py +3 -0
  51. androidctl/workspace/resolve.py +27 -0
  52. androidctl-0.1.0.dist-info/METADATA +217 -0
  53. androidctl-0.1.0.dist-info/RECORD +187 -0
  54. androidctl-0.1.0.dist-info/WHEEL +5 -0
  55. androidctl-0.1.0.dist-info/entry_points.txt +3 -0
  56. androidctl-0.1.0.dist-info/licenses/LICENSE +674 -0
  57. androidctl-0.1.0.dist-info/top_level.txt +3 -0
  58. androidctl_contracts/__init__.py +55 -0
  59. androidctl_contracts/_version.py +1 -0
  60. androidctl_contracts/_wire_helpers.py +31 -0
  61. androidctl_contracts/base.py +142 -0
  62. androidctl_contracts/command_catalog.py +414 -0
  63. androidctl_contracts/command_results.py +630 -0
  64. androidctl_contracts/daemon_api.py +335 -0
  65. androidctl_contracts/errors.py +44 -0
  66. androidctl_contracts/paths.py +5 -0
  67. androidctl_contracts/public_screen.py +579 -0
  68. androidctl_contracts/user_state.py +23 -0
  69. androidctl_contracts/vocabulary.py +82 -0
  70. androidctld/__init__.py +5 -0
  71. androidctld/__main__.py +63 -0
  72. androidctld/_version.py +1 -0
  73. androidctld/actions/__init__.py +1 -0
  74. androidctld/actions/action_target.py +142 -0
  75. androidctld/actions/capabilities.py +539 -0
  76. androidctld/actions/executor.py +894 -0
  77. androidctld/actions/focus_confirmation.py +177 -0
  78. androidctld/actions/focused_input_admissibility.py +120 -0
  79. androidctld/actions/fresh_current.py +176 -0
  80. androidctld/actions/postconditions.py +473 -0
  81. androidctld/actions/repair.py +101 -0
  82. androidctld/actions/request_builder.py +204 -0
  83. androidctld/actions/settle.py +146 -0
  84. androidctld/actions/submit_confirmation.py +211 -0
  85. androidctld/actions/submit_routing.py +311 -0
  86. androidctld/actions/type_confirmation.py +257 -0
  87. androidctld/app_targets.py +71 -0
  88. androidctld/artifacts/__init__.py +1 -0
  89. androidctld/artifacts/models.py +26 -0
  90. androidctld/artifacts/screen_lookup.py +241 -0
  91. androidctld/artifacts/screen_payloads.py +109 -0
  92. androidctld/artifacts/writer.py +286 -0
  93. androidctld/auth/__init__.py +1 -0
  94. androidctld/auth/active_registry.py +266 -0
  95. androidctld/auth/secret_files.py +52 -0
  96. androidctld/auth/token_store.py +59 -0
  97. androidctld/commands/__init__.py +1 -0
  98. androidctld/commands/assembly.py +231 -0
  99. androidctld/commands/command_models.py +254 -0
  100. androidctld/commands/dispatch.py +99 -0
  101. androidctld/commands/executor.py +31 -0
  102. androidctld/commands/from_boundary.py +175 -0
  103. androidctld/commands/handlers/__init__.py +15 -0
  104. androidctld/commands/handlers/action.py +439 -0
  105. androidctld/commands/handlers/connect.py +94 -0
  106. androidctld/commands/handlers/list_apps.py +215 -0
  107. androidctld/commands/handlers/observe.py +121 -0
  108. androidctld/commands/handlers/screenshot.py +105 -0
  109. androidctld/commands/handlers/wait.py +286 -0
  110. androidctld/commands/models.py +65 -0
  111. androidctld/commands/open_targets.py +56 -0
  112. androidctld/commands/orchestration.py +353 -0
  113. androidctld/commands/registry.py +116 -0
  114. androidctld/commands/result_builders.py +40 -0
  115. androidctld/commands/result_models.py +555 -0
  116. androidctld/commands/results.py +108 -0
  117. androidctld/commands/semantic_command_names.py +17 -0
  118. androidctld/commands/semantic_error_mapping.py +93 -0
  119. androidctld/commands/semantic_truth.py +135 -0
  120. androidctld/commands/service.py +67 -0
  121. androidctld/config.py +75 -0
  122. androidctld/daemon/__init__.py +1 -0
  123. androidctld/daemon/active_slot.py +326 -0
  124. androidctld/daemon/envelope.py +30 -0
  125. androidctld/daemon/http_host.py +123 -0
  126. androidctld/daemon/ingress.py +112 -0
  127. androidctld/daemon/ownership_probe.py +204 -0
  128. androidctld/daemon/server.py +286 -0
  129. androidctld/daemon/service.py +99 -0
  130. androidctld/device/__init__.py +1 -0
  131. androidctld/device/action_models.py +154 -0
  132. androidctld/device/action_serialization.py +121 -0
  133. androidctld/device/adapters.py +220 -0
  134. androidctld/device/bootstrap.py +153 -0
  135. androidctld/device/connectors.py +231 -0
  136. androidctld/device/errors.py +100 -0
  137. androidctld/device/interfaces.py +58 -0
  138. androidctld/device/parsing.py +320 -0
  139. androidctld/device/rpc.py +483 -0
  140. androidctld/device/schema.py +114 -0
  141. androidctld/device/types.py +161 -0
  142. androidctld/errors/__init__.py +94 -0
  143. androidctld/logging/__init__.py +22 -0
  144. androidctld/observation.py +98 -0
  145. androidctld/protocol.py +53 -0
  146. androidctld/refs/__init__.py +1 -0
  147. androidctld/refs/models.py +54 -0
  148. androidctld/refs/repair.py +284 -0
  149. androidctld/refs/service.py +422 -0
  150. androidctld/rendering/__init__.py +1 -0
  151. androidctld/rendering/screen_xml.py +256 -0
  152. androidctld/runtime/__init__.py +21 -0
  153. androidctld/runtime/kernel.py +548 -0
  154. androidctld/runtime/lifecycle.py +19 -0
  155. androidctld/runtime/models.py +48 -0
  156. androidctld/runtime/screen_state.py +117 -0
  157. androidctld/runtime/state_repo.py +70 -0
  158. androidctld/runtime/store.py +76 -0
  159. androidctld/runtime_policy.py +127 -0
  160. androidctld/schema/__init__.py +5 -0
  161. androidctld/schema/base.py +132 -0
  162. androidctld/schema/core.py +35 -0
  163. androidctld/schema/daemon_api.py +108 -0
  164. androidctld/schema/persistence.py +161 -0
  165. androidctld/schema/persistence_io.py +41 -0
  166. androidctld/schema/validation_errors.py +309 -0
  167. androidctld/semantics/__init__.py +1 -0
  168. androidctld/semantics/compiler.py +610 -0
  169. androidctld/semantics/continuity.py +107 -0
  170. androidctld/semantics/labels.py +252 -0
  171. androidctld/semantics/models.py +25 -0
  172. androidctld/semantics/policy.py +23 -0
  173. androidctld/semantics/public_models.py +123 -0
  174. androidctld/semantics/registries.py +13 -0
  175. androidctld/semantics/submit_refs.py +417 -0
  176. androidctld/semantics/surface.py +254 -0
  177. androidctld/semantics/targets.py +167 -0
  178. androidctld/snapshots/__init__.py +1 -0
  179. androidctld/snapshots/models.py +219 -0
  180. androidctld/snapshots/refresh.py +273 -0
  181. androidctld/snapshots/schema.py +74 -0
  182. androidctld/snapshots/service.py +138 -0
  183. androidctld/text_equivalence.py +67 -0
  184. androidctld/waits/__init__.py +1 -0
  185. androidctld/waits/evaluators.py +216 -0
  186. androidctld/waits/loop.py +305 -0
  187. androidctld/waits/matcher.py +41 -0
androidctl/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """androidctl package."""
2
+
3
+ from ._version import __version__
4
+
5
+ __all__ = ["__version__"]
androidctl/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .app import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
androidctl/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
androidctl/app.py ADDED
@@ -0,0 +1,73 @@
1
+ from pathlib import Path
2
+
3
+ import click
4
+ import typer
5
+ from typer.core import TyperGroup
6
+
7
+ from androidctl import __version__
8
+ from androidctl.cli_options import CliOptions
9
+ from androidctl.command_views import help_order_for_public_command
10
+ from androidctl.commands.actions import register as register_action_commands
11
+ from androidctl.commands.adb_wireless import register as register_adb_wireless_commands
12
+ from androidctl.commands.close import register as register_close_command
13
+ from androidctl.commands.connect import register as register_connect_command
14
+ from androidctl.commands.list_apps import register as register_list_apps_command
15
+ from androidctl.commands.observe import register as register_observe_command
16
+ from androidctl.commands.open import register as register_open_command
17
+ from androidctl.commands.screenshot import register as register_screenshot_command
18
+ from androidctl.commands.setup import register as register_setup_command
19
+ from androidctl.commands.wait import register as register_wait_command
20
+
21
+
22
+ class OrderedHelpGroup(TyperGroup):
23
+ def list_commands(self, ctx: click.Context) -> list[str]:
24
+ names = list(super().list_commands(ctx))
25
+ return sorted(names, key=help_order_for_public_command)
26
+
27
+
28
+ app = typer.Typer(
29
+ name="androidctl",
30
+ cls=OrderedHelpGroup,
31
+ help=(
32
+ "Agent loop: observe/list-apps/open -> act -> wait. Retained support routes: "
33
+ "connect, screenshot, close."
34
+ ),
35
+ no_args_is_help=True,
36
+ pretty_exceptions_enable=False,
37
+ )
38
+
39
+
40
+ def _show_version(value: bool) -> None:
41
+ if not value:
42
+ return
43
+ typer.echo(__version__)
44
+ raise typer.Exit()
45
+
46
+
47
+ @app.callback()
48
+ def main(
49
+ ctx: typer.Context,
50
+ version: bool = typer.Option(
51
+ False,
52
+ "--version",
53
+ callback=_show_version,
54
+ is_eager=True,
55
+ help="Show the androidctl release version and exit.",
56
+ ),
57
+ workspace_root: Path | None = typer.Option(None, "--workspace-root"),
58
+ ) -> None:
59
+ """Public happy path for driving the daemon-backed device runtime."""
60
+ del version
61
+ ctx.obj = CliOptions(workspace_root=workspace_root)
62
+
63
+
64
+ register_connect_command(app)
65
+ register_observe_command(app)
66
+ register_list_apps_command(app)
67
+ register_open_command(app)
68
+ register_action_commands(app)
69
+ register_wait_command(app)
70
+ register_screenshot_command(app)
71
+ register_close_command(app)
72
+ register_setup_command(app)
73
+ register_adb_wireless_commands(app)
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ import typer
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class CliOptions:
11
+ workspace_root: Path | None
12
+
13
+
14
+ def read_cli_options(ctx: typer.Context) -> CliOptions:
15
+ payload = ctx.obj
16
+ if isinstance(payload, CliOptions):
17
+ return payload
18
+ return CliOptions(workspace_root=None)
19
+
20
+
21
+ def command_cli_options(
22
+ ctx: typer.Context,
23
+ *,
24
+ workspace_root: Path | None = None,
25
+ ) -> CliOptions:
26
+ root_options = read_cli_options(ctx)
27
+ return CliOptions(workspace_root=workspace_root or root_options.workspace_root)
@@ -0,0 +1,264 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Final, Literal, TypeAlias
5
+
6
+ from androidctl_contracts.daemon_api import (
7
+ DaemonCommandPayload,
8
+ GlobalActionCommandPayload,
9
+ GonePredicatePayload,
10
+ LiveScreenBoundCommandPayload,
11
+ RefActionCommandPayload,
12
+ ScreenChangePredicatePayload,
13
+ ScreenRelativeWaitPredicatePayload,
14
+ ScrollCommandPayload,
15
+ TypeCommandPayload,
16
+ WaitCommandPayload,
17
+ WaitPredicatePayload,
18
+ )
19
+
20
+ ScrollDirection = Literal["up", "down", "left", "right", "backward"]
21
+ RefActionKind = Literal["tap", "longTap", "focus", "submit"]
22
+ GlobalActionKind = Literal["back", "home", "recents", "notifications"]
23
+ LateBoundActionKind = Literal["tap", "longTap", "focus", "submit", "type", "scroll"]
24
+ SCROLL_DIRECTIONS: Final = frozenset({"up", "down", "left", "right", "backward"})
25
+
26
+
27
+ def _normalize_optional_cli_string(value: str | None) -> str | None:
28
+ if value is None:
29
+ return None
30
+ normalized = value.strip()
31
+ return normalized or None
32
+
33
+
34
+ def _normalize_source_screen_id(value: str | None) -> str | None:
35
+ if value is None:
36
+ return None
37
+ normalized = value.strip()
38
+ if not normalized:
39
+ raise ValueError("source_screen_id must be non-empty when provided")
40
+ return normalized
41
+
42
+
43
+ def _required_source_screen_id(value: str) -> str:
44
+ normalized = _normalize_optional_cli_string(value)
45
+ if normalized is None:
46
+ raise RuntimeError("prepared command is missing source_screen_id")
47
+ return normalized
48
+
49
+
50
+ def _validate_timeout_ms(value: int | None) -> int | None:
51
+ if isinstance(value, bool):
52
+ raise TypeError("timeout_ms must be an int, not bool")
53
+ if value is not None and not isinstance(value, int):
54
+ raise TypeError("timeout_ms must be an int")
55
+ if value is not None and value < 0:
56
+ raise ValueError("timeout_ms must be >= 0")
57
+ return value
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class LateBoundActionCommand:
62
+ kind: LateBoundActionKind
63
+ ref: str
64
+ text: str | None = None
65
+ direction: ScrollDirection | None = None
66
+
67
+ def __post_init__(self) -> None:
68
+ if _normalize_optional_cli_string(self.ref) is None:
69
+ raise ValueError("late-bound action command requires ref")
70
+ if self.kind == "type":
71
+ if self.text is None:
72
+ raise ValueError("late-bound type command requires text")
73
+ if self.direction is not None:
74
+ raise ValueError("late-bound type command does not accept direction")
75
+ return
76
+ if self.kind == "scroll":
77
+ if self.direction is None:
78
+ raise ValueError("late-bound scroll command requires direction")
79
+ if self.direction not in SCROLL_DIRECTIONS:
80
+ raise ValueError("late-bound scroll command requires a valid direction")
81
+ if self.text is not None:
82
+ raise ValueError("late-bound scroll command does not accept text")
83
+ return
84
+ if self.text is not None or self.direction is not None:
85
+ raise ValueError(
86
+ "late-bound ref action commands only accept kind and ref fields"
87
+ )
88
+
89
+ def bind(self, source_screen_id: str) -> LiveScreenBoundCommandPayload:
90
+ resolved_source_screen_id = _required_source_screen_id(source_screen_id)
91
+ if self.kind == "type":
92
+ if self.text is None:
93
+ raise RuntimeError("prepared type command is missing text")
94
+ return TypeCommandPayload(
95
+ kind="type",
96
+ ref=self.ref,
97
+ text=self.text,
98
+ source_screen_id=resolved_source_screen_id,
99
+ )
100
+ if self.kind == "scroll":
101
+ if self.direction is None:
102
+ raise RuntimeError("prepared scroll command is missing direction")
103
+ return ScrollCommandPayload(
104
+ kind="scroll",
105
+ ref=self.ref,
106
+ direction=self.direction,
107
+ source_screen_id=resolved_source_screen_id,
108
+ )
109
+ return RefActionCommandPayload(
110
+ kind=self.kind,
111
+ ref=self.ref,
112
+ source_screen_id=resolved_source_screen_id,
113
+ )
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class LateBoundScreenRelativePredicate:
118
+ kind: Literal["screen-change", "gone"]
119
+ ref: str | None = None
120
+
121
+ def __post_init__(self) -> None:
122
+ normalized_ref = _normalize_optional_cli_string(self.ref)
123
+ if self.kind == "gone" and normalized_ref is None:
124
+ raise ValueError("late-bound gone predicate requires a non-empty ref")
125
+ if self.kind == "screen-change" and self.ref is not None:
126
+ raise ValueError("screen-change predicate does not accept ref")
127
+
128
+ def bind(self, source_screen_id: str) -> ScreenRelativeWaitPredicatePayload:
129
+ resolved_source_screen_id = _required_source_screen_id(source_screen_id)
130
+ if self.kind == "screen-change":
131
+ return ScreenChangePredicatePayload(
132
+ kind="screen-change",
133
+ source_screen_id=resolved_source_screen_id,
134
+ )
135
+ if self.ref is None:
136
+ raise RuntimeError("prepared gone predicate is missing ref")
137
+ return GonePredicatePayload(
138
+ kind="gone",
139
+ ref=self.ref,
140
+ source_screen_id=resolved_source_screen_id,
141
+ )
142
+
143
+
144
+ CliWaitPredicatePayload: TypeAlias = (
145
+ WaitPredicatePayload | LateBoundScreenRelativePredicate
146
+ )
147
+
148
+
149
+ @dataclass(frozen=True)
150
+ class LateBoundWaitCommand:
151
+ predicate: LateBoundScreenRelativePredicate
152
+ timeout_ms: int | None = None
153
+
154
+ def __post_init__(self) -> None:
155
+ _validate_timeout_ms(self.timeout_ms)
156
+
157
+ def bind(self, source_screen_id: str) -> WaitCommandPayload:
158
+ return WaitCommandPayload(
159
+ kind="wait",
160
+ predicate=self.predicate.bind(source_screen_id),
161
+ timeout_ms=self.timeout_ms,
162
+ )
163
+
164
+
165
+ @dataclass(frozen=True)
166
+ class LateBoundGlobalActionCommand:
167
+ kind: GlobalActionKind
168
+
169
+ def bind(self, source_screen_id: str | None) -> GlobalActionCommandPayload:
170
+ normalized_source_screen_id = (
171
+ None if source_screen_id is None else source_screen_id.strip() or None
172
+ )
173
+ return GlobalActionCommandPayload(
174
+ kind=self.kind,
175
+ source_screen_id=normalized_source_screen_id,
176
+ )
177
+
178
+
179
+ LateBoundCommand: TypeAlias = (
180
+ LateBoundActionCommand | LateBoundGlobalActionCommand | LateBoundWaitCommand
181
+ )
182
+ CliCommandPayload: TypeAlias = DaemonCommandPayload | LateBoundCommand
183
+
184
+
185
+ def build_ref_action_command(
186
+ *,
187
+ kind: RefActionKind,
188
+ ref: str,
189
+ source_screen_id: str | None,
190
+ ) -> CliCommandPayload:
191
+ normalized_source_screen_id = _normalize_source_screen_id(source_screen_id)
192
+ if normalized_source_screen_id is None:
193
+ return LateBoundActionCommand(kind=kind, ref=ref)
194
+ return RefActionCommandPayload(
195
+ kind=kind,
196
+ ref=ref,
197
+ source_screen_id=normalized_source_screen_id,
198
+ )
199
+
200
+
201
+ def build_type_command(
202
+ *,
203
+ ref: str,
204
+ text: str,
205
+ source_screen_id: str | None,
206
+ ) -> CliCommandPayload:
207
+ normalized_source_screen_id = _normalize_source_screen_id(source_screen_id)
208
+ if normalized_source_screen_id is None:
209
+ return LateBoundActionCommand(kind="type", ref=ref, text=text)
210
+ return TypeCommandPayload(
211
+ kind="type",
212
+ ref=ref,
213
+ text=text,
214
+ source_screen_id=normalized_source_screen_id,
215
+ )
216
+
217
+
218
+ def build_scroll_command(
219
+ *,
220
+ ref: str,
221
+ direction: ScrollDirection,
222
+ source_screen_id: str | None,
223
+ ) -> CliCommandPayload:
224
+ normalized_source_screen_id = _normalize_source_screen_id(source_screen_id)
225
+ if normalized_source_screen_id is None:
226
+ return LateBoundActionCommand(kind="scroll", ref=ref, direction=direction)
227
+ return ScrollCommandPayload(
228
+ kind="scroll",
229
+ ref=ref,
230
+ direction=direction,
231
+ source_screen_id=normalized_source_screen_id,
232
+ )
233
+
234
+
235
+ def build_wait_command(
236
+ *,
237
+ predicate: CliWaitPredicatePayload,
238
+ timeout_ms: int | None,
239
+ ) -> CliCommandPayload:
240
+ validated_timeout_ms = _validate_timeout_ms(timeout_ms)
241
+ if isinstance(predicate, LateBoundScreenRelativePredicate):
242
+ return LateBoundWaitCommand(
243
+ predicate=predicate,
244
+ timeout_ms=validated_timeout_ms,
245
+ )
246
+ return WaitCommandPayload(
247
+ kind="wait",
248
+ predicate=predicate,
249
+ timeout_ms=validated_timeout_ms,
250
+ )
251
+
252
+
253
+ def build_global_action_command(
254
+ *,
255
+ kind: GlobalActionKind,
256
+ source_screen_id: str | None,
257
+ ) -> CliCommandPayload:
258
+ normalized_source_screen_id = _normalize_source_screen_id(source_screen_id)
259
+ if normalized_source_screen_id is None:
260
+ return LateBoundGlobalActionCommand(kind=kind)
261
+ return GlobalActionCommandPayload(
262
+ kind=kind,
263
+ source_screen_id=normalized_source_screen_id,
264
+ )
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from androidctl_contracts.command_catalog import (
6
+ PUBLIC_COMMAND_NAMES,
7
+ entry_for_public_command,
8
+ )
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class CommandView:
13
+ public_name: str
14
+ help_order: int
15
+ pre_dispatch_execution_outcome: str | None
16
+
17
+
18
+ def _command_view(
19
+ public_name: str,
20
+ *,
21
+ help_order: int,
22
+ pre_dispatch_execution_outcome: str | None,
23
+ ) -> CommandView:
24
+ if entry_for_public_command(public_name) is None:
25
+ raise RuntimeError(f"missing shared command catalog entry for {public_name!r}")
26
+ return CommandView(
27
+ public_name=public_name,
28
+ help_order=help_order,
29
+ pre_dispatch_execution_outcome=pre_dispatch_execution_outcome,
30
+ )
31
+
32
+
33
+ _COMMAND_VIEWS = (
34
+ _command_view(
35
+ public_name="observe",
36
+ help_order=0,
37
+ pre_dispatch_execution_outcome="notApplicable",
38
+ ),
39
+ _command_view(
40
+ public_name="list-apps",
41
+ help_order=1,
42
+ pre_dispatch_execution_outcome="notApplicable",
43
+ ),
44
+ _command_view(
45
+ public_name="open",
46
+ help_order=2,
47
+ pre_dispatch_execution_outcome="notAttempted",
48
+ ),
49
+ _command_view(
50
+ public_name="tap",
51
+ help_order=3,
52
+ pre_dispatch_execution_outcome="notAttempted",
53
+ ),
54
+ _command_view(
55
+ public_name="long-tap",
56
+ help_order=4,
57
+ pre_dispatch_execution_outcome="notAttempted",
58
+ ),
59
+ _command_view(
60
+ public_name="focus",
61
+ help_order=5,
62
+ pre_dispatch_execution_outcome="notAttempted",
63
+ ),
64
+ _command_view(
65
+ public_name="type",
66
+ help_order=6,
67
+ pre_dispatch_execution_outcome="notAttempted",
68
+ ),
69
+ _command_view(
70
+ public_name="submit",
71
+ help_order=7,
72
+ pre_dispatch_execution_outcome="notAttempted",
73
+ ),
74
+ _command_view(
75
+ public_name="scroll",
76
+ help_order=8,
77
+ pre_dispatch_execution_outcome="notAttempted",
78
+ ),
79
+ _command_view(
80
+ public_name="back",
81
+ help_order=9,
82
+ pre_dispatch_execution_outcome="notAttempted",
83
+ ),
84
+ _command_view(
85
+ public_name="home",
86
+ help_order=10,
87
+ pre_dispatch_execution_outcome="notAttempted",
88
+ ),
89
+ _command_view(
90
+ public_name="recents",
91
+ help_order=11,
92
+ pre_dispatch_execution_outcome="notAttempted",
93
+ ),
94
+ _command_view(
95
+ public_name="notifications",
96
+ help_order=12,
97
+ pre_dispatch_execution_outcome="notAttempted",
98
+ ),
99
+ _command_view(
100
+ public_name="wait",
101
+ help_order=13,
102
+ pre_dispatch_execution_outcome="notApplicable",
103
+ ),
104
+ _command_view(
105
+ public_name="connect",
106
+ help_order=14,
107
+ pre_dispatch_execution_outcome="notApplicable",
108
+ ),
109
+ _command_view(
110
+ public_name="screenshot",
111
+ help_order=15,
112
+ pre_dispatch_execution_outcome="notApplicable",
113
+ ),
114
+ _command_view(
115
+ public_name="close",
116
+ help_order=16,
117
+ pre_dispatch_execution_outcome="notApplicable",
118
+ ),
119
+ )
120
+
121
+ _command_view_names = [view.public_name for view in _COMMAND_VIEWS]
122
+ _help_orders = [view.help_order for view in _COMMAND_VIEWS]
123
+ if len(_command_view_names) != len(set(_command_view_names)):
124
+ raise RuntimeError("duplicate public command in CLI command views")
125
+ if len(_help_orders) != len(set(_help_orders)):
126
+ raise RuntimeError("duplicate help order in CLI command views")
127
+ if set(_command_view_names) != PUBLIC_COMMAND_NAMES:
128
+ missing = sorted(PUBLIC_COMMAND_NAMES - set(_command_view_names))
129
+ extra = sorted(set(_command_view_names) - PUBLIC_COMMAND_NAMES)
130
+ raise RuntimeError(
131
+ "CLI command views drifted from shared public catalog: "
132
+ f"missing={missing}, extra={extra}"
133
+ )
134
+
135
+ _COMMAND_VIEW_BY_PUBLIC_NAME = {view.public_name: view for view in _COMMAND_VIEWS}
136
+
137
+
138
+ def command_view_for_public_command(public_name: str) -> CommandView | None:
139
+ return _COMMAND_VIEW_BY_PUBLIC_NAME.get(public_name)
140
+
141
+
142
+ def help_order_for_public_command(public_name: str) -> int:
143
+ view = command_view_for_public_command(public_name)
144
+ if view is None:
145
+ return len(_COMMAND_VIEWS)
146
+ return view.help_order
147
+
148
+
149
+ def pre_dispatch_execution_outcome_for_public_command(
150
+ public_name: str | None,
151
+ ) -> str | None:
152
+ if public_name is None:
153
+ return None
154
+ view = command_view_for_public_command(public_name)
155
+ if view is None:
156
+ return None
157
+ return view.pre_dispatch_execution_outcome
@@ -0,0 +1 @@
1
+ """CLI command handlers."""