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/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
- ... print(f"Desktop ready at: {desktop.endpoint()}")
38
- ... # Open the URL in your browser to see the desktop
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, monitor_https_port=3002) as desktop:
43
- ... url = desktop.endpoint()
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
- # Always use xfce desktop
47
- _IMAGE_REFERENCE = "lscr.io/linuxserver/webtop:ubuntu-xfce"
48
- # Webtop uses port 3001 with HTTPS
49
- _GUEST_MONITOR_HTTP_PORT = 3000
50
- _GUEST_MONITOR_HTTPS_PORT = 3001
51
- # Webtop display number
52
- _DISPLAY_NUMBER = ":1"
53
- # Expected display resolution when SELKIES_IS_MANUAL_RESOLUTION_MODE=true (Anthropic requires ≤ 1280x800)
54
- # Webtop/Selkies defaults to 1024x768 in manual resolution mode
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
- monitor_https_port: Port for web-based desktop monitor (default: 3001)
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
- self._monitor_port = monitor_https_port
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=self._IMAGE_REFERENCE,
82
+ image=const.COMPUTERBOX_IMAGE,
75
83
  memory_mib=memory,
76
84
  cpus=cpu,
77
85
  runtime=runtime,
78
- env=[
79
- ("DISPLAY", self._DISPLAY_NUMBER),
80
- # X11 display resolution (works for initial X server size)
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 endpoint(self) -> str:
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(f"Desktop did not become ready within {timeout} seconds")
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
- result = exec_result.stdout
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 'xfdesktop' in result and expected_size in result:
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(f"Desktop not ready yet (waited {elapsed:.1f}s), retrying...")
149
- await asyncio.sleep(retry_delay)
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 Exception as e:
129
+ except (ExecError, ConnectionError, OSError, asyncio.TimeoutError) as e:
152
130
  logger.debug(f"Desktop not ready: {e}, retrying...")
153
- await asyncio.sleep(retry_delay)
154
-
155
- # GUI Automation Methods
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 using PIL.ImageGrab (pre-installed).
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 containing:
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
- # Use PIL.ImageGrab (pre-installed in webtop) to capture screenshot
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": b64_data,
215
- "width": self._DEFAULT_DISPLAY_WIDTH_PX,
216
- "height": self._DEFAULT_DISPLAY_HEIGHT_PX,
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(f"mouse_move({x}, {y})", exec_result.exit_code, exec_result.stderr)
236
- logger.debug(f"Mouse moved to ({x}, {y})")
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
- Double-click left mouse button at current position.
286
-
287
- Example:
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
- Triple-click left mouse button at current position.
302
-
303
- Useful for selecting entire lines or paragraphs of text.
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", str(start_x), str(start_y),
337
- "mousedown", "1",
338
- "sleep", "0.1",
339
- "mousemove", str(end_x), str(end_y),
340
- "sleep", "0.1",
341
- "mouseup", "1"
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("left_click_drag()", exec_result.exit_code, exec_result.stderr)
345
- logger.debug(f"Drag completed")
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
- logger.error(f"xdotool failed with exit code {exec_result.exit_code}")
367
- logger.error(f"stderr: {exec_result.stderr}")
368
- raise ExecError("cursor_position()", exec_result.exit_code, exec_result.stderr)
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('\n'):
373
- clean_line = line.strip()
374
- if clean_line.startswith('X='):
375
- x = int(clean_line[2:])
376
- elif clean_line.startswith('Y='):
377
- y = int(clean_line[2:])
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: X coordinate where to scroll
433
- y: Y coordinate where to scroll
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
- logger.info(f"Scrolling {direction} at ({x}, {y}), amount={amount}")
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", str(x), str(y),
464
- "click", "--repeat", str(amount), button
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
- logger.error(f"xdotool failed with exit code {exec_result.exit_code}")
495
- logger.error(f"stderr: {exec_result.stderr}")
496
- # Raise exception with stderr content so wait_until_ready() can detect it
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
- logger.error("Failed to parse screen size from xdotool output")
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