pctr 0.3.5__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Space-lab515
3
+ Copyright (c) 2026 NTUserDev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pctr
3
- Version: 0.3.5
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.
3
+ Version: 0.4.0
4
+ Summary: PCTR - Personal Computer Tactile Response. 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: NTUserDev
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/NTUserDev/pctr
@@ -31,12 +31,18 @@ Provides-Extra: vision
31
31
  Requires-Dist: ultralytics>=8.2; extra == "vision"
32
32
  Provides-Extra: mcp
33
33
  Requires-Dist: mcp>=2; extra == "mcp"
34
+ Provides-Extra: pad
35
+ Requires-Dist: vgamepad>=0.1.0; extra == "pad"
36
+ Provides-Extra: serial
37
+ Requires-Dist: pyserial>=3.5; extra == "serial"
34
38
  Provides-Extra: all
35
- Requires-Dist: pctr[mcp,vision]; extra == "all"
39
+ Requires-Dist: pctr[mcp,pad,serial,vision]; extra == "all"
36
40
  Dynamic: license-file
37
41
 
38
42
  # pctr
39
43
 
44
+ **PCTR — Personal Computer Tactile Response**
45
+
40
46
  Element-based Windows UI Automation CLI for AI agents.
41
47
 
42
48
  Instead of screenshotting and guessing pixel coordinates, `pctr` finds controls
@@ -60,6 +66,8 @@ detect. Recorded with OBS, which pctr also piloted.*
60
66
  pip install pctr # core (UIA + OCR)
61
67
  pip install "pctr[vision]" # + ultralytics, for `pctr look`
62
68
  pip install "pctr[mcp]" # + MCP SDK, for the MCP server
69
+ pip install "pctr[pad]" # + vgamepad, for `pctr pad` (needs ViGEmBus)
70
+ pip install "pctr[serial]" # + pyserial, for `pctr hid` / the serial-hid backend
63
71
  pip install "pctr[all]" # everything
64
72
  ```
65
73
 
@@ -124,6 +132,8 @@ run in the background, reachable at `http://127.0.0.1:8765/mcp` (or `…/sse`).
124
132
  | `pctr ocrclick --text RE [--nth N] [--lang TAG]` | OCR then click a word. |
125
133
  | `pctr look --for "a dog" [--title RE] [--conf X]` | YOLO-World detect objects by text prompt. |
126
134
  | `pctr lookclick --for "a dog" [--nth N]` | Detect then click the top match. |
135
+ | `pctr pad <button\|stick\|trigger\|status>` | Virtual Xbox 360 gamepad via ViGEmBus. |
136
+ | `pctr hid <action> --port COMx` | Serial-HID bridge to a real USB-HID microcontroller. |
127
137
  | `pctr mcp <start\|stop\|restart\|status\|help>` | Manage the MCP server (stdio or background http/sse). |
128
138
  | `pctr skill [--install]` / `pctr setup` | Print/install the agent skill + an AGENTS.md section. |
129
139
 
@@ -190,6 +200,49 @@ pctr desktop move-window --title RE --to N
190
200
  - **`move-window` is blocked on Windows 11** (third-party `MoveWindowToDesktop`
191
201
  returns `Access denied`); it works on Windows 10.
192
202
 
203
+ ## Raw input backends
204
+
205
+ The OS flags `SendInput` events as injected, so anything built on user32 is
206
+ detectable in principle. `pctr` can route raw mouse/keyboard through a different
207
+ backend with `--backend`:
208
+
209
+ | Backend | How | Detectable? |
210
+ |---|---|---|
211
+ | `pyautogui` (default / `sendinput`) | user32 `SendInput`, virtual keys | yes (`injected` flag) |
212
+ | `pydirectinput` (or `--direct`) | `SendInput` with **scancodes** — better for DirectInput games | yes |
213
+ | `serial-hid` | a **microcontroller presenting real USB HID** over serial | no — it *is* real hardware |
214
+
215
+ ```bash
216
+ pctr move --backend pydirectinput --x 800 --y 400
217
+ pctr keyhold --key w --ms 400 --backend pydirectinput
218
+ pctr type --text "hi" --backend serial-hid --port COM5 # needs a board
219
+ ```
220
+
221
+ Applies to `move` / `down` / `up` / `hold` / `keydown` / `keyup` / `keyhold` /
222
+ `hotkey` / `drag` / `type` / `desktop`.
223
+
224
+ ### Gamepad
225
+
226
+ ```bash
227
+ pip install "pctr[pad]" # + the ViGEmBus driver
228
+ pctr pad button --name A --ms 150
229
+ pctr pad button --name DPAD_UP --ms 100
230
+ pctr pad stick --x 1.0 --y -0.5 --ms 400
231
+ pctr pad trigger --right 1.0 --ms 300
232
+ ```
233
+
234
+ ### Serial-HID bridge
235
+
236
+ Firmware protocol, newline-terminated ASCII:
237
+ `MOVE x y` · `CLICK b` · `DOWN b` · `UP b` · `KEY k DOWN|UP` · `WRITE text` · `HOTKEY a+b`
238
+
239
+ ```bash
240
+ pip install "pctr[serial]"
241
+ pctr hid ports # list COM ports
242
+ pctr hid move --x 500 --y 300 --port COM5
243
+ pctr hid type --text "hello" --port COM5
244
+ ```
245
+
193
246
  ## Exit codes
