-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcomputer.py
More file actions
155 lines (115 loc) · 4.99 KB
/
Copy pathcomputer.py
File metadata and controls
155 lines (115 loc) · 4.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"""Host-side executor for the Linux computer-use demo.
Drives the running desktop container (linuxserver Webtop / XFCE on DISPLAY :1)
through `docker exec xdotool ...`, and grabs screenshots via `xwd` inside the
container converted to PNG on the host with ImageMagick `convert`.
No agent code runs inside the container: the model loop lives on the host and
only pokes the desktop from outside. Keeps the demo trivial to swap/retarget.
"""
from __future__ import annotations
import base64
import os
import subprocess
import tempfile
CONTAINER = os.environ.get("CU_CONTAINER", "cu-live")
DISPLAY = os.environ.get("CU_DISPLAY", ":1")
def _x(*args) -> None:
subprocess.run(
["docker", "exec", "-e", f"DISPLAY={DISPLAY}", CONTAINER, "xdotool", *map(str, args)],
check=False, capture_output=True, text=True,
)
def click(x: int, y: int, button: int = 1) -> None:
_x("mousemove", x, y, "click", button)
def double_click(x: int, y: int) -> None:
_x("mousemove", x, y, "click", "--repeat", "2", "1")
def right_click(x: int, y: int) -> None:
_x("mousemove", x, y, "click", "3")
def drag(x1: int, y1: int, x2: int, y2: int) -> None:
"""Press at (x1,y1), drag to (x2,y2), release. For drawing, selecting, sliders."""
_x("mousemove", x1, y1, "mousedown", "1", "mousemove", x2, y2, "mouseup", "1")
def move(x: int, y: int) -> None:
_x("mousemove", x, y)
def type_text(text: str) -> None:
_x("type", "--delay", "40", text)
def key(combo: str) -> None:
_x("key", combo)
def scroll(amount: int) -> None:
button = "4" if amount > 0 else "5"
for _ in range(abs(amount)):
_x("click", button)
def launch(cmd: str) -> None:
"""Start a GUI app detached on the desktop, e.g. launch('blender')."""
subprocess.run(
["docker", "exec", "-d", CONTAINER, "sh", "-c", f"DISPLAY={DISPLAY} {cmd}"],
check=False,
)
_ATSPI_SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "atspi_dump.py")
_atspi_ready = False
def ui_tree(app_filter: str = "", timeout: int = 25) -> str:
"""Return the accessibility tree of on-screen apps: interactive elements with
their EXACT click coordinates. The efficient alternative to guessing from a
screenshot (Linux AT-SPI, like Playwright's a11y snapshot)."""
global _atspi_ready
if not _atspi_ready:
subprocess.run(["docker", "cp", _ATSPI_SRC, f"{CONTAINER}:/root/atspi_dump.py"],
check=False, capture_output=True)
_atspi_ready = True
try:
r = subprocess.run(
["docker", "exec", "-e", f"DISPLAY={DISPLAY}", CONTAINER,
"python3", "/root/atspi_dump.py", app_filter or ""],
capture_output=True, text=True, errors="replace", timeout=timeout,
)
out = ((r.stdout or "") or (r.stderr or "")).strip()
return out[:4000] if out else "(no interactive elements found)"
except subprocess.TimeoutExpired:
return "(ui tree timed out)"
def bash(cmd: str, timeout: int = 90) -> str:
"""Run a shell command INSIDE the VM and return its output (agent-driven)."""
try:
r = subprocess.run(
["docker", "exec", "-e", f"DISPLAY={DISPLAY}", CONTAINER, "bash", "-lc", cmd],
capture_output=True, text=True, errors="replace", timeout=timeout,
)
out = ((r.stdout or "") + (r.stderr or "")).strip()
return out[:2000] if out else f"(no output, exit {r.returncode})"
except subprocess.TimeoutExpired:
return f"(command still running after {timeout}s; use launch for GUI apps)"
def geometry() -> tuple[int, int]:
r = subprocess.run(
["docker", "exec", "-e", f"DISPLAY={DISPLAY}", CONTAINER, "xdotool", "getdisplaygeometry"],
capture_output=True, text=True, check=False,
)
try:
w, h = r.stdout.split()
return int(w), int(h)
except ValueError:
return 1024, 768
def screenshot_png() -> bytes:
subprocess.run(
["docker", "exec", CONTAINER, "sh", "-c", f"DISPLAY={DISPLAY} xwd -root -out /tmp/_cu.xwd"],
check=False, capture_output=True,
)
xwd = tempfile.NamedTemporaryFile(suffix=".xwd", delete=False).name
png = xwd[:-4] + ".png"
subprocess.run(["docker", "cp", f"{CONTAINER}:/tmp/_cu.xwd", xwd], check=False, capture_output=True)
subprocess.run(["convert", xwd, png], check=False, capture_output=True)
try:
with open(png, "rb") as fh:
return fh.read()
finally:
for p in (xwd, png):
try:
os.unlink(p)
except OSError:
pass
def screenshot_b64() -> str:
return base64.b64encode(screenshot_png()).decode()
def screenshot_file(path: str) -> str:
"""Grab a screenshot and save it to disk; return the path."""
with open(path, "wb") as f:
f.write(screenshot_png())
return path
def file_b64(path: str) -> str:
"""Read a saved screenshot back as base64 (loaded into context on demand)."""
with open(path, "rb") as f:
return base64.b64encode(f.read()).decode()