computeruseprotocol 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- computeruseprotocol-0.1.0.dist-info/METADATA +225 -0
- computeruseprotocol-0.1.0.dist-info/RECORD +27 -0
- computeruseprotocol-0.1.0.dist-info/WHEEL +4 -0
- computeruseprotocol-0.1.0.dist-info/entry_points.txt +3 -0
- computeruseprotocol-0.1.0.dist-info/licenses/LICENSE +21 -0
- cup/__init__.py +548 -0
- cup/__main__.py +222 -0
- cup/_base.py +123 -0
- cup/_router.py +63 -0
- cup/actions/__init__.py +9 -0
- cup/actions/_handler.py +62 -0
- cup/actions/_keys.py +56 -0
- cup/actions/_linux.py +1008 -0
- cup/actions/_macos.py +1090 -0
- cup/actions/_web.py +555 -0
- cup/actions/_windows.py +984 -0
- cup/actions/executor.py +162 -0
- cup/format.py +653 -0
- cup/mcp/__init__.py +1 -0
- cup/mcp/__main__.py +11 -0
- cup/mcp/server.py +418 -0
- cup/platforms/__init__.py +0 -0
- cup/platforms/linux.py +1060 -0
- cup/platforms/macos.py +1005 -0
- cup/platforms/web.py +1009 -0
- cup/platforms/windows.py +935 -0
- cup/search.py +583 -0
cup/actions/_web.py
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"""Web action handler — CDP-based action execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from cup.actions._handler import ActionHandler
|
|
10
|
+
from cup.actions._keys import parse_combo
|
|
11
|
+
from cup.actions.executor import ActionResult
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# CDP key mapping for Input.dispatchKeyEvent
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
_CDP_KEY_MAP: dict[str, dict[str, str]] = {
|
|
18
|
+
"enter": {"key": "Enter", "code": "Enter"},
|
|
19
|
+
"tab": {"key": "Tab", "code": "Tab"},
|
|
20
|
+
"escape": {"key": "Escape", "code": "Escape"},
|
|
21
|
+
"backspace": {"key": "Backspace", "code": "Backspace"},
|
|
22
|
+
"delete": {"key": "Delete", "code": "Delete"},
|
|
23
|
+
"space": {"key": " ", "code": "Space"},
|
|
24
|
+
"up": {"key": "ArrowUp", "code": "ArrowUp"},
|
|
25
|
+
"down": {"key": "ArrowDown", "code": "ArrowDown"},
|
|
26
|
+
"left": {"key": "ArrowLeft", "code": "ArrowLeft"},
|
|
27
|
+
"right": {"key": "ArrowRight", "code": "ArrowRight"},
|
|
28
|
+
"home": {"key": "Home", "code": "Home"},
|
|
29
|
+
"end": {"key": "End", "code": "End"},
|
|
30
|
+
"pageup": {"key": "PageUp", "code": "PageUp"},
|
|
31
|
+
"pagedown": {"key": "PageDown", "code": "PageDown"},
|
|
32
|
+
"f1": {"key": "F1", "code": "F1"},
|
|
33
|
+
"f2": {"key": "F2", "code": "F2"},
|
|
34
|
+
"f3": {"key": "F3", "code": "F3"},
|
|
35
|
+
"f4": {"key": "F4", "code": "F4"},
|
|
36
|
+
"f5": {"key": "F5", "code": "F5"},
|
|
37
|
+
"f6": {"key": "F6", "code": "F6"},
|
|
38
|
+
"f7": {"key": "F7", "code": "F7"},
|
|
39
|
+
"f8": {"key": "F8", "code": "F8"},
|
|
40
|
+
"f9": {"key": "F9", "code": "F9"},
|
|
41
|
+
"f10": {"key": "F10", "code": "F10"},
|
|
42
|
+
"f11": {"key": "F11", "code": "F11"},
|
|
43
|
+
"f12": {"key": "F12", "code": "F12"},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_CDP_MODIFIER_MAP: dict[str, dict[str, Any]] = {
|
|
47
|
+
"ctrl": {"key": "Control", "code": "ControlLeft", "bit": 2},
|
|
48
|
+
"alt": {"key": "Alt", "code": "AltLeft", "bit": 1},
|
|
49
|
+
"shift": {"key": "Shift", "code": "ShiftLeft", "bit": 8},
|
|
50
|
+
"meta": {"key": "Meta", "code": "MetaLeft", "bit": 4},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_click_point(box_model: dict) -> tuple[float, float]:
|
|
55
|
+
"""Compute the center point of a DOM.getBoxModel result.
|
|
56
|
+
|
|
57
|
+
The content quad is returned as [x1,y1, x2,y2, x3,y3, x4,y4].
|
|
58
|
+
We average all four corners to get the center.
|
|
59
|
+
"""
|
|
60
|
+
content = box_model.get("model", {}).get("content", [])
|
|
61
|
+
if len(content) >= 8:
|
|
62
|
+
xs = [content[i] for i in range(0, 8, 2)]
|
|
63
|
+
ys = [content[i] for i in range(1, 8, 2)]
|
|
64
|
+
return sum(xs) / 4, sum(ys) / 4
|
|
65
|
+
# Fallback: use border quad
|
|
66
|
+
border = box_model.get("model", {}).get("border", [])
|
|
67
|
+
if len(border) >= 8:
|
|
68
|
+
xs = [border[i] for i in range(0, 8, 2)]
|
|
69
|
+
ys = [border[i] for i in range(1, 8, 2)]
|
|
70
|
+
return sum(xs) / 4, sum(ys) / 4
|
|
71
|
+
raise RuntimeError("Cannot determine element position from box model")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# WebActionHandler
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class WebActionHandler(ActionHandler):
|
|
80
|
+
"""Execute CUP actions on web pages via Chrome DevTools Protocol.
|
|
81
|
+
|
|
82
|
+
Native refs are (ws_url, backend_dom_node_id) tuples stored by the
|
|
83
|
+
web adapter during tree capture.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, *, cdp_host: str | None = None):
|
|
87
|
+
self._host = cdp_host or os.environ.get("CUP_CDP_HOST", "127.0.0.1")
|
|
88
|
+
|
|
89
|
+
def action(
|
|
90
|
+
self,
|
|
91
|
+
native_ref: Any,
|
|
92
|
+
action: str,
|
|
93
|
+
params: dict[str, Any],
|
|
94
|
+
) -> ActionResult:
|
|
95
|
+
from cup.platforms.web import _cdp_close, _cdp_connect
|
|
96
|
+
|
|
97
|
+
ws_url, backend_node_id = native_ref
|
|
98
|
+
ws = _cdp_connect(ws_url, self._host)
|
|
99
|
+
try:
|
|
100
|
+
return self._dispatch(ws, backend_node_id, action, params)
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
return ActionResult(
|
|
103
|
+
success=False,
|
|
104
|
+
message="",
|
|
105
|
+
error=f"Web action '{action}' failed: {exc}",
|
|
106
|
+
)
|
|
107
|
+
finally:
|
|
108
|
+
_cdp_close(ws)
|
|
109
|
+
|
|
110
|
+
def press(self, combo: str) -> ActionResult:
|
|
111
|
+
"""Send a keyboard shortcut via CDP Input.dispatchKeyEvent.
|
|
112
|
+
|
|
113
|
+
This sends to the currently focused element in the most recently
|
|
114
|
+
used tab. We need a websocket URL — use the CDP target list.
|
|
115
|
+
"""
|
|
116
|
+
from cup.platforms.web import (
|
|
117
|
+
_cdp_close,
|
|
118
|
+
_cdp_connect,
|
|
119
|
+
_cdp_get_targets,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
port = int(os.environ.get("CUP_CDP_PORT", "9222"))
|
|
123
|
+
try:
|
|
124
|
+
targets = _cdp_get_targets(self._host, port)
|
|
125
|
+
except Exception as exc:
|
|
126
|
+
return ActionResult(
|
|
127
|
+
success=False,
|
|
128
|
+
message="",
|
|
129
|
+
error=f"Cannot connect to CDP for press: {exc}",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
page_targets = [t for t in targets if t.get("type") == "page"]
|
|
133
|
+
if not page_targets:
|
|
134
|
+
return ActionResult(
|
|
135
|
+
success=False,
|
|
136
|
+
message="",
|
|
137
|
+
error="No browser tabs found for press",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
ws_url = page_targets[0]["webSocketDebuggerUrl"]
|
|
141
|
+
ws = _cdp_connect(ws_url, self._host)
|
|
142
|
+
try:
|
|
143
|
+
self._send_key_combo(ws, combo)
|
|
144
|
+
return ActionResult(success=True, message=f"Pressed {combo}")
|
|
145
|
+
except Exception as exc:
|
|
146
|
+
return ActionResult(
|
|
147
|
+
success=False,
|
|
148
|
+
message="",
|
|
149
|
+
error=f"Failed to press keys: {exc}",
|
|
150
|
+
)
|
|
151
|
+
finally:
|
|
152
|
+
_cdp_close(ws)
|
|
153
|
+
|
|
154
|
+
# -- dispatch -----------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
def _dispatch(
|
|
157
|
+
self,
|
|
158
|
+
ws: Any,
|
|
159
|
+
backend_node_id: int,
|
|
160
|
+
action: str,
|
|
161
|
+
params: dict,
|
|
162
|
+
) -> ActionResult:
|
|
163
|
+
|
|
164
|
+
if action == "click":
|
|
165
|
+
return self._click(ws, backend_node_id)
|
|
166
|
+
elif action == "rightclick":
|
|
167
|
+
return self._mouse_click(ws, backend_node_id, button="right")
|
|
168
|
+
elif action == "doubleclick":
|
|
169
|
+
return self._mouse_click(ws, backend_node_id, button="left", click_count=2)
|
|
170
|
+
elif action == "type":
|
|
171
|
+
text = params.get("value", "")
|
|
172
|
+
return self._type(ws, backend_node_id, text)
|
|
173
|
+
elif action == "setvalue":
|
|
174
|
+
text = params.get("value", "")
|
|
175
|
+
return self._setvalue(ws, backend_node_id, text)
|
|
176
|
+
elif action == "toggle":
|
|
177
|
+
return self._toggle(ws, backend_node_id)
|
|
178
|
+
elif action in ("expand", "collapse"):
|
|
179
|
+
return self._click(ws, backend_node_id)
|
|
180
|
+
elif action == "select":
|
|
181
|
+
return self._select(ws, backend_node_id)
|
|
182
|
+
elif action == "scroll":
|
|
183
|
+
direction = params.get("direction", "down")
|
|
184
|
+
return self._scroll(ws, backend_node_id, direction)
|
|
185
|
+
elif action == "focus":
|
|
186
|
+
return self._focus(ws, backend_node_id)
|
|
187
|
+
elif action == "dismiss":
|
|
188
|
+
return self._dismiss(ws)
|
|
189
|
+
elif action == "increment":
|
|
190
|
+
return self._arrow_key(ws, backend_node_id, "ArrowUp")
|
|
191
|
+
elif action == "decrement":
|
|
192
|
+
return self._arrow_key(ws, backend_node_id, "ArrowDown")
|
|
193
|
+
else:
|
|
194
|
+
return ActionResult(
|
|
195
|
+
success=False,
|
|
196
|
+
message="",
|
|
197
|
+
error=f"Action '{action}' not implemented for web",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# -- individual actions -------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def _click(self, ws: Any, backend_node_id: int) -> ActionResult:
|
|
203
|
+
return self._mouse_click(ws, backend_node_id, button="left", click_count=1)
|
|
204
|
+
|
|
205
|
+
def _mouse_click(
|
|
206
|
+
self,
|
|
207
|
+
ws: Any,
|
|
208
|
+
backend_node_id: int,
|
|
209
|
+
*,
|
|
210
|
+
button: str = "left",
|
|
211
|
+
click_count: int = 1,
|
|
212
|
+
) -> ActionResult:
|
|
213
|
+
from cup.platforms.web import _cdp_send
|
|
214
|
+
|
|
215
|
+
resp = _cdp_send(
|
|
216
|
+
ws,
|
|
217
|
+
"DOM.getBoxModel",
|
|
218
|
+
{
|
|
219
|
+
"backendNodeId": backend_node_id,
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
x, y = _get_click_point(resp.get("result", {}))
|
|
223
|
+
|
|
224
|
+
for i in range(click_count):
|
|
225
|
+
_cdp_send(
|
|
226
|
+
ws,
|
|
227
|
+
"Input.dispatchMouseEvent",
|
|
228
|
+
{
|
|
229
|
+
"type": "mousePressed",
|
|
230
|
+
"x": x,
|
|
231
|
+
"y": y,
|
|
232
|
+
"button": button,
|
|
233
|
+
"clickCount": i + 1,
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
_cdp_send(
|
|
237
|
+
ws,
|
|
238
|
+
"Input.dispatchMouseEvent",
|
|
239
|
+
{
|
|
240
|
+
"type": "mouseReleased",
|
|
241
|
+
"x": x,
|
|
242
|
+
"y": y,
|
|
243
|
+
"button": button,
|
|
244
|
+
"clickCount": i + 1,
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
action_name = {
|
|
249
|
+
("left", 1): "Clicked",
|
|
250
|
+
("left", 2): "Double-clicked",
|
|
251
|
+
("right", 1): "Right-clicked",
|
|
252
|
+
}.get((button, click_count), f"Mouse {button} x{click_count}")
|
|
253
|
+
return ActionResult(success=True, message=action_name)
|
|
254
|
+
|
|
255
|
+
def _type(self, ws: Any, backend_node_id: int, text: str) -> ActionResult:
|
|
256
|
+
from cup.platforms.web import _cdp_send
|
|
257
|
+
|
|
258
|
+
# Focus the element first
|
|
259
|
+
_cdp_send(ws, "DOM.focus", {"backendNodeId": backend_node_id})
|
|
260
|
+
time.sleep(0.05)
|
|
261
|
+
|
|
262
|
+
# Select all existing content, then type new text
|
|
263
|
+
self._send_key_combo(ws, "ctrl+a")
|
|
264
|
+
time.sleep(0.05)
|
|
265
|
+
|
|
266
|
+
# Use insertText for reliable text input
|
|
267
|
+
_cdp_send(ws, "Input.insertText", {"text": text})
|
|
268
|
+
|
|
269
|
+
return ActionResult(success=True, message=f"Typed: {text}")
|
|
270
|
+
|
|
271
|
+
def _setvalue(self, ws: Any, backend_node_id: int, text: str) -> ActionResult:
|
|
272
|
+
from cup.platforms.web import _cdp_send
|
|
273
|
+
|
|
274
|
+
# Resolve the backend node to a Runtime object
|
|
275
|
+
resp = _cdp_send(
|
|
276
|
+
ws,
|
|
277
|
+
"DOM.resolveNode",
|
|
278
|
+
{
|
|
279
|
+
"backendNodeId": backend_node_id,
|
|
280
|
+
},
|
|
281
|
+
)
|
|
282
|
+
object_id = resp.get("result", {}).get("object", {}).get("objectId")
|
|
283
|
+
if not object_id:
|
|
284
|
+
return ActionResult(
|
|
285
|
+
success=False,
|
|
286
|
+
message="",
|
|
287
|
+
error="Cannot resolve DOM node for setvalue",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Set value and dispatch input/change events
|
|
291
|
+
_cdp_send(
|
|
292
|
+
ws,
|
|
293
|
+
"Runtime.callFunctionOn",
|
|
294
|
+
{
|
|
295
|
+
"objectId": object_id,
|
|
296
|
+
"functionDeclaration": """function(v) {
|
|
297
|
+
this.value = v;
|
|
298
|
+
this.dispatchEvent(new Event('input', {bubbles: true}));
|
|
299
|
+
this.dispatchEvent(new Event('change', {bubbles: true}));
|
|
300
|
+
}""",
|
|
301
|
+
"arguments": [{"value": text}],
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
return ActionResult(success=True, message=f"Set value to: {text}")
|
|
305
|
+
|
|
306
|
+
def _scroll(
|
|
307
|
+
self,
|
|
308
|
+
ws: Any,
|
|
309
|
+
backend_node_id: int,
|
|
310
|
+
direction: str,
|
|
311
|
+
) -> ActionResult:
|
|
312
|
+
from cup.platforms.web import _cdp_send
|
|
313
|
+
|
|
314
|
+
resp = _cdp_send(
|
|
315
|
+
ws,
|
|
316
|
+
"DOM.getBoxModel",
|
|
317
|
+
{
|
|
318
|
+
"backendNodeId": backend_node_id,
|
|
319
|
+
},
|
|
320
|
+
)
|
|
321
|
+
x, y = _get_click_point(resp.get("result", {}))
|
|
322
|
+
|
|
323
|
+
delta_x, delta_y = 0, 0
|
|
324
|
+
if direction == "up":
|
|
325
|
+
delta_y = -200
|
|
326
|
+
elif direction == "down":
|
|
327
|
+
delta_y = 200
|
|
328
|
+
elif direction == "left":
|
|
329
|
+
delta_x = -200
|
|
330
|
+
elif direction == "right":
|
|
331
|
+
delta_x = 200
|
|
332
|
+
|
|
333
|
+
_cdp_send(
|
|
334
|
+
ws,
|
|
335
|
+
"Input.dispatchMouseEvent",
|
|
336
|
+
{
|
|
337
|
+
"type": "mouseWheel",
|
|
338
|
+
"x": x,
|
|
339
|
+
"y": y,
|
|
340
|
+
"deltaX": delta_x,
|
|
341
|
+
"deltaY": delta_y,
|
|
342
|
+
},
|
|
343
|
+
)
|
|
344
|
+
return ActionResult(success=True, message=f"Scrolled {direction}")
|
|
345
|
+
|
|
346
|
+
def _focus(self, ws: Any, backend_node_id: int) -> ActionResult:
|
|
347
|
+
from cup.platforms.web import _cdp_send
|
|
348
|
+
|
|
349
|
+
_cdp_send(ws, "DOM.focus", {"backendNodeId": backend_node_id})
|
|
350
|
+
return ActionResult(success=True, message="Focused")
|
|
351
|
+
|
|
352
|
+
def _toggle(self, ws: Any, backend_node_id: int) -> ActionResult:
|
|
353
|
+
from cup.platforms.web import _cdp_send
|
|
354
|
+
|
|
355
|
+
# Use JS .click() for reliable toggling of checkboxes/switches
|
|
356
|
+
resp = _cdp_send(
|
|
357
|
+
ws,
|
|
358
|
+
"DOM.resolveNode",
|
|
359
|
+
{
|
|
360
|
+
"backendNodeId": backend_node_id,
|
|
361
|
+
},
|
|
362
|
+
)
|
|
363
|
+
object_id = resp.get("result", {}).get("object", {}).get("objectId")
|
|
364
|
+
if not object_id:
|
|
365
|
+
# Fallback to coordinate click
|
|
366
|
+
return self._click(ws, backend_node_id)
|
|
367
|
+
|
|
368
|
+
_cdp_send(
|
|
369
|
+
ws,
|
|
370
|
+
"Runtime.callFunctionOn",
|
|
371
|
+
{
|
|
372
|
+
"objectId": object_id,
|
|
373
|
+
"functionDeclaration": "function() { this.click(); }",
|
|
374
|
+
},
|
|
375
|
+
)
|
|
376
|
+
return ActionResult(success=True, message="Toggled")
|
|
377
|
+
|
|
378
|
+
def _select(self, ws: Any, backend_node_id: int) -> ActionResult:
|
|
379
|
+
from cup.platforms.web import _cdp_send
|
|
380
|
+
|
|
381
|
+
# Handle <option> elements by setting selected on the option
|
|
382
|
+
# and dispatching change on the parent <select>
|
|
383
|
+
resp = _cdp_send(
|
|
384
|
+
ws,
|
|
385
|
+
"DOM.resolveNode",
|
|
386
|
+
{
|
|
387
|
+
"backendNodeId": backend_node_id,
|
|
388
|
+
},
|
|
389
|
+
)
|
|
390
|
+
object_id = resp.get("result", {}).get("object", {}).get("objectId")
|
|
391
|
+
if not object_id:
|
|
392
|
+
return self._click(ws, backend_node_id)
|
|
393
|
+
|
|
394
|
+
_cdp_send(
|
|
395
|
+
ws,
|
|
396
|
+
"Runtime.callFunctionOn",
|
|
397
|
+
{
|
|
398
|
+
"objectId": object_id,
|
|
399
|
+
"functionDeclaration": """function() {
|
|
400
|
+
if (this.tagName === 'OPTION') {
|
|
401
|
+
this.selected = true;
|
|
402
|
+
if (this.parentElement) {
|
|
403
|
+
this.parentElement.dispatchEvent(
|
|
404
|
+
new Event('change', {bubbles: true})
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
this.click();
|
|
409
|
+
}
|
|
410
|
+
}""",
|
|
411
|
+
},
|
|
412
|
+
)
|
|
413
|
+
return ActionResult(success=True, message="Selected")
|
|
414
|
+
|
|
415
|
+
def _dismiss(self, ws: Any) -> ActionResult:
|
|
416
|
+
from cup.platforms.web import _cdp_send
|
|
417
|
+
|
|
418
|
+
_cdp_send(
|
|
419
|
+
ws,
|
|
420
|
+
"Input.dispatchKeyEvent",
|
|
421
|
+
{
|
|
422
|
+
"type": "keyDown",
|
|
423
|
+
"key": "Escape",
|
|
424
|
+
"code": "Escape",
|
|
425
|
+
},
|
|
426
|
+
)
|
|
427
|
+
_cdp_send(
|
|
428
|
+
ws,
|
|
429
|
+
"Input.dispatchKeyEvent",
|
|
430
|
+
{
|
|
431
|
+
"type": "keyUp",
|
|
432
|
+
"key": "Escape",
|
|
433
|
+
"code": "Escape",
|
|
434
|
+
},
|
|
435
|
+
)
|
|
436
|
+
return ActionResult(success=True, message="Dismissed (Escape)")
|
|
437
|
+
|
|
438
|
+
def _arrow_key(
|
|
439
|
+
self,
|
|
440
|
+
ws: Any,
|
|
441
|
+
backend_node_id: int,
|
|
442
|
+
key: str,
|
|
443
|
+
) -> ActionResult:
|
|
444
|
+
from cup.platforms.web import _cdp_send
|
|
445
|
+
|
|
446
|
+
_cdp_send(ws, "DOM.focus", {"backendNodeId": backend_node_id})
|
|
447
|
+
time.sleep(0.05)
|
|
448
|
+
code = key # ArrowUp, ArrowDown are both key and code
|
|
449
|
+
_cdp_send(
|
|
450
|
+
ws,
|
|
451
|
+
"Input.dispatchKeyEvent",
|
|
452
|
+
{
|
|
453
|
+
"type": "keyDown",
|
|
454
|
+
"key": key,
|
|
455
|
+
"code": code,
|
|
456
|
+
},
|
|
457
|
+
)
|
|
458
|
+
_cdp_send(
|
|
459
|
+
ws,
|
|
460
|
+
"Input.dispatchKeyEvent",
|
|
461
|
+
{
|
|
462
|
+
"type": "keyUp",
|
|
463
|
+
"key": key,
|
|
464
|
+
"code": code,
|
|
465
|
+
},
|
|
466
|
+
)
|
|
467
|
+
verb = "Incremented" if key == "ArrowUp" else "Decremented"
|
|
468
|
+
return ActionResult(success=True, message=verb)
|
|
469
|
+
|
|
470
|
+
# -- keyboard helpers ---------------------------------------------------
|
|
471
|
+
|
|
472
|
+
def _send_key_combo(self, ws: Any, combo: str) -> None:
|
|
473
|
+
"""Parse a key combo and send via CDP Input.dispatchKeyEvent."""
|
|
474
|
+
from cup.platforms.web import _cdp_send
|
|
475
|
+
|
|
476
|
+
modifiers, keys = parse_combo(combo)
|
|
477
|
+
|
|
478
|
+
# Calculate modifier bitmask
|
|
479
|
+
mod_bits = 0
|
|
480
|
+
for mod in modifiers:
|
|
481
|
+
info = _CDP_MODIFIER_MAP.get(mod)
|
|
482
|
+
if info:
|
|
483
|
+
mod_bits |= info["bit"]
|
|
484
|
+
|
|
485
|
+
# Press modifiers down
|
|
486
|
+
for mod in modifiers:
|
|
487
|
+
info = _CDP_MODIFIER_MAP.get(mod)
|
|
488
|
+
if info:
|
|
489
|
+
_cdp_send(
|
|
490
|
+
ws,
|
|
491
|
+
"Input.dispatchKeyEvent",
|
|
492
|
+
{
|
|
493
|
+
"type": "keyDown",
|
|
494
|
+
"key": info["key"],
|
|
495
|
+
"code": info["code"],
|
|
496
|
+
"modifiers": mod_bits,
|
|
497
|
+
},
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# Press and release main keys
|
|
501
|
+
for key in keys:
|
|
502
|
+
mapped = _CDP_KEY_MAP.get(key)
|
|
503
|
+
if mapped:
|
|
504
|
+
cdp_key = mapped["key"]
|
|
505
|
+
cdp_code = mapped["code"]
|
|
506
|
+
text = ""
|
|
507
|
+
elif len(key) == 1:
|
|
508
|
+
cdp_key = key
|
|
509
|
+
cdp_code = f"Key{key.upper()}" if key.isalpha() else ""
|
|
510
|
+
text = key
|
|
511
|
+
else:
|
|
512
|
+
continue
|
|
513
|
+
|
|
514
|
+
params: dict[str, Any] = {
|
|
515
|
+
"type": "keyDown",
|
|
516
|
+
"key": cdp_key,
|
|
517
|
+
"code": cdp_code,
|
|
518
|
+
"modifiers": mod_bits,
|
|
519
|
+
}
|
|
520
|
+
if text and not mod_bits:
|
|
521
|
+
params["text"] = text
|
|
522
|
+
_cdp_send(ws, "Input.dispatchKeyEvent", params)
|
|
523
|
+
|
|
524
|
+
_cdp_send(
|
|
525
|
+
ws,
|
|
526
|
+
"Input.dispatchKeyEvent",
|
|
527
|
+
{
|
|
528
|
+
"type": "keyUp",
|
|
529
|
+
"key": cdp_key,
|
|
530
|
+
"code": cdp_code,
|
|
531
|
+
"modifiers": mod_bits,
|
|
532
|
+
},
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# Release modifiers
|
|
536
|
+
for mod in reversed(modifiers):
|
|
537
|
+
info = _CDP_MODIFIER_MAP.get(mod)
|
|
538
|
+
if info:
|
|
539
|
+
_cdp_send(
|
|
540
|
+
ws,
|
|
541
|
+
"Input.dispatchKeyEvent",
|
|
542
|
+
{
|
|
543
|
+
"type": "keyUp",
|
|
544
|
+
"key": info["key"],
|
|
545
|
+
"code": info["code"],
|
|
546
|
+
"modifiers": 0,
|
|
547
|
+
},
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
def open_app(self, name: str) -> ActionResult:
|
|
551
|
+
return ActionResult(
|
|
552
|
+
success=False,
|
|
553
|
+
message="",
|
|
554
|
+
error="open_app is not applicable for web platform",
|
|
555
|
+
)
|