boxlite 0.2.0.dev0__cp310-cp310-macosx_14_0_arm64.whl → 0.5.6__cp310-cp310-macosx_14_0_arm64.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.
- boxlite/__init__.py +54 -23
- boxlite/boxlite.cpython-310-darwin.so +0 -0
- boxlite/browserbox.py +10 -3
- boxlite/codebox.py +7 -7
- boxlite/computerbox.py +129 -336
- boxlite/constants.py +25 -0
- boxlite/errors.py +8 -2
- boxlite/exec.py +2 -1
- boxlite/interactivebox.py +50 -56
- boxlite/runtime/boxlite-guest +0 -0
- boxlite/runtime/boxlite-shim +0 -0
- boxlite/runtime/debugfs +0 -0
- boxlite/runtime/libgvproxy.dylib +0 -0
- boxlite/runtime/libkrun.1.16.0.dylib +0 -0
- boxlite/runtime/{libkrunfw.4.dylib → libkrunfw.5.dylib} +0 -0
- boxlite/runtime/mke2fs +0 -0
- boxlite/simplebox.py +81 -34
- boxlite/sync_api/__init__.py +65 -0
- boxlite/sync_api/_box.py +133 -0
- boxlite/sync_api/_boxlite.py +377 -0
- boxlite/sync_api/_codebox.py +145 -0
- boxlite/sync_api/_execution.py +203 -0
- boxlite/sync_api/_simplebox.py +180 -0
- boxlite/sync_api/_sync_base.py +137 -0
- boxlite-0.5.6.dist-info/METADATA +845 -0
- boxlite-0.5.6.dist-info/RECORD +27 -0
- {boxlite-0.2.0.dev0.dist-info → boxlite-0.5.6.dist-info}/WHEEL +1 -1
- boxlite-0.2.0.dev0.dist-info/METADATA +0 -9
- boxlite-0.2.0.dev0.dist-info/RECORD +0 -17
boxlite/computerbox.py
CHANGED
|
@@ -6,10 +6,10 @@ that can be viewed from a browser, with full GUI automation support.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
|
-
import base64
|
|
10
9
|
import logging
|
|
11
10
|
from typing import Optional, Tuple, TYPE_CHECKING
|
|
12
11
|
|
|
12
|
+
from . import constants as const
|
|
13
13
|
from .errors import ExecError, TimeoutError, ParseError
|
|
14
14
|
from .simplebox import SimpleBox
|
|
15
15
|
|
|
@@ -18,7 +18,6 @@ if TYPE_CHECKING:
|
|
|
18
18
|
|
|
19
19
|
__all__ = ["ComputerBox"]
|
|
20
20
|
|
|
21
|
-
# Configure logger
|
|
22
21
|
logger = logging.getLogger("boxlite.computerbox")
|
|
23
22
|
|
|
24
23
|
|
|
@@ -34,476 +33,270 @@ class ComputerBox(SimpleBox):
|
|
|
34
33
|
|
|
35
34
|
Usage:
|
|
36
35
|
>>> async with ComputerBox() as desktop:
|
|
37
|
-
...
|
|
38
|
-
...
|
|
39
|
-
... await asyncio.sleep(300) # Keep running for 5 minutes
|
|
36
|
+
... await desktop.wait_until_ready()
|
|
37
|
+
... screenshot = await desktop.screenshot()
|
|
40
38
|
|
|
41
39
|
Example with custom settings:
|
|
42
|
-
>>> async with ComputerBox(memory=4096, cpu=4
|
|
43
|
-
...
|
|
40
|
+
>>> async with ComputerBox(memory=4096, cpu=4) as desktop:
|
|
41
|
+
... await desktop.mouse_move(100, 200)
|
|
42
|
+
... await desktop.left_click()
|
|
44
43
|
"""
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
_DEFAULT_DISPLAY_WIDTH_PX = 1024
|
|
56
|
-
_DEFAULT_DISPLAY_HEIGHT_PX = 768
|
|
57
|
-
|
|
58
|
-
def __init__(self, cpu: int = 2, memory: int = 2048, monitor_http_port: int = 3000,
|
|
59
|
-
monitor_https_port: int = 3001, runtime: Optional['Boxlite'] = None):
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
cpu: int = const.COMPUTERBOX_CPUS,
|
|
48
|
+
memory: int = const.COMPUTERBOX_MEMORY_MIB,
|
|
49
|
+
gui_http_port: int = const.COMPUTERBOX_GUI_HTTP_PORT,
|
|
50
|
+
gui_https_port: int = const.COMPUTERBOX_GUI_HTTPS_PORT,
|
|
51
|
+
runtime: Optional["Boxlite"] = None,
|
|
52
|
+
**kwargs,
|
|
53
|
+
):
|
|
60
54
|
"""
|
|
61
55
|
Create and auto-start a desktop environment.
|
|
62
56
|
|
|
63
57
|
Args:
|
|
64
|
-
memory: Memory in MiB (default: 2048)
|
|
65
58
|
cpu: Number of CPU cores (default: 2)
|
|
66
|
-
|
|
59
|
+
memory: Memory in MiB (default: 2048)
|
|
60
|
+
gui_http_port: Port for HTTP desktop GUI (default: 3000)
|
|
61
|
+
gui_https_port: Port for HTTPS desktop GUI (default: 3001)
|
|
67
62
|
runtime: Optional runtime instance (uses global default if None)
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
**kwargs: Additional configuration options (volumes, etc.)
|
|
64
|
+
"""
|
|
65
|
+
user_env = kwargs.pop("env", [])
|
|
66
|
+
default_env = [
|
|
67
|
+
("DISPLAY", const.COMPUTERBOX_DISPLAY_NUMBER),
|
|
68
|
+
("DISPLAY_SIZEW", str(const.COMPUTERBOX_DISPLAY_WIDTH)),
|
|
69
|
+
("DISPLAY_SIZEH", str(const.COMPUTERBOX_DISPLAY_HEIGHT)),
|
|
70
|
+
("SELKIES_MANUAL_WIDTH", str(const.COMPUTERBOX_DISPLAY_WIDTH)),
|
|
71
|
+
("SELKIES_MANUAL_HEIGHT", str(const.COMPUTERBOX_DISPLAY_HEIGHT)),
|
|
72
|
+
("SELKIES_UI_SHOW_SIDEBAR", "false"),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
user_ports = kwargs.pop("ports", [])
|
|
76
|
+
default_ports = [
|
|
77
|
+
(gui_http_port, const.COMPUTERBOX_GUI_HTTP_PORT),
|
|
78
|
+
(gui_https_port, const.COMPUTERBOX_GUI_HTTPS_PORT),
|
|
79
|
+
]
|
|
70
80
|
|
|
71
|
-
# Initialize base box with environment variables and port mapping
|
|
72
|
-
# Set both Xvfb initial resolution AND Selkies resolution for consistency
|
|
73
81
|
super().__init__(
|
|
74
|
-
image=
|
|
82
|
+
image=const.COMPUTERBOX_IMAGE,
|
|
75
83
|
memory_mib=memory,
|
|
76
84
|
cpus=cpu,
|
|
77
85
|
runtime=runtime,
|
|
78
|
-
env=
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
("DISPLAY_SIZEW", str(self._DEFAULT_DISPLAY_WIDTH_PX)),
|
|
82
|
-
("DISPLAY_SIZEH", str(self._DEFAULT_DISPLAY_HEIGHT_PX)),
|
|
83
|
-
# Selkies manual resolution (forces browser resolution)
|
|
84
|
-
("SELKIES_MANUAL_WIDTH", str(self._DEFAULT_DISPLAY_WIDTH_PX)),
|
|
85
|
-
("SELKIES_MANUAL_HEIGHT", str(self._DEFAULT_DISPLAY_HEIGHT_PX)),
|
|
86
|
-
("SELKIES_UI_SHOW_SIDEBAR", "false"), # Hide sidebar for cleaner UI
|
|
87
|
-
],
|
|
88
|
-
ports=[(monitor_http_port, self._GUEST_MONITOR_HTTP_PORT),
|
|
89
|
-
(monitor_https_port, self._GUEST_MONITOR_HTTPS_PORT)]
|
|
86
|
+
env=default_env + list(user_env),
|
|
87
|
+
ports=default_ports + list(user_ports),
|
|
88
|
+
**kwargs,
|
|
90
89
|
)
|
|
91
90
|
|
|
92
|
-
def
|
|
93
|
-
"""
|
|
94
|
-
Get the web interface endpoint.
|
|
95
|
-
|
|
96
|
-
Returns:
|
|
97
|
-
HTTPS endpoint URL to access the desktop in your browser.
|
|
98
|
-
Note: Uses self-signed certificate - browser will show security warning.
|
|
99
|
-
|
|
100
|
-
Example:
|
|
101
|
-
>>> async with ComputerBox() as desktop:
|
|
102
|
-
... url = desktop.endpoint()
|
|
103
|
-
... print(f"Open this URL: {url}")
|
|
104
|
-
... # Navigate to the URL in your browser
|
|
105
|
-
... # Accept the self-signed certificate warning
|
|
106
|
-
"""
|
|
107
|
-
return f"https://localhost:{self._monitor_port}"
|
|
108
|
-
|
|
109
|
-
async def wait_until_ready(self, timeout: int = 60):
|
|
91
|
+
async def wait_until_ready(self, timeout: int = const.DESKTOP_READY_TIMEOUT):
|
|
110
92
|
"""
|
|
111
93
|
Wait until the desktop environment is fully loaded and ready.
|
|
112
94
|
|
|
113
|
-
Waits for xfdesktop to render the desktop, which ensures screenshots won't be black.
|
|
114
|
-
|
|
115
95
|
Args:
|
|
116
96
|
timeout: Maximum time to wait in seconds (default: 60)
|
|
117
97
|
|
|
118
98
|
Raises:
|
|
119
99
|
TimeoutError: If desktop doesn't become ready within timeout period
|
|
120
|
-
|
|
121
|
-
Example:
|
|
122
|
-
>>> async with ComputerBox() as desktop:
|
|
123
|
-
... await desktop.wait_until_ready()
|
|
124
|
-
... # Desktop is now ready for automation and screenshots
|
|
125
100
|
"""
|
|
126
101
|
logger.info("Waiting for desktop to become ready...")
|
|
127
102
|
import time
|
|
103
|
+
|
|
128
104
|
start_time = time.time()
|
|
129
|
-
retry_delay = 0.5
|
|
130
105
|
|
|
131
106
|
while True:
|
|
132
107
|
elapsed = time.time() - start_time
|
|
133
108
|
if elapsed > timeout:
|
|
134
|
-
raise TimeoutError(
|
|
109
|
+
raise TimeoutError(
|
|
110
|
+
f"Desktop did not become ready within {timeout} seconds"
|
|
111
|
+
)
|
|
135
112
|
|
|
136
113
|
try:
|
|
137
|
-
# Check if xfdesktop window exists at correct resolution
|
|
138
114
|
exec_result = await self.exec("xwininfo", "-tree", "-root")
|
|
139
|
-
|
|
140
|
-
expected_size = f'{self._DEFAULT_DISPLAY_WIDTH_PX}x{self._DEFAULT_DISPLAY_HEIGHT_PX}'
|
|
141
|
-
|
|
142
|
-
logger.debug(f"stdout {result}")
|
|
115
|
+
expected_size = f"{const.COMPUTERBOX_DISPLAY_WIDTH}x{const.COMPUTERBOX_DISPLAY_HEIGHT}"
|
|
143
116
|
|
|
144
|
-
if
|
|
117
|
+
if (
|
|
118
|
+
"xfdesktop" in exec_result.stdout
|
|
119
|
+
and expected_size in exec_result.stdout
|
|
120
|
+
):
|
|
145
121
|
logger.info(f"Desktop ready after {elapsed:.1f} seconds")
|
|
146
122
|
return
|
|
147
123
|
|
|
148
|
-
logger.debug(
|
|
149
|
-
|
|
124
|
+
logger.debug(
|
|
125
|
+
f"Desktop not ready yet (waited {elapsed:.1f}s), retrying..."
|
|
126
|
+
)
|
|
127
|
+
await asyncio.sleep(const.DESKTOP_READY_RETRY_DELAY)
|
|
150
128
|
|
|
151
|
-
except
|
|
129
|
+
except (ExecError, ConnectionError, OSError, asyncio.TimeoutError) as e:
|
|
152
130
|
logger.debug(f"Desktop not ready: {e}, retrying...")
|
|
153
|
-
await asyncio.sleep(
|
|
154
|
-
|
|
155
|
-
|
|
131
|
+
await asyncio.sleep(const.DESKTOP_READY_RETRY_DELAY)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.error(f"Fatal error in wait_until_ready: {e}")
|
|
134
|
+
raise
|
|
156
135
|
|
|
157
136
|
async def screenshot(self) -> dict:
|
|
158
137
|
"""
|
|
159
|
-
Capture a screenshot of the desktop
|
|
160
|
-
|
|
161
|
-
Note: Screenshots may be black if taken before the XFCE desktop has fully
|
|
162
|
-
initialized. Use wait_until_ready() before taking screenshots to ensure
|
|
163
|
-
the desktop has been rendered.
|
|
138
|
+
Capture a screenshot of the desktop.
|
|
164
139
|
|
|
165
140
|
Returns:
|
|
166
|
-
Dictionary
|
|
167
|
-
- data: Base64-encoded PNG images data
|
|
168
|
-
- width: Display width in pixels (1024)
|
|
169
|
-
- height: Display height in pixels (768)
|
|
170
|
-
- format: Image format ("png")
|
|
171
|
-
|
|
172
|
-
Example:
|
|
173
|
-
>>> async with ComputerBox() as desktop:
|
|
174
|
-
... await desktop.wait_until_ready() # Ensure desktop is rendered
|
|
175
|
-
... result = await desktop.screenshot()
|
|
176
|
-
... image_data = base64.b64decode(result['data'])
|
|
177
|
-
... with open('screenshot.png', 'wb') as f:
|
|
178
|
-
... f.write(image_data)
|
|
141
|
+
Dictionary with: data (base64 PNG), width, height, format
|
|
179
142
|
"""
|
|
180
143
|
logger.info("Taking screenshot...")
|
|
181
144
|
|
|
182
|
-
|
|
183
|
-
# This avoids needing to install scrot and is faster
|
|
184
|
-
logger.debug("Capturing screenshot with PIL.ImageGrab...")
|
|
185
|
-
python_code = '''
|
|
145
|
+
python_code = """
|
|
186
146
|
from PIL import ImageGrab
|
|
187
147
|
import io
|
|
188
148
|
import base64
|
|
189
|
-
|
|
190
|
-
# Capture screenshot
|
|
191
149
|
img = ImageGrab.grab()
|
|
192
|
-
|
|
193
|
-
# Convert to PNG in memory
|
|
194
150
|
buffer = io.BytesIO()
|
|
195
151
|
img.save(buffer, format="PNG")
|
|
196
|
-
|
|
197
|
-
# Output base64-encoded PNG
|
|
198
152
|
print(base64.b64encode(buffer.getvalue()).decode("utf-8"))
|
|
199
|
-
|
|
200
|
-
# Execute and get stdout
|
|
153
|
+
"""
|
|
201
154
|
exec_result = await self.exec("python3", "-c", python_code)
|
|
202
155
|
|
|
203
|
-
# Check if screenshot command succeeded
|
|
204
156
|
if exec_result.exit_code != 0:
|
|
205
|
-
logger.error(f"Screenshot failed with exit code {exec_result.exit_code}")
|
|
206
|
-
logger.error(f"stderr: {exec_result.stderr}")
|
|
207
157
|
raise ExecError("screenshot()", exec_result.exit_code, exec_result.stderr)
|
|
208
158
|
|
|
209
|
-
b64_data = exec_result.stdout.strip()
|
|
210
|
-
|
|
211
|
-
logger.info(
|
|
212
|
-
f"Screenshot captured: {self._DEFAULT_DISPLAY_WIDTH_PX}x{self._DEFAULT_DISPLAY_HEIGHT_PX}")
|
|
213
159
|
return {
|
|
214
|
-
"data":
|
|
215
|
-
"width":
|
|
216
|
-
"height":
|
|
217
|
-
"format": "png"
|
|
160
|
+
"data": exec_result.stdout.strip(),
|
|
161
|
+
"width": const.COMPUTERBOX_DISPLAY_WIDTH,
|
|
162
|
+
"height": const.COMPUTERBOX_DISPLAY_HEIGHT,
|
|
163
|
+
"format": "png",
|
|
218
164
|
}
|
|
219
165
|
|
|
220
166
|
async def mouse_move(self, x: int, y: int):
|
|
221
|
-
"""
|
|
222
|
-
Move mouse cursor to absolute coordinates.
|
|
223
|
-
|
|
224
|
-
Args:
|
|
225
|
-
x: X coordinate
|
|
226
|
-
y: Y coordinate
|
|
227
|
-
|
|
228
|
-
Example:
|
|
229
|
-
>>> async with ComputerBox() as desktop:
|
|
230
|
-
... await desktop.mouse_move(100, 200)
|
|
231
|
-
"""
|
|
232
|
-
logger.info(f"Moving mouse to ({x}, {y})")
|
|
167
|
+
"""Move mouse cursor to absolute coordinates."""
|
|
233
168
|
exec_result = await self.exec("xdotool", "mousemove", str(x), str(y))
|
|
234
169
|
if exec_result.exit_code != 0:
|
|
235
|
-
raise ExecError(
|
|
236
|
-
|
|
170
|
+
raise ExecError(
|
|
171
|
+
f"mouse_move({x}, {y})", exec_result.exit_code, exec_result.stderr
|
|
172
|
+
)
|
|
237
173
|
|
|
238
174
|
async def left_click(self):
|
|
239
|
-
"""
|
|
240
|
-
Click left mouse button at current position.
|
|
241
|
-
|
|
242
|
-
Example:
|
|
243
|
-
>>> async with ComputerBox() as desktop:
|
|
244
|
-
... await desktop.mouse_move(100, 200)
|
|
245
|
-
... await desktop.left_click()
|
|
246
|
-
"""
|
|
247
|
-
logger.info("Clicking left mouse button")
|
|
175
|
+
"""Click left mouse button at current position."""
|
|
248
176
|
exec_result = await self.exec("xdotool", "click", "1")
|
|
249
177
|
if exec_result.exit_code != 0:
|
|
250
178
|
raise ExecError("left_click()", exec_result.exit_code, exec_result.stderr)
|
|
251
|
-
logger.debug("Clicked left button")
|
|
252
179
|
|
|
253
180
|
async def right_click(self):
|
|
254
|
-
"""
|
|
255
|
-
Click right mouse button at current position.
|
|
256
|
-
|
|
257
|
-
Example:
|
|
258
|
-
>>> async with ComputerBox() as desktop:
|
|
259
|
-
... await desktop.mouse_move(100, 200)
|
|
260
|
-
... await desktop.right_click()
|
|
261
|
-
"""
|
|
262
|
-
logger.info("Clicking right mouse button")
|
|
181
|
+
"""Click right mouse button at current position."""
|
|
263
182
|
exec_result = await self.exec("xdotool", "click", "3")
|
|
264
183
|
if exec_result.exit_code != 0:
|
|
265
184
|
raise ExecError("right_click()", exec_result.exit_code, exec_result.stderr)
|
|
266
|
-
logger.debug("Clicked right button")
|
|
267
185
|
|
|
268
186
|
async def middle_click(self):
|
|
269
|
-
"""
|
|
270
|
-
Click middle mouse button at current position.
|
|
271
|
-
|
|
272
|
-
Example:
|
|
273
|
-
>>> async with ComputerBox() as desktop:
|
|
274
|
-
... await desktop.mouse_move(100, 200)
|
|
275
|
-
... await desktop.middle_click()
|
|
276
|
-
"""
|
|
277
|
-
logger.info("Clicking middle mouse button")
|
|
187
|
+
"""Click middle mouse button at current position."""
|
|
278
188
|
exec_result = await self.exec("xdotool", "click", "2")
|
|
279
189
|
if exec_result.exit_code != 0:
|
|
280
190
|
raise ExecError("middle_click()", exec_result.exit_code, exec_result.stderr)
|
|
281
|
-
logger.debug("Clicked middle button")
|
|
282
191
|
|
|
283
192
|
async def double_click(self):
|
|
284
|
-
"""
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
>>> async with ComputerBox() as desktop:
|
|
289
|
-
... await desktop.mouse_move(100, 200)
|
|
290
|
-
... await desktop.double_click()
|
|
291
|
-
"""
|
|
292
|
-
logger.info("Double-clicking left mouse button")
|
|
293
|
-
exec_result = await self.exec("xdotool", "click", "--repeat", "2", "--delay",
|
|
294
|
-
"100", "1")
|
|
193
|
+
"""Double-click left mouse button at current position."""
|
|
194
|
+
exec_result = await self.exec(
|
|
195
|
+
"xdotool", "click", "--repeat", "2", "--delay", "100", "1"
|
|
196
|
+
)
|
|
295
197
|
if exec_result.exit_code != 0:
|
|
296
198
|
raise ExecError("double_click()", exec_result.exit_code, exec_result.stderr)
|
|
297
|
-
logger.debug("Double-clicked left button")
|
|
298
199
|
|
|
299
200
|
async def triple_click(self):
|
|
300
|
-
"""
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
Example:
|
|
306
|
-
>>> async with ComputerBox() as desktop:
|
|
307
|
-
... await desktop.mouse_move(100, 200)
|
|
308
|
-
... await desktop.triple_click()
|
|
309
|
-
"""
|
|
310
|
-
logger.info("Triple-clicking left mouse button")
|
|
311
|
-
# Anthropic requires 100-200ms delays between clicks
|
|
312
|
-
exec_result = await self.exec("xdotool", "click", "--repeat", "3", "--delay",
|
|
313
|
-
"100", "1")
|
|
201
|
+
"""Triple-click left mouse button at current position."""
|
|
202
|
+
exec_result = await self.exec(
|
|
203
|
+
"xdotool", "click", "--repeat", "3", "--delay", "100", "1"
|
|
204
|
+
)
|
|
314
205
|
if exec_result.exit_code != 0:
|
|
315
206
|
raise ExecError("triple_click()", exec_result.exit_code, exec_result.stderr)
|
|
316
|
-
logger.debug("Triple-clicked left button")
|
|
317
207
|
|
|
318
208
|
async def left_click_drag(self, start_x: int, start_y: int, end_x: int, end_y: int):
|
|
319
|
-
"""
|
|
320
|
-
Drag mouse from start position to end position with left button held.
|
|
321
|
-
|
|
322
|
-
Args:
|
|
323
|
-
start_x: Starting X coordinate
|
|
324
|
-
start_y: Starting Y coordinate
|
|
325
|
-
end_x: Ending X coordinate
|
|
326
|
-
end_y: Ending Y coordinate
|
|
327
|
-
|
|
328
|
-
Example:
|
|
329
|
-
>>> async with ComputerBox() as desktop:
|
|
330
|
-
... await desktop.left_click_drag(100, 100, 200, 200)
|
|
331
|
-
"""
|
|
332
|
-
logger.info(f"Dragging from ({start_x}, {start_y}) to ({end_x}, {end_y})")
|
|
333
|
-
# Chain all operations in single xdotool command: move, press, move, release
|
|
209
|
+
"""Drag mouse from start position to end position with left button held."""
|
|
334
210
|
exec_result = await self.exec(
|
|
335
211
|
"xdotool",
|
|
336
|
-
"mousemove",
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
"
|
|
340
|
-
"
|
|
341
|
-
"
|
|
212
|
+
"mousemove",
|
|
213
|
+
str(start_x),
|
|
214
|
+
str(start_y),
|
|
215
|
+
"mousedown",
|
|
216
|
+
"1",
|
|
217
|
+
"sleep",
|
|
218
|
+
"0.1",
|
|
219
|
+
"mousemove",
|
|
220
|
+
str(end_x),
|
|
221
|
+
str(end_y),
|
|
222
|
+
"sleep",
|
|
223
|
+
"0.1",
|
|
224
|
+
"mouseup",
|
|
225
|
+
"1",
|
|
342
226
|
)
|
|
343
227
|
if exec_result.exit_code != 0:
|
|
344
|
-
raise ExecError(
|
|
345
|
-
|
|
228
|
+
raise ExecError(
|
|
229
|
+
"left_click_drag()", exec_result.exit_code, exec_result.stderr
|
|
230
|
+
)
|
|
346
231
|
|
|
347
232
|
async def cursor_position(self) -> Tuple[int, int]:
|
|
348
|
-
"""
|
|
349
|
-
Get the current mouse cursor position.
|
|
350
|
-
|
|
351
|
-
Returns:
|
|
352
|
-
Tuple of (x, y) coordinates
|
|
353
|
-
|
|
354
|
-
Example:
|
|
355
|
-
>>> async with ComputerBox() as desktop:
|
|
356
|
-
... x, y = await desktop.cursor_position()
|
|
357
|
-
... print(f"Cursor at ({x}, {y})")
|
|
358
|
-
"""
|
|
359
|
-
logger.info("Getting cursor position")
|
|
360
|
-
|
|
361
|
-
# Use xdotool to get mouse location
|
|
233
|
+
"""Get the current mouse cursor position. Returns (x, y) tuple."""
|
|
362
234
|
exec_result = await self.exec("xdotool", "getmouselocation", "--shell")
|
|
363
|
-
|
|
364
|
-
# Check if command succeeded
|
|
365
235
|
if exec_result.exit_code != 0:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
236
|
+
raise ExecError(
|
|
237
|
+
"cursor_position()", exec_result.exit_code, exec_result.stderr
|
|
238
|
+
)
|
|
369
239
|
|
|
370
|
-
# Parse output (format: "X=123\nY=456\nSCREEN=0\nWINDOW=...")
|
|
371
240
|
x, y = None, None
|
|
372
|
-
for line in exec_result.stdout.split(
|
|
373
|
-
|
|
374
|
-
if
|
|
375
|
-
x = int(
|
|
376
|
-
elif
|
|
377
|
-
y = int(
|
|
241
|
+
for line in exec_result.stdout.split("\n"):
|
|
242
|
+
line = line.strip()
|
|
243
|
+
if line.startswith("X="):
|
|
244
|
+
x = int(line[2:])
|
|
245
|
+
elif line.startswith("Y="):
|
|
246
|
+
y = int(line[2:])
|
|
378
247
|
|
|
379
248
|
if x is not None and y is not None:
|
|
380
|
-
logger.info(f"Cursor position: ({x}, {y})")
|
|
381
249
|
return (x, y)
|
|
382
|
-
|
|
383
|
-
logger.error("Failed to parse cursor position from xdotool output")
|
|
384
250
|
raise ParseError("Failed to parse cursor position from xdotool output")
|
|
385
251
|
|
|
386
252
|
async def type(self, text: str):
|
|
387
|
-
"""
|
|
388
|
-
Type text using the keyboard.
|
|
389
|
-
|
|
390
|
-
Args:
|
|
391
|
-
text: Text to type
|
|
392
|
-
|
|
393
|
-
Example:
|
|
394
|
-
>>> async with ComputerBox() as desktop:
|
|
395
|
-
... await desktop.type("Hello World!")
|
|
396
|
-
"""
|
|
397
|
-
logger.info(f"Typing text: {text[:50]}{'...' if len(text) > 50 else ''}")
|
|
398
|
-
|
|
399
|
-
# Escape special characters for xdotool
|
|
253
|
+
"""Type text using the keyboard."""
|
|
400
254
|
exec_result = await self.exec("xdotool", "type", "--", text)
|
|
401
255
|
if exec_result.exit_code != 0:
|
|
402
256
|
raise ExecError("type()", exec_result.exit_code, exec_result.stderr)
|
|
403
|
-
logger.debug(f"Typed {len(text)} characters")
|
|
404
257
|
|
|
405
258
|
async def key(self, text: str):
|
|
406
|
-
"""
|
|
407
|
-
Press a special key or key combination.
|
|
408
|
-
|
|
409
|
-
Args:
|
|
410
|
-
text: Key to press (e.g., 'Return', 'Escape', 'ctrl+c', 'alt+F4')
|
|
411
|
-
|
|
412
|
-
Special keys: Return, Escape, Tab, space, BackSpace, Delete,
|
|
413
|
-
Up, Down, Left, Right, Home, End, Page_Up, Page_Down,
|
|
414
|
-
F1-F12, etc.
|
|
415
|
-
|
|
416
|
-
Example:
|
|
417
|
-
>>> async with ComputerBox() as desktop:
|
|
418
|
-
... await desktop.key("Return")
|
|
419
|
-
... await desktop.key("ctrl+c")
|
|
420
|
-
"""
|
|
421
|
-
logger.info(f"Pressing key: {text}")
|
|
259
|
+
"""Press a special key or key combination (e.g., 'Return', 'ctrl+c')."""
|
|
422
260
|
exec_result = await self.exec("xdotool", "key", text)
|
|
423
261
|
if exec_result.exit_code != 0:
|
|
424
262
|
raise ExecError("key()", exec_result.exit_code, exec_result.stderr)
|
|
425
|
-
logger.debug(f"Pressed key: {text}")
|
|
426
263
|
|
|
427
264
|
async def scroll(self, x: int, y: int, direction: str, amount: int = 3):
|
|
428
265
|
"""
|
|
429
266
|
Scroll at a specific position.
|
|
430
267
|
|
|
431
268
|
Args:
|
|
432
|
-
x:
|
|
433
|
-
|
|
434
|
-
direction: Scroll direction - 'up', 'down', 'left', or 'right'
|
|
269
|
+
x, y: Coordinates where to scroll
|
|
270
|
+
direction: 'up', 'down', 'left', or 'right'
|
|
435
271
|
amount: Number of scroll units (default: 3)
|
|
436
|
-
|
|
437
|
-
Example:
|
|
438
|
-
>>> async with ComputerBox() as desktop:
|
|
439
|
-
... # Scroll up in the middle of the screen
|
|
440
|
-
... await desktop.scroll(512, 384, "up", amount=5)
|
|
441
272
|
"""
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
# Map scroll directions to xdotool mouse button numbers
|
|
445
|
-
# In X11, scroll is simulated using mouse button clicks:
|
|
446
|
-
# Button 4 = scroll up, Button 5 = scroll down
|
|
447
|
-
# Button 6 = scroll left, Button 7 = scroll right
|
|
448
|
-
direction_map = {
|
|
449
|
-
"up": "4",
|
|
450
|
-
"down": "5",
|
|
451
|
-
"left": "6",
|
|
452
|
-
"right": "7"
|
|
453
|
-
}
|
|
454
|
-
|
|
273
|
+
direction_map = {"up": "4", "down": "5", "left": "6", "right": "7"}
|
|
455
274
|
button = direction_map.get(direction.lower())
|
|
456
275
|
if not button:
|
|
457
|
-
raise ValueError(
|
|
458
|
-
f"Invalid scroll direction: {direction}. Must be 'up', 'down', 'left', or 'right'")
|
|
276
|
+
raise ValueError(f"Invalid scroll direction: {direction}")
|
|
459
277
|
|
|
460
|
-
# Chain mousemove and repeated clicks in single xdotool command
|
|
461
278
|
exec_result = await self.exec(
|
|
462
279
|
"xdotool",
|
|
463
|
-
"mousemove",
|
|
464
|
-
|
|
280
|
+
"mousemove",
|
|
281
|
+
str(x),
|
|
282
|
+
str(y),
|
|
283
|
+
"click",
|
|
284
|
+
"--repeat",
|
|
285
|
+
str(amount),
|
|
286
|
+
button,
|
|
465
287
|
)
|
|
466
|
-
|
|
467
|
-
# Check if command succeeded
|
|
468
288
|
if exec_result.exit_code != 0:
|
|
469
|
-
logger.error(f"xdotool scroll failed with exit code {exec_result.exit_code}")
|
|
470
|
-
logger.error(f"stderr: {exec_result.stderr}")
|
|
471
289
|
raise ExecError("scroll()", exec_result.exit_code, exec_result.stderr)
|
|
472
290
|
|
|
473
|
-
logger.debug(f"Scrolled {direction} {amount} times at ({x}, {y})")
|
|
474
|
-
|
|
475
291
|
async def get_screen_size(self) -> Tuple[int, int]:
|
|
476
|
-
"""
|
|
477
|
-
Get the screen resolution.
|
|
478
|
-
|
|
479
|
-
Returns:
|
|
480
|
-
Tuple of (width, height)
|
|
481
|
-
|
|
482
|
-
Example:
|
|
483
|
-
>>> async with ComputerBox() as desktop:
|
|
484
|
-
... width, height = await desktop.get_screen_size()
|
|
485
|
-
... print(f"Screen: {width}x{height}")
|
|
486
|
-
"""
|
|
487
|
-
logger.info("Getting screen size")
|
|
488
|
-
|
|
489
|
-
# Use xdotool to get screen size
|
|
292
|
+
"""Get the screen resolution. Returns (width, height) tuple."""
|
|
490
293
|
exec_result = await self.exec("xdotool", "getdisplaygeometry")
|
|
491
|
-
|
|
492
|
-
# Check if command succeeded (exit code is more reliable than stderr presence)
|
|
493
294
|
if exec_result.exit_code != 0:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
raise ExecError("get_screen_size()", exec_result.exit_code, exec_result.stderr)
|
|
498
|
-
|
|
499
|
-
# Parse stdout (format: "width height")
|
|
500
|
-
result = exec_result.stdout.strip()
|
|
501
|
-
logger.debug(f"stdout result: {result}")
|
|
502
|
-
parts = result.split()
|
|
503
|
-
if len(parts) == 2:
|
|
504
|
-
size = (int(parts[0]), int(parts[1]))
|
|
505
|
-
logger.info(f"Screen size: {size[0]}x{size[1]}")
|
|
506
|
-
return size
|
|
295
|
+
raise ExecError(
|
|
296
|
+
"get_screen_size()", exec_result.exit_code, exec_result.stderr
|
|
297
|
+
)
|
|
507
298
|
|
|
508
|
-
|
|
299
|
+
parts = exec_result.stdout.strip().split()
|
|
300
|
+
if len(parts) == 2:
|
|
301
|
+
return (int(parts[0]), int(parts[1]))
|
|
509
302
|
raise ParseError("Failed to parse screen size from xdotool output")
|
boxlite/constants.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized constants for BoxLite Python SDK.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Default VM resources
|
|
6
|
+
DEFAULT_CPUS = 1
|
|
7
|
+
DEFAULT_MEMORY_MIB = 2048
|
|
8
|
+
|
|
9
|
+
# ComputerBox defaults (higher resources for desktop)
|
|
10
|
+
COMPUTERBOX_CPUS = 2
|
|
11
|
+
COMPUTERBOX_MEMORY_MIB = 2048
|
|
12
|
+
COMPUTERBOX_IMAGE = "lscr.io/linuxserver/webtop:ubuntu-xfce"
|
|
13
|
+
|
|
14
|
+
# ComputerBox display settings
|
|
15
|
+
COMPUTERBOX_DISPLAY_NUMBER = ":1"
|
|
16
|
+
COMPUTERBOX_DISPLAY_WIDTH = 1024
|
|
17
|
+
COMPUTERBOX_DISPLAY_HEIGHT = 768
|
|
18
|
+
|
|
19
|
+
# ComputerBox network ports (webtop defaults)
|
|
20
|
+
COMPUTERBOX_GUI_HTTP_PORT = 3000
|
|
21
|
+
COMPUTERBOX_GUI_HTTPS_PORT = 3001
|
|
22
|
+
|
|
23
|
+
# Timeouts (seconds)
|
|
24
|
+
DESKTOP_READY_TIMEOUT = 60
|
|
25
|
+
DESKTOP_READY_RETRY_DELAY = 0.5
|