use-computer-cli 0.1.0__tar.gz

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 (32) hide show
  1. use_computer_cli-0.1.0/.gitignore +47 -0
  2. use_computer_cli-0.1.0/LICENSE +21 -0
  3. use_computer_cli-0.1.0/PKG-INFO +150 -0
  4. use_computer_cli-0.1.0/README.md +94 -0
  5. use_computer_cli-0.1.0/pyproject.toml +86 -0
  6. use_computer_cli-0.1.0/src/use_computer/__init__.py +93 -0
  7. use_computer_cli-0.1.0/src/use_computer/actions.py +184 -0
  8. use_computer_cli-0.1.0/src/use_computer/backends/__init__.py +31 -0
  9. use_computer_cli-0.1.0/src/use_computer/backends/base.py +66 -0
  10. use_computer_cli-0.1.0/src/use_computer/backends/local.py +214 -0
  11. use_computer_cli-0.1.0/src/use_computer/backends/vnc.py +186 -0
  12. use_computer_cli-0.1.0/src/use_computer/cli.py +676 -0
  13. use_computer_cli-0.1.0/src/use_computer/compare.py +121 -0
  14. use_computer_cli-0.1.0/src/use_computer/config.py +532 -0
  15. use_computer_cli-0.1.0/src/use_computer/coordinates.py +104 -0
  16. use_computer_cli-0.1.0/src/use_computer/errors.py +53 -0
  17. use_computer_cli-0.1.0/src/use_computer/keys.py +209 -0
  18. use_computer_cli-0.1.0/src/use_computer/runner.py +304 -0
  19. use_computer_cli-0.1.0/src/use_computer/skill/SKILL.md +118 -0
  20. use_computer_cli-0.1.0/src/use_computer/skill/__init__.py +185 -0
  21. use_computer_cli-0.1.0/tests/__init__.py +1 -0
  22. use_computer_cli-0.1.0/tests/conftest.py +76 -0
  23. use_computer_cli-0.1.0/tests/fake_backend.py +91 -0
  24. use_computer_cli-0.1.0/tests/test_backends.py +117 -0
  25. use_computer_cli-0.1.0/tests/test_cli.py +243 -0
  26. use_computer_cli-0.1.0/tests/test_compare.py +60 -0
  27. use_computer_cli-0.1.0/tests/test_config.py +167 -0
  28. use_computer_cli-0.1.0/tests/test_config_init.py +228 -0
  29. use_computer_cli-0.1.0/tests/test_coordinates.py +62 -0
  30. use_computer_cli-0.1.0/tests/test_keys.py +56 -0
  31. use_computer_cli-0.1.0/tests/test_runner.py +191 -0
  32. use_computer_cli-0.1.0/tests/test_skill.py +129 -0
