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,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
|
+
]
|