cua-bench-ui 0.1.0__tar.gz

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.
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.1
2
+ Name: cua-bench-ui
3
+ Version: 0.1.0
4
+ Summary: Lightweight webUI window launcher for CUA bench using pywebview
5
+ Author-Email: TryCua <gh@trycua.com>
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: pywebview>=5.3
8
+ Requires-Dist: aiohttp>=3.9.0
9
+ Description-Content-Type: text/markdown
10
+
11
+ # CUA Bench UI
12
+
13
+ Lightweight webUI window controller for CUA bench environments using pywebview
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ from bench_ui import launch_window, get_element_rect, execute_javascript
19
+ ```
20
+
21
+ See `examples/simple_example.py` for a complete example.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install cua-bench-ui
27
+ ```
@@ -0,0 +1,17 @@
1
+ # CUA Bench UI
2
+
3
+ Lightweight webUI window controller for CUA bench environments using pywebview
4
+
5
+ ## Usage
6
+
7
+ ```python
8
+ from bench_ui import launch_window, get_element_rect, execute_javascript
9
+ ```
10
+
11
+ See `examples/simple_example.py` for a complete example.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install cua-bench-ui
17
+ ```
@@ -0,0 +1,3 @@
1
+ from .api import launch_window, get_element_rect, execute_javascript
2
+
3
+ __all__ = ["launch_window", "get_element_rect", "execute_javascript"]
@@ -0,0 +1,149 @@
1
+ import json
2
+ import os
3
+ import subprocess
4
+ import sys
5
+ import tempfile
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Optional, Dict, Any
9
+ from urllib import request
10
+ from urllib.error import HTTPError, URLError
11
+
12
+ # Map child PID -> listening port
13
+ _pid_to_port: Dict[int, int] = {}
14
+
15
+
16
+ def _post_json(url: str, payload: Dict[str, Any]) -> Dict[str, Any]:
17
+ data = json.dumps(payload).encode("utf-8")
18
+ req = request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST")
19
+ try:
20
+ with request.urlopen(req, timeout=5) as resp:
21
+ text = resp.read().decode("utf-8")
22
+ return json.loads(text)
23
+ except HTTPError as e:
24
+ try:
25
+ body = (e.read() or b"").decode("utf-8", errors="ignore")
26
+ return json.loads(body)
27
+ except Exception:
28
+ return {"error": "http_error", "status": getattr(e, 'code', None)}
29
+ except URLError as e:
30
+ return {"error": "url_error", "reason": str(e.reason)}
31
+
32
+
33
+ def launch_window(
34
+ url: Optional[str] = None,
35
+ *,
36
+ html: Optional[str] = None,
37
+ title: str = "Window",
38
+ x: Optional[int] = None,
39
+ y: Optional[int] = None,
40
+ width: int = 600,
41
+ height: int = 400,
42
+ icon: Optional[str] = None,
43
+ use_inner_size: bool = False,
44
+ title_bar_style: str = "default",
45
+ ) -> int:
46
+ """Create a pywebview window in a child process and return its PID.
47
+
48
+ Preferred input is a URL via the positional `url` parameter.
49
+ To load inline HTML instead, pass `html=...`.
50
+
51
+ Spawns `python -m bench_ui.child` with a JSON config passed via a temp file.
52
+ The child prints a single JSON line: {"pid": <pid>, "port": <port>}.
53
+ We cache pid->port for subsequent control calls like get_element_rect.
54
+ """
55
+ if not url and not html:
56
+ raise ValueError("launch_window requires either a url or html")
57
+
58
+ config = {
59
+ "url": url,
60
+ "html": html,
61
+ "title": title,
62
+ "x": x,
63
+ "y": y,
64
+ "width": width,
65
+ "height": height,
66
+ "icon": icon,
67
+ "use_inner_size": use_inner_size,
68
+ "title_bar_style": title_bar_style,
69
+ }
70
+
71
+ with tempfile.NamedTemporaryFile("w", delete=False, suffix=".json") as f:
72
+ json.dump(config, f)
73
+ cfg_path = f.name
74
+
75
+ try:
76
+ # Launch child process
77
+ proc = subprocess.Popen(
78
+ [sys.executable, "-m", "bench_ui.child", cfg_path],
79
+ stdout=subprocess.PIPE,
80
+ stderr=subprocess.STDOUT,
81
+ text=True,
82
+ )
83
+ assert proc.stdout is not None
84
+ # Read first line with startup info
85
+ line = proc.stdout.readline().strip()
86
+ info = json.loads(line)
87
+ pid = int(info["pid"]) if "pid" in info else proc.pid
88
+ port = int(info["port"]) # required
89
+ _pid_to_port[pid] = port
90
+ return pid
91
+ finally:
92
+ try:
93
+ os.unlink(cfg_path)
94
+ except Exception:
95
+ pass
96
+
97
+
98
+ def get_element_rect(pid: int, selector: str, *, space: str = "window"):
99
+ """Ask the child process to compute element client rect via injected JS.
100
+
101
+ Returns a dict like {"x": float, "y": float, "width": float, "height": float} or None if not found.
102
+ """
103
+ if pid not in _pid_to_port:
104
+ raise RuntimeError(f"Unknown pid {pid}; no registered bench-ui window")
105
+ port = _pid_to_port[pid]
106
+ url = f"http://127.0.0.1:{port}/rect"
107
+ last: Dict[str, Any] = {}
108
+ for _ in range(30): # ~3s total
109
+ resp = _post_json(url, {"selector": selector, "space": space})
110
+ last = resp or {}
111
+ rect = last.get("rect") if isinstance(last, dict) else None
112
+ err = last.get("error") if isinstance(last, dict) else None
113
+ if rect is not None:
114
+ return rect
115
+ if err in ("window_not_ready", "invalid_json"):
116
+ time.sleep(0.1)
117
+ continue
118
+ # If other transient errors, brief retry
119
+ if err:
120
+ time.sleep(0.1)
121
+ continue
122
+ time.sleep(0.1)
123
+ raise RuntimeError(f"Failed to get element rect: {last}")
124
+
125
+
126
+ def execute_javascript(pid: int, javascript: str):
127
+ """Execute arbitrary JavaScript in the window and return its result.
128
+
129
+ Retries briefly while the window is still becoming ready.
130
+ """
131
+ if pid not in _pid_to_port:
132
+ raise RuntimeError(f"Unknown pid {pid}; no registered bench-ui window")
133
+ port = _pid_to_port[pid]
134
+ url = f"http://127.0.0.1:{port}/eval"
135
+ last: Dict[str, Any] = {}
136
+ for _ in range(30): # ~3s total
137
+ resp = _post_json(url, {"javascript": javascript})
138
+ last = resp or {}
139
+ if isinstance(last, dict):
140
+ if "result" in last:
141
+ return last["result"]
142
+ if last.get("error") in ("window_not_ready", "invalid_json"):
143
+ time.sleep(0.1)
144
+ continue
145
+ if last.get("error"):
146
+ time.sleep(0.1)
147
+ continue
148
+ time.sleep(0.1)
149
+ raise RuntimeError(f"Failed to execute JavaScript: {last}")
@@ -0,0 +1,174 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import random
5
+ import socket
6
+ import sys
7
+ import threading
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import webview
12
+ from aiohttp import web
13
+
14
+
15
+ def _get_free_port() -> int:
16
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
17
+ s.bind(("127.0.0.1", 0))
18
+ return s.getsockname()[1]
19
+
20
+
21
+ def _start_http_server(window: webview.Window, port: int, ready_event: threading.Event):
22
+ async def rect_handler(request: web.Request):
23
+ try:
24
+ data = await request.json()
25
+ except Exception:
26
+ return web.json_response({"error": "invalid_json"}, status=400)
27
+ selector = data.get("selector")
28
+ space = data.get("space", "window")
29
+ if not isinstance(selector, str):
30
+ return web.json_response({"error": "selector_required"}, status=400)
31
+
32
+ # Ensure window content is loaded
33
+ if not ready_event.is_set():
34
+ # give it a short chance to finish loading
35
+ ready_event.wait(timeout=2.0)
36
+ if not ready_event.is_set():
37
+ return web.json_response({"error": "window_not_ready"}, status=409)
38
+
39
+ # Safely embed selector into JS
40
+ selector_js = json.dumps(selector)
41
+ if space == "screen":
42
+ # Compute approximate screen coordinates using window metrics
43
+ js = (
44
+ "(function(){"
45
+ f"const s = {selector_js};"
46
+ "const el = document.querySelector(s);"
47
+ "if(!el){return null;}"
48
+ "const r = el.getBoundingClientRect();"
49
+ "const sx = (window.screenX ?? window.screenLeft ?? 0);"
50
+ "const syRaw = (window.screenY ?? window.screenTop ?? 0);"
51
+ "const frameH = (window.outerHeight - window.innerHeight) || 0;"
52
+ "const sy = syRaw + frameH;"
53
+ "return {x:sx + r.left, y:sy + r.top, width:r.width, height:r.height};"
54
+ "})()"
55
+ )
56
+ else:
57
+ js = (
58
+ "(function(){"
59
+ f"const s = {selector_js};"
60
+ "const el = document.querySelector(s);"
61
+ "if(!el){return null;}"
62
+ "const r = el.getBoundingClientRect();"
63
+ "return {x:r.left,y:r.top,width:r.width,height:r.height};"
64
+ "})()"
65
+ )
66
+ try:
67
+ # Evaluate JS on the target window; this call is thread-safe in pywebview
68
+ result = window.evaluate_js(js)
69
+ except Exception as e:
70
+ return web.json_response({"error": str(e)}, status=500)
71
+ return web.json_response({"rect": result})
72
+
73
+ async def eval_handler(request: web.Request):
74
+ try:
75
+ data = await request.json()
76
+ except Exception:
77
+ return web.json_response({"error": "invalid_json"}, status=400)
78
+ code = data.get("javascript") or data.get("code")
79
+ if not isinstance(code, str):
80
+ return web.json_response({"error": "javascript_required"}, status=400)
81
+
82
+ if not ready_event.is_set():
83
+ ready_event.wait(timeout=2.0)
84
+ if not ready_event.is_set():
85
+ return web.json_response({"error": "window_not_ready"}, status=409)
86
+
87
+ try:
88
+ result = window.evaluate_js(code)
89
+ except Exception as e:
90
+ return web.json_response({"error": str(e)}, status=500)
91
+ return web.json_response({"result": result})
92
+
93
+ app = web.Application()
94
+ app.router.add_post("/rect", rect_handler)
95
+ app.router.add_post("/eval", eval_handler)
96
+
97
+ loop = asyncio.new_event_loop()
98
+
99
+ def run_loop():
100
+ asyncio.set_event_loop(loop)
101
+ runner = web.AppRunner(app)
102
+ loop.run_until_complete(runner.setup())
103
+ site = web.TCPSite(runner, "127.0.0.1", port)
104
+ loop.run_until_complete(site.start())
105
+ loop.run_forever()
106
+
107
+ t = threading.Thread(target=run_loop, daemon=True)
108
+ t.start()
109
+
110
+
111
+ def main():
112
+ if len(sys.argv) < 2:
113
+ print("Usage: python -m bench_ui.child <config.json>", file=sys.stderr)
114
+ sys.exit(2)
115
+
116
+ cfg_path = Path(sys.argv[1])
117
+ cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
118
+
119
+ html: Optional[str] = cfg.get("html") or ""
120
+ url: Optional[str] = cfg.get("url")
121
+ title: str = cfg.get("title", "Window")
122
+ x: Optional[int] = cfg.get("x")
123
+ y: Optional[int] = cfg.get("y")
124
+ width: int = int(cfg.get("width", 600))
125
+ height: int = int(cfg.get("height", 400))
126
+ icon: Optional[str] = cfg.get("icon")
127
+ use_inner_size: bool = bool(cfg.get("use_inner_size", False))
128
+ title_bar_style: str = cfg.get("title_bar_style", "default")
129
+
130
+ # Create window
131
+ if url:
132
+ window = webview.create_window(
133
+ title,
134
+ url=url,
135
+ width=width,
136
+ height=height,
137
+ x=x,
138
+ y=y,
139
+ confirm_close=False,
140
+ text_select=True,
141
+ background_color="#FFFFFF",
142
+ )
143
+ else:
144
+ window = webview.create_window(
145
+ title,
146
+ html=html,
147
+ width=width,
148
+ height=height,
149
+ x=x,
150
+ y=y,
151
+ confirm_close=False,
152
+ text_select=True,
153
+ background_color="#FFFFFF",
154
+ )
155
+
156
+ # Track when the page is loaded so JS execution succeeds
157
+ window_ready = threading.Event()
158
+ def _on_loaded():
159
+ window_ready.set()
160
+ window.events.loaded += _on_loaded # type: ignore[attr-defined]
161
+
162
+ # Start HTTP server for control
163
+ port = _get_free_port()
164
+ _start_http_server(window, port, window_ready)
165
+
166
+ # Print startup info for parent to read
167
+ print(json.dumps({"pid": os.getpid(), "port": port}), flush=True)
168
+
169
+ # Start GUI (blocking)
170
+ webview.start()
171
+
172
+
173
+ if __name__ == "__main__":
174
+ main()
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = [
3
+ "pdm-backend",
4
+ ]
5
+ build-backend = "pdm.backend"
6
+
7
+ [project]
8
+ name = "cua-bench-ui"
9
+ version = "0.1.0"
10
+ description = "Lightweight webUI window launcher for CUA bench using pywebview"
11
+ readme = "README.md"
12
+ authors = [
13
+ { name = "TryCua", email = "gh@trycua.com" },
14
+ ]
15
+ dependencies = [
16
+ "pywebview>=5.3",
17
+ "aiohttp>=3.9.0",
18
+ ]
19
+ requires-python = ">=3.12"
20
+
21
+ [tool.pdm]
22
+ distribution = true
23
+
24
+ [tool.pdm.build]
25
+ includes = [
26
+ "bench_ui/",
27
+ ]
28
+ source-includes = [
29
+ "README.md",
30
+ ]