@@ -0,0 +1,47 @@
1
+ # --- sdd -------------------------------------------------------------------
2
+ # config.yaml carries the sdd remote api-key. This repository is public, so it
3
+ # stays out. If you want the project description versioned, commit this file
4
+ # only after moving the key elsewhere.
5
+ .sdd/config.yaml
6
+
7
+ # --- local project config (use-computer's own .use-computer/) --------------
8
+ # This is a scratch config for driving the tool during development, not part of
9
+ # the project. It carries allow-local, which is a per-machine opt-in and must
10
+ # never arrive pre-enabled for whoever clones this.
11
+ .use-computer/
12
+ .env
13
+ .env.*
14
+ !.env.example
15
+
16
+ # --- Python ----------------------------------------------------------------
17
+ __pycache__/
18
+ *.py[cod]
19
+ *$py.class
20
+ *.egg-info/
21
+ *.egg
22
+ build/
23
+ dist/
24
+ .eggs/
25
+ .venv/
26
+ venv/
27
+ env/
28
+ uv.lock
29
+
30
+ # --- Tooling ---------------------------------------------------------------
31
+ .mypy_cache/
32
+ .ruff_cache/
33
+ .pytest_cache/
34
+ .coverage
35
+ htmlcov/
36
+
37
+ # --- Node (sdd is an npm CLI) ---------------------------------------------
38
+ node_modules/
39
+
40
+ # --- Screenshots captured while driving the tool by hand -------------------
41
+ *.png
42
+ !product/**/*.png
43
+
44
+ # --- Editor / OS -----------------------------------------------------------
45
+ .DS_Store
46
+ .vscode/
47
+ .idea/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bruno Fortunato
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.5
2
+ Name: use-computer-cli
3
+ Version: 0.1.0
4
+ Summary: Execute input on a screen for computer-use agents: move, click, drag, scroll, type, key, screenshot.
5
+ Project-URL: Homepage, https://github.com/applica-software-guru/use-computer
6
+ Author: Bruno Fortunato
7
+ License: MIT License
8
+
9
+ Copyright (c) 2026 Bruno Fortunato
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ License-File: LICENSE
29
+ Keywords: agent,automation,computer-use,screenshot,vnc
30
+ Classifier: Development Status :: 3 - Alpha
31
+ Classifier: Environment :: Console
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: Programming Language :: Python :: 3.10
34
+ Classifier: Programming Language :: Python :: 3.11
35
+ Classifier: Programming Language :: Python :: 3.12
36
+ Classifier: Typing :: Typed
37
+ Requires-Python: >=3.10
38
+ Requires-Dist: pillow>=10
39
+ Requires-Dist: pydantic-settings>=2.2
40
+ Requires-Dist: pydantic>=2.7
41
+ Requires-Dist: python-dotenv>=1.0
42
+ Requires-Dist: rich>=13
43
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
44
+ Requires-Dist: typer>=0.16
45
+ Provides-Extra: dev
46
+ Requires-Dist: mypy>=1.11; extra == 'dev'
47
+ Requires-Dist: pytest-mock>=3.12; extra == 'dev'
48
+ Requires-Dist: pytest>=8; extra == 'dev'
49
+ Requires-Dist: ruff>=0.6; extra == 'dev'
50
+ Provides-Extra: local
51
+ Requires-Dist: mss>=10; extra == 'local'
52
+ Requires-Dist: pynput>=1.8; extra == 'local'
53
+ Provides-Extra: vnc
54
+ Requires-Dist: vncdotool>=1.3; extra == 'vnc'
55
+ Description-Content-Type: text/markdown
56
+
57
+ # use-computer
58
+
59
+ Executes input on a screen for computer-use agents: move, click, double-click, right-click,
60
+ drag, scroll, type text, press key combinations, and capture a screenshot.
61
+
62
+ `use-computer` is the **acting** half of a pair. [ui-locator](https://github.com/applica-software-guru/ui-locator)
63
+ answers *where* the Invia button is and returns pixel coordinates; `use-computer` performs the
64
+ click there. Both are driven by another AI agent through a CLI that emits JSON on stdout and
65
+ diagnostics on stderr, with a Python API underneath.
66
+
67
+ ## Install
68
+
69
+ ```bash
70
+ pip install use-computer-cli # no backend
71
+ pip install "use-computer-cli[local]" # drive this machine's display (pynput + mss)
72
+ pip install "use-computer-cli[vnc]" # drive a remote framebuffer over RFB (vncdotool)
73
+ ```
74
+
75
+ Backends are optional extras, imported lazily, so the package installs without them.
76
+
77
+ The distribution is `use-computer-cli` because `use-computer` is taken on PyPI by an unrelated
78
+ project. The command it installs is `use-computer`, and the package it imports is `use_computer`.
79
+
80
+ ## Use
81
+
82
+ ```bash
83
+ use-computer click --x 120 --y 340 --use staging
84
+ use-computer type --text "hello" --use staging
85
+ use-computer key ctrl+s --use staging
86
+ use-computer screenshot --use laptop --out shot.png
87
+
88
+ # a batch runs over one connection -- the default command, so `batch` may be omitted
89
+ echo '[{"action":"click","x":120,"y":340},{"action":"key","combo":"enter"}]' \
90
+ | use-computer - --use staging --verify
91
+ ```
92
+
93
+ stdout is one JSON object per run; every diagnostic goes to stderr. Exit codes: `0` success,
94
+ `1` failure, `2` bad usage.
95
+
96
+ ## Three problems it solves
97
+
98
+ - **Coordinate spaces.** A screenshot on a HiDPI display is larger than the space the OS clicks
99
+ in. Every coordinate carries its space, `use-computer` scales between them, and it refuses to
100
+ guess when the ratio is unknown.
101
+ - **Setup cost.** Opening a VNC connection dominates a single action, so one run performs a
102
+ batch of actions over one connection.
103
+ - **Blind actuation.** A click that lands on nothing looks exactly like a click that worked, so
104
+ `--verify` compares the screen before and after and reports whether it changed.
105
+
106
+ ## Configure
107
+
108
+ ```bash
109
+ use-computer config init # asks, then proves it works
110
+ use-computer config init --backend vnc --host 10.0.0.5 # doesn't ask
111
+ use-computer config init --backend local --allow-local
112
+ ```
113
+
114
+ `config init` writes the file below, then opens the backend it just configured and reports the
115
+ screen geometry and scale — so a coordinate space whose ratio cannot be derived surfaces at setup
116
+ rather than at the first click that lands in the wrong place.
117
+
118
+ It writes `.use-computer/config.toml` at the project root (found by walking up, the way git finds
119
+ its own):
120
+
121
+ ```toml
122
+ default-profile = "laptop"
123
+ delay = 0.1
124
+
125
+ [profiles.laptop]
126
+ backend = "local"
127
+ allow-local = true
128
+
129
+ [profiles.staging]
130
+ backend = "vnc"
131
+ host = "10.0.0.5"
132
+ port = 5900
133
+ ```
134
+
135
+ Secrets go in `.use-computer/.env`, which is not committed. `use-computer config show` prints
136
+ every resolved value, the layer it came from and the variable that would override it.
137
+
138
+ ## The agent skill
139
+
140
+ Instructions for the calling agent ship inside the package and are installed from it, so they
141
+ always match the installed version:
142
+
143
+ ```bash
144
+ use-computer skill install --scope project
145
+ ```
146
+
147
+ ## Documentation
148
+
149
+ This package is developed with [SDD](https://github.com/applica-software-guru/sdd). The specs
150
+ it implements live in `product/` and `system/` at the repository root.
@@ -0,0 +1,94 @@
1
+ # use-computer
2
+
3
+ Executes input on a screen for computer-use agents: move, click, double-click, right-click,
4
+ drag, scroll, type text, press key combinations, and capture a screenshot.
5
+
6
+ `use-computer` is the **acting** half of a pair. [ui-locator](https://github.com/applica-software-guru/ui-locator)
7
+ answers *where* the Invia button is and returns pixel coordinates; `use-computer` performs the
8
+ click there. Both are driven by another AI agent through a CLI that emits JSON on stdout and
9
+ diagnostics on stderr, with a Python API underneath.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install use-computer-cli # no backend
15
+ pip install "use-computer-cli[local]" # drive this machine's display (pynput + mss)
16
+ pip install "use-computer-cli[vnc]" # drive a remote framebuffer over RFB (vncdotool)
17
+ ```
18
+
19
+ Backends are optional extras, imported lazily, so the package installs without them.
20
+
21
+ The distribution is `use-computer-cli` because `use-computer` is taken on PyPI by an unrelated
22
+ project. The command it installs is `use-computer`, and the package it imports is `use_computer`.
23
+
24
+ ## Use
25
+
26
+ ```bash
27
+ use-computer click --x 120 --y 340 --use staging
28
+ use-computer type --text "hello" --use staging
29
+ use-computer key ctrl+s --use staging
30
+ use-computer screenshot --use laptop --out shot.png
31
+
32
+ # a batch runs over one connection -- the default command, so `batch` may be omitted
33
+ echo '[{"action":"click","x":120,"y":340},{"action":"key","combo":"enter"}]' \
34
+ | use-computer - --use staging --verify
35
+ ```
36
+
37
+ stdout is one JSON object per run; every diagnostic goes to stderr. Exit codes: `0` success,
38
+ `1` failure, `2` bad usage.
39
+
40
+ ## Three problems it solves
41
+
42
+ - **Coordinate spaces.** A screenshot on a HiDPI display is larger than the space the OS clicks
43
+ in. Every coordinate carries its space, `use-computer` scales between them, and it refuses to
44
+ guess when the ratio is unknown.
45
+ - **Setup cost.** Opening a VNC connection dominates a single action, so one run performs a
46
+ batch of actions over one connection.
47
+ - **Blind actuation.** A click that lands on nothing looks exactly like a click that worked, so
48
+ `--verify` compares the screen before and after and reports whether it changed.
49
+
50
+ ## Configure
51
+
52
+ ```bash
53
+ use-computer config init # asks, then proves it works
54
+ use-computer config init --backend vnc --host 10.0.0.5 # doesn't ask
55
+ use-computer config init --backend local --allow-local
56
+ ```
57
+
58
+ `config init` writes the file below, then opens the backend it just configured and reports the
59
+ screen geometry and scale — so a coordinate space whose ratio cannot be derived surfaces at setup
60
+ rather than at the first click that lands in the wrong place.
61
+
62
+ It writes `.use-computer/config.toml` at the project root (found by walking up, the way git finds
63
+ its own):
64
+
65
+ ```toml
66
+ default-profile = "laptop"
67
+ delay = 0.1
68
+
69
+ [profiles.laptop]
70
+ backend = "local"
71
+ allow-local = true
72
+
73
+ [profiles.staging]
74
+ backend = "vnc"
75
+ host = "10.0.0.5"
76
+ port = 5900
77
+ ```
78
+
79
+ Secrets go in `.use-computer/.env`, which is not committed. `use-computer config show` prints
80
+ every resolved value, the layer it came from and the variable that would override it.
81
+
82
+ ## The agent skill
83
+
84
+ Instructions for the calling agent ship inside the package and are installed from it, so they
85
+ always match the installed version:
86
+
87
+ ```bash
88
+ use-computer skill install --scope project
89
+ ```
90
+
91
+ ## Documentation
92
+
93
+ This package is developed with [SDD](https://github.com/applica-software-guru/sdd). The specs
94
+ it implements live in `product/` and `system/` at the repository root.
@@ -0,0 +1,86 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "use-computer-cli"
7
+ version = "0.1.0"
8
+ description = "Execute input on a screen for computer-use agents: move, click, drag, scroll, type, key, screenshot."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "Bruno Fortunato" }]
13
+ keywords = ["computer-use", "automation", "vnc", "agent", "screenshot"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Typing :: Typed",
22
+ ]
23
+
24
+ # Floors are the versions actually tested in CI. typer 0.12 is explicitly unsupported:
25
+ # it predates the API this CLI relies on.
26
+ dependencies = [
27
+ "pydantic>=2.7",
28
+ "pydantic-settings>=2.2",
29
+ "typer>=0.16",
30
+ "rich>=13",
31
+ "pillow>=10",
32
+ "python-dotenv>=1.0",
33
+ "tomli>=2.0; python_version < '3.11'",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ # Backends are optional extras, imported lazily, so the package installs without them.
38
+ # pyautogui is deliberately not used: its last release (0.9.54) dates from 2023.
39
+ local = ["pynput>=1.8", "mss>=10"]
40
+ vnc = ["vncdotool>=1.3"]
41
+ dev = [
42
+ "pytest>=8",
43
+ "pytest-mock>=3.12",
44
+ "ruff>=0.6",
45
+ "mypy>=1.11",
46
+ ]
47
+
48
+ [project.scripts]
49
+ use-computer = "use_computer.cli:main"
50
+
51
+ [project.urls]
52
+ Homepage = "https://github.com/applica-software-guru/use-computer"
53
+
54
+ [tool.hatch.build.targets.wheel]
55
+ packages = ["src/use_computer"]
56
+
57
+ [tool.hatch.build.targets.wheel.force-include]
58
+ # The agent skill ships inside the wheel. A copy at the repository root would not.
59
+ "src/use_computer/skill/SKILL.md" = "use_computer/skill/SKILL.md"
60
+
61
+ [tool.hatch.build.targets.sdist]
62
+ include = ["src/use_computer", "tests", "README.md", "LICENSE", "pyproject.toml"]
63
+
64
+ [tool.ruff]
65
+ line-length = 100
66
+ src = ["src", "tests"]
67
+
68
+ [tool.ruff.lint]
69
+ select = ["E", "F", "I", "UP", "B", "SIM", "C4"]
70
+
71
+ [tool.ruff.lint.per-file-ignores]
72
+ # typer idiom: Option()/Argument() defaults are function calls in the signature.
73
+ "src/use_computer/cli.py" = ["B008"]
74
+
75
+ [tool.mypy]
76
+ python_version = "3.10"
77
+ strict = true
78
+ files = ["src", "tests"]
79
+
80
+ [[tool.mypy.overrides]]
81
+ module = ["pynput.*", "mss.*", "vncdotool.*", "ApplicationServices.*", "Quartz.*", "tomli"]
82
+ ignore_missing_imports = true
83
+
84
+ [tool.pytest.ini_options]
85
+ testpaths = ["tests"]
86
+ addopts = "-q"
@@ -0,0 +1,93 @@
1
+ """use-computer: execute input on a screen for computer-use agents.
2
+
3
+ The acting half of a pair. ui-locator answers where; this performs the action there.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from importlib.metadata import PackageNotFoundError, version
9
+
10
+ from use_computer.actions import (
11
+ Action,
12
+ ClickAction,
13
+ DoubleClickAction,
14
+ DragAction,
15
+ KeyAction,
16
+ MouseButton,
17
+ MoveAction,
18
+ RightClickAction,
19
+ ScreenshotAction,
20
+ ScrollAction,
21
+ ScrollDirection,
22
+ TypeAction,
23
+ )
24
+ from use_computer.backends import Backend, create_backend
25
+ from use_computer.compare import ChangeReport, Screenshot, compare
26
+ from use_computer.config import BackendProfile, ResolvedConfig, Settings
27
+ from use_computer.config import load as load_config
28
+ from use_computer.coordinates import Coordinate, CoordinateSpace, ScreenInfo, convert
29
+ from use_computer.errors import (
30
+ ActionFailedError,
31
+ BackendNotAvailableError,
32
+ ConfigError,
33
+ CoordinateSpaceError,
34
+ KeySyntaxError,
35
+ PermissionDeniedError,
36
+ UseComputerError,
37
+ )
38
+ from use_computer.keys import KeyCombo, parse_combo
39
+ from use_computer.runner import ActionResult, ErrorInfo, RunResult, Session, run_actions
40
+
41
+ #: The distribution name on PyPI, which differs from the import package: `use-computer` was
42
+ #: already taken there. importlib.metadata is keyed by the distribution, so this is the name
43
+ #: that must appear here.
44
+ DISTRIBUTION = "use-computer-cli"
45
+
46
+ try:
47
+ __version__ = version(DISTRIBUTION)
48
+ except PackageNotFoundError: # pragma: no cover - source checkout without an install
49
+ __version__ = "0.0.0"
50
+
51
+ __all__ = [
52
+ "DISTRIBUTION",
53
+ "Action",
54
+ "ActionFailedError",
55
+ "ActionResult",
56
+ "Backend",
57
+ "BackendNotAvailableError",
58
+ "BackendProfile",
59
+ "ChangeReport",
60
+ "ClickAction",
61
+ "ConfigError",
62
+ "Coordinate",
63
+ "CoordinateSpace",
64
+ "CoordinateSpaceError",
65
+ "DoubleClickAction",
66
+ "DragAction",
67
+ "ErrorInfo",
68
+ "KeyAction",
69
+ "KeyCombo",
70
+ "KeySyntaxError",
71
+ "MouseButton",
72
+ "MoveAction",
73
+ "PermissionDeniedError",
74
+ "ResolvedConfig",
75
+ "RightClickAction",
76
+ "RunResult",
77
+ "ScreenInfo",
78
+ "Screenshot",
79
+ "ScreenshotAction",
80
+ "ScrollAction",
81
+ "ScrollDirection",
82
+ "Session",
83
+ "Settings",
84
+ "TypeAction",
85
+ "UseComputerError",
86
+ "__version__",
87
+ "compare",
88
+ "convert",
89
+ "create_backend",
90
+ "load_config",
91
+ "parse_combo",
92
+ "run_actions",
93
+ ]
@@ -0,0 +1,184 @@
1
+ """The action models, and the resolution that happens before a backend sees them.
2
+
3
+ The action set is the whole surface of what use-computer does to a screen. Coordinate scaling
4
+ and key parsing are applied here, so that everything crossing the backend boundary is already
5
+ in actuation units and canonical key names.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import Enum
11
+ from pathlib import Path
12
+ from typing import Annotated, Literal
13
+
14
+ from pydantic import BaseModel, Field, TypeAdapter, field_validator
15
+
16
+ from use_computer.coordinates import Coordinate, CoordinateSpace, ScreenInfo, convert
17
+ from use_computer.keys import KeyCombo, canonical, parse_combo
18
+
19
+
20
+ class MouseButton(str, Enum):
21
+ LEFT = "left"
22
+ RIGHT = "right"
23
+ MIDDLE = "middle"
24
+
25
+
26
+ class ScrollDirection(str, Enum):
27
+ UP = "up"
28
+ DOWN = "down"
29
+ LEFT = "left"
30
+ RIGHT = "right"
31
+
32
+
33
+ class BaseAction(BaseModel):
34
+ """Fields every action shares."""
35
+
36
+ delay: float | None = Field(default=None, ge=0, description="Seconds to wait afterwards.")
37
+ verify: bool = Field(default=False, description="Compare the screen before and after.")
38
+
39
+ @property
40
+ def target(self) -> Coordinate | None:
41
+ """The point this action acts on, or None if it has no coordinate."""
42
+ return None
43
+
44
+ @property
45
+ def origin(self) -> Coordinate | None:
46
+ """The point this action starts from -- only a drag has one."""
47
+ return None
48
+
49
+
50
+ class _Positioned(BaseAction):
51
+ """An action that may carry a coordinate. Omitting it acts where the pointer already is."""
52
+
53
+ x: int | None = None
54
+ y: int | None = None
55
+ space: CoordinateSpace | None = None
56
+
57
+ @property
58
+ def target(self) -> Coordinate | None:
59
+ if self.x is None or self.y is None:
60
+ return None
61
+ space = self.space if self.space is not None else CoordinateSpace.SCREENSHOT
62
+ return Coordinate(x=self.x, y=self.y, space=space)
63
+
64
+
65
+ class MoveAction(_Positioned):
66
+ action: Literal["move"] = "move"
67
+ x: int
68
+ y: int
69
+
70
+
71
+ class ClickAction(_Positioned):
72
+ action: Literal["click"] = "click"
73
+ button: MouseButton = MouseButton.LEFT
74
+
75
+
76
+ class DoubleClickAction(_Positioned):
77
+ action: Literal["double_click"] = "double_click"
78
+ button: MouseButton = MouseButton.LEFT
79
+
80
+
81
+ class RightClickAction(_Positioned):
82
+ action: Literal["right_click"] = "right_click"
83
+
84
+
85
+ class DragAction(BaseAction):
86
+ action: Literal["drag"] = "drag"
87
+ from_x: int
88
+ from_y: int
89
+ to_x: int
90
+ to_y: int
91
+ space: CoordinateSpace | None = None
92
+ button: MouseButton = MouseButton.LEFT
93
+
94
+ @property
95
+ def origin(self) -> Coordinate:
96
+ return Coordinate(x=self.from_x, y=self.from_y, space=self._space)
97
+
98
+ @property
99
+ def target(self) -> Coordinate:
100
+ return Coordinate(x=self.to_x, y=self.to_y, space=self._space)
101
+
102
+ @property
103
+ def _space(self) -> CoordinateSpace:
104
+ return self.space if self.space is not None else CoordinateSpace.SCREENSHOT
105
+
106
+
107
+ class ScrollAction(_Positioned):
108
+ action: Literal["scroll"] = "scroll"
109
+ amount: int
110
+ direction: ScrollDirection = ScrollDirection.DOWN
111
+
112
+
113
+ class TypeAction(BaseAction):
114
+ action: Literal["type"] = "type"
115
+ text: str
116
+ rate: float | None = Field(
117
+ default=None,
118
+ ge=0,
119
+ description="Seconds between keystrokes; None uses the configured rate.",
120
+ )
121
+
122
+
123
+ class KeyAction(BaseAction):
124
+ action: Literal["key"] = "key"
125
+ combo: str
126
+
127
+ @field_validator("combo")
128
+ @classmethod
129
+ def _canonicalise(cls, value: str) -> str:
130
+ # Parse at construction: an unknown key name must fail before a connection is opened,
131
+ # and the canonical spelling is what appears in results and logs.
132
+ return canonical(value)
133
+
134
+ @property
135
+ def key_combo(self) -> KeyCombo:
136
+ return parse_combo(self.combo)
137
+
138
+
139
+ class ScreenshotAction(BaseAction):
140
+ action: Literal["screenshot"] = "screenshot"
141
+ out: Path | None = None
142
+ base64: bool = False
143
+
144
+
145
+ Action = Annotated[
146
+ MoveAction
147
+ | ClickAction
148
+ | DoubleClickAction
149
+ | RightClickAction
150
+ | DragAction
151
+ | ScrollAction
152
+ | TypeAction
153
+ | KeyAction
154
+ | ScreenshotAction,
155
+ Field(discriminator="action"),
156
+ ]
157
+
158
+ #: Parses a batch file: a JSON array of action objects, discriminated on `action`.
159
+ ActionListAdapter: TypeAdapter[list[Action]] = TypeAdapter(list[Action])
160
+
161
+ #: Parses a single action object.
162
+ ActionAdapter: TypeAdapter[Action] = TypeAdapter(Action)
163
+
164
+
165
+ def with_default_space(action: Action, space: CoordinateSpace) -> Action:
166
+ """Fill in the coordinate space an action did not state, from configuration."""
167
+ if getattr(action, "space", "missing") is None:
168
+ return action.model_copy(update={"space": space})
169
+ return action
170
+
171
+
172
+ def resolve(action: Action, screen: ScreenInfo) -> tuple[Coordinate | None, Coordinate | None]:
173
+ """Convert an action's coordinates into actuation units.
174
+
175
+ Returns ``(target, origin)``; either may be ``None`` when the action carries no coordinate.
176
+
177
+ Raises:
178
+ CoordinateSpaceError: when the scale needed for the conversion is unknown.
179
+ """
180
+ target = action.target
181
+ origin = action.origin
182
+ resolved_target = convert(target, CoordinateSpace.ACTUATION, screen) if target else None
183
+ resolved_origin = convert(origin, CoordinateSpace.ACTUATION, screen) if origin else None
184
+ return resolved_target, resolved_origin