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.
- {pctr-0.3.5 → pctr-0.4.0}/LICENSE +1 -1
- {pctr-0.3.5/src/pctr.egg-info → pctr-0.4.0}/PKG-INFO +56 -3
- {pctr-0.3.5 → pctr-0.4.0}/README.md +49 -0
- {pctr-0.3.5 → pctr-0.4.0}/pyproject.toml +5 -3
- pctr-0.4.0/src/pctr/__init__.py +1 -0
- {pctr-0.3.5 → pctr-0.4.0}/src/pctr/cli.py +226 -8
- {pctr-0.3.5 → pctr-0.4.0}/src/pctr/data/AGENTS.md +3 -0
- {pctr-0.3.5 → pctr-0.4.0}/src/pctr/data/SKILL.md +52 -5
- {pctr-0.3.5 → pctr-0.4.0/src/pctr.egg-info}/PKG-INFO +56 -3
- {pctr-0.3.5 → pctr-0.4.0}/src/pctr.egg-info/requires.txt +7 -1
- pctr-0.3.5/src/pctr/__init__.py +0 -1
- {pctr-0.3.5 → pctr-0.4.0}/setup.cfg +0 -0
- {pctr-0.3.5 → pctr-0.4.0}/src/pctr/__main__.py +0 -0
- {pctr-0.3.5 → pctr-0.4.0}/src/pctr/mcp_server.py +0 -0
- {pctr-0.3.5 → pctr-0.4.0}/src/pctr/py.typed +0 -0
- {pctr-0.3.5 → pctr-0.4.0}/src/pctr.egg-info/SOURCES.txt +0 -0
- {pctr-0.3.5 → pctr-0.4.0}/src/pctr.egg-info/dependency_links.txt +0 -0
- {pctr-0.3.5 → pctr-0.4.0}/src/pctr.egg-info/entry_points.txt +0 -0
- {pctr-0.3.5 → pctr-0.4.0}/src/pctr.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pctr
|
|
3
|
-
Version: 0.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
-
|
|
368
|
-
|
|
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.
|
|
422
|
+
pydirectinput.FAILSAFE = False
|
|
423
|
+
try:
|
|
424
|
+
pydirectinput.PAUSE = 0.01
|
|
425
|
+
except Exception:
|
|
426
|
+
pass
|
|
371
427
|
return pydirectinput
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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[
|
|
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.
|
|
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.
|
pctr-0.3.5/src/pctr/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.5"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|