desk-proxy 0.1.2__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.
desk_proxy/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ desk-proxy: Non-MCP CLI proxy for Linux desktop automation.
3
+
4
+ Screenshots, keyboard/mouse input, OCR, and HITL review — ADN envelope
5
+ compatible with tick-proxy / mail-proxy / tg-proxy.
6
+ """
7
+
8
+ from importlib.metadata import PackageNotFoundError, version
9
+
10
+ try:
11
+ __version__ = version("desk-proxy")
12
+ except PackageNotFoundError:
13
+ __version__ = "0.0.0"
@@ -0,0 +1,5 @@
1
+ """desk-proxy actions package — domain modules + registry."""
2
+
3
+ from .registry import ACTION_COUNT, REGISTRY, by_group, get
4
+
5
+ __all__ = ["ACTION_COUNT", "REGISTRY", "by_group", "get"]
@@ -0,0 +1,116 @@
1
+ """
2
+ Action framework — declarative approval policy for desk-proxy.
3
+
4
+ Same ADN contract as tick/mail: an ``ActionDef`` carries the action name, its
5
+ colocated Pydantic payload model and the handler. ``@require_approval`` derives
6
+ HITL policy from the handler — ``cli.py`` has no separate policy table.
7
+
8
+ Handler signature (local desktop — no remote client)::
9
+
10
+ handler(payload) -> dict
11
+ """
12
+
13
+ from collections.abc import Callable
14
+ from dataclasses import dataclass, field
15
+ from functools import wraps
16
+ from typing import Any
17
+
18
+ from pydantic import BaseModel
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class ActionDef:
23
+ """One ``do`` action: name, payload model, handler and its policies.
24
+
25
+ Attributes:
26
+ name (str): The flat kebab-case action name, e.g. ``shot-take``.
27
+ payload (type[BaseModel] | None): Pydantic model validating the payload
28
+ (None when the action takes no payload).
29
+ handler (Callable): ``handler(payload) -> dict`` — no client argument.
30
+ hitl (bool): Derived from the handler's ``@require_approval`` declaration.
31
+ group (str): Catalog group used by ``do --help``, e.g. ``"Screen"``.
32
+ aliases (tuple[str, ...]): Optional command aliases.
33
+
34
+ Examples:
35
+ >>> ActionDef("shot-take", None, lambda p: {}, group="Screen").name
36
+ 'shot-take'
37
+ >>> ActionDef("shot-take", None, lambda p: {}).hitl
38
+ False
39
+ """
40
+
41
+ name: str
42
+ payload: type[BaseModel] | None
43
+ handler: Callable[..., Any]
44
+ hitl: bool = False
45
+ group: str = "Misc"
46
+ aliases: tuple[str, ...] = field(default_factory=tuple)
47
+
48
+
49
+ def require_approval() -> Callable:
50
+ """Declare a handler's mandatory centralized HITL review policy.
51
+
52
+ Returns:
53
+ Callable: A decorator carrying auditable review metadata.
54
+
55
+ Examples:
56
+ >>> @require_approval()
57
+ ... def click(payload): return {}
58
+ >>> click.__require_approval__
59
+ True
60
+ >>> click.__review_mode__
61
+ 'default'
62
+ >>> callable(click)
63
+ True
64
+ """
65
+
66
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
67
+ @wraps(func)
68
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
69
+ return func(*args, **kwargs)
70
+
71
+ wrapper.__require_approval__ = True # type: ignore[attr-defined]
72
+ wrapper.__review_mode__ = "default" # type: ignore[attr-defined]
73
+ return wrapper
74
+
75
+ return decorator
76
+
77
+
78
+ def action_def(
79
+ name: str,
80
+ payload: type[BaseModel] | None,
81
+ handler: Callable[..., Any],
82
+ *,
83
+ group: str = "Misc",
84
+ aliases: tuple[str, ...] = (),
85
+ ) -> ActionDef:
86
+ """Build an action definition from visible handler decorators.
87
+
88
+ Args:
89
+ name (str): Flat registered action name.
90
+ payload (type[BaseModel] | None): Pydantic payload model.
91
+ handler (Callable[..., Any]): Decorated implementation
92
+ (``handler(payload) -> dict``).
93
+ group (str): Help catalog group.
94
+ aliases (tuple[str, ...]): Optional command aliases.
95
+
96
+ Returns:
97
+ ActionDef: HITL policy derived from ``__require_approval__``.
98
+
99
+ Examples:
100
+ >>> @require_approval()
101
+ ... def type_text(payload): return {}
102
+ >>> action_def("type-text", None, type_text).hitl
103
+ True
104
+ >>> action_def("shot-take", None, lambda p: {}).hitl
105
+ False
106
+ >>> action_def("raw", None, lambda p: {}, group="Escape hatch").group
107
+ 'Escape hatch'
108
+ """
109
+ return ActionDef(
110
+ name=name,
111
+ payload=payload,
112
+ handler=handler,
113
+ hitl=bool(getattr(handler, "__require_approval__", False)),
114
+ group=group,
115
+ aliases=aliases,
116
+ )
@@ -0,0 +1,68 @@
1
+ """Clipboard group — get and set system clipboard text."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from desk_proxy.api import clipboard as clip_api
10
+
11
+ from .base import action_def, require_approval
12
+
13
+
14
+ class ClipboardSetPayload(BaseModel):
15
+ """Text to place on the clipboard."""
16
+
17
+ text: str = Field(..., description="Clipboard contents to write")
18
+
19
+
20
+ def clipboard_get(_payload: Any = None) -> dict[str, Any]:
21
+ """Read the current clipboard text (wl-paste or xclip).
22
+
23
+ Parameters:
24
+ - (none): Payload may be null or ``{}``.
25
+
26
+ Examples:
27
+ - Read clipboard:
28
+ `desk-proxy do clipboard-get '{}'`
29
+ → {"text": "hello from clipboard"}
30
+
31
+ - Empty clipboard:
32
+ `desk-proxy do clipboard-get`
33
+ → {"text": ""}
34
+
35
+ - After a set:
36
+ `desk-proxy do clipboard-get '{}'`
37
+ → {"text": "desk-proxy-probe"}
38
+ """
39
+ return {"text": clip_api.clipboard_get()}
40
+
41
+
42
+ @require_approval()
43
+ def clipboard_set(p: ClipboardSetPayload) -> dict[str, Any]:
44
+ """Write text to the clipboard. HITL required.
45
+
46
+ Parameters:
47
+ - text (str): Text to place on the clipboard (required).
48
+
49
+ Examples:
50
+ - Set a short string:
51
+ `desk-proxy do clipboard-set '{"text":"hello"}'`
52
+ → {"text": "hello"}
53
+
54
+ - Clear clipboard:
55
+ `desk-proxy do clipboard-set '{"text":""}'`
56
+ → {"text": ""}
57
+
58
+ - HITL rejection:
59
+ `desk-proxy do clipboard-set '{"text":"secret"}'`
60
+ → {"meta": {"status": "rejected", "comment": "not now"}, "data": null}
61
+ """
62
+ return {"text": clip_api.clipboard_set(p.text)}
63
+
64
+
65
+ ACTIONS = [
66
+ action_def("clipboard-get", None, clipboard_get, group="Clipboard"),
67
+ action_def("clipboard-set", ClipboardSetPayload, clipboard_set, group="Clipboard"),
68
+ ]
@@ -0,0 +1,158 @@
1
+ """Control group — wait, action chains, and raw backend escape hatch."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any, Literal
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from desk_proxy.api.run import run_cmd
11
+ from desk_proxy.exceptions import DeskProxyError
12
+
13
+ from .base import action_def, require_approval
14
+
15
+ RawBackend = Literal["xdotool", "ydotool", "wmctrl", "shell"]
16
+
17
+
18
+ class WaitPayload(BaseModel):
19
+ """Sleep for a fixed duration."""
20
+
21
+ seconds: float = Field(..., description="Seconds to sleep (>= 0)", ge=0)
22
+
23
+
24
+ class ChainStep(BaseModel):
25
+ """One step inside a ``chain`` action."""
26
+
27
+ action: str = Field(..., description="Registered kebab-case action name")
28
+ payload: dict[str, Any] = Field(
29
+ default_factory=dict, description="JSON payload for that action"
30
+ )
31
+
32
+
33
+ class ChainPayload(BaseModel):
34
+ """Sequential execution of registered desk-proxy actions."""
35
+
36
+ steps: list[ChainStep] = Field(..., description="Ordered list of {action, payload}")
37
+
38
+
39
+ class RawPayload(BaseModel):
40
+ """Escape hatch — run a desktop backend with explicit argv."""
41
+
42
+ backend: RawBackend = Field(..., description="xdotool | ydotool | wmctrl | shell")
43
+ args: list[str] = Field(..., description="Arguments (or full argv for shell)")
44
+
45
+
46
+ def wait(p: WaitPayload) -> dict[str, Any]:
47
+ """Sleep for ``seconds`` then return the duration slept.
48
+
49
+ Parameters:
50
+ - seconds (float): Duration in seconds (>= 0).
51
+
52
+ Examples:
53
+ - Short pause:
54
+ `desk-proxy do wait '{"seconds":0.5}'`
55
+ → {"slept": 0.5}
56
+
57
+ - One second:
58
+ `desk-proxy do wait '{"seconds":1}'`
59
+ → {"slept": 1.0}
60
+
61
+ - Zero (no-op):
62
+ `desk-proxy do wait '{"seconds":0}'`
63
+ → {"slept": 0.0}
64
+ """
65
+ time.sleep(float(p.seconds))
66
+ return {"slept": float(p.seconds)}
67
+
68
+
69
+ @require_approval()
70
+ def chain(p: ChainPayload) -> dict[str, Any]:
71
+ """Execute registered actions sequentially. HITL required for the whole chain.
72
+
73
+ Unknown action names are rejected. Nested HITL is not re-prompted — the
74
+ outer chain approval covers every step. Each step receives a validated
75
+ payload model when the action declares one.
76
+
77
+ Parameters:
78
+ - steps (list): Ordered ``{action, payload}`` objects.
79
+
80
+ Examples:
81
+ - Info then mouse:
82
+ `desk-proxy do chain '{"steps":[{"action":"screen-info","payload":{}},{"action":"mouse-get","payload":{}}]}'`
83
+ → {"results": [{"action": "screen-info", "data": {"width": 1920}}, {"action": "mouse-get", "data": {"x": 10, "y": 20}}]}
84
+
85
+ - Wait then shot:
86
+ `desk-proxy do chain '{"steps":[{"action":"wait","payload":{"seconds":0.1}},{"action":"screen-shot","payload":{}}]}'`
87
+ → {"results": [{"action": "wait", "data": {"slept": 0.1}}, {"action": "screen-shot", "data": {"path": "/tmp/desk-proxy-shots/full_1.png"}}]}
88
+
89
+ - Unknown action fails:
90
+ `desk-proxy do chain '{"steps":[{"action":"nope","payload":{}}]}'`
91
+ → {"meta": {"status": "error"}, "data": null}
92
+ """
93
+ # Lazy import avoids circular registry ↔ control dependency.
94
+ from desk_proxy.actions.registry import REGISTRY
95
+
96
+ if not p.steps:
97
+ raise DeskProxyError("chain requires at least one step")
98
+
99
+ results: list[dict[str, Any]] = []
100
+ for step in p.steps:
101
+ action = REGISTRY.get(step.action)
102
+ if action is None:
103
+ raise DeskProxyError(f"Unknown action in chain: {step.action}")
104
+ raw = step.payload if isinstance(step.payload, dict) else {}
105
+ if action.payload is not None:
106
+ validated: Any = action.payload(**raw)
107
+ else:
108
+ validated = raw
109
+ data = action.handler(validated)
110
+ results.append({"action": step.action, "data": data})
111
+ return {"results": results}
112
+
113
+
114
+ @require_approval()
115
+ def raw(p: RawPayload) -> dict[str, Any]:
116
+ """Run a desktop backend with explicit args. HITL required.
117
+
118
+ For ``xdotool`` / ``ydotool`` / ``wmctrl`` the binary is prepended.
119
+ For ``shell`` the ``args`` list is the full argv (no shell expansion).
120
+
121
+ Parameters:
122
+ - backend (str): ``xdotool`` | ``ydotool`` | ``wmctrl`` | ``shell``.
123
+ - args (list[str]): Arguments after the backend binary (or full argv).
124
+
125
+ Examples:
126
+ - Display geometry via xdotool:
127
+ `desk-proxy do raw '{"backend":"xdotool","args":["getdisplaygeometry"]}'`
128
+ → {"backend": "xdotool", "argv": ["xdotool", "getdisplaygeometry"], "returncode": 0, "stdout": "1920 1080", "stderr": ""}
129
+
130
+ - List windows via wmctrl:
131
+ `desk-proxy do raw '{"backend":"wmctrl","args":["-l"]}'`
132
+ → {"backend": "wmctrl", "argv": ["wmctrl", "-l"], "returncode": 0, "stdout": "0x01a00007 0 host Terminal", "stderr": ""}
133
+
134
+ - Shell argv (no expansion):
135
+ `desk-proxy do raw '{"backend":"shell","args":["true"]}'`
136
+ → {"backend": "shell", "argv": ["true"], "returncode": 0, "stdout": "", "stderr": ""}
137
+ """
138
+ if p.backend == "shell":
139
+ if not p.args:
140
+ raise DeskProxyError("raw shell backend requires a non-empty args list")
141
+ argv = list(p.args)
142
+ else:
143
+ argv = [p.backend, *p.args]
144
+ result = run_cmd(argv, timeout=30)
145
+ return {
146
+ "backend": p.backend,
147
+ "argv": argv,
148
+ "returncode": result.returncode,
149
+ "stdout": result.stdout or "",
150
+ "stderr": result.stderr or "",
151
+ }
152
+
153
+
154
+ ACTIONS = [
155
+ action_def("wait", WaitPayload, wait, group="Control"),
156
+ action_def("chain", ChainPayload, chain, group="Control"),
157
+ action_def("raw", RawPayload, raw, group="Control"),
158
+ ]
@@ -0,0 +1,75 @@
1
+ """Keyboard group — type text and press key chords."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from desk_proxy.api import input as input_api
10
+
11
+ from .base import action_def
12
+
13
+
14
+ class KeyboardTypePayload(BaseModel):
15
+ """Unicode text injection at the current focus."""
16
+
17
+ text: str = Field(..., description="Text to type")
18
+ delay_ms: int = Field(12, description="Inter-key delay in milliseconds", ge=0)
19
+
20
+
21
+ class KeyboardKeyPayload(BaseModel):
22
+ """Single key or chord (xdotool-style)."""
23
+
24
+ combo: str = Field(..., description="Key or chord, e.g. Return, ctrl+c, alt+F4")
25
+
26
+
27
+ def keyboard_type(p: KeyboardTypePayload) -> dict[str, Any]:
28
+ """Type Unicode text at the current keyboard focus.
29
+
30
+ Parameters:
31
+ - text (str): Text to type (required).
32
+ - delay_ms (int): Inter-key delay in ms (default 12).
33
+
34
+ Examples:
35
+ - Type a short string:
36
+ `desk-proxy do keyboard-type '{"text":"hello"}'`
37
+ → {"typed": "hello", "backend": "wtype", "delay_ms": 12}
38
+
39
+ - Slower typing:
40
+ `desk-proxy do keyboard-type '{"text":"slow","delay_ms":40}'`
41
+ → {"typed": "slow", "backend": "xdotool", "delay_ms": 40}
42
+
43
+ - Long text is preview-truncated:
44
+ `desk-proxy do keyboard-type '{"text":"abcdefghijklmnopqrstuvwxyz0123456789EXTRA"}'`
45
+ → {"typed": "abcdefghijklmnopqrstuvwxyz0123456789EXTR...", "backend": "wtype", "delay_ms": 12}
46
+ """
47
+ return input_api.type_text(p.text, delay_ms=p.delay_ms)
48
+
49
+
50
+ def keyboard_key(p: KeyboardKeyPayload) -> dict[str, Any]:
51
+ """Press a key or chord (``ctrl+c``, ``Return``, ``alt+F4``, …).
52
+
53
+ Parameters:
54
+ - combo (str): xdotool-style combo string (required).
55
+
56
+ Examples:
57
+ - Escape:
58
+ `desk-proxy do keyboard-key '{"combo":"Escape"}'`
59
+ → {"combo": "Escape", "backend": "xdotool"}
60
+
61
+ - Copy chord:
62
+ `desk-proxy do keyboard-key '{"combo":"ctrl+c"}'`
63
+ → {"combo": "ctrl+c", "backend": "xdotool"}
64
+
65
+ - Enter:
66
+ `desk-proxy do keyboard-key '{"combo":"Return"}'`
67
+ → {"combo": "Return", "backend": "wtype"}
68
+ """
69
+ return input_api.press_key(p.combo)
70
+
71
+
72
+ ACTIONS = [
73
+ action_def("keyboard-type", KeyboardTypePayload, keyboard_type, group="Keyboard"),
74
+ action_def("keyboard-key", KeyboardKeyPayload, keyboard_key, group="Keyboard"),
75
+ ]
@@ -0,0 +1,183 @@
1
+ """Mouse group — pointer query, move, click, drag, scroll."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from desk_proxy.api import input as input_api
10
+
11
+ from .base import action_def
12
+
13
+ ButtonName = Literal["left", "right", "middle"]
14
+ ScrollDir = Literal["up", "down", "left", "right"]
15
+
16
+
17
+ class MouseMovePayload(BaseModel):
18
+ """Absolute pointer destination."""
19
+
20
+ x: int = Field(..., description="Target X in pixels")
21
+ y: int = Field(..., description="Target Y in pixels")
22
+
23
+
24
+ class MouseClickPayload(BaseModel):
25
+ """Click at absolute coordinates."""
26
+
27
+ x: int = Field(..., description="Click X")
28
+ y: int = Field(..., description="Click Y")
29
+ button: ButtonName = Field("left", description="left | right | middle")
30
+ clicks: int = Field(1, description="Click count (>= 1; 2 = double-click)", ge=1)
31
+
32
+
33
+ class MouseDragPayload(BaseModel):
34
+ """Drag from (x1,y1) to (x2,y2)."""
35
+
36
+ x1: int = Field(..., description="Start X")
37
+ y1: int = Field(..., description="Start Y")
38
+ x2: int = Field(..., description="End X")
39
+ y2: int = Field(..., description="End Y")
40
+ button: ButtonName = Field("left", description="Button held during drag")
41
+ duration_ms: int = Field(300, description="Approximate drag duration in ms", ge=1)
42
+
43
+
44
+ class MouseScrollPayload(BaseModel):
45
+ """Scroll at coordinates."""
46
+
47
+ x: int = Field(..., description="Pointer X before scrolling")
48
+ y: int = Field(..., description="Pointer Y before scrolling")
49
+ direction: ScrollDir = Field("down", description="up | down | left | right")
50
+ clicks: int = Field(3, description="Number of scroll ticks", ge=1)
51
+
52
+
53
+ def mouse_get(_payload: Any = None) -> dict[str, Any]:
54
+ """Return the current pointer position.
55
+
56
+ Parameters:
57
+ - (none): Payload may be null or ``{}``.
58
+
59
+ Examples:
60
+ - Read pointer:
61
+ `desk-proxy do mouse-get '{}'`
62
+ → {"x": 960, "y": 540}
63
+
64
+ - Empty payload:
65
+ `desk-proxy do mouse-get`
66
+ → {"x": 12, "y": 34}
67
+
68
+ - After a move:
69
+ `desk-proxy do mouse-get '{}'`
70
+ → {"x": 100, "y": 200}
71
+ """
72
+ return input_api.get_mouse()
73
+
74
+
75
+ def mouse_move(p: MouseMovePayload) -> dict[str, Any]:
76
+ """Move the pointer to absolute coordinates.
77
+
78
+ Parameters:
79
+ - x (int): Target X in pixels.
80
+ - y (int): Target Y in pixels.
81
+
82
+ Examples:
83
+ - Move to center-ish:
84
+ `desk-proxy do mouse-move '{"x":960,"y":540}'`
85
+ → {"x": 960, "y": 540}
86
+
87
+ - Origin:
88
+ `desk-proxy do mouse-move '{"x":0,"y":0}'`
89
+ → {"x": 0, "y": 0}
90
+
91
+ - Button corner:
92
+ `desk-proxy do mouse-move '{"x":100,"y":200}'`
93
+ → {"x": 100, "y": 200}
94
+ """
95
+ return input_api.move_mouse(p.x, p.y)
96
+
97
+
98
+ def mouse_click(p: MouseClickPayload) -> dict[str, Any]:
99
+ """Move to ``(x, y)`` and click.
100
+
101
+ Parameters:
102
+ - x (int): Click X.
103
+ - y (int): Click Y.
104
+ - button (str): ``left`` | ``right`` | ``middle`` (default left).
105
+ - clicks (int): Repeat count (default 1; use 2 for double-click).
106
+
107
+ Examples:
108
+ - Left click:
109
+ `desk-proxy do mouse-click '{"x":100,"y":200}'`
110
+ → {"x": 100, "y": 200, "button": "left", "clicks": 1}
111
+
112
+ - Double-click:
113
+ `desk-proxy do mouse-click '{"x":100,"y":200,"clicks":2}'`
114
+ → {"x": 100, "y": 200, "button": "left", "clicks": 2}
115
+
116
+ - Right-click:
117
+ `desk-proxy do mouse-click '{"x":50,"y":50,"button":"right"}'`
118
+ → {"x": 50, "y": 50, "button": "right", "clicks": 1}
119
+ """
120
+ return input_api.click(p.x, p.y, button=p.button, clicks=p.clicks)
121
+
122
+
123
+ def mouse_drag(p: MouseDragPayload) -> dict[str, Any]:
124
+ """Drag from ``(x1,y1)`` to ``(x2,y2)`` with a button held.
125
+
126
+ Parameters:
127
+ - x1 (int): Start X.
128
+ - y1 (int): Start Y.
129
+ - x2 (int): End X.
130
+ - y2 (int): End Y.
131
+ - button (str): Button held (default left).
132
+ - duration_ms (int): Approximate drag duration in milliseconds.
133
+
134
+ Examples:
135
+ - Short drag:
136
+ `desk-proxy do mouse-drag '{"x1":10,"y1":10,"x2":100,"y2":100}'`
137
+ → {"x1": 10, "y1": 10, "x2": 100, "y2": 100, "button": "left", "duration_ms": 300}
138
+
139
+ - Fast drag:
140
+ `desk-proxy do mouse-drag '{"x1":0,"y1":0,"x2":50,"y2":50,"duration_ms":50}'`
141
+ → {"x1": 0, "y1": 0, "x2": 50, "y2": 50, "button": "left", "duration_ms": 50}
142
+
143
+ - Middle-button drag:
144
+ `desk-proxy do mouse-drag '{"x1":20,"y1":20,"x2":80,"y2":80,"button":"middle"}'`
145
+ → {"x1": 20, "y1": 20, "x2": 80, "y2": 80, "button": "middle", "duration_ms": 300}
146
+ """
147
+ return input_api.drag(
148
+ p.x1, p.y1, p.x2, p.y2, button=p.button, duration_ms=p.duration_ms
149
+ )
150
+
151
+
152
+ def mouse_scroll(p: MouseScrollPayload) -> dict[str, Any]:
153
+ """Scroll at coordinates.
154
+
155
+ Parameters:
156
+ - x (int): Pointer X before scrolling.
157
+ - y (int): Pointer Y before scrolling.
158
+ - direction (str): ``up`` | ``down`` | ``left`` | ``right`` (default down).
159
+ - clicks (int): Number of scroll ticks (default 3).
160
+
161
+ Examples:
162
+ - Scroll down:
163
+ `desk-proxy do mouse-scroll '{"x":500,"y":500}'`
164
+ → {"x": 500, "y": 500, "direction": "down", "clicks": 3}
165
+
166
+ - Scroll up twice:
167
+ `desk-proxy do mouse-scroll '{"x":500,"y":500,"direction":"up","clicks":2}'`
168
+ → {"x": 500, "y": 500, "direction": "up", "clicks": 2}
169
+
170
+ - Horizontal:
171
+ `desk-proxy do mouse-scroll '{"x":100,"y":100,"direction":"right","clicks":1}'`
172
+ → {"x": 100, "y": 100, "direction": "right", "clicks": 1}
173
+ """
174
+ return input_api.scroll(p.x, p.y, direction=p.direction, clicks=p.clicks)
175
+
176
+
177
+ ACTIONS = [
178
+ action_def("mouse-get", None, mouse_get, group="Mouse"),
179
+ action_def("mouse-move", MouseMovePayload, mouse_move, group="Mouse"),
180
+ action_def("mouse-click", MouseClickPayload, mouse_click, group="Mouse"),
181
+ action_def("mouse-drag", MouseDragPayload, mouse_drag, group="Mouse"),
182
+ action_def("mouse-scroll", MouseScrollPayload, mouse_scroll, group="Mouse"),
183
+ ]