-
Notifications
You must be signed in to change notification settings - Fork 216
Expand file tree
/
Copy pathbench
More file actions
executable file
·550 lines (541 loc) · 34.4 KB
/
Copy pathbench
File metadata and controls
executable file
·550 lines (541 loc) · 34.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
#!/usr/bin/env python3
"""Drive the Search you already have open, from the shell.
./bench tabs every tab, yours and the bench's
./bench find TEXT [next N] [back N] [case] [words]
test run: Find on Page as typed into the bar, then Next or Previous
N times → what the bar says, and how long the page took
./bench open URL [private] a bench tab at the end of the row → its id; private: a private one,
--test runs only
./bench go ID URL send a bench tab somewhere else
./bench wait ID [SECONDS] until the page has loaded
./bench sleep ID put a tab to sleep now, or say what keeps it awake
./bench select ID bring a tab to the front — --test runs only
./bench resize W H [STEPS] take the window to W×H in steps, like a drag — --test runs only
./bench key ID TEXT press keys on a tab as real key events — --test runs only
./bench press CODE CHARS [MODS…] press a key on the whole app (cmd shift ctrl opt, repeat) — --test runs only
./bench tap ID SELECTOR [MODS…] a real mouse click on an element (or text=Words), shift/cmd/opt held — --test runs only
./bench text ID the page's text
./bench eval ID JS [search] run JavaScript, print what it returns; search: in Search's own world
./bench click ID SELECTOR
./bench type ID SELECTOR TEXT
./bench submit ID SELECTOR the form around SELECTOR
./bench shot ID [PATH] [WIDTH] a PNG of the page → its path
./bench close ID | all only tabs the bench opened
./bench probe the window's own state: panels, modal, windows
./bench extensions installed extensions, loaded or not, with their warnings
./bench little URL|keep|close a small window for a link (made unseen), kept into the row, or closed
./bench site PATH [security] the site card for the tab on screen, drawn to a PNG
./bench ext-add ID|LINK [--yes] install from the Chrome Web Store (--yes only in a --test run)
./bench ext-folder PATH [--yes] install an unpacked extension
./bench ext-press ID press its button
./bench ext-page ID [PATH] one of its pages in a bench tab → tab id, for eval
./bench ext-popup ID JS run JavaScript in its popup while it is open
./bench ext-reload ID take it up again (a folder one is copied in afresh)
./bench ext-pin ID [on|off] keep its button in the row beside the puzzle
./bench ext-menu PATH the puzzle button's list, drawn to a PNG
./bench ext-shot ID PATH a picture of its popup while it is open
./bench ext-answer yes|no|ask test run: answer extensions' questions without asking; lists what was asked
./bench ext-enable ID on|off switch an extension on or off
./bench capture extensions' screen recording: ids given, what records, who may, last request
./bench capture-stop ID stop what an extension is recording, as the pill's Stop does
./bench ext-remove ID
./bench strip PATH [WIDTH] the row of tabs across the top, drawn off screen to a PNG
./bench column PATH [HEIGHT] the column of tabs, drawn off screen to a PNG
./bench consent [grant|revoke] test run: the mark Settings' bench switch leaves in the keychain, in this
world's own account — whether it is there, before and after
./bench fold PATH [WIDTH] test run: the folded strip or column out over the page (ui folded on,
ui peek on), drawn off screen over red — red shows where the page would
./bench sitesearch [state|tab|delete|esc|go|learn PAGE DESCRIPTION|forget]
test run only: search a site from the address field
./bench split state|start|pair|focus|select|detach|fraction|close|new-tab|move|drop|open|private|bench|sleep|reopen|insert|save|space|mouse
test run only: exercise split state in the real browser model
./bench space [new NAME [fresh]|go N|delete|swipe DX|hold DX|release|move N]
test run: the spaces, a new one (signed in with the others, or fresh),
switch to the Nth, delete this one,
two fingers DX points sideways over the column (hold: not let go yet),
move this one to Nth
./bench hit X Y [double|middle] what a press at X Y (points from the window's top left) lands on,
and whether a drag from there would move the window;
double: test run, a double-click there (start the probe hidden)
./bench place ID INDEX put a tab at another place in the row
./bench group [ID new [NAME] | ID NAME | ID none | fold NAME | close NAME]
test run: the tab groups; a tab into a new group, into one by name,
or out of its group; a group folded or opened, or closed with its tabs
./bench tospace ID N test run: Move to Space, the tab to the Nth space
./bench field TEXT [type] [go] test run: TEXT pasted into the address field (type: a key at a time),
each timed until the window has caught up; go: then Return, timed
until the page is loading
./bench visible ID switched off: it made a window (off screen), and no verb makes one for now
./bench ai mock URL | key PROVIDER KEY | ask PROVIDER MODEL TEXT | read ID | check TEXT | signin | engine [PATH MODELS] | use PROVIDER [MODEL] | summarize ID | question ID TEXT | agree | state | picture DIR
test run: the AI add-on's requests, against a stand-in on this Mac;
what of a page would go to the model, and an answer checked against it
./bench answer once|always|no test run: the card of a page asking for the camera, microphone or location
./bench accounts [ROW] test run: the list under the sign-in box the caret is in (passkeys,
then passwords); ROW: a click on that row, from 0
./bench pull DX [STEPS] [MS] test run: two fingers DX points sideways over the page (back is +) — where it went
./bench keyeq CHARS CODE [shift] test run: ⌘CHARS pressed while the page has the keyboard — who took it
./bench import BROWSER [WHAT…] [--profile NAME|all] [--replace]
test run: bookmarks, history, passwords from a made-up profile in the world's Import/
(the one used most recently, unless a profile is named, or all)
./bench import-preview test run: each browser found, its profiles, the one used most recently, and what
it holds as the Bring things over sheet counts it — without asking for a key
./bench import-file PATH test run: a bookmarks .html, a passwords .csv or Safari's export .zip, as File… brings it
./bench import-file-start PATH test run: start file import without waiting for completion
./bench import-file-status test run: current filename, stage, counts and cancellation state
./bench import-file-cancel test run: request cancellation of the current file import
./bench menu [open] test run: the Bookmarks menu as it is about to open, and its first
folder (open: its first bookmark picked)
./bench bookmark URL [new] test run: a bookmark picked from the list, timed until it loads
(new: into a new tab, whose page isn't built yet)
./bench windows [list|new|close N|reopen|frame N X Y W H|front N|link URL]
test run: the windows, oldest first — a new one, the Nth closed,
the last closed one back (⇧⌘T), the Nth moved, the Nth as if in
front, a link as another app hands it over; --window N before
any command aims it at the Nth window
./bench towindow ID N|new|drop X Y test run: a tab to the Nth window, a new one (Move to Window), or let
go at screen point X Y as a drag out of the row
./bench news PATH [notes] | on test run: the card after an update, or every version's notes, to a
PNG; on: the card in the window
./bench quit test run: quit the app as ⌘Q does
./bench middle ID X Y test run: the middle button clicked at X Y of a tab's page
./bench pin ID [off|home] test run: pin a tab, unpin it, or double-click it (home)
./bench fill [STEPS] test run: the page reporting its scroll STEPS times, each timed until the window rests
./bench peek URL|close test run: the peek panel over the tab in front, as a shift-click opens it
./bench window test run: open the browser's window if a hidden probe came up without it
./bench pages on|off test run: let a hidden probe's pages be painted, off every screen (for picture)
./bench picture PATH test run: the whole window, pages included, drawn to a PNG (start the probe hidden)
./bench film peek|unpeek|fold PATH [N] [SECS] the window drawn N times while it animates → PATH-00.png… — --test runs only
./bench film float|land PATH [SECS] the tab in front's video out into its floating window, or back: every
frame the page draws for SECS (1), its size, the video's box and how much
of the view it fills → PATH.json; from a probe started hidden, both
windows kept off every screen — --test runs only
./bench float screens [two] | place X Y | swipe DX DY [STEPS] [MS] | wheel DX DY | click
test run: the floating window's gestures (after film float, so it is
off every screen), on a made-up 1440×900 screen, or two side by side:
where a flick, a pull at a side, a dock or a click on the sliver
leaves it, and how far it swings on the way
./bench ui KEY VALUE settings/passwords/welcome/history/downloads/bookmarks/hidden on|off,
import on|off|extensions|BROWSER,
look light|dark|system, sidebar on|off, side left|right, spaces on|off, hides on|off,
folded on|off, peek on|off, peeklink keep|close, pages120 on|off
Needs Settings › General › "Let a script drive Search" switched on in the
running app. IDs are the first characters of a tab's id, as `tabs` prints
them. With --test, talks to a SEARCH_PROBE run instead of the real one;
with --world NAME, to a SEARCH_PROBE=NAME run, the world "Search (NAME)".
"""
import json
import os
import socket
import sys
def clean(text):
"""A page chooses its title: nothing in it may move the terminal's cursor,
change its colours, or turn the line around."""
import re
return re.sub(r"[\x00-\x1f\x7f\u200e\u200f\u202a-\u202e\u2066-\u2069]", "", str(text or ""))
def folder(world):
name = f"Search ({world})" if world else "Search"
return os.path.expanduser(f"~/Library/Application Support/{name}")
def ask(path, request):
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
try:
s.connect(path)
except (FileNotFoundError, ConnectionRefusedError):
sys.exit("Search isn't listening — is it open, with Settings › General › "
"“Let a script drive Search” on?")
s.sendall((json.dumps(request) + "\n").encode())
chunks = []
while True:
chunk = s.recv(1 << 16)
if not chunk:
break
chunks.append(chunk)
line = b"".join(chunks).split(b"\n", 1)[0]
return json.loads(line or b"{}")
def main(argv):
world = "test" if "--test" in argv else None
if "--world" in argv:
at = argv.index("--world")
if at + 1 >= len(argv): sys.exit("usage: bench --world NAME COMMAND …")
world = "".join(c for c in argv[at + 1].lower() if c.isascii() and (c.isalnum() or c == "-"))
if world in ("", "1"): world = "test"
argv = argv[:at] + argv[at + 2:]
window = None
if "--window" in argv:
at = argv.index("--window")
if at + 1 >= len(argv): sys.exit("usage: bench --window N COMMAND …")
window = int(argv[at + 1])
argv = argv[:at] + argv[at + 2:]
yes = "--yes" in argv
argv = [a for a in argv if a not in ("--test", "--yes")]
if not argv or argv[0] in ("-h", "--help", "help"):
print(__doc__.strip())
return 0
verb, args = argv[0], argv[1:]
request = {"do": verb}
if window is not None: request["window"] = window
if verb == "open":
if len(args) not in (1, 2) or args[1:] not in ([], ["private"]): sys.exit("usage: bench open URL [private]")
request["url"] = args[0]
if args[1:] == ["private"]: request["private"] = True
elif verb == "go":
if len(args) != 2: sys.exit("usage: bench go ID URL")
request["id"], request["url"] = args
elif verb in ("wait",):
if not args: sys.exit("usage: bench wait ID [SECONDS]")
request["id"] = args[0]
if len(args) > 1: request["seconds"] = float(args[1])
elif verb in ("text", "close", "sleep", "select", "visible"):
if len(args) != 1: sys.exit(f"usage: bench {verb} ID")
request["id"] = args[0]
elif verb == "eval":
if len(args) not in (2, 3) or (len(args) == 3 and args[2] != "search"): sys.exit("usage: bench eval ID JS [search]")
request["id"], request["js"] = args[0], args[1]
if len(args) == 3: request["world"] = "search"
elif verb in ("click", "submit", "tap"):
mods = [a for a in args[2:] if a in ("shift", "cmd", "opt")] if verb == "tap" else []
if len(args) != 2 + len(mods): sys.exit(f"usage: bench {verb} ID SELECTOR" + (" [shift] [cmd] [opt]" if verb == "tap" else ""))
request["id"], request["selector"] = args[:2]
if mods: request["mods"] = mods
elif verb == "type":
if len(args) != 3: sys.exit("usage: bench type ID SELECTOR TEXT")
request["id"], request["selector"], request["text"] = args
elif verb == "shot":
if not args: sys.exit("usage: bench shot ID [PATH] [WIDTH]")
request["id"] = args[0]
if len(args) > 1: request["path"] = os.path.abspath(args[1])
if len(args) > 2: request["width"] = float(args[2])
elif verb == "sitesearch":
request["action"] = args[0] if args else "state"
if len(args) == 3 and args[0] == "learn":
request["page"], request["description"] = args[1], args[2]
elif verb == "ui":
if len(args) != 2: sys.exit("usage: bench ui KEY VALUE")
key, value = args
if key == "side" and value not in ("left", "right"): sys.exit("usage: bench ui side left|right")
request[key] = value if key in ("look", "edittab", "peeklink", "import", "side") else value in ("on", "true", "1", "yes")
elif verb in ("ext-add", "ext-press", "ext-remove", "ext-reload"):
if len(args) != 1: sys.exit(f"usage: bench {verb} ID")
request["id"] = args[0]
request["yes"] = yes
elif verb == "capture":
pass
elif verb == "capture-stop":
if len(args) != 1: sys.exit("usage: bench capture-stop ID")
request["id"] = args[0]
elif verb == "ext-enable":
if len(args) != 2: sys.exit("usage: bench ext-enable ID on|off")
request["id"] = args[0]
request["on"] = args[1] in ("on", "true", "1", "yes")
elif verb == "ext-pin":
if not 1 <= len(args) <= 2: sys.exit("usage: bench ext-pin ID [on|off]")
request["id"] = args[0]
request["on"] = (args[1] if len(args) > 1 else "on") in ("on", "true", "1", "yes")
elif verb == "ext-answer":
if len(args) != 1 or args[0] not in ("yes", "no", "ask"): sys.exit("usage: bench ext-answer yes|no|ask")
request["answer"] = args[0]
elif verb == "ext-shot":
if len(args) != 2: sys.exit("usage: bench ext-shot ID PATH")
request["id"] = args[0]
request["path"] = os.path.abspath(args[1])
elif verb == "ext-menu":
if len(args) != 1: sys.exit("usage: bench ext-menu PATH")
request["path"] = os.path.abspath(args[0])
elif verb == "ext-folder":
if len(args) != 1: sys.exit("usage: bench ext-folder PATH")
request["path"] = os.path.abspath(args[0])
request["yes"] = yes
elif verb == "ext-popup":
if len(args) != 2: sys.exit("usage: bench ext-popup ID JS")
request["id"], request["js"] = args
elif verb == "ext-page":
if not args: sys.exit("usage: bench ext-page ID [PATH]")
request["id"] = args[0]
request["path"] = args[1] if len(args) > 1 else ""
elif verb == "press":
if len(args) < 2: sys.exit("usage: bench press KEYCODE CHARS [cmd] [shift] [ctrl] [opt]")
request["code"], request["chars"], request["mods"] = int(args[0]), args[1], args[2:]
elif verb == "key":
if len(args) != 2: sys.exit("usage: bench key ID TEXT")
request["id"], request["text"] = args
elif verb == "resize":
if len(args) not in (2, 3): sys.exit("usage: bench resize WIDTH HEIGHT [STEPS]")
request["width"], request["height"] = float(args[0]), float(args[1])
if len(args) == 3: request["steps"] = int(args[2])
elif verb == "strip":
if not 1 <= len(args) <= 2: sys.exit("usage: bench strip PATH [WIDTH]")
request["path"] = os.path.abspath(args[0])
if len(args) == 2: request["width"] = float(args[1])
elif verb == "consent":
if len(args) > 1 or (args and args[0] not in ("grant", "revoke")): sys.exit("usage: bench consent [grant|revoke]")
if args: request["action"] = args[0]
elif verb == "fold":
if not 1 <= len(args) <= 2: sys.exit("usage: bench fold PATH [WIDTH]")
request["path"] = os.path.abspath(args[0])
if len(args) == 2: request["width"] = float(args[1])
elif verb == "little":
if len(args) != 1: sys.exit("usage: bench little URL|keep|close")
request["what"] = args[0]
elif verb == "site":
if not 1 <= len(args) <= 2: sys.exit("usage: bench site PATH [security]")
request["path"] = os.path.abspath(args[0])
if len(args) == 2: request["security"] = args[1] == "security"
elif verb == "column":
if not 1 <= len(args) <= 2: sys.exit("usage: bench column PATH [HEIGHT]")
request["path"] = os.path.abspath(args[0])
if len(args) == 2: request["height"] = float(args[1])
elif verb == "space":
if args and args[0] == "new":
fresh = args[-1] == "fresh"
request["action"], request["name"], request["fresh"] = "new", " ".join(args[1:-1] if fresh else args[1:]) or "Test", fresh
elif args and args[0] == "go" and len(args) == 2: request["action"], request["index"] = "go", int(args[1])
elif args and args[0] == "delete": request["action"] = "delete"
elif args and args[0] == "swipe" and len(args) == 2: request["action"], request["dx"] = "swipe", float(args[1])
elif args and args[0] == "hold" and len(args) == 2: request["action"], request["dx"] = "hold", float(args[1])
elif args and args[0] == "release": request["action"] = "release"
elif args and args[0] == "move" and len(args) == 2: request["action"], request["index"] = "move", int(args[1])
elif args: sys.exit("usage: bench space [new NAME | go N | delete | swipe DX | hold DX | release | move N]")
elif verb == "split":
if not args: args = ["state"]
action, values = args[0], args[1:]
actions = {
"new-tab": "newTab",
"state": "state", "start": "start", "pair": "pair", "focus": "focus",
"select": "select", "detach": "detach", "fraction": "fraction", "close": "close",
"move": "move", "drop": "drop", "open": "open", "private": "private",
"bench": "bench", "sleep": "sleep", "reopen": "reopen", "insert": "insert",
"save": "save", "space": "space", "mouse": "mouse",
}
if action not in actions:
sys.exit("usage: bench split state|start|pair DRAGGED TARGET left|right|focus ID|select ID|detach ID|fraction UUID VALUE|close ID|new-tab|move ID INDEX|drop ID [BEFORE]|open URL [FROM_ID] [background]|private|bench URL|sleep ID|save|space new NAME|space go UUID")
request["action"] = actions[action]
if action == "pair":
if len(values) != 3 or values[2] not in ("left", "right"): sys.exit("usage: bench split pair DRAGGED TARGET left|right")
request["id"], request["with"], request["side"] = values
elif action in ("focus", "select", "detach", "close", "sleep"):
if len(values) != 1: sys.exit(f"usage: bench split {action} ID")
request["id"] = values[0]
elif action == "fraction":
if len(values) != 2: sys.exit("usage: bench split fraction UUID VALUE")
request["split"], request["fraction"] = values[0], float(values[1])
elif action == "move":
if len(values) != 2: sys.exit("usage: bench split move ID INDEX")
request["id"], request["to"] = values[0], int(values[1])
elif action == "drop":
if not 1 <= len(values) <= 2: sys.exit("usage: bench split drop ID [BEFORE]")
request["id"] = values[0]
if len(values) == 2: request["before"] = values[1]
elif action == "open":
if not 1 <= len(values) <= 4:
sys.exit("usage: bench split open URL [FROM_ID] [background] [end]")
request["url"] = values[0]
extra = values[1:]
flags = [value for value in extra if value in ("background", "end")]
sources = [value for value in extra if value not in ("background", "end")]
if len(sources) > 1 or len(flags) + len(sources) != len(extra):
sys.exit("usage: bench split open URL [FROM_ID] [background] [end]")
if sources: request["from"] = sources[0]
request["foreground"] = "background" not in flags
request["atEnd"] = "end" in flags
elif action == "bench":
if len(values) != 1: sys.exit("usage: bench split bench URL")
request["url"] = values[0]
elif action == "insert":
if len(values) != 2: sys.exit("usage: bench split insert URL INDEX")
request["url"], request["index"] = values[0], int(values[1])
elif action == "space":
if values and values[0] == "new" and len(values) >= 2:
request["spaceAction"], request["name"] = "new", " ".join(values[1:])
elif values and values[0] == "go" and len(values) == 2:
request["spaceAction"], request["spaceID"] = "go", values[1]
else: sys.exit("usage: bench split space new NAME|go UUID")
elif action == "mouse":
if len(values) < 4 or len(values) % 2: sys.exit("usage: bench split mouse X1 Y1 X2 Y2 [X3 Y3 ...]")
request["points"] = [[float(values[i]), float(values[i + 1])] for i in range(0, len(values), 2)]
elif values:
sys.exit(f"usage: bench split {action}")
elif verb == "hit":
if len(args) not in (2, 3) or (len(args) == 3 and args[2] not in ("double", "middle")): sys.exit("usage: bench hit X Y [double|middle]")
request["x"], request["y"] = float(args[0]), float(args[1])
if len(args) == 3: request[args[2]] = True
elif verb == "place":
if len(args) != 2: sys.exit("usage: bench place ID INDEX")
request["id"], request["to"] = args[0], int(args[1])
elif verb == "group":
if args and args[0] == "fold" and len(args) == 2: request["fold"], request["name"] = True, args[1]
elif args and args[0] == "close" and len(args) == 2: request["close"], request["name"] = True, args[1]
elif len(args) >= 2 and args[1] == "new": request["id"], request["new"], request["name"] = args[0], True, " ".join(args[2:])
elif len(args) >= 2: request["id"], request["name"] = args[0], " ".join(args[1:])
elif args: sys.exit("usage: bench group [ID new [NAME] | ID NAME | ID none | fold NAME | close NAME]")
elif verb == "tospace":
if len(args) != 2: sys.exit("usage: bench tospace ID N")
request["id"], request["index"] = args[0], int(args[1])
elif verb == "field":
if not args or any(a not in ("type", "go") for a in args[1:]): sys.exit("usage: bench field TEXT [type] [go]")
request["text"] = args[0]
request["type"] = "type" in args[1:]
request["go"] = "go" in args[1:]
elif verb == "bookmark":
if len(args) not in (1, 2) or (len(args) == 2 and args[1] != "new"): sys.exit("usage: bench bookmark URL [new]")
request["url"] = args[0]
request["new"] = len(args) == 2
elif verb == "import":
kinds = ("bookmarks", "history", "passwords", "spaces")
if "--replace" in args:
request["replace"] = True
args = [a for a in args if a != "--replace"]
if "--profile" in args:
at = args.index("--profile")
if at + 1 >= len(args): sys.exit("usage: bench import BROWSER [WHAT…] --profile NAME|all")
request["profile"] = args[at + 1]
args = args[:at] + args[at + 2:]
if not args or any(a not in kinds for a in args[1:]): sys.exit("usage: bench import BROWSER [bookmarks] [history] [passwords] [spaces] [--profile NAME|all]")
request["from"] = args[0]
request["what"] = args[1:]
elif verb in ("import-file", "import-file-start"):
if len(args) != 1: sys.exit(f"usage: bench {verb} PATH")
request["path"] = os.path.abspath(args[0])
elif verb in ("import-file-status", "import-file-cancel"):
if args: sys.exit(f"usage: bench {verb}")
elif verb == "menu":
if args not in ([], ["open"]): sys.exit("usage: bench menu [open]")
request["open"] = args == ["open"]
elif verb == "keyeq":
if len(args) < 2 or any(a != "shift" for a in args[2:]): sys.exit("usage: bench keyeq CHARS CODE [shift]")
request["chars"], request["code"], request["mods"] = args[0], int(args[1]), args[2:]
elif verb == "news":
if not args or (args[0] != "on" and len(args) > 2): sys.exit("usage: bench news PATH [notes] | news on")
if args[0] == "on": request["on"] = True
else:
request["path"] = os.path.abspath(args[0])
request["notes"] = args[1:] == ["notes"]
elif verb == "towindow":
if len(args) not in (2, 4) or (len(args) == 4 and args[1] != "drop"): sys.exit("usage: bench towindow ID N|new|drop X Y")
request["id"] = args[0]
if args[1] == "drop": request["at"] = [float(args[2]), float(args[3])]
elif args[1] != "new": request["to"] = int(args[1])
elif verb == "windows":
if not args: args = ["list"]
if args[0] not in ("list", "new", "close", "reopen", "frame", "front", "link") or (args[0] in ("close", "frame", "front", "link") and len(args) < 2):
sys.exit("usage: bench windows [list|new|close N|reopen|frame N X Y W H|front N|link URL]")
request["action"] = args[0]
if args[0] == "link": request["url"] = args[1]
elif len(args) > 1: request["n"] = int(args[1])
if args[0] == "frame": request["frame"] = [float(v) for v in args[2:6]]
elif verb == "middle":
if len(args) != 3: sys.exit("usage: bench middle ID X Y")
request["id"], request["x"], request["y"] = args[0], float(args[1]), float(args[2])
elif verb == "pin":
if len(args) not in (1, 2) or args[1:] not in ([], ["off"], ["home"]): sys.exit("usage: bench pin ID [off|home]")
request["id"] = args[0]
request["off"] = args[1:] == ["off"]
request["home"] = args[1:] == ["home"]
elif verb == "fill":
if len(args) > 1: sys.exit("usage: bench fill [STEPS]")
if args: request["steps"] = int(args[0])
elif verb == "peek":
if len(args) != 1: sys.exit("usage: bench peek URL|close")
request["url"] = args[0]
elif verb == "pull":
if len(args) not in (1, 2, 3): sys.exit("usage: bench pull DX [STEPS] [MS]")
request["dx"] = float(args[0])
if len(args) > 1: request["steps"] = int(args[1])
if len(args) > 2: request["ms"] = float(args[2])
elif verb == "window":
pass
elif verb == "pages":
if len(args) != 1 or args[0] not in ("on", "off"): sys.exit("usage: bench pages on|off")
request["on"] = args[0] == "on"
elif verb == "picture":
if len(args) != 1: sys.exit("usage: bench picture PATH")
request["path"] = os.path.abspath(args[0])
elif verb == "float":
usage = "usage: bench float screens [two] | place X Y | swipe DX DY [STEPS] [MS] | wheel DX DY | click"
if not args or args[0] not in ("screens", "place", "swipe", "wheel", "click", "state"): sys.exit(usage)
request["action"] = args[0]
if args[0] == "screens":
request["two"] = args[1:] == ["two"]
else:
try: request["args"] = [float(a) for a in args[1:]]
except ValueError: sys.exit(usage)
elif verb == "film":
if len(args) not in (2, 3, 4) or args[0] not in ("peek", "unpeek", "fold", "none", "float", "land"):
sys.exit("usage: bench film peek|unpeek|fold|none|float|land PATH [FRAMES] [EVERY]")
request["action"], request["path"] = args[0], os.path.abspath(args[1])
if args[0] in ("float", "land"):
if len(args) > 2: request["seconds"] = float(args[2])
else:
if len(args) > 2: request["frames"] = int(args[2])
if len(args) > 3: request["every"] = float(args[3])
elif verb == "image-data":
if len(args) not in (1, 2): sys.exit("usage: bench image-data URL [ID]")
request["url"] = args[0]
if len(args) == 2: request["id"] = args[1]
elif verb == "find":
if not args: sys.exit("usage: bench find TEXT [next N] [back N] [case] [words]")
request["text"] = args[0]
rest = args[1:]
while rest:
word = rest.pop(0)
if word in ("next", "back") and rest and rest[0].isdigit(): request[word] = int(rest.pop(0))
elif word == "case": request["case"] = True
elif word == "words": request["words"] = True
else: sys.exit("usage: bench find TEXT [next N] [back N] [case] [words]")
elif verb == "ai":
usage = "usage: bench ai mock URL | key PROVIDER KEY | ask PROVIDER MODEL TEXT | read ID | check TEXT | signin | engine [PATH MODELS] | use PROVIDER [MODEL] | summarize ID | question ID TEXT | agree | state | picture DIR"
if not args: sys.exit(usage)
request["action"] = args[0]
if args[0] == "mock" and len(args) == 2: request["url"] = args[1]
elif args[0] == "key" and len(args) == 3: request["provider"], request["key"] = args[1], args[2]
elif args[0] == "ask" and len(args) == 4: request["provider"], request["model"], request["text"] = args[1], args[2], args[3]
elif args[0] == "read" and len(args) == 2: request["id"] = args[1]
elif args[0] == "signin" and len(args) == 1: pass
elif args[0] == "engine" and len(args) in (1, 3):
if len(args) == 3: request["path"], request["models"] = args[1], args[2]
elif args[0] == "use" and len(args) in (2, 3):
request["provider"] = args[1]
if len(args) == 3: request["model"] = args[2]
elif args[0] == "summarize" and len(args) == 2: request["id"] = args[1]
elif args[0] == "question" and len(args) == 3: request["id"], request["text"] = args[1], args[2]
elif args[0] in ("agree", "state") and len(args) == 1: pass
elif args[0] == "picture" and len(args) == 2: request["path"] = args[1]
elif args[0] == "check" and len(args) == 2: request["text"] = args[1]
else: sys.exit(usage)
elif verb == "answer":
if args not in (["once"], ["always"], ["no"]): sys.exit("usage: bench answer once|always|no")
request["with"] = args[0]
elif verb == "notifications":
pass
elif verb == "accounts":
if len(args) not in (0, 1) or (args and not args[0].isdigit()): sys.exit("usage: bench accounts [ROW]")
if args: request["pick"] = int(args[0])
elif verb not in ("tabs", "probe", "extensions", "import-preview", "quit"):
sys.exit(f"unknown command “{verb}” — see bench help")
answer = ask(os.path.join(folder(world), "bench.sock"), request)
# A test run that let a window onto a screen has failed, whatever it was doing.
if answer.get("onScreen"):
print("error: a window came onto a screen in this test run — stop the probe: " + "; ".join(answer["onScreen"]), file=sys.stderr)
sys.exit(3)
if "error" in answer:
sys.exit("error: " + str(answer["error"]))
if verb == "text":
print(answer.get("text", ""))
if answer.get("truncated"):
print("\n[… truncated]", file=sys.stderr)
elif verb == "shot":
print(answer["path"])
elif verb in ("open", "ext-page"):
print(answer["id"])
elif verb in ("eval", "ext-popup"):
value = answer.get("value")
print(value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, indent=2))
elif verb == "tabs":
for tab in answer.get("tabs", []):
mark = "⚗" if tab.get("bench") else ("●" if tab.get("active") else " ")
state = " …" if tab.get("loading") else (" z" if tab.get("asleep") else "")
group = f" [{clean(tab['group'])}]" if tab.get("group") else ""
print(f"{mark} {tab['id']} {clean(tab.get('title')) or '—'} {clean(tab.get('url'))}{state}{group}")
else:
print(json.dumps(answer, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))