pctr 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pctr-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Space-lab515
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pctr-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: pctr
3
+ Version: 0.1.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.
5
+ Author: Space-lab515
6
+ License-Expression: MIT
7
+ Keywords: automation,windows,uia,pywinauto,desktop,agent,computer-use,rpa
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Win32 (MS Windows)
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Classifier: Topic :: System :: Networking
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: pywinauto>=0.6.8; platform_system == "Windows"
23
+ Requires-Dist: uiautomation>=2.0; platform_system == "Windows"
24
+ Requires-Dist: pyautogui>=0.9.54; platform_system == "Windows"
25
+ Requires-Dist: pydirectinput>=1.0.4; platform_system == "Windows"
26
+ Dynamic: license-file
27
+
28
+ # pctr
29
+
30
+ Element-based Windows UI Automation CLI for AI agents.
31
+
32
+ Instead of screenshotting and guessing pixel coordinates, `pctr` finds controls
33
+ by their **name / control type / automation id** and acts on them. Window moves,
34
+ DPI changes, and layout shifts don't break your script.
35
+
36
+ Built on `pywinauto` (UI Automation) with `pyautogui` / `pydirectinput` for raw
37
+ mouse and keyboard input.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install pctr
43
+ ```
44
+
45
+ Windows only.
46
+
47
+ ## Commands
48
+
49
+ | Command | What it does |
50
+ |---------|--------------|
51
+ | `pctr windows [--filter RE]` | List top-level windows (`pid=… \| title`). |
52
+ | `pctr tree --title RE [--limit N]` | Dump the UIA control tree of a window. |
53
+ | `pctr find --title RE --name RE [--control-type T] [--auto-id ID]` | List matching elements. |
54
+ | `pctr click --title RE --name RE [--method auto\|invoke\|mouse] [--dbl]` | Click an element. |
55
+ | `pctr set --title RE --name RE --text S` | Set a field via the UIA ValuePattern. |
56
+ | `pctr type [--title RE --name RE] --text S [--delay 0.03] [--chunk 1]` | Type literal text. |
57
+ | `pctr keys --keys "{ENTER}"` | Send a global key combo. |
58
+ | `pctr hotkey --keys win+shift+s` | Send a modifier combo. |
59
+ | `pctr focus --title RE` | Bring a window to the foreground. |
60
+ | `pctr wait --title RE --name RE [--timeout S]` | Wait until an element exists. |
61
+ | `pctr shot [--title RE] --out PATH` | Screenshot the screen or a window. |
62
+ | `pctr size` | Print primary screen size as `WxH`. |
63
+ | `pctr move --x N --y N` | Move the cursor. |
64
+ | `pctr down / up [--button left]` | Press / release a mouse button. |
65
+ | `pctr hold --ms 1000 [--x N --y N]` | Press, hold, release. |
66
+ | `pctr drag --start x,y --end x,y [--duration S]` | Drag between two points. |
67
+ | `pctr keydown / keyup --key a` | Hold / release a keyboard key. |
68
+ | `pctr keyhold --key w --ms 1500` | Hold a key for a duration. |
69
+
70
+ Common filters: `--title` (regex), `--name` (regex), `--control-type`
71
+ (`Button`, `Edit`, `Pane`, `MenuItem`, …), `--auto-id`, `--nth`, `--process`.
72
+
73
+ ## The loop
74
+
75
+ ```bash
76
+ pctr tree --title "Notepad" --limit 60 # see what's clickable
77
+ pctr click --title "Notepad" --name "^File$"
78
+ pctr set --title "Notepad" --name "Text Editor" --text "hello"
79
+ ```
80
+
81
+ ## Notes
82
+
83
+ - Prefer `set` (ValuePattern) over `type` for text fields - it can't drop or
84
+ reorder characters.
85
+ - `type` is sent in small chunks (`--chunk 1 --delay 0.03`); raise the delay for
86
+ Qt/Electron apps that garble fast input.
87
+ - Global `type` / `keys` go to the OS-focused window - `pctr focus` first.
88
+ - Qt apps expose a rich tree including embedded webviews. Electron apps expose
89
+ only the window frame unless accessibility is enabled (use `focus` + `keys`).
90
+
91
+ ## License
92
+
93
+ MIT
pctr-0.1.0/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # pctr
2
+
3
+ Element-based Windows UI Automation CLI for AI agents.
4
+
5
+ Instead of screenshotting and guessing pixel coordinates, `pctr` finds controls
6
+ by their **name / control type / automation id** and acts on them. Window moves,
7
+ DPI changes, and layout shifts don't break your script.
8
+
9
+ Built on `pywinauto` (UI Automation) with `pyautogui` / `pydirectinput` for raw
10
+ mouse and keyboard input.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install pctr
16
+ ```
17
+
18
+ Windows only.
19
+
20
+ ## Commands
21
+
22
+ | Command | What it does |
23
+ |---------|--------------|
24
+ | `pctr windows [--filter RE]` | List top-level windows (`pid=… \| title`). |
25
+ | `pctr tree --title RE [--limit N]` | Dump the UIA control tree of a window. |
26
+ | `pctr find --title RE --name RE [--control-type T] [--auto-id ID]` | List matching elements. |
27
+ | `pctr click --title RE --name RE [--method auto\|invoke\|mouse] [--dbl]` | Click an element. |
28
+ | `pctr set --title RE --name RE --text S` | Set a field via the UIA ValuePattern. |
29
+ | `pctr type [--title RE --name RE] --text S [--delay 0.03] [--chunk 1]` | Type literal text. |
30
+ | `pctr keys --keys "{ENTER}"` | Send a global key combo. |
31
+ | `pctr hotkey --keys win+shift+s` | Send a modifier combo. |
32
+ | `pctr focus --title RE` | Bring a window to the foreground. |
33
+ | `pctr wait --title RE --name RE [--timeout S]` | Wait until an element exists. |
34
+ | `pctr shot [--title RE] --out PATH` | Screenshot the screen or a window. |
35
+ | `pctr size` | Print primary screen size as `WxH`. |
36
+ | `pctr move --x N --y N` | Move the cursor. |
37
+ | `pctr down / up [--button left]` | Press / release a mouse button. |
38
+ | `pctr hold --ms 1000 [--x N --y N]` | Press, hold, release. |
39
+ | `pctr drag --start x,y --end x,y [--duration S]` | Drag between two points. |
40
+ | `pctr keydown / keyup --key a` | Hold / release a keyboard key. |
41
+ | `pctr keyhold --key w --ms 1500` | Hold a key for a duration. |
42
+
43
+ Common filters: `--title` (regex), `--name` (regex), `--control-type`
44
+ (`Button`, `Edit`, `Pane`, `MenuItem`, …), `--auto-id`, `--nth`, `--process`.
45
+
46
+ ## The loop
47
+
48
+ ```bash
49
+ pctr tree --title "Notepad" --limit 60 # see what's clickable
50
+ pctr click --title "Notepad" --name "^File$"
51
+ pctr set --title "Notepad" --name "Text Editor" --text "hello"
52
+ ```
53
+
54
+ ## Notes
55
+
56
+ - Prefer `set` (ValuePattern) over `type` for text fields - it can't drop or
57
+ reorder characters.
58
+ - `type` is sent in small chunks (`--chunk 1 --delay 0.03`); raise the delay for
59
+ Qt/Electron apps that garble fast input.
60
+ - Global `type` / `keys` go to the OS-focused window - `pctr focus` first.
61
+ - Qt apps expose a rich tree including embedded webviews. Electron apps expose
62
+ only the window frame unless accessibility is enabled (use `focus` + `keys`).
63
+
64
+ ## License
65
+
66
+ MIT
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pctr"
7
+ version = "0.1.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."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "Space-lab515" }]
13
+ keywords = ["automation", "windows", "uia", "pywinauto", "desktop", "agent", "computer-use", "rpa"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Win32 (MS Windows)",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: Microsoft :: Windows",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development :: Testing",
25
+ "Topic :: System :: Networking",
26
+ ]
27
+ dependencies = [
28
+ "pywinauto>=0.6.8; platform_system == 'Windows'",
29
+ "uiautomation>=2.0; platform_system == 'Windows'",
30
+ "pyautogui>=0.9.54; platform_system == 'Windows'",
31
+ "pydirectinput>=1.0.4; platform_system == 'Windows'",
32
+ ]
33
+
34
+ [project.scripts]
35
+ pctr = "pctr.cli:main"
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["src"]
pctr-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,453 @@
1
+ import argparse
2
+ import re
3
+ import sys
4
+ import time
5
+
6
+ for _stream in (sys.stdout, sys.stderr):
7
+ _reconf = getattr(_stream, "reconfigure", None)
8
+ if _reconf is not None:
9
+ try:
10
+ _reconf(encoding="utf-8", errors="replace")
11
+ except Exception:
12
+ pass
13
+
14
+ from pywinauto import Desktop
15
+
16
+ DESCRIPTION = "Element-based Windows UI Automation CLI for AI agents."
17
+
18
+
19
+ def _iter_windows():
20
+ return Desktop(backend="uia").windows()
21
+
22
+
23
+ def _find_window(title_re, process=None, timeout=5.0, index=0):
24
+ desktop = Desktop(backend="uia")
25
+ deadline = time.time() + timeout
26
+ last = None
27
+ while time.time() < deadline:
28
+ matches = []
29
+ for w in desktop.windows():
30
+ try:
31
+ t = w.window_text()
32
+ except Exception:
33
+ continue
34
+ if title_re and not re.search(title_re, t, re.I):
35
+ continue
36
+ if process is not None and w.process_id() != process:
37
+ continue
38
+ matches.append(w)
39
+ if len(matches) > index:
40
+ return matches[index]
41
+ last = matches
42
+ time.sleep(0.25)
43
+ raise SystemExit(
44
+ "window not found: title_re=%r process=%r (saw %d match(es))"
45
+ % (title_re, process, len(last or []))
46
+ )
47
+
48
+
49
+ def _matches(win, name=None, control_type=None, auto_id=None):
50
+ out = []
51
+ for el in win.descendants():
52
+ try:
53
+ info = el.element_info
54
+ except Exception:
55
+ continue
56
+ if name is not None:
57
+ n = info.name or ""
58
+ if re.search(name, n, re.I) is None:
59
+ continue
60
+ if control_type is not None:
61
+ c = (info.control_type or "")
62
+ if c.lower() != control_type.lower():
63
+ continue
64
+ if auto_id is not None:
65
+ a = (info.automation_id or "")
66
+ if a != auto_id:
67
+ continue
68
+ out.append(el)
69
+ return out
70
+
71
+
72
+ def _describe(el):
73
+ info = el.element_info
74
+ r = info.rectangle
75
+ return "%s | type=%s | id=%s | rect=%d,%d,%d,%d" % (
76
+ info.name,
77
+ info.control_type,
78
+ info.automation_id,
79
+ r.left,
80
+ r.top,
81
+ r.right,
82
+ r.bottom,
83
+ )
84
+
85
+
86
+ def _get_element(args):
87
+ win = _find_window(args.title, getattr(args, "process", None), getattr(args, "timeout", 5.0))
88
+ els = _matches(win, args.name, getattr(args, "control_type", None), getattr(args, "auto_id", None))
89
+ onscreen = [e for e in els if e.element_info.rectangle.right > 0 and e.element_info.rectangle.bottom > 0]
90
+ if onscreen:
91
+ els = onscreen
92
+ if not els:
93
+ raise SystemExit("no element matched name=%r type=%r id=%r" % (
94
+ getattr(args, "name", None), getattr(args, "control_type", None), getattr(args, "auto_id", None)))
95
+ idx = getattr(args, "nth", 0)
96
+ if idx >= len(els):
97
+ raise SystemExit("matched %d element(s); nth=%d out of range" % (len(els), idx))
98
+ return win, els[idx], len(els)
99
+
100
+
101
+ def cmd_windows(args):
102
+ for w in _iter_windows():
103
+ t = ""
104
+ try:
105
+ t = w.window_text()
106
+ except Exception:
107
+ pass
108
+ if args.filter and re.search(args.filter, t, re.I) is None:
109
+ continue
110
+ print("pid=%s | %s" % (w.process_id(), t))
111
+
112
+
113
+ def cmd_tree(args):
114
+ win = _find_window(args.title, args.process, args.timeout)
115
+ print("WINDOW: %s (pid=%s)" % (win.window_text(), win.process_id()))
116
+ count = 0
117
+ for el in win.descendants():
118
+ try:
119
+ el.element_info
120
+ except Exception:
121
+ continue
122
+ print(" %s" % _describe(el))
123
+ count += 1
124
+ if count >= args.limit:
125
+ print("... (limit %d reached)" % args.limit)
126
+ break
127
+
128
+
129
+ def cmd_find(args):
130
+ win = _find_window(args.title, args.process, args.timeout)
131
+ els = _matches(win, args.name, args.control_type, args.auto_id)
132
+ if not els:
133
+ print("no matches")
134
+ return
135
+ print("WINDOW: %s" % win.window_text())
136
+ for i, el in enumerate(els):
137
+ print("[%d] %s" % (i, _describe(el)))
138
+
139
+
140
+ def cmd_click(args):
141
+ win, el, n = _get_element(args)
142
+ print("target: %s (%d match(es))" % (_describe(el), n))
143
+ if args.method == "invoke" or (args.method == "auto" and hasattr(el, "invoke")):
144
+ try:
145
+ el.invoke()
146
+ print("invoked")
147
+ return
148
+ except Exception as e:
149
+ print("invoke failed (%s), falling back to mouse" % e)
150
+ el.click_input(button=args.button)
151
+ if args.dbl:
152
+ el.click_input(button=args.button)
153
+ print("clicked")
154
+
155
+
156
+ def cmd_set(args):
157
+ win, el, n = _get_element(args)
158
+ print("target: %s" % _describe(el))
159
+ try:
160
+ el.set_edit_text(args.text)
161
+ print("set via ValuePattern")
162
+ except Exception:
163
+ el.click_input()
164
+ el.type_keys("^a{BACKSPACE}")
165
+ el.type_keys(args.text, with_spaces=True, with_newlines=True)
166
+ print("set via type_keys")
167
+
168
+
169
+ def _chunks(text, size):
170
+ return [text[i:i + size] for i in range(0, len(text), size)]
171
+
172
+
173
+ def cmd_type(args):
174
+ delay = getattr(args, "delay", 0.02)
175
+ chunk = max(1, getattr(args, "chunk", 10))
176
+ if args.title:
177
+ win, el, n = _get_element(args)
178
+ for part in _chunks(args.text, chunk):
179
+ el.type_keys(part, with_spaces=True, with_newlines=True)
180
+ time.sleep(delay)
181
+ else:
182
+ import pyautogui
183
+ pyautogui.write(args.text, interval=delay)
184
+ print("typed")
185
+
186
+
187
+ def cmd_keys(args):
188
+ from pywinauto.keyboard import send_keys
189
+ send_keys(args.keys, with_spaces=True)
190
+ print("sent keys: %s" % args.keys)
191
+
192
+
193
+ def cmd_focus(args):
194
+ win = _find_window(args.title, args.process, args.timeout)
195
+ win.set_focus()
196
+ print("focused: %s" % win.window_text())
197
+
198
+
199
+ def cmd_wait(args):
200
+ deadline = time.time() + args.timeout
201
+ while time.time() < deadline:
202
+ try:
203
+ win = _find_window(args.title, args.process, timeout=1.0)
204
+ if _matches(win, args.name, args.control_type, args.auto_id):
205
+ print("found")
206
+ return
207
+ except SystemExit:
208
+ pass
209
+ time.sleep(0.4)
210
+ raise SystemExit("timed out waiting for element")
211
+
212
+
213
+ def cmd_shot(args):
214
+ import pyautogui
215
+ if args.title:
216
+ win = _find_window(args.title, args.process, args.timeout)
217
+ r = win.rectangle()
218
+ img = pyautogui.screenshot(region=(r.left, r.top, r.width(), r.height()))
219
+ else:
220
+ img = pyautogui.screenshot()
221
+ img.save(args.out)
222
+ print("saved %s (%dx%d)" % (args.out, img.width, img.height))
223
+
224
+
225
+ def _mouse_backend(args):
226
+ if getattr(args, "direct", False):
227
+ import pydirectinput
228
+ pydirectinput.PAUSE = 0.01
229
+ return pydirectinput
230
+ import pyautogui
231
+ pyautogui.FAILSAFE = False
232
+ return pyautogui
233
+
234
+
235
+ def _parse_pair(text):
236
+ x, y = text.split(",")
237
+ return int(float(x)), int(float(y))
238
+
239
+
240
+ def cmd_move(args):
241
+ m = _mouse_backend(args)
242
+ m.moveTo(args.x, args.y, duration=getattr(args, "duration", 0.0))
243
+ print("moved to %d,%d" % (args.x, args.y))
244
+
245
+
246
+ def cmd_down(args):
247
+ m = _mouse_backend(args)
248
+ m.mouseDown(button=args.button)
249
+ print("mouse down (%s)" % args.button)
250
+
251
+
252
+ def cmd_up(args):
253
+ m = _mouse_backend(args)
254
+ m.mouseUp(button=args.button)
255
+ print("mouse up (%s)" % args.button)
256
+
257
+
258
+ def cmd_hold(args):
259
+ m = _mouse_backend(args)
260
+ if args.x is not None and args.y is not None:
261
+ m.moveTo(args.x, args.y, duration=0.0)
262
+ m.mouseDown(button=args.button)
263
+ print("holding %s for %dms..." % (args.button, args.ms))
264
+ time.sleep(args.ms / 1000.0)
265
+ m.mouseUp(button=args.button)
266
+ print("released")
267
+
268
+
269
+ def cmd_keydown(args):
270
+ m = _mouse_backend(args)
271
+ m.keyDown(args.key)
272
+ print("key down: %s" % args.key)
273
+
274
+
275
+ def cmd_keyup(args):
276
+ m = _mouse_backend(args)
277
+ m.keyUp(args.key)
278
+ print("key up: %s" % args.key)
279
+
280
+
281
+ def cmd_keyhold(args):
282
+ m = _mouse_backend(args)
283
+ m.keyDown(args.key)
284
+ print("holding key %s for %dms..." % (args.key, args.ms))
285
+ time.sleep(args.ms / 1000.0)
286
+ m.keyUp(args.key)
287
+ print("released")
288
+
289
+
290
+ def cmd_hotkey(args):
291
+ m = _mouse_backend(args)
292
+ keys = [k.strip() for k in args.keys.split("+") if k.strip()]
293
+ for k in keys:
294
+ m.keyDown(k)
295
+ for k in reversed(keys):
296
+ m.keyUp(k)
297
+ print("hotkey: %s" % "+".join(keys))
298
+
299
+
300
+ def cmd_size(args):
301
+ import pyautogui
302
+ w, h = pyautogui.size()
303
+ print("%dx%d" % (w, h))
304
+
305
+
306
+ def cmd_drag(args):
307
+ m = _mouse_backend(args)
308
+ x1, y1 = _parse_pair(args.start)
309
+ x2, y2 = _parse_pair(args.end)
310
+ m.moveTo(x1, y1, duration=0.0)
311
+ m.mouseDown(button=args.button)
312
+ m.moveTo(x2, y2, duration=getattr(args, "duration", 0.3))
313
+ m.mouseUp(button=args.button)
314
+ print("dragged %d,%d -> %d,%d" % (x1, y1, x2, y2))
315
+
316
+
317
+ def add_target(p, require_name=False):
318
+ p.add_argument("--title", required=require_name, help="Window title regex")
319
+ p.add_argument("--process", type=int, default=None, help="Filter by process id")
320
+ p.add_argument("--timeout", type=float, default=5.0)
321
+ p.add_argument("--name", required=require_name, help="Element name regex")
322
+ p.add_argument("--control-type", default=None, help="Exact UIA control type (Button, Edit, ...)")
323
+ p.add_argument("--auto-id", default=None)
324
+ p.add_argument("--nth", type=int, default=0)
325
+
326
+
327
+ def build_parser():
328
+ ap = argparse.ArgumentParser(prog="pctr", description=DESCRIPTION, formatter_class=argparse.RawDescriptionHelpFormatter)
329
+ sub = ap.add_subparsers(dest="cmd", required=True)
330
+
331
+ w = sub.add_parser("windows", help="List top-level windows")
332
+ w.add_argument("--filter", default=None)
333
+ w.set_defaults(func=cmd_windows)
334
+
335
+ t = sub.add_parser("tree", help="Dump UIA tree")
336
+ add_target(t)
337
+ t.add_argument("--depth", type=int, default=99)
338
+ t.add_argument("--limit", type=int, default=400)
339
+ t.set_defaults(func=cmd_tree)
340
+
341
+ f = sub.add_parser("find", help="Find elements")
342
+ add_target(f)
343
+ f.set_defaults(func=cmd_find)
344
+
345
+ c = sub.add_parser("click", help="Click an element")
346
+ add_target(c, require_name=True)
347
+ c.add_argument("--method", choices=["auto", "invoke", "mouse"], default="auto")
348
+ c.add_argument("--button", default="left")
349
+ c.add_argument("--dbl", action="store_true")
350
+ c.set_defaults(func=cmd_click)
351
+
352
+ s = sub.add_parser("set", help="Set text on an element")
353
+ add_target(s, require_name=True)
354
+ s.add_argument("--text", required=True)
355
+ s.set_defaults(func=cmd_set)
356
+
357
+ ty = sub.add_parser("type", help="Send keystrokes")
358
+ ty.add_argument("--title", default=None)
359
+ ty.add_argument("--name", default=None)
360
+ ty.add_argument("--control-type", default=None)
361
+ ty.add_argument("--auto-id", default=None)
362
+ ty.add_argument("--nth", type=int, default=0)
363
+ ty.add_argument("--process", type=int, default=None)
364
+ ty.add_argument("--timeout", type=float, default=5.0)
365
+ ty.add_argument("--delay", type=float, default=0.03, help="Seconds between chars/chunks")
366
+ ty.add_argument("--chunk", type=int, default=1, help="Chunk size when typing into an element")
367
+ ty.add_argument("--text", required=True)
368
+ ty.set_defaults(func=cmd_type)
369
+
370
+ k = sub.add_parser("keys", help="Send a key combo globally")
371
+ k.add_argument("--keys", required=True, help="pywinauto send_keys syntax, e.g. {ENTER} or ^s")
372
+ k.set_defaults(func=cmd_keys)
373
+
374
+ fo = sub.add_parser("focus", help="Focus a window")
375
+ fo.add_argument("--title", required=True)
376
+ fo.add_argument("--process", type=int, default=None)
377
+ fo.add_argument("--timeout", type=float, default=5.0)
378
+ fo.set_defaults(func=cmd_focus)
379
+
380
+ wa = sub.add_parser("wait", help="Wait for an element")
381
+ add_target(wa, require_name=True)
382
+ wa.set_defaults(func=cmd_wait)
383
+
384
+ sh = sub.add_parser("shot", help="Screenshot")
385
+ sh.add_argument("--out", required=True)
386
+ sh.add_argument("--title", default=None)
387
+ sh.add_argument("--process", type=int, default=None)
388
+ sh.add_argument("--timeout", type=float, default=5.0)
389
+ sh.set_defaults(func=cmd_shot)
390
+
391
+ mv = sub.add_parser("move", help="Move the cursor")
392
+ mv.add_argument("--x", type=int, required=True)
393
+ mv.add_argument("--y", type=int, required=True)
394
+ mv.add_argument("--duration", type=float, default=0.0)
395
+ mv.add_argument("--direct", action="store_true", help="Use pydirectinput (games)")
396
+ mv.set_defaults(func=cmd_move)
397
+
398
+ dn = sub.add_parser("down", help="Press and hold a mouse button")
399
+ dn.add_argument("--button", default="left")
400
+ dn.add_argument("--direct", action="store_true")
401
+ dn.set_defaults(func=cmd_down)
402
+
403
+ up = sub.add_parser("up", help="Release a mouse button")
404
+ up.add_argument("--button", default="left")
405
+ up.add_argument("--direct", action="store_true")
406
+ up.set_defaults(func=cmd_up)
407
+
408
+ ho = sub.add_parser("hold", help="Press, hold for --ms, then release")
409
+ ho.add_argument("--ms", type=int, default=1000)
410
+ ho.add_argument("--button", default="left")
411
+ ho.add_argument("--x", type=int, default=None)
412
+ ho.add_argument("--y", type=int, default=None)
413
+ ho.add_argument("--direct", action="store_true")
414
+ ho.set_defaults(func=cmd_hold)
415
+
416
+ kd = sub.add_parser("keydown", help="Press and hold a keyboard key")
417
+ kd.add_argument("--key", required=True)
418
+ kd.add_argument("--direct", action="store_true")
419
+ kd.set_defaults(func=cmd_keydown)
420
+
421
+ ku = sub.add_parser("keyup", help="Release a keyboard key")
422
+ ku.add_argument("--key", required=True)
423
+ ku.add_argument("--direct", action="store_true")
424
+ ku.set_defaults(func=cmd_keyup)
425
+
426
+ kh = sub.add_parser("keyhold", help="Press, hold for --ms, then release a key")
427
+ kh.add_argument("--key", required=True)
428
+ kh.add_argument("--ms", type=int, default=1000)
429
+ kh.add_argument("--direct", action="store_true")
430
+ kh.set_defaults(func=cmd_keyhold)
431
+
432
+ hk = sub.add_parser("hotkey", help="Send a key combo, e.g. --keys win+shift+s")
433
+ hk.add_argument("--keys", required=True, help="Combo joined by +, e.g. ctrl+shift+t")
434
+ hk.add_argument("--direct", action="store_true")
435
+ hk.set_defaults(func=cmd_hotkey)
436
+
437
+ sz = sub.add_parser("size", help="Print primary screen size as WxH")
438
+ sz.set_defaults(func=cmd_size)
439
+
440
+ dr = sub.add_parser("drag", help="Drag from --start to --end (x,y pairs)")
441
+ dr.add_argument("--start", required=True, help="x,y")
442
+ dr.add_argument("--end", required=True, help="x,y")
443
+ dr.add_argument("--button", default="left")
444
+ dr.add_argument("--duration", type=float, default=0.3)
445
+ dr.add_argument("--direct", action="store_true")
446
+ dr.set_defaults(func=cmd_drag)
447
+
448
+ return ap
449
+
450
+
451
+ def main():
452
+ args = build_parser().parse_args()
453
+ args.func(args)
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: pctr
3
+ Version: 0.1.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.
5
+ Author: Space-lab515
6
+ License-Expression: MIT
7
+ Keywords: automation,windows,uia,pywinauto,desktop,agent,computer-use,rpa
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Win32 (MS Windows)
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Classifier: Topic :: System :: Networking
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: pywinauto>=0.6.8; platform_system == "Windows"
23
+ Requires-Dist: uiautomation>=2.0; platform_system == "Windows"
24
+ Requires-Dist: pyautogui>=0.9.54; platform_system == "Windows"
25
+ Requires-Dist: pydirectinput>=1.0.4; platform_system == "Windows"
26
+ Dynamic: license-file
27
+
28
+ # pctr
29
+
30
+ Element-based Windows UI Automation CLI for AI agents.
31
+
32
+ Instead of screenshotting and guessing pixel coordinates, `pctr` finds controls
33
+ by their **name / control type / automation id** and acts on them. Window moves,
34
+ DPI changes, and layout shifts don't break your script.
35
+
36
+ Built on `pywinauto` (UI Automation) with `pyautogui` / `pydirectinput` for raw
37
+ mouse and keyboard input.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install pctr
43
+ ```
44
+
45
+ Windows only.
46
+
47
+ ## Commands
48
+
49
+ | Command | What it does |
50
+ |---------|--------------|
51
+ | `pctr windows [--filter RE]` | List top-level windows (`pid=… \| title`). |
52
+ | `pctr tree --title RE [--limit N]` | Dump the UIA control tree of a window. |
53
+ | `pctr find --title RE --name RE [--control-type T] [--auto-id ID]` | List matching elements. |
54
+ | `pctr click --title RE --name RE [--method auto\|invoke\|mouse] [--dbl]` | Click an element. |
55
+ | `pctr set --title RE --name RE --text S` | Set a field via the UIA ValuePattern. |
56
+ | `pctr type [--title RE --name RE] --text S [--delay 0.03] [--chunk 1]` | Type literal text. |
57
+ | `pctr keys --keys "{ENTER}"` | Send a global key combo. |
58
+ | `pctr hotkey --keys win+shift+s` | Send a modifier combo. |
59
+ | `pctr focus --title RE` | Bring a window to the foreground. |
60
+ | `pctr wait --title RE --name RE [--timeout S]` | Wait until an element exists. |
61
+ | `pctr shot [--title RE] --out PATH` | Screenshot the screen or a window. |
62
+ | `pctr size` | Print primary screen size as `WxH`. |
63
+ | `pctr move --x N --y N` | Move the cursor. |
64
+ | `pctr down / up [--button left]` | Press / release a mouse button. |
65
+ | `pctr hold --ms 1000 [--x N --y N]` | Press, hold, release. |
66
+ | `pctr drag --start x,y --end x,y [--duration S]` | Drag between two points. |
67
+ | `pctr keydown / keyup --key a` | Hold / release a keyboard key. |
68
+ | `pctr keyhold --key w --ms 1500` | Hold a key for a duration. |
69
+
70
+ Common filters: `--title` (regex), `--name` (regex), `--control-type`
71
+ (`Button`, `Edit`, `Pane`, `MenuItem`, …), `--auto-id`, `--nth`, `--process`.
72
+
73
+ ## The loop
74
+
75
+ ```bash
76
+ pctr tree --title "Notepad" --limit 60 # see what's clickable
77
+ pctr click --title "Notepad" --name "^File$"
78
+ pctr set --title "Notepad" --name "Text Editor" --text "hello"
79
+ ```
80
+
81
+ ## Notes
82
+
83
+ - Prefer `set` (ValuePattern) over `type` for text fields - it can't drop or
84
+ reorder characters.
85
+ - `type` is sent in small chunks (`--chunk 1 --delay 0.03`); raise the delay for
86
+ Qt/Electron apps that garble fast input.
87
+ - Global `type` / `keys` go to the OS-focused window - `pctr focus` first.
88
+ - Qt apps expose a rich tree including embedded webviews. Electron apps expose
89
+ only the window frame unless accessibility is enabled (use `focus` + `keys`).
90
+
91
+ ## License
92
+
93
+ MIT
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/pctr/__init__.py
5
+ src/pctr/__main__.py
6
+ src/pctr/cli.py
7
+ src/pctr.egg-info/PKG-INFO
8
+ src/pctr.egg-info/SOURCES.txt
9
+ src/pctr.egg-info/dependency_links.txt
10
+ src/pctr.egg-info/entry_points.txt
11
+ src/pctr.egg-info/requires.txt
12
+ src/pctr.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pctr = pctr.cli:main
@@ -0,0 +1,6 @@
1
+
2
+ [:platform_system == "Windows"]
3
+ pywinauto>=0.6.8
4
+ uiautomation>=2.0
5
+ pyautogui>=0.9.54
6
+ pydirectinput>=1.0.4
@@ -0,0 +1 @@
1
+ pctr