use-computer 0.0.1__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.
mmini/ios/input.py ADDED
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+ import httpx
6
+
7
+ from mmini.models import ActionResult
8
+
9
+
10
+ class Button(str, Enum):
11
+ """Hardware buttons available on iOS simulators."""
12
+
13
+ HOME = "home"
14
+ LOCK = "lock"
15
+ SIRI = "siri"
16
+ SIDE_BUTTON = "side-button"
17
+ APPLE_PAY = "apple-pay"
18
+
19
+
20
+ class Key(int, Enum):
21
+ """Common HID keycodes for iOS simulator key presses."""
22
+
23
+ RETURN = 40
24
+ BACKSPACE = 42
25
+ TAB = 43
26
+ SPACE = 44
27
+ ESCAPE = 41
28
+ DELETE = 76
29
+ UP = 82
30
+ DOWN = 81
31
+ LEFT = 80
32
+ RIGHT = 79
33
+ F1 = 58
34
+ F2 = 59
35
+ F3 = 60
36
+ F4 = 61
37
+ F5 = 62
38
+ F6 = 63
39
+ F7 = 64
40
+ F8 = 65
41
+ F9 = 66
42
+ F10 = 67
43
+
44
+
45
+ class Input:
46
+ def __init__(self, http: httpx.Client, prefix: str):
47
+ self._http = http
48
+ self._prefix = prefix
49
+
50
+ def tap(self, x: float, y: float) -> ActionResult:
51
+ resp = self._http.post(f"{self._prefix}/tap", json={"x": x, "y": y})
52
+ resp.raise_for_status()
53
+ return ActionResult.from_dict(resp.json())
54
+
55
+ def swipe(self, from_x: float, from_y: float, to_x: float, to_y: float) -> ActionResult:
56
+ resp = self._http.post(
57
+ f"{self._prefix}/swipe",
58
+ json={"fromX": from_x, "fromY": from_y, "toX": to_x, "toY": to_y},
59
+ )
60
+ resp.raise_for_status()
61
+ return ActionResult.from_dict(resp.json())
62
+
63
+ def type_text(self, text: str) -> ActionResult:
64
+ resp = self._http.post(f"{self._prefix}/type", json={"text": text})
65
+ resp.raise_for_status()
66
+ return ActionResult.from_dict(resp.json())
67
+
68
+ def press_button(self, button: Button | str) -> ActionResult:
69
+ """Press a hardware button. Use Button enum for type safety."""
70
+ val = button.value if isinstance(button, Button) else button
71
+ resp = self._http.post(f"{self._prefix}/button", json={"button": val})
72
+ resp.raise_for_status()
73
+ return ActionResult.from_dict(resp.json())
74
+
75
+ def press_key(self, keycode: Key | int) -> ActionResult:
76
+ """Press a key by HID keycode. Use Key enum for common keys."""
77
+ val = keycode.value if isinstance(keycode, Key) else keycode
78
+ resp = self._http.post(f"{self._prefix}/key", json={"keycode": val})
79
+ resp.raise_for_status()
80
+ return ActionResult.from_dict(resp.json())
81
+
82
+
83
+ class AsyncInput:
84
+ def __init__(self, http: httpx.AsyncClient, prefix: str):
85
+ self._http = http
86
+ self._prefix = prefix
87
+
88
+ async def tap(self, x: float, y: float) -> ActionResult:
89
+ resp = await self._http.post(f"{self._prefix}/tap", json={"x": x, "y": y})
90
+ resp.raise_for_status()
91
+ return ActionResult.from_dict(resp.json())
92
+
93
+ async def swipe(self, from_x: float, from_y: float, to_x: float, to_y: float) -> ActionResult:
94
+ resp = await self._http.post(
95
+ f"{self._prefix}/swipe",
96
+ json={"fromX": from_x, "fromY": from_y, "toX": to_x, "toY": to_y},
97
+ )
98
+ resp.raise_for_status()
99
+ return ActionResult.from_dict(resp.json())
100
+
101
+ async def type_text(self, text: str) -> ActionResult:
102
+ resp = await self._http.post(f"{self._prefix}/type", json={"text": text})
103
+ resp.raise_for_status()
104
+ return ActionResult.from_dict(resp.json())
105
+
106
+ async def press_button(self, button: Button | str) -> ActionResult:
107
+ """Press a hardware button. Use Button enum for type safety."""
108
+ val = button.value if isinstance(button, Button) else button
109
+ resp = await self._http.post(f"{self._prefix}/button", json={"button": val})
110
+ resp.raise_for_status()
111
+ return ActionResult.from_dict(resp.json())
112
+
113
+ async def press_key(self, keycode: Key | int) -> ActionResult:
114
+ """Press a key by HID keycode. Use Key enum for common keys."""
115
+ val = keycode.value if isinstance(keycode, Key) else keycode
116
+ resp = await self._http.post(f"{self._prefix}/key", json={"keycode": val})
117
+ resp.raise_for_status()
118
+ return ActionResult.from_dict(resp.json())
@@ -0,0 +1,4 @@
1
+ from mmini.macos.keyboard import AsyncKeyboard, Keyboard
2
+ from mmini.macos.mouse import AsyncMouse, Mouse
3
+
4
+ __all__ = ["AsyncKeyboard", "AsyncMouse", "Keyboard", "Mouse"]
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+
5
+ from mmini.models import ActionResult
6
+
7
+
8
+ class Keyboard:
9
+ def __init__(self, http: httpx.Client, prefix: str):
10
+ self._http = http
11
+ self._prefix = prefix
12
+
13
+ def type(self, text: str, delay: int | None = None) -> ActionResult:
14
+ payload: dict = {"text": text}
15
+ if delay is not None:
16
+ payload["delay"] = delay
17
+ resp = self._http.post(f"{self._prefix}/keyboard/type", json=payload)
18
+ resp.raise_for_status()
19
+ return ActionResult.from_dict(resp.json())
20
+
21
+ def press(self, key: str, modifiers: list[str] | None = None) -> ActionResult:
22
+ payload: dict = {"key": key}
23
+ if modifiers:
24
+ payload["modifiers"] = modifiers
25
+ resp = self._http.post(f"{self._prefix}/keyboard/press", json=payload)
26
+ resp.raise_for_status()
27
+ return ActionResult.from_dict(resp.json())
28
+
29
+ def hotkey(self, keys: str) -> ActionResult:
30
+ resp = self._http.post(f"{self._prefix}/keyboard/hotkey", json={"keys": keys})
31
+ resp.raise_for_status()
32
+ return ActionResult.from_dict(resp.json())
33
+
34
+
35
+ class AsyncKeyboard:
36
+ def __init__(self, http: httpx.AsyncClient, prefix: str):
37
+ self._http = http
38
+ self._prefix = prefix
39
+
40
+ async def type(self, text: str, delay: int | None = None) -> ActionResult:
41
+ payload: dict = {"text": text}
42
+ if delay is not None:
43
+ payload["delay"] = delay
44
+ resp = await self._http.post(f"{self._prefix}/keyboard/type", json=payload)
45
+ resp.raise_for_status()
46
+ return ActionResult.from_dict(resp.json())
47
+
48
+ async def press(self, key: str, modifiers: list[str] | None = None) -> ActionResult:
49
+ payload: dict = {"key": key}
50
+ if modifiers:
51
+ payload["modifiers"] = modifiers
52
+ resp = await self._http.post(f"{self._prefix}/keyboard/press", json=payload)
53
+ resp.raise_for_status()
54
+ return ActionResult.from_dict(resp.json())
55
+
56
+ async def hotkey(self, keys: str) -> ActionResult:
57
+ resp = await self._http.post(f"{self._prefix}/keyboard/hotkey", json={"keys": keys})
58
+ resp.raise_for_status()
59
+ return ActionResult.from_dict(resp.json())
mmini/macos/mouse.py ADDED
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+
5
+ from mmini.models import ActionResult, CursorPosition
6
+
7
+
8
+ class Mouse:
9
+ def __init__(self, http: httpx.Client, prefix: str):
10
+ self._http = http
11
+ self._prefix = prefix
12
+
13
+ def click(self, x: int, y: int, button: str = "left", double: bool = False) -> ActionResult:
14
+ resp = self._http.post(
15
+ f"{self._prefix}/mouse/click",
16
+ json={"x": x, "y": y, "button": button, "double": double},
17
+ )
18
+ resp.raise_for_status()
19
+ return ActionResult.from_dict(resp.json())
20
+
21
+ def move(self, x: int, y: int) -> ActionResult:
22
+ resp = self._http.post(f"{self._prefix}/mouse/move", json={"x": x, "y": y})
23
+ resp.raise_for_status()
24
+ return ActionResult.from_dict(resp.json())
25
+
26
+ def drag(
27
+ self, start_x: int, start_y: int, end_x: int, end_y: int, button: str = "left"
28
+ ) -> ActionResult:
29
+ resp = self._http.post(
30
+ f"{self._prefix}/mouse/drag",
31
+ json={
32
+ "startX": start_x,
33
+ "startY": start_y,
34
+ "endX": end_x,
35
+ "endY": end_y,
36
+ "button": button,
37
+ },
38
+ )
39
+ resp.raise_for_status()
40
+ return ActionResult.from_dict(resp.json())
41
+
42
+ def scroll(self, x: int, y: int, direction: str = "down", amount: int = 3) -> ActionResult:
43
+ resp = self._http.post(
44
+ f"{self._prefix}/mouse/scroll",
45
+ json={"x": x, "y": y, "direction": direction, "amount": amount},
46
+ )
47
+ resp.raise_for_status()
48
+ return ActionResult.from_dict(resp.json())
49
+
50
+ def get_position(self) -> CursorPosition:
51
+ resp = self._http.get(f"{self._prefix}/mouse/position")
52
+ resp.raise_for_status()
53
+ return CursorPosition.from_dict(resp.json())
54
+
55
+
56
+ class AsyncMouse:
57
+ def __init__(self, http: httpx.AsyncClient, prefix: str):
58
+ self._http = http
59
+ self._prefix = prefix
60
+
61
+ async def click(
62
+ self, x: int, y: int, button: str = "left", double: bool = False
63
+ ) -> ActionResult:
64
+ resp = await self._http.post(
65
+ f"{self._prefix}/mouse/click",
66
+ json={"x": x, "y": y, "button": button, "double": double},
67
+ )
68
+ resp.raise_for_status()
69
+ return ActionResult.from_dict(resp.json())
70
+
71
+ async def move(self, x: int, y: int) -> ActionResult:
72
+ resp = await self._http.post(f"{self._prefix}/mouse/move", json={"x": x, "y": y})
73
+ resp.raise_for_status()
74
+ return ActionResult.from_dict(resp.json())
75
+
76
+ async def drag(
77
+ self, start_x: int, start_y: int, end_x: int, end_y: int, button: str = "left"
78
+ ) -> ActionResult:
79
+ resp = await self._http.post(
80
+ f"{self._prefix}/mouse/drag",
81
+ json={
82
+ "startX": start_x,
83
+ "startY": start_y,
84
+ "endX": end_x,
85
+ "endY": end_y,
86
+ "button": button,
87
+ },
88
+ )
89
+ resp.raise_for_status()
90
+ return ActionResult.from_dict(resp.json())
91
+
92
+ async def scroll(
93
+ self, x: int, y: int, direction: str = "down", amount: int = 3
94
+ ) -> ActionResult:
95
+ resp = await self._http.post(
96
+ f"{self._prefix}/mouse/scroll",
97
+ json={"x": x, "y": y, "direction": direction, "amount": amount},
98
+ )
99
+ resp.raise_for_status()
100
+ return ActionResult.from_dict(resp.json())
101
+
102
+ async def get_position(self) -> CursorPosition:
103
+ resp = await self._http.get(f"{self._prefix}/mouse/position")
104
+ resp.raise_for_status()
105
+ return CursorPosition.from_dict(resp.json())
mmini/models.py ADDED
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ @dataclass
7
+ class ActionResult:
8
+ """Result from a mouse/keyboard/tap/swipe action."""
9
+
10
+ status: str = "ok"
11
+
12
+ @classmethod
13
+ def from_dict(cls, d: dict) -> ActionResult:
14
+ return cls(status=d.get("status", "ok"))
15
+
16
+
17
+ @dataclass
18
+ class CursorPosition:
19
+ x: int
20
+ y: int
21
+
22
+ @classmethod
23
+ def from_dict(cls, d: dict) -> CursorPosition:
24
+ return cls(x=d.get("x", 0), y=d.get("y", 0))
25
+
26
+
27
+ @dataclass
28
+ class DisplayInfo:
29
+ width: int
30
+ height: int
31
+ scale: float = 1.0
32
+
33
+ @classmethod
34
+ def from_dict(cls, d: dict) -> DisplayInfo:
35
+ return cls(
36
+ width=d.get("width", 0),
37
+ height=d.get("height", 0),
38
+ scale=d.get("scale", 1.0),
39
+ )
40
+
41
+
42
+ @dataclass
43
+ class RecordingInfo:
44
+ id: str
45
+ status: str = ""
46
+ name: str | None = None
47
+ filename: str = ""
48
+ file_size: int = 0
49
+
50
+ @classmethod
51
+ def from_dict(cls, d: dict) -> RecordingInfo:
52
+ return cls(
53
+ id=d.get("id") or d.get("recording_id", ""),
54
+ status=d.get("status", ""),
55
+ name=d.get("name"),
56
+ filename=d.get("filename", ""),
57
+ file_size=d.get("file_size", 0),
58
+ )
59
+
60
+
61
+ @dataclass
62
+ class ExecResult:
63
+ return_code: int
64
+ stdout: str
65
+
66
+ @classmethod
67
+ def from_dict(cls, d: dict) -> ExecResult:
68
+ return cls(
69
+ return_code=d.get("return_code", 0),
70
+ stdout=d.get("stdout", ""),
71
+ )
72
+
73
+
74
+ @dataclass
75
+ class ActResult:
76
+ """Result from a compound act() call."""
77
+
78
+ screenshot: bytes | None = None
79
+ data: dict = field(default_factory=dict)
mmini/parsers.py ADDED
@@ -0,0 +1,308 @@
1
+ """Parse pyautogui and xdotool commands into mmini Sandbox calls.
2
+
3
+ Models like Claude and GPT sometimes output raw pyautogui or xdotool commands.
4
+ These parsers translate them into Sandbox method calls.
5
+
6
+ Usage:
7
+ from mmini.parsers import parse_pyautogui, parse_xdotool
8
+
9
+ for action in parse_pyautogui("pyautogui.click(100, 200)"):
10
+ action.execute(sandbox)
11
+
12
+ for action in parse_xdotool("xdotool mousemove 100 200 click 1"):
13
+ action.execute(sandbox)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from dataclasses import dataclass, field
20
+ from typing import TYPE_CHECKING, Any
21
+
22
+ if TYPE_CHECKING:
23
+ from mmini.sandbox import Sandbox
24
+
25
+
26
+ @dataclass
27
+ class Action:
28
+ """A parsed action that can be executed on a Sandbox."""
29
+
30
+ method: str
31
+ args: list = field(default_factory=list)
32
+ kwargs: dict = field(default_factory=dict)
33
+
34
+ def execute(self, sandbox: Sandbox) -> dict | bytes | None:
35
+ parts = self.method.split(".")
36
+ obj: Any = sandbox
37
+ for part in parts:
38
+ obj = getattr(obj, part)
39
+ return obj(*self.args, **self.kwargs)
40
+
41
+ def __repr__(self) -> str:
42
+ arg_str = ", ".join(
43
+ [repr(a) for a in self.args] + [f"{k}={v!r}" for k, v in self.kwargs.items()]
44
+ )
45
+ return f"Action({self.method}({arg_str}))"
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # pyautogui parser
50
+ # ---------------------------------------------------------------------------
51
+
52
+ # Map pyautogui function names to (method, arg_parser)
53
+ _PYAUTOGUI_MAP = {
54
+ "click": "mouse.click",
55
+ "rightClick": "mouse.click",
56
+ "doubleClick": "mouse.click",
57
+ "moveTo": "mouse.move",
58
+ "moveRel": "mouse.move",
59
+ "drag": "mouse.drag",
60
+ "dragTo": "mouse.drag",
61
+ "scroll": "mouse.scroll",
62
+ "write": "keyboard.type",
63
+ "typewrite": "keyboard.type",
64
+ "press": "keyboard.press",
65
+ "hotkey": "keyboard.hotkey",
66
+ "screenshot": "screenshot.take_full_screen",
67
+ }
68
+
69
+ _PYAUTOGUI_RE = re.compile(r"pyautogui\.(\w+)\(([^)]*)\)")
70
+
71
+
72
+ def _parse_py_args(raw: str) -> tuple[list, dict]:
73
+ """Parse Python-style function arguments."""
74
+ args = []
75
+ kwargs = {}
76
+ if not raw.strip():
77
+ return args, kwargs
78
+
79
+ for part in _split_args(raw):
80
+ part = part.strip()
81
+ if "=" in part and not part.startswith(("'", '"')):
82
+ key, val = part.split("=", 1)
83
+ kwargs[key.strip()] = _coerce(val.strip())
84
+ else:
85
+ args.append(_coerce(part))
86
+ return args, kwargs
87
+
88
+
89
+ def _split_args(raw: str) -> list[str]:
90
+ """Split arguments respecting strings and nested parens."""
91
+ parts = []
92
+ depth = 0
93
+ current = ""
94
+ in_str = None
95
+ for c in raw:
96
+ if c in ("'", '"') and in_str is None:
97
+ in_str = c
98
+ elif c == in_str:
99
+ in_str = None
100
+ elif in_str is None:
101
+ if c == "(":
102
+ depth += 1
103
+ elif c == ")":
104
+ depth -= 1
105
+ elif c == "," and depth == 0:
106
+ parts.append(current)
107
+ current = ""
108
+ continue
109
+ current += c
110
+ if current.strip():
111
+ parts.append(current)
112
+ return parts
113
+
114
+
115
+ def _coerce(val: str):
116
+ """Coerce a string value to int, float, bool, or str."""
117
+ val = val.strip().strip("'\"")
118
+ if val.lower() == "true":
119
+ return True
120
+ if val.lower() == "false":
121
+ return False
122
+ try:
123
+ return int(val)
124
+ except ValueError:
125
+ pass
126
+ try:
127
+ return float(val)
128
+ except ValueError:
129
+ pass
130
+ return val
131
+
132
+
133
+ def _pyautogui_to_action(func: str, args: list, kwargs: dict) -> Action | None:
134
+ if func == "click":
135
+ x = args[0] if len(args) > 0 else kwargs.get("x", 0)
136
+ y = args[1] if len(args) > 1 else kwargs.get("y", 0)
137
+ return Action("mouse.click", [x, y])
138
+
139
+ if func == "rightClick":
140
+ x = args[0] if len(args) > 0 else kwargs.get("x", 0)
141
+ y = args[1] if len(args) > 1 else kwargs.get("y", 0)
142
+ return Action("mouse.click", [x, y, "right"])
143
+
144
+ if func == "doubleClick":
145
+ x = args[0] if len(args) > 0 else kwargs.get("x", 0)
146
+ y = args[1] if len(args) > 1 else kwargs.get("y", 0)
147
+ return Action("mouse.click", [x, y], {"double": True})
148
+
149
+ if func in ("moveTo", "moveRel"):
150
+ x = args[0] if len(args) > 0 else kwargs.get("x", 0)
151
+ y = args[1] if len(args) > 1 else kwargs.get("y", 0)
152
+ return Action("mouse.move", [x, y])
153
+
154
+ if func in ("drag", "dragTo"):
155
+ # pyautogui.drag(xOffset, yOffset) is relative
156
+ # We need current position for relative, but just map the args
157
+ x = args[0] if len(args) > 0 else kwargs.get("x", 0)
158
+ y = args[1] if len(args) > 1 else kwargs.get("y", 0)
159
+ return Action("mouse.drag", [0, 0, x, y])
160
+
161
+ if func == "scroll":
162
+ amount = args[0] if len(args) > 0 else kwargs.get("clicks", 3)
163
+ x = args[1] if len(args) > 1 else kwargs.get("x", 0)
164
+ y = args[2] if len(args) > 2 else kwargs.get("y", 0)
165
+ direction = "up" if amount > 0 else "down"
166
+ return Action("mouse.scroll", [x, y, direction, abs(amount)])
167
+
168
+ if func in ("write", "typewrite"):
169
+ text = args[0] if len(args) > 0 else kwargs.get("message", "")
170
+ kw = {}
171
+ interval = kwargs.get("interval")
172
+ if interval:
173
+ kw["delay"] = int(interval * 1000)
174
+ return Action("keyboard.type", [text], kw)
175
+
176
+ if func == "press":
177
+ key = args[0] if len(args) > 0 else kwargs.get("key", "")
178
+ return Action("keyboard.press", [key])
179
+
180
+ if func == "hotkey":
181
+ keys = "+".join(str(a) for a in args)
182
+ return Action("keyboard.hotkey", [keys])
183
+
184
+ if func == "screenshot":
185
+ return Action("screenshot.take_full_screen")
186
+
187
+ return None
188
+
189
+
190
+ def parse_pyautogui(text: str) -> list[Action]:
191
+ """Parse pyautogui commands from text into Actions."""
192
+ actions = []
193
+ for match in _PYAUTOGUI_RE.finditer(text):
194
+ func = match.group(1)
195
+ raw_args = match.group(2)
196
+ args, kwargs = _parse_py_args(raw_args)
197
+ action = _pyautogui_to_action(func, args, kwargs)
198
+ if action:
199
+ actions.append(action)
200
+ return actions
201
+
202
+
203
+ # ---------------------------------------------------------------------------
204
+ # xdotool parser
205
+ # ---------------------------------------------------------------------------
206
+
207
+ _XDOTOOL_KEY_MAP = {
208
+ "Return": "enter",
209
+ "Escape": "escape",
210
+ "BackSpace": "backspace",
211
+ "Tab": "tab",
212
+ "space": "space",
213
+ "Delete": "delete",
214
+ "Home": "home",
215
+ "End": "end",
216
+ "Page_Up": "pageup",
217
+ "Page_Down": "pagedown",
218
+ "Up": "up",
219
+ "Down": "down",
220
+ "Left": "left",
221
+ "Right": "right",
222
+ }
223
+
224
+ _XDOTOOL_BUTTON_MAP = {
225
+ "1": "left",
226
+ "2": "middle",
227
+ "3": "right",
228
+ }
229
+
230
+
231
+ def parse_xdotool(text: str) -> list[Action]:
232
+ """Parse xdotool commands from text into Actions.
233
+
234
+ Handles:
235
+ xdotool mousemove X Y
236
+ xdotool click BUTTON
237
+ xdotool key KEY [KEY...]
238
+ xdotool type TEXT
239
+ xdotool mousemove X Y click BUTTON (chained)
240
+ """
241
+ actions = []
242
+
243
+ for line in text.strip().splitlines():
244
+ line = line.strip()
245
+ if not line.startswith("xdotool"):
246
+ continue
247
+
248
+ tokens = line.split()[1:] # drop "xdotool"
249
+ actions.extend(_parse_xdotool_tokens(tokens))
250
+
251
+ return actions
252
+
253
+
254
+ def _parse_xdotool_tokens(tokens: list[str]) -> list[Action]:
255
+ """Parse xdotool token stream (supports chained commands)."""
256
+ actions = []
257
+ i = 0
258
+
259
+ while i < len(tokens):
260
+ cmd = tokens[i]
261
+ i += 1
262
+
263
+ if cmd == "mousemove" and i + 1 < len(tokens):
264
+ x, y = int(tokens[i]), int(tokens[i + 1])
265
+ i += 2
266
+ actions.append(Action("mouse.move", [x, y]))
267
+
268
+ elif cmd == "click" and i < len(tokens):
269
+ button = _XDOTOOL_BUTTON_MAP.get(tokens[i], "left")
270
+ i += 1
271
+ actions.append(Action("mouse.click", [0, 0, button]))
272
+
273
+ elif cmd == "mousedown" and i < len(tokens):
274
+ i += 1 # skip button, no direct mapping
275
+ pass
276
+
277
+ elif cmd == "mouseup" and i < len(tokens):
278
+ i += 1
279
+ pass
280
+
281
+ elif cmd == "key" and i < len(tokens):
282
+ key_combo = tokens[i]
283
+ i += 1
284
+ # xdotool uses "super+a", "ctrl+c", etc.
285
+ if "+" in key_combo:
286
+ combo = key_combo.replace("super", "command").replace("ctrl", "control")
287
+ actions.append(Action("keyboard.hotkey", [combo]))
288
+ else:
289
+ mapped = _XDOTOOL_KEY_MAP.get(key_combo, key_combo.lower())
290
+ actions.append(Action("keyboard.press", [mapped]))
291
+
292
+ elif cmd == "type" and i < len(tokens):
293
+ # Collect remaining tokens as the text
294
+ text = " ".join(tokens[i:])
295
+ # Strip surrounding quotes if present
296
+ if len(text) >= 2 and text[0] in ("'", '"') and text[-1] == text[0]:
297
+ text = text[1:-1]
298
+ actions.append(Action("keyboard.type", [text]))
299
+ break # type consumes everything
300
+
301
+ elif cmd == "--delay" and i < len(tokens):
302
+ i += 1 # skip delay value, not mapped
303
+ elif cmd == "--clearmodifiers":
304
+ pass # skip
305
+ elif cmd.startswith("--"):
306
+ i += 1 # skip flag + value
307
+
308
+ return actions
mmini/py.typed ADDED
@@ -0,0 +1 @@
1
+