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 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
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
- # Always use xfce desktop
47
- _IMAGE_REFERENCE = "lscr.io/linuxserver/webtop:ubuntu-xfce"
48
- # Webtop uses port 3001 with HTTPS
49
- _GUEST_GUI_HTTP_PORT = 3000
50
- _GUEST_GUI_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, 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
- gui_http_port: Port for web-based desktop GUI over HTTP (default: 3000)
68
- gui_https_port: Port for web-based desktop GUI over HTTPS (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)
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", self._DISPLAY_NUMBER),
76
- ("DISPLAY_SIZEW", str(self._DEFAULT_DISPLAY_WIDTH_PX)),
77
- ("DISPLAY_SIZEH", str(self._DEFAULT_DISPLAY_HEIGHT_PX)),
78
- ("SELKIES_MANUAL_WIDTH", str(self._DEFAULT_DISPLAY_WIDTH_PX)),
79
- ("SELKIES_MANUAL_HEIGHT", str(self._DEFAULT_DISPLAY_HEIGHT_PX)),
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, self._GUEST_GUI_HTTP_PORT),
88
- (gui_https_port, self._GUEST_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=self._IMAGE_REFERENCE,
82
+ image=const.COMPUTERBOX_IMAGE,
94
83
  memory_mib=memory,
95
84
  cpus=cpu,
96
85
  runtime=runtime,
97
- env=merged_env,
98
- ports=merged_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 = 60):
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
- result = exec_result.stdout
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
- logger.debug(f"stdout {result}")
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(retry_delay)
119
+ await asyncio.sleep(const.DESKTOP_READY_RETRY_DELAY)
143
120
 
144
- except Exception as e:
121
+ except (ExecError, ConnectionError, OSError, asyncio.TimeoutError) as e:
145
122
  logger.debug(f"Desktop not ready: {e}, retrying...")
146
- await asyncio.sleep(retry_delay)
147
-
148
- # GUI Automation Methods
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 using PIL.ImageGrab (pre-installed).
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 containing:
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": b64_data,
208
- "width": self._DEFAULT_DISPLAY_WIDTH_PX,
209
- "height": self._DEFAULT_DISPLAY_HEIGHT_PX,
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
- Double-click left mouse button at current position.
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
- Triple-click left mouse button at current position.
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
- clean_line = line.strip()
367
- if clean_line.startswith('X='):
368
- x = int(clean_line[2:])
369
- elif clean_line.startswith('Y='):
370
- y = int(clean_line[2:])
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: X coordinate where to scroll
426
- y: Y coordinate where to scroll
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
- logger.info(f"Scrolling {direction} at ({x}, {y}), amount={amount}")
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
- # Parse stdout (format: "width height")
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
- size = (int(parts[0]), int(parts[1]))
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