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