194
247
 
195
248
  - `0` success, `1` no match / window not found, `2` usage/validation error.
@@ -1,5 +1,7 @@
1
1
  # pctr
2
2
 
3
+ **PCTR — Personal Computer Tactile Response**
4
+
3
5
  Element-based Windows UI Automation CLI for AI agents.
4
6
 
5
7
  Instead of screenshotting and guessing pixel coordinates, `pctr` finds controls
@@ -23,6 +25,8 @@ detect. Recorded with OBS, which pctr also piloted.*
23
25
  pip install pctr # core (UIA + OCR)
24
26
  pip install "pctr[vision]" # + ultralytics, for `pctr look`
25
27
  pip install "pctr[mcp]" # + MCP SDK, for the MCP server
28
+ pip install "pctr[pad]" # + vgamepad, for `pctr pad` (needs ViGEmBus)
29
+ pip install "pctr[serial]" # + pyserial, for `pctr hid` / the serial-hid backend
26
30
  pip install "pctr[all]" # everything
27
31
  ```
28
32
 
@@ -87,6 +91,8 @@ run in the background, reachable at `http://127.0.0.1:8765/mcp` (or `…/sse`).
87
91
  | `pctr ocrclick --text RE [--nth N] [--lang TAG]` | OCR then click a word. |
88
92
  | `pctr look --for "a dog" [--title RE] [--conf X]` | YOLO-World detect objects by text prompt. |
89
93
  | `pctr lookclick --for "a dog" [--nth N]` | Detect then click the top match. |
94
+ | `pctr pad <button\|stick\|trigger\|status>` | Virtual Xbox 360 gamepad via ViGEmBus. |
95
+ | `pctr hid <action> --port COMx` | Serial-HID bridge to a real USB-HID microcontroller. |
90
96
  | `pctr mcp <start\|stop\|restart\|status\|help>` | Manage the MCP server (stdio or background http/sse). |
91
97
  | `pctr skill [--install]` / `pctr setup` | Print/install the agent skill + an AGENTS.md section. |
92
98
 
@@ -153,6 +159,49 @@ pctr desktop move-window --title RE --to N
153
159
  - **`move-window` is blocked on Windows 11** (third-party `MoveWindowToDesktop`
154
160
  returns `Access denied`); it works on Windows 10.
155
161
 
162
+ ## Raw input backends
163
+
164
+ The OS flags `SendInput` events as injected, so anything built on user32 is
165
+ detectable in principle. `pctr` can route raw mouse/keyboard through a different
166
+ backend with `--backend`:
167
+
168
+ | Backend | How | Detectable? |
169
+ |---|---|---|
170
+ | `pyautogui` (default / `sendinput`) | user32 `SendInput`, virtual keys | yes (`injected` flag) |
171
+ | `pydirectinput` (or `--direct`) | `SendInput` with **scancodes** — better for DirectInput games | yes |
172
+ | `serial-hid` | a **microcontroller presenting real USB HID** over serial | no — it *is* real hardware |
173
+
174
+ ```bash
175
+ pctr move --backend pydirectinput --x 800 --y 400
176
+ pctr keyhold --key w --ms 400 --backend pydirectinput
177
+ pctr type --text "hi" --backend serial-hid --port COM5 # needs a board
178
+ ```
179
+
180
+ Applies to `move` / `down` / `up` / `hold` / `keydown` / `keyup` / `keyhold` /
181
+ `hotkey` / `drag` / `type` / `desktop`.
182
+
183
+ ### Gamepad
184
+
185
+ ```bash
186
+ pip install "pctr[pad]" # + the ViGEmBus driver
187
+ pctr pad button --name A --ms 150
188
+ pctr pad button --name DPAD_UP --ms 100
189
+ pctr pad stick --x 1.0 --y -0.5 --ms 400
190
+ pctr pad trigger --right 1.0 --ms 300
191
+ ```
192
+
193
+ ### Serial-HID bridge
194
+
195
+ Firmware protocol, newline-terminated ASCII:
196
+ `MOVE x y` · `CLICK b` · `DOWN b` · `UP b` · `KEY k DOWN|UP` · `WRITE text` · `HOTKEY a+b`
197
+
198
+ ```bash
199
+ pip install "pctr[serial]"
200
+ pctr hid ports # list COM ports
201
+ pctr hid move --x 500 --y 300 --port COM5
202
+ pctr hid type --text "hello" --port COM5
203
+ ```
204
+
156
205
  ## Exit codes
157
206
 
158
207
  - `0` success, `1` no match / window not found, `2` usage/validation error.
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pctr"
7
- version = "0.3.5"
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."
7
+ version = "0.4.0"
8
+ description = "PCTR - Personal Computer Tactile Response. 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"
11
11
  license = "MIT"
