pctr 0.1.0__tar.gz → 0.2.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.
- {pctr-0.1.0/src/pctr.egg-info → pctr-0.2.0}/PKG-INFO +1 -1
- {pctr-0.1.0 → pctr-0.2.0}/pyproject.toml +4 -1
- {pctr-0.1.0 → pctr-0.2.0}/src/pctr/cli.py +268 -0
- pctr-0.2.0/src/pctr/data/AGENTS.md +17 -0
- pctr-0.2.0/src/pctr/data/SKILL.md +81 -0
- {pctr-0.1.0 → pctr-0.2.0/src/pctr.egg-info}/PKG-INFO +1 -1
- {pctr-0.1.0 → pctr-0.2.0}/src/pctr.egg-info/SOURCES.txt +3 -1
- {pctr-0.1.0 → pctr-0.2.0}/LICENSE +0 -0
- {pctr-0.1.0 → pctr-0.2.0}/README.md +0 -0
- {pctr-0.1.0 → pctr-0.2.0}/setup.cfg +0 -0
- {pctr-0.1.0 → pctr-0.2.0}/src/pctr/__init__.py +0 -0
- {pctr-0.1.0 → pctr-0.2.0}/src/pctr/__main__.py +0 -0
- {pctr-0.1.0 → pctr-0.2.0}/src/pctr.egg-info/dependency_links.txt +0 -0
- {pctr-0.1.0 → pctr-0.2.0}/src/pctr.egg-info/entry_points.txt +0 -0
- {pctr-0.1.0 → pctr-0.2.0}/src/pctr.egg-info/requires.txt +0 -0
- {pctr-0.1.0 → pctr-0.2.0}/src/pctr.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pctr
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Element-based Windows UI Automation CLI for AI agents. Find controls by name/type/id and click, type, hold, drag, and screenshot - no pixel coordinates.
|
|
5
5
|
Author: Space-lab515
|
|
6
6
|
License-Expression: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pctr"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Element-based Windows UI Automation CLI for AI agents. Find controls by name/type/id and click, type, hold, drag, and screenshot - no pixel coordinates."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -36,3 +36,6 @@ pctr = "pctr.cli:main"
|
|
|
36
36
|
|
|
37
37
|
[tool.setuptools.packages.find]
|
|
38
38
|
where = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.package-data]
|
|
41
|
+
pctr = ["data/*.md"]
|
|
@@ -314,6 +314,223 @@ def cmd_drag(args):
|
|
|
314
314
|
print("dragged %d,%d -> %d,%d" % (x1, y1, x2, y2))
|
|
315
315
|
|
|
316
316
|
|
|
317
|
+
_OCR_PS = r'''
|
|
318
|
+
param([string]$Path)
|
|
319
|
+
Add-Type -AssemblyName System.Runtime.WindowsRuntime
|
|
320
|
+
$asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0]
|
|
321
|
+
function Await($op, $type) {
|
|
322
|
+
$m = $asTaskGeneric.MakeGenericMethod($type)
|
|
323
|
+
$t = $m.Invoke($null, @($op))
|
|
324
|
+
$t.Wait(-1) | Out-Null
|
|
325
|
+
$t.Result
|
|
326
|
+
}
|
|
327
|
+
$null = [Windows.Storage.StorageFile,Windows.Storage,ContentType=WindowsRuntime]
|
|
328
|
+
$null = [Windows.Media.Ocr.OcrEngine,Windows.Foundation,ContentType=WindowsRuntime]
|
|
329
|
+
$null = [Windows.Graphics.Imaging.BitmapDecoder,Windows.Foundation,ContentType=WindowsRuntime]
|
|
330
|
+
$file = Await ([Windows.Storage.StorageFile]::GetFileFromPathAsync($Path)) ([Windows.Storage.StorageFile])
|
|
331
|
+
$stream = Await ($file.OpenAsync([Windows.Storage.FileAccessMode]::Read)) ([Windows.Storage.Streams.IRandomAccessStream])
|
|
332
|
+
$decoder = Await ([Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($stream)) ([Windows.Graphics.Imaging.BitmapDecoder])
|
|
333
|
+
$bitmap = Await ($decoder.GetSoftwareBitmapAsync()) ([Windows.Graphics.Imaging.SoftwareBitmap])
|
|
334
|
+
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()
|
|
335
|
+
if (-not $engine) {
|
|
336
|
+
$null = [Windows.Globalization.Language,Windows.Foundation,ContentType=WindowsRuntime]
|
|
337
|
+
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromLanguage((New-Object Windows.Globalization.Language 'en-US'))
|
|
338
|
+
}
|
|
339
|
+
if (-not $engine) { Write-Error 'no OCR engine available'; exit 2 }
|
|
340
|
+
$res = Await ($engine.RecognizeAsync($bitmap)) ([Windows.Media.Ocr.OcrResult])
|
|
341
|
+
$out = foreach ($line in $res.Lines) { foreach ($w in $line.Words) { [pscustomobject]@{ text = $w.Text; x = [int]$w.BoundingRect.X; y = [int]$w.BoundingRect.Y; w = [int]$w.BoundingRect.Width; h = [int]$w.BoundingRect.Height } } }
|
|
342
|
+
ConvertTo-Json -Compress -InputObject @($out)
|
|
343
|
+
'''
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _windows_ocr(image_path):
|
|
347
|
+
import json
|
|
348
|
+
import os
|
|
349
|
+
import subprocess
|
|
350
|
+
import tempfile
|
|
351
|
+
script = os.path.join(tempfile.gettempdir(), "pctr_ocr.ps1")
|
|
352
|
+
with open(script, "w", encoding="utf-8") as f:
|
|
353
|
+
f.write(_OCR_PS)
|
|
354
|
+
out = subprocess.run(
|
|
355
|
+
["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script, "-Path", image_path],
|
|
356
|
+
capture_output=True, text=True,
|
|
357
|
+
)
|
|
358
|
+
txt = (out.stdout or "").strip()
|
|
359
|
+
if not txt:
|
|
360
|
+
raise SystemExit("OCR failed: " + (out.stderr or "").strip()[:400])
|
|
361
|
+
data = json.loads(txt, strict=False)
|
|
362
|
+
if isinstance(data, dict):
|
|
363
|
+
data = [data]
|
|
364
|
+
return [w for w in data if w and w.get("text")]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _ocr_capture(args):
|
|
368
|
+
import os
|
|
369
|
+
import tempfile
|
|
370
|
+
import pyautogui
|
|
371
|
+
path = os.path.join(tempfile.gettempdir(), "pctr_ocr.png")
|
|
372
|
+
ox = oy = 0
|
|
373
|
+
title = getattr(args, "title", None)
|
|
374
|
+
if title:
|
|
375
|
+
win = _find_window(title, getattr(args, "process", None), getattr(args, "timeout", 5.0))
|
|
376
|
+
r = win.rectangle()
|
|
377
|
+
img = pyautogui.screenshot(region=(r.left, r.top, r.width(), r.height()))
|
|
378
|
+
ox, oy = r.left, r.top
|
|
379
|
+
else:
|
|
380
|
+
img = pyautogui.screenshot()
|
|
381
|
+
img.save(path)
|
|
382
|
+
return _windows_ocr(path), ox, oy
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def cmd_ocr(args):
|
|
386
|
+
words, ox, oy = _ocr_capture(args)
|
|
387
|
+
for w in words:
|
|
388
|
+
print("%s | %d,%d %dx%d" % (w["text"], ox + w["x"], oy + w["y"], w["w"], w["h"]))
|
|
389
|
+
print("(%d words)" % len(words))
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def cmd_ocrfind(args):
|
|
393
|
+
import re
|
|
394
|
+
words, ox, oy = _ocr_capture(args)
|
|
395
|
+
rx = re.compile(args.text, re.I)
|
|
396
|
+
hits = [w for w in words if rx.search(w["text"])]
|
|
397
|
+
for i, w in enumerate(hits):
|
|
398
|
+
print("[%d] %s | center=%d,%d" % (i, w["text"], ox + w["x"] + w["w"] // 2, oy + w["y"] + w["h"] // 2))
|
|
399
|
+
if not hits:
|
|
400
|
+
print("no OCR match for %r" % args.text)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def cmd_ocrclick(args):
|
|
404
|
+
import re
|
|
405
|
+
import pyautogui
|
|
406
|
+
words, ox, oy = _ocr_capture(args)
|
|
407
|
+
rx = re.compile(args.text, re.I)
|
|
408
|
+
hits = [w for w in words if rx.search(w["text"])]
|
|
409
|
+
if not hits:
|
|
410
|
+
raise SystemExit("no OCR match for %r" % args.text)
|
|
411
|
+
idx = getattr(args, "nth", 0)
|
|
412
|
+
if idx >= len(hits):
|
|
413
|
+
raise SystemExit("matched %d word(s); nth=%d out of range" % (len(hits), idx))
|
|
414
|
+
w = hits[idx]
|
|
415
|
+
cx = ox + w["x"] + w["w"] // 2
|
|
416
|
+
cy = oy + w["y"] + w["h"] // 2
|
|
417
|
+
pyautogui.FAILSAFE = False
|
|
418
|
+
pyautogui.moveTo(cx, cy, duration=0.0)
|
|
419
|
+
pyautogui.click()
|
|
420
|
+
print("ocr-clicked %r at %d,%d" % (w["text"], cx, cy))
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _look_detect(args):
|
|
424
|
+
import os
|
|
425
|
+
import tempfile
|
|
426
|
+
import pyautogui
|
|
427
|
+
try:
|
|
428
|
+
from ultralytics.models import YOLOWorld
|
|
429
|
+
except Exception as e:
|
|
430
|
+
raise SystemExit("YOLO-World needs ultralytics: pip install ultralytics (%s)" % e)
|
|
431
|
+
path = os.path.join(tempfile.gettempdir(), "pctr_look.png")
|
|
432
|
+
ox = oy = 0
|
|
433
|
+
title = getattr(args, "title", None)
|
|
434
|
+
if title:
|
|
435
|
+
win = _find_window(title, getattr(args, "process", None), getattr(args, "timeout", 5.0))
|
|
436
|
+
r = win.rectangle()
|
|
437
|
+
img = pyautogui.screenshot(region=(r.left, r.top, r.width(), r.height()))
|
|
438
|
+
ox, oy = r.left, r.top
|
|
439
|
+
else:
|
|
440
|
+
img = pyautogui.screenshot()
|
|
441
|
+
img.save(path)
|
|
442
|
+
model = YOLOWorld(getattr(args, "model", None) or "yolov8s-worldv2.pt")
|
|
443
|
+
model.set_classes([args.for_])
|
|
444
|
+
conf = args.conf if getattr(args, "conf", None) is not None else 0.35
|
|
445
|
+
results = model.predict(path, conf=conf, agnostic_nms=True, verbose=False)
|
|
446
|
+
r0 = results[0]
|
|
447
|
+
names = r0.names
|
|
448
|
+
boxes = r0.boxes
|
|
449
|
+
dets = []
|
|
450
|
+
if boxes is None:
|
|
451
|
+
return dets
|
|
452
|
+
for i in range(len(boxes)):
|
|
453
|
+
x1, y1, x2, y2 = [float(v) for v in boxes.xyxy[i].cpu().numpy()]
|
|
454
|
+
dets.append({
|
|
455
|
+
"label": names.get(int(boxes.cls[i]), str(int(boxes.cls[i]))) if isinstance(names, dict) else str(int(boxes.cls[i])),
|
|
456
|
+
"conf": float(boxes.conf[i].cpu().numpy()),
|
|
457
|
+
"x": int(ox + (x1 + x2) / 2),
|
|
458
|
+
"y": int(oy + (y1 + y2) / 2),
|
|
459
|
+
"box": (int(ox + x1), int(oy + y1), int(ox + x2), int(oy + y2)),
|
|
460
|
+
})
|
|
461
|
+
dets.sort(key=lambda d: -d["conf"])
|
|
462
|
+
return dets
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def cmd_look(args):
|
|
466
|
+
dets = _look_detect(args)
|
|
467
|
+
for i, d in enumerate(dets):
|
|
468
|
+
print("[%d] %s %.2f | center=%d,%d | box=%d,%d,%d,%d" % (i, d["label"], d["conf"], d["x"], d["y"], *d["box"]))
|
|
469
|
+
if not dets:
|
|
470
|
+
print("nothing found for %r" % args.for_)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def cmd_lookclick(args):
|
|
474
|
+
import pyautogui
|
|
475
|
+
dets = _look_detect(args)
|
|
476
|
+
if not dets:
|
|
477
|
+
raise SystemExit("nothing found for %r" % args.for_)
|
|
478
|
+
idx = getattr(args, "nth", 0)
|
|
479
|
+
if idx >= len(dets):
|
|
480
|
+
raise SystemExit("found %d; nth=%d out of range" % (len(dets), idx))
|
|
481
|
+
d = dets[idx]
|
|
482
|
+
pyautogui.FAILSAFE = False
|
|
483
|
+
pyautogui.moveTo(d["x"], d["y"], duration=0.0)
|
|
484
|
+
pyautogui.click()
|
|
485
|
+
print("look-clicked %r (%.2f) at %d,%d" % (d["label"], d["conf"], d["x"], d["y"]))
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
SETUP_TARGETS = {
|
|
489
|
+
"opencode": (".config/opencode/skills/pctr", "SKILL.md"),
|
|
490
|
+
"claude": (".claude/skills/pctr", "SKILL.md"),
|
|
491
|
+
"agents": (".agents/skills/pctr", "SKILL.md"),
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _data_text(name):
|
|
496
|
+
from importlib import resources
|
|
497
|
+
return resources.files("pctr").joinpath("data", name).read_text(encoding="utf-8")
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def cmd_skill(args):
|
|
501
|
+
if getattr(args, "install", False):
|
|
502
|
+
return cmd_setup(args)
|
|
503
|
+
print(_data_text("SKILL.md"))
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def cmd_setup(args):
|
|
507
|
+
import os
|
|
508
|
+
from pathlib import Path
|
|
509
|
+
skill = _data_text("SKILL.md")
|
|
510
|
+
frag = _data_text("AGENTS.md")
|
|
511
|
+
home = Path.home()
|
|
512
|
+
targets = list(SETUP_TARGETS) if getattr(args, "target", "all") in (None, "all") else [args.target]
|
|
513
|
+
for t in targets:
|
|
514
|
+
rel, fname = SETUP_TARGETS[t]
|
|
515
|
+
d = home / rel
|
|
516
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
517
|
+
(d / fname).write_text(skill, encoding="utf-8")
|
|
518
|
+
print("installed skill: %s" % (d / fname))
|
|
519
|
+
proj = Path(getattr(args, "project", None) or os.getcwd())
|
|
520
|
+
ag = proj / "AGENTS.md"
|
|
521
|
+
existing = ag.read_text(encoding="utf-8", errors="replace") if ag.exists() else ""
|
|
522
|
+
marker = "<!-- pctr -->"
|
|
523
|
+
if marker in existing:
|
|
524
|
+
print("AGENTS.md already has a pctr section: %s" % ag)
|
|
525
|
+
else:
|
|
526
|
+
with open(ag, "a", encoding="utf-8") as f:
|
|
527
|
+
if existing and not existing.endswith("\n"):
|
|
528
|
+
f.write("\n")
|
|
529
|
+
f.write("\n" + frag + "\n")
|
|
530
|
+
print("appended pctr section to: %s" % ag)
|
|
531
|
+
print("done. restart your agent so it loads the skill.")
|
|
532
|
+
|
|
533
|
+
|
|
317
534
|
def add_target(p, require_name=False):
|
|
318
535
|
p.add_argument("--title", required=require_name, help="Window title regex")
|
|
319
536
|
p.add_argument("--process", type=int, default=None, help="Filter by process id")
|
|
@@ -445,6 +662,57 @@ def build_parser():
|
|
|
445
662
|
dr.add_argument("--direct", action="store_true")
|
|
446
663
|
dr.set_defaults(func=cmd_drag)
|
|
447
664
|
|
|
665
|
+
oc = sub.add_parser("ocr", help="OCR the screen (or a window) and list words with boxes")
|
|
666
|
+
oc.add_argument("--title", default=None)
|
|
667
|
+
oc.add_argument("--process", type=int, default=None)
|
|
668
|
+
oc.add_argument("--timeout", type=float, default=5.0)
|
|
669
|
+
oc.set_defaults(func=cmd_ocr)
|
|
670
|
+
|
|
671
|
+
ocf = sub.add_parser("ocrfind", help="OCR then list words matching --text (with click centers)")
|
|
672
|
+
ocf.add_argument("--title", default=None)
|
|
673
|
+
ocf.add_argument("--process", type=int, default=None)
|
|
674
|
+
ocf.add_argument("--timeout", type=float, default=5.0)
|
|
675
|
+
ocf.add_argument("--text", required=True)
|
|
676
|
+
ocf.set_defaults(func=cmd_ocrfind)
|
|
677
|
+
|
|
678
|
+
occ = sub.add_parser("ocrclick", help="OCR then click the word matching --text")
|
|
679
|
+
occ.add_argument("--title", default=None)
|
|
680
|
+
occ.add_argument("--process", type=int, default=None)
|
|
681
|
+
occ.add_argument("--timeout", type=float, default=5.0)
|
|
682
|
+
occ.add_argument("--text", required=True)
|
|
683
|
+
occ.add_argument("--nth", type=int, default=0)
|
|
684
|
+
occ.set_defaults(func=cmd_ocrclick)
|
|
685
|
+
|
|
686
|
+
lk = sub.add_parser("look", help="Open-vocab detect objects by text prompt (YOLO-World)")
|
|
687
|
+
lk.add_argument("--for", dest="for_", required=True, help="object to look for, e.g. 'save button'")
|
|
688
|
+
lk.add_argument("--title", default=None)
|
|
689
|
+
lk.add_argument("--process", type=int, default=None)
|
|
690
|
+
lk.add_argument("--timeout", type=float, default=5.0)
|
|
691
|
+
lk.add_argument("--conf", type=float, default=None)
|
|
692
|
+
lk.add_argument("--model", default=None)
|
|
693
|
+
lk.set_defaults(func=cmd_look)
|
|
694
|
+
|
|
695
|
+
lkc = sub.add_parser("lookclick", help="Open-vocab detect then click the top match")
|
|
696
|
+
lkc.add_argument("--for", dest="for_", required=True, help="object to look for")
|
|
697
|
+
lkc.add_argument("--title", default=None)
|
|
698
|
+
lkc.add_argument("--process", type=int, default=None)
|
|
699
|
+
lkc.add_argument("--timeout", type=float, default=5.0)
|
|
700
|
+
lkc.add_argument("--conf", type=float, default=None)
|
|
701
|
+
lkc.add_argument("--model", default=None)
|
|
702
|
+
lkc.add_argument("--nth", type=int, default=0)
|
|
703
|
+
lkc.set_defaults(func=cmd_lookclick)
|
|
704
|
+
|
|
705
|
+
sk = sub.add_parser("skill", help="Print the agent skill (SKILL.md), or --install it")
|
|
706
|
+
sk.add_argument("--install", action="store_true")
|
|
707
|
+
sk.add_argument("--target", default="all", choices=["all", "opencode", "claude", "agents"])
|
|
708
|
+
sk.add_argument("--project", default=None)
|
|
709
|
+
sk.set_defaults(func=cmd_skill)
|
|
710
|
+
|
|
711
|
+
st = sub.add_parser("setup", help="Install the pctr skill for your agents + append to AGENTS.md")
|
|
712
|
+
st.add_argument("--target", default="all", choices=["all", "opencode", "claude", "agents"])
|
|
713
|
+
st.add_argument("--project", default=None)
|
|
714
|
+
st.set_defaults(func=cmd_setup)
|
|
715
|
+
|
|
448
716
|
return ap
|
|
449
717
|
|
|
450
718
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!-- pctr -->
|
|
2
|
+
## Desktop automation (pctr)
|
|
3
|
+
|
|
4
|
+
`pctr` is installed. Use it to drive Windows GUI apps by **element name**, not
|
|
5
|
+
pixel coordinates:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pctr windows # list windows
|
|
9
|
+
pctr tree --title "App" --limit 100 # inspect controls
|
|
10
|
+
pctr click --title "App" --name "Save"
|
|
11
|
+
pctr set --title "App" --name "Search" --text "hello"
|
|
12
|
+
pctr ocrfind --text "Sign in" # fallback: read text off the screen
|
|
13
|
+
pctr look --for "a red error icon" # fallback: open-vocab object detection
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Prefer `set` over `type` for fields. Global `type`/`keys` need `pctr focus` first.
|
|
17
|
+
Run `pctr -h` for all commands. Full skill: `pctr skill`.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pctr
|
|
3
|
+
description: Drive native Windows desktop apps (not browsers) with the pctr CLI powered by pywinauto/UIA, with OCR and YOLO-World fallbacks. Use whenever you must control or inspect a desktop GUI - list windows, dump a control tree, click buttons, type into fields, read on-screen text by OCR, find objects by description, send hotkeys, hold keys, drag, or screenshot an app. Element-based, no pixel hunting. Triggers: pctr, UIA, UI automation, pywinauto, desktop automation, click a button in an app, type into an app, drive a GUI, control a Windows application, usecomputer.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# pctr - desktop control
|
|
7
|
+
|
|
8
|
+
Element-based Windows automation. Find a control by its name / control type /
|
|
9
|
+
automation id and act on it, so window moves, DPI, and layout changes don't break
|
|
10
|
+
you. Windows only (UI Automation). Built on `pywinauto` + `uiautomation`, with
|
|
11
|
+
`pyautogui` / `pydirectinput` for raw mouse and keyboard.
|
|
12
|
+
|
|
13
|
+
## Three ways to find a target
|
|
14
|
+
|
|
15
|
+
1. **UIA elements** (best) - `tree` / `find` / `click` by control name/id.
|
|
16
|
+
2. **OCR** - `ocr` / `ocrfind` / `ocrclick` (Windows built-in `Windows.Media.Ocr`, no deps) reads on-screen text that isn't exposed as a control.
|
|
17
|
+
3. **YOLO-World** - `look --for "..."` / `lookclick --for "..."` open-vocabulary object detection by natural-language prompt (canvas, games, custom-drawn UI).
|
|
18
|
+
|
|
19
|
+
## The loop
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pctr windows # top-level windows + pids
|
|
23
|
+
pctr tree --title "Notepad" --limit 120 # dump the control tree
|
|
24
|
+
pctr find --title "Notepad" --name "Submit"
|
|
25
|
+
pctr click --title "Notepad" --name "^File$" --control-type MenuItem
|
|
26
|
+
pctr shot --title "Notepad" --out "C:\temp\np.png"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Look at `tree`/`find` output first - the name/type/id it prints is what you pass
|
|
30
|
+
back as `--name`, `--control-type`, `--auto-id`.
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
| Command | What it does |
|
|
35
|
+
|---------|--------------|
|
|
36
|
+
| `windows [--filter RE]` | List top-level windows. |
|
|
37
|
+
| `tree --title RE [--limit N]` | Dump the UIA control tree. |
|
|
38
|
+
| `find --title RE --name RE [--control-type T] [--auto-id ID]` | List matching elements. |
|
|
39
|
+
| `click --title RE --name RE [--method auto\|invoke\|mouse] [--dbl]` | Click an element. |
|
|
40
|
+
| `set --title RE --name RE --text S` | Set a field via the UIA ValuePattern (exact, instant). |
|
|
41
|
+
| `type [--title RE --name RE] --text S [--delay 0.03] [--chunk 1]` | Type literal text. |
|
|
42
|
+
| `keys --keys "{ENTER}"` | Global key combo (pywinauto send_keys syntax). |
|
|
43
|
+
| `hotkey --keys win+shift+s` | Modifier combo. |
|
|
44
|
+
| `focus --title RE` | Bring a window to the foreground. |
|
|
45
|
+
| `wait --title RE --name RE [--timeout S]` | Wait for an element. |
|
|
46
|
+
| `shot [--title RE] --out PATH` | Screenshot the screen or a window. |
|
|
47
|
+
| `size` | Print primary screen size as WxH. |
|
|
48
|
+
| `move --x N --y N` / `down` / `up` / `hold --ms N` | Raw mouse control. |
|
|
49
|
+
| `drag --start x,y --end x,y [--duration S]` | Drag between points. |
|
|
50
|
+
| `keydown / keyup --key a` / `keyhold --key w --ms 1500` | Hold keyboard keys. |
|
|
51
|
+
| `ocr [--title RE]` | OCR the screen (or window); list words with boxes. |
|
|
52
|
+
| `ocrfind --text RE` | OCR then list matches with click centers. |
|
|
53
|
+
| `ocrclick --text RE [--nth N]` | OCR then click a word. |
|
|
54
|
+
| `look --for "a dog" [--title RE] [--conf X]` | YOLO-World detect objects by text prompt. |
|
|
55
|
+
| `lookclick --for "a dog" [--nth N]` | Detect then click the top match. |
|
|
56
|
+
| `skill [--install]` / `setup` | Print/install this skill + an AGENTS.md section. |
|
|
57
|
+
|
|
58
|
+
Filters: `--title` (regex), `--name` (regex), `--control-type`, `--auto-id`,
|
|
59
|
+
`--nth`, `--process`, `--timeout`.
|
|
60
|
+
|
|
61
|
+
## Patterns
|
|
62
|
+
|
|
63
|
+
- **Text fields -> use `set`, not `type`.** `set` uses the ValuePattern: atomic, can't drop chars.
|
|
64
|
+
- **`type` is char-by-char** (`--chunk 1 --delay 0.03`); raise `--delay` for Qt/Electron.
|
|
65
|
+
- **Global `type`/`keys` go to the OS-focused window** - `pctr focus --title ...` first.
|
|
66
|
+
- Click falls back to a real mouse click when a control has no invoke pattern.
|
|
67
|
+
- Disambiguate with `--control-type` / `--auto-id` / `--nth`.
|
|
68
|
+
|
|
69
|
+
## App notes
|
|
70
|
+
|
|
71
|
+
- **Qt apps** expose a rich tree including embedded webviews.
|
|
72
|
+
- **Electron apps** (Discord, VS Code, OpenCode) hide web content - use `focus` +
|
|
73
|
+
global `type`/`keys`, verify with `shot` (and fall back to `ocr`/`look`).
|
|
74
|
+
- **Canvas / games / custom UI**: no tree - use `ocr` for text and `look` for objects.
|
|
75
|
+
|
|
76
|
+
## Setup
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install pctr
|
|
80
|
+
pctr setup # installs this skill for opencode / claude / agents + appends to ./AGENTS.md
|
|
81
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pctr
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Element-based Windows UI Automation CLI for AI agents. Find controls by name/type/id and click, type, hold, drag, and screenshot - no pixel coordinates.
|
|
5
5
|
Author: Space-lab515
|
|
6
6
|
License-Expression: MIT
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|