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 +13 -0
- desk_proxy/actions/__init__.py +5 -0
- desk_proxy/actions/base.py +116 -0
- desk_proxy/actions/clipboard.py +68 -0
- desk_proxy/actions/control.py +158 -0
- desk_proxy/actions/keyboard.py +75 -0
- desk_proxy/actions/mouse.py +183 -0
- desk_proxy/actions/registry.py +78 -0
- desk_proxy/actions/screen.py +184 -0
- desk_proxy/actions/windows.py +262 -0
- desk_proxy/admin.py +199 -0
- desk_proxy/api/__init__.py +58 -0
- desk_proxy/api/atspi_script.py +131 -0
- desk_proxy/api/clipboard.py +108 -0
- desk_proxy/api/input.py +574 -0
- desk_proxy/api/ocr.py +217 -0
- desk_proxy/api/run.py +93 -0
- desk_proxy/api/screen.py +83 -0
- desk_proxy/api/screenshot.py +442 -0
- desk_proxy/api/windows.py +492 -0
- desk_proxy/cli.py +403 -0
- desk_proxy/config.py +376 -0
- desk_proxy/display.py +109 -0
- desk_proxy/doc.py +104 -0
- desk_proxy/exceptions.py +47 -0
- desk_proxy/hitl.py +304 -0
- desk_proxy/logger.py +42 -0
- desk_proxy/models.py +103 -0
- desk_proxy/templates/hitl.html +61 -0
- desk_proxy-0.1.2.dist-info/METADATA +9 -0
- desk_proxy-0.1.2.dist-info/RECORD +33 -0
- desk_proxy-0.1.2.dist-info/WHEEL +4 -0
- desk_proxy-0.1.2.dist-info/entry_points.txt +3 -0
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,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
|
+
]
|