@@ -35,7 +35,9 @@ dependencies = [
35
35
  [project.optional-dependencies]
36
36
  vision = ["ultralytics>=8.2"]
37
37
  mcp = ["mcp>=2"]
38
- all = ["pctr[vision,mcp]"]
38
+ pad = ["vgamepad>=0.1.0"]
39
+ serial = ["pyserial>=3.5"]
40
+ all = ["pctr[vision,mcp,pad,serial]"]
39
41
 
40
42
  [project.scripts]
41
43
  pctr = "pctr.cli:main"
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0"
@@ -318,8 +318,8 @@ def cmd_type(args):
318
318
  except Exception as e:
319
319
  _fail("type failed (text has pywinauto key syntax like { } + ^ %% ~ ()): %s" % e)
320
320
  else:
321
- import pyautogui
322
- pyautogui.write(args.text, interval=delay)
321
+ m = _mouse_backend(args)
322
+ m.write(args.text, interval=delay)
323
323
  print("typed")
324
324
 
325
325
 
@@ -364,14 +364,78 @@ def cmd_shot(args):
364
364
  print("saved %s (%dx%d)" % (args.out, img.width, img.height))
365
365
 
366
366
 
367
- def _mouse_backend(args):
368
- if getattr(args, "direct", False):
367
+ class _SerialHID:
368
+ """Raw input via a microcontroller presenting a REAL USB HID device.
369
+
370
+ Talks newline-terminated ASCII over a serial port. Firmware protocol:
371
+ MOVE x y ; CLICK b ; DOWN b ; UP b ; KEY k DOWN|UP ; WRITE text ; HOTKEY a+b
372
+ Port from --port or the PCTR_SERIAL_PORT env var.
373
+ """
374
+
375
+ def __init__(self, port=None, baud=115200):
376
+ try:
377
+ import serial
378
+ except Exception as e:
379
+ raise RuntimeError('serial-hid needs pyserial: pip install "pctr[serial]" (%s)' % e)
380
+ port = port or __import__("os").environ.get("PCTR_SERIAL_PORT")
381
+ if not port:
382
+ raise RuntimeError("serial-hid needs a port: pass --port COMx or set PCTR_SERIAL_PORT")
383
+ self._ser = serial.Serial(port, baud, timeout=1)
384
+
385
+ def _send(self, line):
386
+ self._ser.write((line + "\n").encode("utf-8"))
387
+
388
+ def moveTo(self, x, y, duration=0.0):
389
+ self._send("MOVE %d %d" % (int(x), int(y)))
390
+
391
+ def mouseDown(self, button="left"):
392
+ self._send("DOWN %s" % button)
393
+
394
+ def mouseUp(self, button="left"):
395
+ self._send("UP %s" % button)
396
+
397
+ def click(self, button="left"):
398
+ self._send("CLICK %s" % button)
399
+
400
+ def keyDown(self, key):
401
+ self._send("KEY %s DOWN" % key)
402
+
403
+ def keyUp(self, key):
404
+ self._send("KEY %s UP" % key)
405
+
406
+ def write(self, text, interval=0.0):
407
+ self._send("WRITE %s" % text.replace("\r", "").replace("\n", "\\n"))
408
+
409
+ def hotkey(self, *keys):
410
+ self._send("HOTKEY %s" % "+".join(keys))
411
+
412
+
413
+ def _resolve_input_backend(name, port=None):
414
+ name = (name or "auto").lower()
415
+ if name in ("", "auto", "pyautogui", "sendinput"):
416
+ import pyautogui
417
+ pyautogui.FAILSAFE = False
418
+ pyautogui.PAUSE = 0.01
419
+ return pyautogui
420
+ if name in ("pydirectinput", "direct"):
369
421
  import pydirectinput
370
- pydirectinput.PAUSE = 0.01
422
+ pydirectinput.FAILSAFE = False
423
+ try:
424
+ pydirectinput.PAUSE = 0.01
425
+ except Exception:
426
+ pass
371
427
  return pydirectinput
372
- import pyautogui
373
- pyautogui.FAILSAFE = False
374
- return pyautogui
428
+ if name in ("serial", "serial-hid", "hid"):
429
+ return _SerialHID(port)
430
+ raise RuntimeError("unknown backend %r (pyautogui | pydirectinput | serial-hid)" % name)
431
+
432
+
433
+ def _mouse_backend(args):
434
+ name = getattr(args, "backend", None) or ("pydirectinput" if getattr(args, "direct", False) else "auto")
435
+ try:
436
+ return _resolve_input_backend(name, getattr(args, "port", None))
437
+ except RuntimeError as e:
438
+ _fail(str(e))
375
439
 
376
440
 
377
441
  def _parse_pair(text):
@@ -886,6 +950,117 @@ def cmd_lookclick(args):
886
950
  print("look-clicked %r (%.2f) at %d,%d" % (d["label"], d["conf"], d["x"], d["y"]))
887
951
 
888
952
 
953
+ def _pad_button(vg, name):
954
+ key = "XUSB_GAMEPAD_" + name.strip().upper().replace(" ", "_").replace("-", "_")
955
+ return getattr(vg.XUSB_BUTTON, key, None)
956
+
957
+
958
+ def cmd_pad(args):
959
+ action = args.action
960
+ try:
961
+ import vgamepad as vg
962
+ except Exception as e:
963
+ _fail('the pad backend needs vgamepad + the ViGEmBus driver: pip install "pctr[pad]" (%s)' % e)
964
+ if action == "status":
965
+ print("vgamepad %s present; a virtual Xbox 360 pad needs the ViGEmBus driver "
966
+ "(github.com/nefarius/ViGEmBus/releases)" % getattr(vg, "__version__", "?"))
967
+ return
968
+ pad = vg.VX360Gamepad()
969
+ ms = getattr(args, "ms", 200)
970
+ try:
971
+ if action == "button":
972
+ if not args.name:
973
+ _fail("pad button needs --name A|B|X|Y|LB|RB|BACK|START|DPAD_UP|...")
974
+ btn = _pad_button(vg, args.name)
975
+ if btn is None:
976
+ _fail("unknown button %r (try A, B, X, Y, LB, RB, BACK, START, LEFT_THUMB, RIGHT_THUMB, DPAD_UP/DOWN/LEFT/RIGHT)" % args.name)
977
+ pad.press_button(btn)
978
+ pad.update()
979
+ print("pad: %s down" % args.name)
980
+ time.sleep(ms / 1000.0)
981
+ pad.release_button(btn)
982
+ pad.update()
983
+ elif action == "stick":
984
+ side = (args.side or "left").lower()
985
+ if side == "right":
986
+ pad.right_joystick_float(x_value_float=args.x, y_value_float=args.y)
987
+ else:
988
+ pad.left_joystick_float(x_value_float=args.x, y_value_float=args.y)
989
+ pad.update()
990
+ print("pad: %s stick x=%.2f y=%.2f" % (side, args.x, args.y))
991
+ time.sleep(ms / 1000.0)
992
+ pad.left_joystick_float(0.0, 0.0)
993
+ pad.right_joystick_float(0.0, 0.0)
994
+ pad.update()
995
+ elif action == "trigger":
996
+ pad.left_trigger_float(value_float=args.left)
997
+ pad.right_trigger_float(value_float=args.right)
998
+ pad.update()
999
+ print("pad: triggers left=%.2f right=%.2f" % (args.left, args.right))
1000
+ time.sleep(ms / 1000.0)
1001
+ pad.left_trigger_float(0.0)
1002
+ pad.right_trigger_float(0.0)
1003
+ pad.update()
1004
+ else:
1005
+ _fail("unknown pad action %r" % action)
1006
+ finally:
1007
+ try:
1008
+ pad.reset()
1009
+ pad.update()
1010
+ except Exception:
1011
+ pass
1012
+
1013
+
1014
+ def cmd_hid(args):
1015
+ action = args.action
1016
+ try:
1017
+ import serial
1018
+ except Exception as e:
1019
+ _fail('the hid bridge needs pyserial: pip install "pctr[serial]" (%s)' % e)
1020
+ if action == "ports":
1021
+ from serial.tools import list_ports
1022
+ found = list(list_ports.comports())
1023
+ for p in found:
1024
+ print("%s | %s | %s" % (p.device, p.description, p.hwid))
1025
+ if not found:
1026
+ print("(no serial ports)")
1027
+ return
1028
+ import os
1029
+ port = getattr(args, "port", None) or os.environ.get("PCTR_SERIAL_PORT")
1030
+ if not port:
1031
+ _fail("hid needs --port COMx (or set PCTR_SERIAL_PORT)")
1032
+ if action == "move":
1033
+ line = "MOVE %d %d" % (args.x, args.y)
1034
+ elif action == "click":
1035
+ line = "CLICK %s" % args.button
1036
+ elif action == "down":
1037
+ line = "DOWN %s" % args.button
1038
+ elif action == "up":
1039
+ line = "UP %s" % args.button
1040
+ elif action == "key":
1041
+ if not args.key:
1042
+ _fail("hid key needs --key a")
1043
+ line = "KEY %s %s" % (args.key, "DOWN" if getattr(args, "down", False) else "UP")
1044
+ elif action == "type":
1045
+ line = "WRITE %s" % (args.text or "").replace("\n", "\\n")
1046
+ elif action == "hotkey":
1047
+ if not args.keys:
1048
+ _fail("hid hotkey needs --keys ctrl+s")
1049
+ line = "HOTKEY %s" % args.keys
1050
+ elif action == "raw":
1051
+ if not args.line:
1052
+ _fail("hid raw needs --line '...'")
1053
+ line = args.line
1054
+ else:
1055
+ _fail("unknown hid action %r" % action)
1056
+ ser = serial.Serial(port, getattr(args, "baud", 115200), timeout=1)
1057
+ try:
1058
+ ser.write((line + "\n").encode("utf-8"))
1059
+ finally:
1060
+ ser.close()
1061
+ print("hid -> %s (%s)" % (line, port))
1062
+
1063
+
889
1064
  SETUP_TARGETS = {
890
1065
  "opencode": (".config/opencode/skills/pctr", "SKILL.md"),
891
1066
  "claude": (".claude/skills/pctr", "SKILL.md"),
@@ -1164,6 +1339,13 @@ def _add_lang(p):
1164
1339
  p.add_argument("--lang", default=None, help="OCR language BCP-47 tag, e.g. en-US or de-DE")
1165
1340
 
1166
1341
 
1342
+ def _add_input(p):
1343
+ p.add_argument("--backend", default=None,
1344
+ choices=["pyautogui", "sendinput", "pydirectinput", "direct", "serial-hid", "hid"],
1345
+ help="Input backend (default pyautogui; --direct == pydirectinput; serial-hid needs --port)")
1346
+ p.add_argument("--port", default=None, help="Serial port for the serial-hid backend, e.g. COM5")
1347
+
1348
+
1167
1349
  def build_parser():
1168
1350
  ap = argparse.ArgumentParser(prog="pctr", description=DESCRIPTION, formatter_class=argparse.RawDescriptionHelpFormatter)
1169
1351
  ap.add_argument("--version", action="version", version="pctr %s" % _VERSION)
@@ -1215,6 +1397,7 @@ def build_parser():
1215
1397
  ty.add_argument("--delay", type=float, default=0.03, help="Seconds between chars/chunks")
1216
1398
  ty.add_argument("--chunk", type=int, default=1, help="Chunk size when typing into an element")
1217
1399
  ty.add_argument("--text", required=True)
1400
+ _add_input(ty)
1218
1401
  ty.set_defaults(func=cmd_type)
1219
1402
 
1220
1403
  k = sub.add_parser("keys", help="Send a key combo globally")
@@ -1243,16 +1426,19 @@ def build_parser():
1243
1426
  mv.add_argument("--y", type=int, required=True)
1244
1427
  mv.add_argument("--duration", type=float, default=0.0)
1245
1428
  mv.add_argument("--direct", action="store_true", help="Use pydirectinput (games)")
1429
+ _add_input(mv)
1246
1430
  mv.set_defaults(func=cmd_move)
1247
1431
 
1248
1432
  dn = sub.add_parser("down", help="Press and hold a mouse button")
1249
1433
  dn.add_argument("--button", default="left")
1250
1434
  dn.add_argument("--direct", action="store_true")
1435
+ _add_input(dn)
1251
1436
  dn.set_defaults(func=cmd_down)
1252
1437
 
1253
1438
  up = sub.add_parser("up", help="Release a mouse button")
1254
1439
  up.add_argument("--button", default="left")
1255
1440
  up.add_argument("--direct", action="store_true")
1441
+ _add_input(up)
1256
1442
  up.set_defaults(func=cmd_up)
1257
1443
 
1258
1444
  ho = sub.add_parser("hold", help="Press, hold for --ms, then release")
@@ -1261,27 +1447,32 @@ def build_parser():
1261
1447
  ho.add_argument("--x", type=int, default=None)
1262
1448
  ho.add_argument("--y", type=int, default=None)
1263
1449
  ho.add_argument("--direct", action="store_true")
1450
+ _add_input(ho)
1264
1451
  ho.set_defaults(func=cmd_hold)
1265
1452
 
1266
1453
  kd = sub.add_parser("keydown", help="Press and hold a keyboard key")
1267
1454
  kd.add_argument("--key", required=True)
1268
1455
  kd.add_argument("--direct", action="store_true")
1456
+ _add_input(kd)
1269
1457
  kd.set_defaults(func=cmd_keydown)
1270
1458
 
1271
1459
  ku = sub.add_parser("keyup", help="Release a keyboard key")
1272
1460
  ku.add_argument("--key", required=True)
1273
1461
  ku.add_argument("--direct", action="store_true")
1462
+ _add_input(ku)
1274
1463
  ku.set_defaults(func=cmd_keyup)
1275
1464
 
1276
1465
  kh = sub.add_parser("keyhold", help="Press, hold for --ms, then release a key")
1277
1466
  kh.add_argument("--key", required=True)
1278
1467
  kh.add_argument("--ms", type=int, default=1000)
1279
1468
  kh.add_argument("--direct", action="store_true")
1469
+ _add_input(kh)
1280
1470
  kh.set_defaults(func=cmd_keyhold)
1281
1471
 
1282
1472
  hk = sub.add_parser("hotkey", help="Send a key combo, e.g. --keys win+shift+s")
1283
1473
  hk.add_argument("--keys", required=True, help="Combo joined by +, e.g. ctrl+shift+t")
1284
1474
  hk.add_argument("--direct", action="store_true")
1475
+ _add_input(hk)
1285
1476
  hk.set_defaults(func=cmd_hotkey)
1286
1477
 
1287
1478
  sz = sub.add_parser("size", help="Print primary screen size as WxH")
@@ -1296,6 +1487,7 @@ def build_parser():
1296
1487
  dp.add_argument("--desktop", type=int, default=None, help="Filter `windows` to this desktop index")
1297
1488
  dp.add_argument("--to", type=int, default=None, help="Target desktop index for move-window")
1298
1489
  _add_json(dp)
1490
+ _add_input(dp)
1299
1491
  dp.set_defaults(func=cmd_desktop)
1300
1492
 
1301
1493
  dr = sub.add_parser("drag", help="Drag from --start to --end (x,y pairs)")
@@ -1304,6 +1496,7 @@ def build_parser():
1304
1496
  dr.add_argument("--button", default="left")
1305
1497
  dr.add_argument("--duration", type=float, default=0.3)
1306
1498
  dr.add_argument("--direct", action="store_true")
1499
+ _add_input(dr)
1307
1500
  dr.set_defaults(func=cmd_drag)
1308
1501
 
1309
1502
  oc = sub.add_parser("ocr", help="OCR the screen (or a window) and list words with boxes")
@@ -1372,6 +1565,31 @@ def build_parser():
1372
1565
  mc.add_argument("--foreground", action="store_true", help="Run http/sse in the foreground")
1373
1566
  mc.set_defaults(func=cmd_mcp)
1374
1567
 
1568
+ pd = sub.add_parser("pad", help="Virtual Xbox 360 gamepad via ViGEmBus: button / stick / trigger / status")
1569
+ pd.add_argument("action", choices=["button", "stick", "trigger", "status"])
1570
+ pd.add_argument("--name", default=None, help="Button: A, B, X, Y, LB, RB, BACK, START, LEFT_THUMB, DPAD_UP, ...")
1571
+ pd.add_argument("--side", choices=["left", "right"], default="left", help="Stick side")
1572
+ pd.add_argument("--x", type=float, default=0.0, help="Stick X (-1..1)")
1573
+ pd.add_argument("--y", type=float, default=0.0, help="Stick Y (-1..1)")
1574
+ pd.add_argument("--left", type=float, default=0.0, help="Left trigger (0..1)")
1575
+ pd.add_argument("--right", type=float, default=0.0, help="Right trigger (0..1)")
1576
+ pd.add_argument("--ms", type=int, default=200, help="How long to hold the input")
1577
+ pd.set_defaults(func=cmd_pad)
1578
+
1579
+ hd = sub.add_parser("hid", help="Serial-HID bridge: drive a USB-HID microcontroller (rawest input)")
1580
+ hd.add_argument("action", choices=["ports", "move", "click", "down", "up", "key", "type", "hotkey", "raw"])
1581
+ hd.add_argument("--port", default=None, help="Serial port, e.g. COM5 (or PCTR_SERIAL_PORT)")
1582
+ hd.add_argument("--baud", type=int, default=115200)
1583
+ hd.add_argument("--x", type=int, default=0)
1584
+ hd.add_argument("--y", type=int, default=0)
1585
+ hd.add_argument("--button", default="left")
1586
+ hd.add_argument("--key", default=None)
1587
+ hd.add_argument("--down", action="store_true", help="Key down (default: up)")
1588
+ hd.add_argument("--text", default="")
1589
+ hd.add_argument("--keys", default=None)
1590
+ hd.add_argument("--line", default=None, help="Raw protocol line for `hid raw`")
1591
+ hd.set_defaults(func=cmd_hid)
1592
+
1375
1593
  return ap
1376
1594
 
1377
1595
 
@@ -23,6 +23,9 @@ pctr look --for "a red error icon" # fallback: open-vocab object detection
23
23
  - `--json` (global or per-command) gives machine-readable output for `windows/tree/find/ocr/ocrfind/look/desktop`; progress goes to stderr so stdout stays clean.
24
24
  - `--file FILE` runs a batch of commands (one per line, `#` comments, stops on first error).
25
25
  - `pctr ocr --lang de-DE` picks the OCR language (BCP-47).
26
+ - Raw input backends: `--backend pyautogui|pydirectinput|serial-hid` (also `--direct`). `serial-hid` drives a real USB-HID microcontroller over `--port COMx` - the OS sees genuine hardware, so there is no "injected" flag.
27
+ - Gamepad: `pctr pad button|stick|trigger|status` (needs `pip install "pctr[pad]"` + the ViGEmBus driver).
28
+ - Hardware HID bridge: `pctr hid ports|move|click|down|up|key|type|hotkey|raw --port COMx` (needs `pip install "pctr[serial]"`).
26
29
 
27
30
  Run `pctr -h` for all commands. Full skill: `pctr skill`.
28
31
  MCP: `pip install "pctr[mcp]"`, then `pctr mcp start` (background http) or register `pctr-mcp` as a stdio server. Manage with `pctr mcp start|stop|restart|status|help`.
@@ -5,10 +5,11 @@ description: Drive native Windows desktop apps (not browsers) with the pctr CLI
5
5
 
6
6
  # pctr - desktop control
7
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.
8
+ **PCTR — Personal Computer Tactile Response.** Element-based Windows automation.
9
+ Find a control by its name / control type / automation id and act on it, so
10
+ window moves, DPI, and layout changes don't break you. Windows only (UI
11
+ Automation). Built on `pywinauto` + `uiautomation`, with `pyautogui` /
12
+ `pydirectinput` for raw mouse and keyboard.
12
13
 
13
14
  ## Three ways to find a target
14
15
 
@@ -54,6 +55,8 @@ back as `--name`, `--control-type`, `--auto-id`.
54
55
  | `ocrclick --text RE [--nth N] [--lang TAG]` | OCR then click a word. |
55
56
  | `look --for "a dog" [--title RE] [--conf X]` | YOLO-World detect objects by text prompt. |
56
57
  | `lookclick --for "a dog" [--nth N]` | Detect then click the top match. |
58
+ | `pad <button\|stick\|trigger\|status>` | Virtual Xbox 360 gamepad via ViGEmBus (`pctr[pad]`). |
59
+ | `hid <action> --port COMx` | Serial-HID bridge to a microcontroller that presents real USB HID. |
57
60
  | `mcp <start\|stop\|restart\|status\|help>` | Manage the MCP server (stdio or background http/sse). |
58
61
  | `skill [--install]` / `setup` | Print/install this skill + an AGENTS.md section. |
59
62
 
@@ -104,6 +107,48 @@ pctr --json --file script.txt # --json applies to every command in the file
104
107
  Stops on the first failing line and reports its line number. Each line is parsed
105
108
  exactly like a normal `pctr ...` invocation (same flags, same quoting rules).
106
109
 
110
+ ### Input backends
111
+
112
+ Raw mouse/keyboard commands take `--backend`:
113
+
114
+ - `pyautogui` (default) / `sendinput` — virtual keys via user32 `SendInput`.
115
+ - `pydirectinput` (also `--direct`) — scancodes; better for DirectInput games.
116
+ - `serial-hid` — talk to a **microcontroller presenting a real USB HID device**
117
+ (`--port COMx` or `PCTR_SERIAL_PORT`). Rawest option: the OS sees a genuine
118
+ keyboard/mouse, so there's no "injected" flag to detect.
119
+
120
+ Applies to `move` / `down` / `up` / `hold` / `keydown` / `keyup` / `keyhold` /
121
+ `hotkey` / `drag` / `type` / `desktop`.
122
+
123
+ ```bash
124
+ pctr move --backend pydirectinput --x 800 --y 400
125
+ pctr keyhold --key w --ms 400 --backend pydirectinput
126
+ pctr type --text "hello" --backend serial-hid --port COM5
127
+ ```
128
+
129
+ Gamepads (virtual Xbox 360 pad, needs the ViGEmBus driver):
130
+
131
+ ```bash
132
+ pip install "pctr[pad]"
133
+ pctr pad button --name A --ms 150
134
+ pctr pad button --name DPAD_UP --ms 100
135
+ pctr pad stick --x 1.0 --y -0.5 --ms 400
136
+ pctr pad trigger --right 1.0 --ms 300
137
+ pctr pad status
138
+ ```
139
+
140
+ Serial-HID firmware protocol (newline-terminated ASCII on the board):
141
+ `MOVE x y` · `CLICK b` · `DOWN b` · `UP b` · `KEY k DOWN|UP` · `WRITE text` · `HOTKEY a+b`
142
+
143
+ ```bash
144
+ pip install "pctr[serial]"
145
+ pctr hid ports # list COM ports
146
+ pctr hid move --x 500 --y 300 --port COM5
147
+ pctr hid key --key a --down --port COM5
148
+ pctr hid type --text "hello" --port COM5
149
+ pctr hid raw --line "..." --port COM5
150
+ ```
151
+
107
152
  ## Patterns
108
153
 
109
154
  - **Text fields -> use `set`, not `type`.** `set` uses the ValuePattern: atomic, exact, can't drop chars. `type`'s element path goes through pywinauto `type_keys`, which interprets `{ } + ^ % ~ ( )` as key syntax - so `--text "a{b}"` is **not** literal and an unmatched `{` fails. Prefer `set`; use `type` only for plain text on fields with no ValuePattern.
@@ -173,6 +218,8 @@ pctr mcp help
173
218
  pip install pctr # core (UIA + OCR)
174
219
  pip install "pctr[vision]" # + ultralytics, for `pctr look`
175
220
  pip install "pctr[mcp]" # + MCP SDK, for `pctr-mcp` / `pctr mcp`
176
- pip install "pctr[all]" # all of the above
221
+ pip install "pctr[pad]" # + vgamepad, for `pctr pad` (needs ViGEmBus)
222
+ pip install "pctr[serial]" # + pyserial, for `pctr hid` / the serial-hid backend
223
+ pip install "pctr[all]" # everything
177
224
  pctr setup # installs this skill for opencode / claude / agents + appends to ./AGENTS.md
178
225
  ```
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pctr
3
- Version: 0.3.5
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.
3
+ Version: 0.4.0
4
+ Summary: PCTR - Personal Computer Tactile Response. 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: NTUserDev
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/NTUserDev/pctr
@@ -31,12 +31,18 @@ Provides-Extra: vision
31
31
  Requires-Dist: ultralytics>=8.2; extra == "vision"
32
32
  Provides-Extra: mcp
33
33
  Requires-Dist: mcp>=2; extra == "mcp"
34
+ Provides-Extra: pad
35
+ Requires-Dist: vgamepad>=0.1.0; extra == "pad"
36
+ Provides-Extra: serial
37
+ Requires-Dist: pyserial>=3.5; extra == "serial"
34
38
  Provides-Extra: all
35
- Requires-Dist: pctr[mcp,vision]; extra == "all"
39
+ Requires-Dist: pctr[mcp,pad,serial,vision]; extra == "all"
36
40
  Dynamic: license-file
37
41
 
38
42
  # pctr
39
43
 
44
+ **PCTR — Personal Computer Tactile Response**
45
+
40
46
  Element-based Windows UI Automation CLI for AI agents.
41
47
 
42
48
  Instead of screenshotting and guessing pixel coordinates, `pctr` finds controls
@@ -60,6 +66,8 @@ detect. Recorded with OBS, which pctr also piloted.*
60
66
  pip install pctr # core (UIA + OCR)
61
67
  pip install "pctr[vision]" # + ultralytics, for `pctr look`
62
68
  pip install "pctr[mcp]" # + MCP SDK, for the MCP server
69
+ pip install "pctr[pad]" # + vgamepad, for `pctr pad` (needs ViGEmBus)
70
+ pip install "pctr[serial]" # + pyserial, for `pctr hid` / the serial-hid backend
63
71
  pip install "pctr[all]" # everything
64
72
  ```
65
73
 
@@ -124,6 +132,8 @@ run in the background, reachable at `http://127.0.0.1:8765/mcp` (or `…/sse`).
124
132
  | `pctr ocrclick --text RE [--nth N] [--lang TAG]` | OCR then click a word. |
125
133
  | `pctr look --for "a dog" [--title RE] [--conf X]` | YOLO-World detect objects by text prompt. |
126
134
  | `pctr lookclick --for "a dog" [--nth N]` | Detect then click the top match. |
135
+ | `pctr pad <button\|stick\|trigger\|status>` | Virtual Xbox 360 gamepad via ViGEmBus. |
136
+ | `pctr hid <action> --port COMx` | Serial-HID bridge to a real USB-HID microcontroller. |
127
137
  | `pctr mcp <start\|stop\|restart\|status\|help>` | Manage the MCP server (stdio or background http/sse). |
128
138
  | `pctr skill [--install]` / `pctr setup` | Print/install the agent skill + an AGENTS.md section. |
129
139
 
@@ -190,6 +200,49 @@ pctr desktop move-window --title RE --to N
190
200
  - **`move-window` is blocked on Windows 11** (third-party `MoveWindowToDesktop`
191
201
  returns `Access denied`); it works on Windows 10.
192
202
 
203
+ ## Raw input backends
204
+
205
+ The OS flags `SendInput` events as injected, so anything built on user32 is
206
+ detectable in principle. `pctr` can route raw mouse/keyboard through a different
207
+ backend with `--backend`:
208
+
209
+ | Backend | How | Detectable? |
210
+ |---|---|---|
211
+ | `pyautogui` (default / `sendinput`) | user32 `SendInput`, virtual keys | yes (`injected` flag) |
212
+ | `pydirectinput` (or `--direct`) | `SendInput` with **scancodes** — better for DirectInput games | yes |
213
+ | `serial-hid` | a **microcontroller presenting real USB HID** over serial | no — it *is* real hardware |
214
+
215
+ ```bash
216
+ pctr move --backend pydirectinput --x 800 --y 400
217
+ pctr keyhold --key w --ms 400 --backend pydirectinput
218
+ pctr type --text "hi" --backend serial-hid --port COM5 # needs a board
219
+ ```
220
+
221
+ Applies to `move` / `down` / `up` / `hold` / `keydown` / `keyup` / `keyhold` /
222
+ `hotkey` / `drag` / `type` / `desktop`.
223
+
224
+ ### Gamepad
225
+
226
+ ```bash
227
+ pip install "pctr[pad]" # + the ViGEmBus driver
228
+ pctr pad button --name A --ms 150
229
+ pctr pad button --name DPAD_UP --ms 100
230
+ pctr pad stick --x 1.0 --y -0.5 --ms 400
231
+ pctr pad trigger --right 1.0 --ms 300
232
+ ```
233
+
234
+ ### Serial-HID bridge
235
+
236
+ Firmware protocol, newline-terminated ASCII:
237
+ `MOVE x y` · `CLICK b` · `DOWN b` · `UP b` · `KEY k DOWN|UP` · `WRITE text` · `HOTKEY a+b`
238
+
239
+ ```bash
240
+ pip install "pctr[serial]"
241
+ pctr hid ports # list COM ports
242
+ pctr hid move --x 500 --y 300 --port COM5
243
+ pctr hid type --text "hello" --port COM5
244
+ ```
245
+
193
246
  ## Exit codes
194
247
 
195
248
  - `0` success, `1` no match / window not found, `2` usage/validation error.
@@ -7,10 +7,16 @@ pydirectinput>=1.0.4
7
7
  pillow>=9.0
8
8
 
9
9
  [all]
10
- pctr[mcp,vision]
10
+ pctr[mcp,pad,serial,vision]
11
11
 
12
12
  [mcp]
13
13
  mcp>=2
14
14
 
15
+ [pad]
16
+ vgamepad>=0.1.0
17
+
18
+ [serial]
19
+ pyserial>=3.5
20
+
15
21
  [vision]
16
22
  ultralytics>=8.2
@@ -1 +0,0 @@
1
- __version__ = "0.3.5"
File without changes
File without changes
File without changes
File without changes
File without changes