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 +21 -0
- pctr-0.1.0/PKG-INFO +93 -0
- pctr-0.1.0/README.md +66 -0
- pctr-0.1.0/pyproject.toml +38 -0
- pctr-0.1.0/setup.cfg +4 -0
- pctr-0.1.0/src/pctr/__init__.py +1 -0
- pctr-0.1.0/src/pctr/__main__.py +4 -0
- pctr-0.1.0/src/pctr/cli.py +453 -0
- pctr-0.1.0/src/pctr.egg-info/PKG-INFO +93 -0
- pctr-0.1.0/src/pctr.egg-info/SOURCES.txt +12 -0
- pctr-0.1.0/src/pctr.egg-info/dependency_links.txt +1 -0
- pctr-0.1.0/src/pctr.egg-info/entry_points.txt +2 -0
- pctr-0.1.0/src/pctr.egg-info/requires.txt +6 -0
- pctr-0.1.0/src/pctr.egg-info/top_level.txt +1 -0
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 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pctr
|