clickmate 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ivand890
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.
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.3
2
+ Name: clickmate
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Keywords: autoclicker,clickmate,automation,macos,quartz,mouse,cli
6
+ Author: ivand890
7
+ License: MIT License
8
+
9
+ Copyright (c) 2025 ivand890
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ Classifier: Development Status :: 4 - Beta
29
+ Classifier: Intended Audience :: End Users/Desktop
30
+ Classifier: Topic :: Utilities
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Operating System :: MacOS
33
+ Classifier: Operating System :: Microsoft :: Windows
34
+ Classifier: Operating System :: POSIX :: Linux
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3 :: Only
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Requires-Dist: pyautogui>=0.9.54
41
+ Requires-Dist: pynput>=1.8.1
42
+ Requires-Dist: pyobjc-framework-quartz>=10.3 ; sys_platform == 'darwin' and extra == 'mac'
43
+ Requires-Python: >=3.11
44
+ Project-URL: Changelog, https://github.com/ivand890/autoclicker/releases
45
+ Project-URL: Homepage, https://github.com/ivand890/autoclicker
46
+ Project-URL: Issues, https://github.com/ivand890/autoclicker/issues
47
+ Project-URL: Repository, https://github.com/ivand890/autoclicker
48
+ Provides-Extra: mac
49
+ Description-Content-Type: text/markdown
50
+
51
+ # Clickmate
52
+
53
+ Clickmate is a Python autoclicker with a macOS-friendly multi-engine click system. It prefers native Quartz CoreGraphics events on macOS (when available) for best compatibility, and falls back to `pynput` or `pyautogui` if needed. Randomized delays, adjustable “pressure” (hold time), and a single-line status display are built in.
54
+
55
+ ## Features
56
+
57
+ - Start/Pause with `s`
58
+ - Random delay per click (0.5×–1.5× of the base delay)
59
+ - Adjust click speed on the fly with `+` (faster) and `-` (slower)
60
+ - Adjust click “pressure” (hold duration) with `]` (increase) and `[` (decrease)
61
+ - Toggle click method with `m` (cycles: `pynput` → `quartz` → `pyautogui`)
62
+ - One-off test click with `c`
63
+ - Optional debug logs with `d`
64
+ - Optional tiny jitter movement before each click (`j`) to improve acceptance in some apps
65
+ - Global hotkey to lock/unlock local keys: `Ctrl+Alt+K` (works system-wide)
66
+
67
+ ### Defaults
68
+
69
+ - Base delay: `0.50s`
70
+ - Hold time (pressure): `0.10s`
71
+ - Click method: `quartz` if Quartz/PyObjC is available; otherwise `pynput`
72
+ - Jitter: disabled by default
73
+
74
+ ## Installation & CLI usage (uv)
75
+
76
+ Requires Python `>=3.11` and `uv` installed.
77
+
78
+ - One-off run without installation (preferred for quick use):
79
+
80
+ ```bash
81
+ uvx clickmate --help
82
+ uvx clickmate --version
83
+ uvx clickmate
84
+ ```
85
+
86
+ - Install globally as a persistent tool (adds `autoclicker` to your `PATH`):
87
+
88
+ ```bash
89
+ uv tool install clickmate
90
+ # then simply:
91
+ clickmate
92
+ ```
93
+
94
+ - Run from a local checkout without publishing:
95
+
96
+ ```bash
97
+ # ephemeral run from the current workspace
98
+ # NOTE: `uv tool run` uses a cached, isolated env. Add `--refresh` to
99
+ # pick up local source changes if you haven't bumped the version.
100
+ uv tool run --from . --refresh clickmate
101
+ # or install the local project as a tool
102
+ uv tool install .
103
+ ```
104
+
105
+ Development workflow (recommended):
106
+
107
+ - Set up the project environment and run the packaged script:
108
+
109
+ ```bash
110
+ uv sync
111
+ # see flags
112
+ uv run clickmate --help
113
+ uv run clickmate --version
114
+ # run the console script
115
+ uv run clickmate
116
+ # or run the module directly
117
+ uv run -m autoclicker --help
118
+ ```
119
+
120
+ Tip: If `uv tool run --from . autoclicker` dont reflect your local changes,
121
+ it's likely using a previously cached tool environment. Fix it by either:
122
+
123
+ - adding `--refresh` (recommended for local dev):
124
+
125
+ ```bash
126
+ uv tool run --from . --refresh clickmate --help
127
+ uv tool run --from . --refresh clickmate --version
128
+ ```
129
+
130
+ - or bumping the package `version` in `pyproject.toml` (forces a new cache key)
131
+ - or pruning caches:
132
+
133
+ ```bash
134
+ uv cache clean autoclicker
135
+ ```
136
+
137
+ ## Usage
138
+
139
+ 1. Run the program (any of the methods above):
140
+
141
+ 2. Move your mouse to the desired click position.
142
+ 3. Press `s` to start; press `s` again to pause.
143
+ 4. Use `+`/`-` to change speed; use `]`/`[` to change hold time.
144
+ 5. Use `m` to switch engines if your app ignores clicks; `quartz` generally works best on macOS.
145
+ 6. Use `Ctrl+Alt+K` anytime to lock/unlock local keys (prevents accidental hotkey presses). This does not start/pause clicking; it only enables/disables local hotkeys.
146
+ 7. Press `Ctrl+C` to exit.
147
+
148
+ ### Hotkeys (summary)
149
+
150
+ - `s` — start/pause
151
+ - `+` / `-` — faster / slower
152
+ - `]` / `[` — increase / decrease hold time (pressure)
153
+ - `m` — toggle click method (`pynput` → `quartz` → `pyautogui`)
154
+ - `c` — single test click
155
+ - `j` — toggle jitter movement
156
+ - `d` — toggle debug logs
157
+ - `Ctrl+Alt+K` — global lock/unlock local keys (works system-wide)
158
+
159
+ ## Status line
160
+
161
+ When running, the app renders a single, continuously-updated status line. It uses fixed-width fields and stays on one terminal line (no log spam). A small spinner animates while clicking.
162
+
163
+ Example:
164
+
165
+ ```
166
+ ▶ | delay: 0.50s | rng: 0.25–0.75 | hold: 0.10s | meth: QTZ | jitter: - | debug: - | lock: no | clicks: 000042 | last: 0.18s | next: 0.43s | cps: 3.5 | fb:1 | method quartz
167
+ ```
168
+
169
+ Field notes:
170
+
171
+ - `delay` — base delay; each actual delay is randomized within `rng` (0.5×–1.5×)
172
+ - `hold` — how long the button is held down per click (the “pressure”)
173
+ - `meth` — click engine: `QTZ` (Quartz), `PNP` (pynput), `PGA` (pyautogui)
174
+ - `jitter` — `J` when enabled, otherwise `-`
175
+ - `debug` — `D` when debug logging is on, otherwise `-`
176
+ - `lock` — `yes` when local keys are locked (only `Ctrl+Alt+K` works); `no` otherwise
177
+ - `clicks` — total clicks since start
178
+ - `last` — time since the last click
179
+ - `next` — the next randomized delay (shown while waiting)
180
+ - `cps` — average clicks per second since start
181
+ - `fb:N` — shown when a click had to fall back to another engine
182
+ - trailing message — brief, ephemeral messages (e.g., when switching method)
183
+
184
+ Notes:
185
+
186
+ - Colors are used when the output is a TTY; `lock:yes` is highlighted.
187
+ - Messages are inline and fade after ~2.5s; no extra newlines are printed.
188
+
189
+ ## macOS permissions
190
+
191
+ For keyboard and mouse automation to work, macOS must trust your terminal:
192
+
193
+ 1. System Settings → Privacy & Security → Accessibility → add your terminal (Terminal, iTerm, or VS Code)
194
+ 2. System Settings → Privacy & Security → Input Monitoring → add your terminal
195
+ 3. Restart the terminal and run again
196
+
197
+ Quartz (CoreGraphics) clicks require the `Quartz` Python module (PyObjC). If it’s not installed, the app will automatically use `pynput`. You don’t need to install PyObjC unless you specifically want Quartz.
198
+
199
+ ## Troubleshooting
200
+
201
+ - Keys don’t work or clicks are blocked: verify Accessibility and Input Monitoring permissions (see above).
202
+ - Clicks ignored by the target app:
203
+ - Press `m` to switch engines; try `quartz` first on macOS
204
+ - Enable jitter with `j` (adds a subtle 1px move before click)
205
+ - Increase hold time with `]` to ~0.02–0.05s
206
+ - Terminal output looks noisy: that’s likely not a TTY; run in a normal terminal to see the single-line status, or consider adding flags later to reduce output.
207
+
208
+ ## CI/CD (GitHub Actions + uv)
209
+
210
+ This repo uses GitHub Actions with `uv` for linting, testing, building, and publishing:
211
+
212
+ - CI (`.github/workflows/ci.yml`): runs on pushes/PRs, installs with `uv sync`, lints with `ruff`, and runs `pytest` across Python 3.11–3.13.
213
+ - Release (`.github/workflows/release.yml`): on tags like `v0.1.0`, builds via `uv build --no-sources` and publishes via `uv publish` using PyPI Trusted Publishing (OIDC).
214
+
215
+ To publish:
216
+
217
+ 1. Configure a Trusted Publisher for the PyPI project `autoclicker` (and optionally TestPyPI).
218
+ 2. Push a tag `vX.Y.Z` to trigger the release workflow.
219
+ 3. Pre-release tags containing `rc` will also publish to TestPyPI.
220
+
221
+ ## Contributing and community
222
+
223
+ - Please see `CONTRIBUTING.md` for how to set up your environment and submit PRs.
224
+ - See `CODE_OF_CONDUCT.md` for expected community behavior.
225
+ - Security reports: see `SECURITY.md`.
226
+
227
+ ## License
228
+
229
+ This project is licensed under the MIT License. See the `LICENSE` file for details.
230
+
231
+ ## Dependencies
232
+
233
+ - `pyautogui` — automation
234
+ - `pynput` — input handling and alternative click path
235
+ - Optional: `pyobjc` (provides the `Quartz` module) — enables the Quartz click engine on macOS
@@ -0,0 +1,185 @@
1
+ # Clickmate
2
+
3
+ Clickmate is a Python autoclicker with a macOS-friendly multi-engine click system. It prefers native Quartz CoreGraphics events on macOS (when available) for best compatibility, and falls back to `pynput` or `pyautogui` if needed. Randomized delays, adjustable “pressure” (hold time), and a single-line status display are built in.
4
+
5
+ ## Features
6
+
7
+ - Start/Pause with `s`
8
+ - Random delay per click (0.5×–1.5× of the base delay)
9
+ - Adjust click speed on the fly with `+` (faster) and `-` (slower)
10
+ - Adjust click “pressure” (hold duration) with `]` (increase) and `[` (decrease)
11
+ - Toggle click method with `m` (cycles: `pynput` → `quartz` → `pyautogui`)
12
+ - One-off test click with `c`
13
+ - Optional debug logs with `d`
14
+ - Optional tiny jitter movement before each click (`j`) to improve acceptance in some apps
15
+ - Global hotkey to lock/unlock local keys: `Ctrl+Alt+K` (works system-wide)
16
+
17
+ ### Defaults
18
+
19
+ - Base delay: `0.50s`
20
+ - Hold time (pressure): `0.10s`
21
+ - Click method: `quartz` if Quartz/PyObjC is available; otherwise `pynput`
22
+ - Jitter: disabled by default
23
+
24
+ ## Installation & CLI usage (uv)
25
+
26
+ Requires Python `>=3.11` and `uv` installed.
27
+
28
+ - One-off run without installation (preferred for quick use):
29
+
30
+ ```bash
31
+ uvx clickmate --help
32
+ uvx clickmate --version
33
+ uvx clickmate
34
+ ```
35
+
36
+ - Install globally as a persistent tool (adds `autoclicker` to your `PATH`):
37
+
38
+ ```bash
39
+ uv tool install clickmate
40
+ # then simply:
41
+ clickmate
42
+ ```
43
+
44
+ - Run from a local checkout without publishing:
45
+
46
+ ```bash
47
+ # ephemeral run from the current workspace
48
+ # NOTE: `uv tool run` uses a cached, isolated env. Add `--refresh` to
49
+ # pick up local source changes if you haven't bumped the version.
50
+ uv tool run --from . --refresh clickmate
51
+ # or install the local project as a tool
52
+ uv tool install .
53
+ ```
54
+
55
+ Development workflow (recommended):
56
+
57
+ - Set up the project environment and run the packaged script:
58
+
59
+ ```bash
60
+ uv sync
61
+ # see flags
62
+ uv run clickmate --help
63
+ uv run clickmate --version
64
+ # run the console script
65
+ uv run clickmate
66
+ # or run the module directly
67
+ uv run -m autoclicker --help
68
+ ```
69
+
70
+ Tip: If `uv tool run --from . autoclicker` dont reflect your local changes,
71
+ it's likely using a previously cached tool environment. Fix it by either:
72
+
73
+ - adding `--refresh` (recommended for local dev):
74
+
75
+ ```bash
76
+ uv tool run --from . --refresh clickmate --help
77
+ uv tool run --from . --refresh clickmate --version
78
+ ```
79
+
80
+ - or bumping the package `version` in `pyproject.toml` (forces a new cache key)
81
+ - or pruning caches:
82
+
83
+ ```bash
84
+ uv cache clean autoclicker
85
+ ```
86
+
87
+ ## Usage
88
+
89
+ 1. Run the program (any of the methods above):
90
+
91
+ 2. Move your mouse to the desired click position.
92
+ 3. Press `s` to start; press `s` again to pause.
93
+ 4. Use `+`/`-` to change speed; use `]`/`[` to change hold time.
94
+ 5. Use `m` to switch engines if your app ignores clicks; `quartz` generally works best on macOS.
95
+ 6. Use `Ctrl+Alt+K` anytime to lock/unlock local keys (prevents accidental hotkey presses). This does not start/pause clicking; it only enables/disables local hotkeys.
96
+ 7. Press `Ctrl+C` to exit.
97
+
98
+ ### Hotkeys (summary)
99
+
100
+ - `s` — start/pause
101
+ - `+` / `-` — faster / slower
102
+ - `]` / `[` — increase / decrease hold time (pressure)
103
+ - `m` — toggle click method (`pynput` → `quartz` → `pyautogui`)
104
+ - `c` — single test click
105
+ - `j` — toggle jitter movement
106
+ - `d` — toggle debug logs
107
+ - `Ctrl+Alt+K` — global lock/unlock local keys (works system-wide)
108
+
109
+ ## Status line
110
+
111
+ When running, the app renders a single, continuously-updated status line. It uses fixed-width fields and stays on one terminal line (no log spam). A small spinner animates while clicking.
112
+
113
+ Example:
114
+
115
+ ```
116
+ ▶ | delay: 0.50s | rng: 0.25–0.75 | hold: 0.10s | meth: QTZ | jitter: - | debug: - | lock: no | clicks: 000042 | last: 0.18s | next: 0.43s | cps: 3.5 | fb:1 | method quartz
117
+ ```
118
+
119
+ Field notes:
120
+
121
+ - `delay` — base delay; each actual delay is randomized within `rng` (0.5×–1.5×)
122
+ - `hold` — how long the button is held down per click (the “pressure”)
123
+ - `meth` — click engine: `QTZ` (Quartz), `PNP` (pynput), `PGA` (pyautogui)
124
+ - `jitter` — `J` when enabled, otherwise `-`
125
+ - `debug` — `D` when debug logging is on, otherwise `-`
126
+ - `lock` — `yes` when local keys are locked (only `Ctrl+Alt+K` works); `no` otherwise
127
+ - `clicks` — total clicks since start
128
+ - `last` — time since the last click
129
+ - `next` — the next randomized delay (shown while waiting)
130
+ - `cps` — average clicks per second since start
131
+ - `fb:N` — shown when a click had to fall back to another engine
132
+ - trailing message — brief, ephemeral messages (e.g., when switching method)
133
+
134
+ Notes:
135
+
136
+ - Colors are used when the output is a TTY; `lock:yes` is highlighted.
137
+ - Messages are inline and fade after ~2.5s; no extra newlines are printed.
138
+
139
+ ## macOS permissions
140
+
141
+ For keyboard and mouse automation to work, macOS must trust your terminal:
142
+
143
+ 1. System Settings → Privacy & Security → Accessibility → add your terminal (Terminal, iTerm, or VS Code)
144
+ 2. System Settings → Privacy & Security → Input Monitoring → add your terminal
145
+ 3. Restart the terminal and run again
146
+
147
+ Quartz (CoreGraphics) clicks require the `Quartz` Python module (PyObjC). If it’s not installed, the app will automatically use `pynput`. You don’t need to install PyObjC unless you specifically want Quartz.
148
+
149
+ ## Troubleshooting
150
+
151
+ - Keys don’t work or clicks are blocked: verify Accessibility and Input Monitoring permissions (see above).
152
+ - Clicks ignored by the target app:
153
+ - Press `m` to switch engines; try `quartz` first on macOS
154
+ - Enable jitter with `j` (adds a subtle 1px move before click)
155
+ - Increase hold time with `]` to ~0.02–0.05s
156
+ - Terminal output looks noisy: that’s likely not a TTY; run in a normal terminal to see the single-line status, or consider adding flags later to reduce output.
157
+
158
+ ## CI/CD (GitHub Actions + uv)
159
+
160
+ This repo uses GitHub Actions with `uv` for linting, testing, building, and publishing:
161
+
162
+ - CI (`.github/workflows/ci.yml`): runs on pushes/PRs, installs with `uv sync`, lints with `ruff`, and runs `pytest` across Python 3.11–3.13.
163
+ - Release (`.github/workflows/release.yml`): on tags like `v0.1.0`, builds via `uv build --no-sources` and publishes via `uv publish` using PyPI Trusted Publishing (OIDC).
164
+
165
+ To publish:
166
+
167
+ 1. Configure a Trusted Publisher for the PyPI project `autoclicker` (and optionally TestPyPI).
168
+ 2. Push a tag `vX.Y.Z` to trigger the release workflow.
169
+ 3. Pre-release tags containing `rc` will also publish to TestPyPI.
170
+
171
+ ## Contributing and community
172
+
173
+ - Please see `CONTRIBUTING.md` for how to set up your environment and submit PRs.
174
+ - See `CODE_OF_CONDUCT.md` for expected community behavior.
175
+ - Security reports: see `SECURITY.md`.
176
+
177
+ ## License
178
+
179
+ This project is licensed under the MIT License. See the `LICENSE` file for details.
180
+
181
+ ## Dependencies
182
+
183
+ - `pyautogui` — automation
184
+ - `pynput` — input handling and alternative click path
185
+ - Optional: `pyobjc` (provides the `Quartz` module) — enables the Quartz click engine on macOS
@@ -0,0 +1,71 @@
1
+ [project]
2
+ name = "clickmate"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "pyautogui>=0.9.54",
9
+ "pynput>=1.8.1",
10
+ ]
11
+ license = { file = "LICENSE" }
12
+ authors = [
13
+ { name = "ivand890" }
14
+ ]
15
+ keywords = ["autoclicker", "clickmate", "automation", "macos", "quartz", "mouse", "cli"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: End Users/Desktop",
19
+ "Topic :: Utilities",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: MacOS",
22
+ "Operating System :: Microsoft :: Windows",
23
+ "Operating System :: POSIX :: Linux",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/ivand890/autoclicker"
33
+ Repository = "https://github.com/ivand890/autoclicker"
34
+ Issues = "https://github.com/ivand890/autoclicker/issues"
35
+ Changelog = "https://github.com/ivand890/autoclicker/releases"
36
+
37
+ [project.optional-dependencies]
38
+ mac = [
39
+ # PyObjC provides the Quartz module used by the preferred macOS engine
40
+ "pyobjc-framework-Quartz>=10.3; platform_system=='Darwin'",
41
+ ]
42
+
43
+ [project.scripts]
44
+ clickmate = "autoclicker.cli:main"
45
+
46
+ [build-system]
47
+ requires = ["uv_build>=0.8.11,<0.9.0"]
48
+ build-backend = "uv_build"
49
+
50
+ [tool.uv]
51
+ # Ensure the project is treated as a real package and built/installed for tools
52
+ package = true
53
+
54
+ # Allow trusted publishing checks in CI contexts
55
+ # trusted-publishing = "auto"
56
+
57
+ [[tool.uv.index]]
58
+ # Optional alternate index for pre-releases/testing via `uv publish --index testpypi`
59
+ name = "testpypi"
60
+ url = "https://test.pypi.org/simple/"
61
+ publish-url = "https://test.pypi.org/legacy/"
62
+ explicit = true
63
+
64
+ [dependency-groups]
65
+ dev = [
66
+ "pytest>=8.2",
67
+ "ruff>=0.6.2",
68
+ ]
69
+
70
+ [tool.uv.build-backend]
71
+ module-name = "autoclicker"
@@ -0,0 +1,7 @@
1
+ """Autoclicker package.
2
+
3
+ Exports the CLI entrypoint via ``autoclicker.cli:main``.
4
+ """
5
+
6
+ __all__ = ["__version__"]
7
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__": # pragma: no cover
4
+ main()
@@ -0,0 +1,529 @@
1
+ import time
2
+ import random
3
+ import pyautogui
4
+ from pynput import keyboard
5
+ from pynput.keyboard import Key
6
+ from pynput.mouse import Controller as MouseController, Button
7
+ import importlib
8
+ import sys
9
+ import shutil
10
+ from dataclasses import dataclass
11
+
12
+ try:
13
+ importlib.import_module("Quartz")
14
+ _HAS_QUARTZ = True
15
+ except Exception:
16
+ _HAS_QUARTZ = False
17
+
18
+
19
+ # Runtime state
20
+ running = False
21
+ delay = 0.5 # base delay in seconds (default)
22
+ min_delay = 0.05
23
+ max_delay = 5.0
24
+ hold_time = 0.1 # how long to hold the button down per click
25
+ # Randomization multipliers for per-click delay
26
+ MIN_DELAY_MULTIPLIER = 0.5
27
+ MAX_DELAY_MULTIPLIER = 1.5
28
+ # Preferred click method: "pynput", "pyautogui", or "quartz" (macOS)
29
+ # Default to Quartz if available for best macOS compatibility
30
+ click_method = "quartz" if _HAS_QUARTZ else "pynput"
31
+ debug = False
32
+ jitter_enabled = False # default jitter disabled (enable with 'j' if needed)
33
+ jitter_px = 1
34
+ local_keys_enabled = True # when False, only the global hotkey works
35
+ _pressed_keys = set() # track currently pressed keys for combo detection
36
+
37
+
38
+ # ---------------- Status line support -----------------
39
+ @dataclass
40
+ class Status:
41
+ start_time: float
42
+ running: bool
43
+ delay: float
44
+ hold_time: float
45
+ click_method: str
46
+ jitter_enabled: bool
47
+ debug: bool
48
+ local_keys_enabled: bool
49
+ click_count: int = 0
50
+ last_click_ts: float | None = None
51
+ next_delay: float | None = None
52
+ fallbacks: int = 0
53
+ message: str | None = None
54
+ message_ts: float | None = None
55
+
56
+
57
+ class StatusRenderer:
58
+ def __init__(self, use_color: bool = True, interval: float = 0.1):
59
+ self.interval = interval
60
+ self.last_render = 0.0
61
+ self.dirty = True
62
+ self.use_color = use_color and sys.stdout.isatty()
63
+ self._cached_line = ""
64
+ self._spinners = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
65
+ self._spinner_idx = 0
66
+ self._last_spinner_tick = 0.0
67
+
68
+ def mark(self):
69
+ self.dirty = True
70
+
71
+ def _color(self, text: str, code: str):
72
+ if not self.use_color:
73
+ return text
74
+ return f"\x1b[{code}m{text}\x1b[0m"
75
+
76
+ def build_line(self, st: Status) -> str:
77
+ now = time.time()
78
+ # Spinner only when running
79
+ spinner = ""
80
+ if st.running and now - self._last_spinner_tick > 0.15:
81
+ self._spinner_idx = (self._spinner_idx + 1) % len(self._spinners)
82
+ self._last_spinner_tick = now
83
+ if st.running:
84
+ spinner = self._spinners[self._spinner_idx] + " "
85
+ state_sym = "▶" if st.running else "⏸"
86
+ # lock shows yes/no for compatibility (yes = locked)
87
+ lock_val = "no" if st.local_keys_enabled else "yes"
88
+ jitter_sym = "J" if st.jitter_enabled else "-"
89
+ debug_sym = "D" if st.debug else "-"
90
+ method_map = {"quartz": "QTZ", "pynput": "PNP", "pyautogui": "PGA"}
91
+ method_disp = method_map.get(st.click_method, st.click_method)
92
+ # Fixed width numeric fields to avoid jitter
93
+ delay_val = f"{st.delay:5.2f}s"
94
+ rng_lo = f"{st.delay * MIN_DELAY_MULTIPLIER:5.2f}"
95
+ rng_hi = f"{st.delay * MAX_DELAY_MULTIPLIER:5.2f}"
96
+ rng_val = f"{rng_lo}–{rng_hi}"
97
+ hold_val = f"{st.hold_time:5.2f}s"
98
+ last_age = (now - st.last_click_ts) if st.last_click_ts else None
99
+ if last_age is not None:
100
+ last_val = f"{last_age:5.2f}s"
101
+ else:
102
+ last_val = " -- "
103
+ if st.next_delay is not None:
104
+ next_val = f"{st.next_delay:5.2f}s"
105
+ else:
106
+ next_val = " -- "
107
+ elapsed = now - st.start_time
108
+ cps = (st.click_count / elapsed) if st.click_count and elapsed > 0 else 0.0
109
+ cps_val = f"{cps:4.1f}" if st.click_count else " 0.0"
110
+ clicks_val = f"{st.click_count:6d}"
111
+ line_parts = [
112
+ f"{spinner}{state_sym}",
113
+ f"delay:{delay_val}",
114
+ f"rng:{rng_val}",
115
+ f"hold:{hold_val}",
116
+ f"meth:{method_disp}",
117
+ f"jitter:{jitter_sym}",
118
+ f"debug:{debug_sym}",
119
+ f"lock:{lock_val}",
120
+ f"clicks:{clicks_val}",
121
+ f"last:{last_val}",
122
+ f"next:{next_val}",
123
+ f"cps:{cps_val}",
124
+ ]
125
+ if st.fallbacks:
126
+ line_parts.append(f"fb:{st.fallbacks}")
127
+ if st.message and st.message_ts and (now - st.message_ts) < 2.5:
128
+ line_parts.append(st.message)
129
+ line = " | ".join(line_parts)
130
+ # Color minimal elements
131
+ if self.use_color:
132
+ if st.running:
133
+ line = line.replace(state_sym, self._color(state_sym, "32"))
134
+ else:
135
+ line = line.replace(state_sym, self._color(state_sym, "33"))
136
+ if not st.local_keys_enabled:
137
+ line = line.replace("lock:yes", self._color("lock:yes", "31"))
138
+ width = shutil.get_terminal_size(fallback=(120, 20)).columns
139
+ if len(line) > width:
140
+ line = line[: max(0, width - 1)] + "…"
141
+ return line
142
+
143
+ def maybe_render(self, st: Status, force: bool = False):
144
+ now = time.time()
145
+ if not self.dirty and not force and (now - self.last_render) < self.interval:
146
+ return
147
+ line = self.build_line(st)
148
+ if line != self._cached_line or force:
149
+ sys.stdout.write("\r" + line + "\x1b[K")
150
+ sys.stdout.flush()
151
+ self._cached_line = line
152
+ self.dirty = False
153
+ self.last_render = now
154
+
155
+ def finalize(self, st: Status):
156
+ # Clear line then print summary
157
+ elapsed = time.time() - st.start_time
158
+ summary = (
159
+ f"Stopped after {st.click_count} clicks in {elapsed:.1f}s "
160
+ f"(avg {(st.click_count / elapsed) if elapsed > 0 else 0:.2f} cps)"
161
+ )
162
+ sys.stdout.write("\r" + " " * max(len(self._cached_line), len(summary)) + "\r")
163
+ print(summary)
164
+
165
+
166
+ status = Status(
167
+ start_time=time.time(),
168
+ running=running,
169
+ delay=delay,
170
+ hold_time=hold_time,
171
+ click_method=click_method,
172
+ jitter_enabled=jitter_enabled,
173
+ debug=debug,
174
+ local_keys_enabled=local_keys_enabled,
175
+ )
176
+ status_renderer = StatusRenderer()
177
+
178
+
179
+ def _refresh_status(mark: bool = True):
180
+ # Sync global vars into status object
181
+ status.running = running
182
+ status.delay = delay
183
+ status.hold_time = hold_time
184
+ status.click_method = click_method
185
+ status.jitter_enabled = jitter_enabled
186
+ status.debug = debug
187
+ status.local_keys_enabled = local_keys_enabled
188
+ if mark:
189
+ status_renderer.mark()
190
+
191
+
192
+ def _set_message(msg: str):
193
+ status.message = msg
194
+ status.message_ts = time.time()
195
+ status_renderer.mark()
196
+
197
+
198
+ # Configure pyautogui safety
199
+ pyautogui.FAILSAFE = False # disable corner failsafe to avoid unintended stops
200
+ pyautogui.PAUSE = 0 # no extra pause added by pyautogui
201
+
202
+ # Shared mouse controller for pynput method
203
+ _mouse = MouseController()
204
+
205
+
206
+ def _click_with_pynput():
207
+ # Click at current pointer location using Quartz events via pynput
208
+ _mouse.press(Button.left)
209
+ if hold_time > 0:
210
+ time.sleep(hold_time)
211
+ _mouse.release(Button.left)
212
+
213
+
214
+ def _click_with_pyautogui():
215
+ # Click at current pointer location via pyautogui
216
+ pyautogui.mouseDown()
217
+ if hold_time > 0:
218
+ time.sleep(hold_time)
219
+ pyautogui.mouseUp()
220
+
221
+
222
+ def _convert_to_quartz_point(pos):
223
+ # Convert top-left origin (pyautogui) to bottom-left origin (Quartz)
224
+ try:
225
+ Quartz = importlib.import_module("Quartz")
226
+ CG = Quartz.CoreGraphics
227
+ bounds = CG.CGDisplayBounds(CG.CGMainDisplayID())
228
+ height = int(bounds.size.height)
229
+ return CG.CGPoint(pos[0], height - pos[1])
230
+ except Exception:
231
+ # Fallback: return as-is (may be wrong on some layouts)
232
+ try:
233
+ CG = Quartz.CoreGraphics # type: ignore
234
+ return CG.CGPoint(pos[0], pos[1])
235
+ except Exception:
236
+ return pos
237
+
238
+
239
+ def _click_with_quartz():
240
+ if not _HAS_QUARTZ:
241
+ raise RuntimeError("Quartz not available")
242
+ Quartz = importlib.import_module("Quartz")
243
+ CG = Quartz.CoreGraphics
244
+ # Use current Quartz cursor location directly to avoid multi-display
245
+ # conversion issues
246
+ evt = CG.CGEventCreate(None)
247
+ qpt = CG.CGEventGetLocation(evt)
248
+ down = CG.CGEventCreateMouseEvent(
249
+ None, CG.kCGEventLeftMouseDown, qpt, CG.kCGMouseButtonLeft
250
+ )
251
+ up = CG.CGEventCreateMouseEvent(
252
+ None, CG.kCGEventLeftMouseUp, qpt, CG.kCGMouseButtonLeft
253
+ )
254
+ CG.CGEventPost(CG.kCGHIDEventTap, down)
255
+ if hold_time > 0:
256
+ time.sleep(hold_time)
257
+ CG.CGEventPost(CG.kCGHIDEventTap, up)
258
+ # tiny yield to ensure event dispatch
259
+ time.sleep(0.001)
260
+
261
+
262
+ def _jitter_move():
263
+ if not jitter_enabled or jitter_px <= 0:
264
+ return
265
+ try:
266
+ if click_method in ("pynput", "quartz"):
267
+ x, y = _mouse.position
268
+ _mouse.position = (x + jitter_px, y)
269
+ _mouse.position = (x, y)
270
+ else:
271
+ pyautogui.moveRel(jitter_px, 0, duration=0)
272
+ pyautogui.moveRel(-jitter_px, 0, duration=0)
273
+ except Exception:
274
+ # ignore jitter failures
275
+ pass
276
+
277
+
278
+ def perform_click(with_jitter: bool = True):
279
+ global click_method
280
+ try:
281
+ if with_jitter:
282
+ _jitter_move()
283
+ if click_method == "pynput":
284
+ _click_with_pynput()
285
+ elif click_method == "quartz":
286
+ _click_with_quartz()
287
+ else:
288
+ _click_with_pyautogui()
289
+ status.click_count += 1
290
+ status.last_click_ts = time.time()
291
+ status_renderer.mark()
292
+ except Exception as e:
293
+ # Fallback to the other method once if one fails
294
+ order = ["pynput", "quartz", "pyautogui"]
295
+ try:
296
+ idx = order.index(click_method)
297
+ except ValueError:
298
+ idx = 0
299
+ other = order[(idx + 1) % len(order)]
300
+ if debug:
301
+ print(f"Click via {click_method} failed: {e} -> trying {other}")
302
+ click_method = other
303
+ status.fallbacks += 1
304
+ status_renderer.mark()
305
+ _set_message(f"fallback->{other}")
306
+ if other == "pynput":
307
+ _click_with_pynput()
308
+ elif other == "quartz":
309
+ _click_with_quartz()
310
+ else:
311
+ _click_with_pyautogui()
312
+ status.click_count += 1
313
+ status.last_click_ts = time.time()
314
+ status_renderer.mark()
315
+
316
+
317
+ def toggle_running():
318
+ global running
319
+ running = not running
320
+ _set_message("started" if running else "paused")
321
+ _refresh_status()
322
+
323
+
324
+ def global_toggle_keys():
325
+ """Toggle local keys on/off via Ctrl+Alt+K (doesn't affect autoclicker)"""
326
+ global local_keys_enabled
327
+ local_keys_enabled = not local_keys_enabled
328
+
329
+ _set_message("keys:unlocked" if local_keys_enabled else "keys:locked")
330
+ _refresh_status()
331
+
332
+
333
+ def increase_speed():
334
+ global delay
335
+ delay = max(min_delay, round(delay - 0.1, 3))
336
+ _set_message(f"delay {delay:.2f}s")
337
+ _refresh_status()
338
+
339
+
340
+ def decrease_speed():
341
+ global delay
342
+ delay = min(max_delay, round(delay + 0.1, 3))
343
+ _set_message(f"delay {delay:.2f}s")
344
+ _refresh_status()
345
+
346
+
347
+ def increase_hold():
348
+ global hold_time
349
+ hold_time = min(0.25, round(hold_time + 0.005, 3))
350
+ _set_message(f"hold {hold_time:.3f}s")
351
+ _refresh_status()
352
+
353
+
354
+ def decrease_hold():
355
+ global hold_time
356
+ hold_time = max(0.0, round(hold_time - 0.005, 3))
357
+ _set_message(f"hold {hold_time:.3f}s")
358
+ _refresh_status()
359
+
360
+
361
+ def toggle_method():
362
+ global click_method
363
+ order = ["pynput", "quartz" if _HAS_QUARTZ else None, "pyautogui"]
364
+ order = [m for m in order if m]
365
+ try:
366
+ idx = order.index(click_method)
367
+ except ValueError:
368
+ idx = 0
369
+ click_method = order[(idx + 1) % len(order)]
370
+ _set_message(f"method {click_method}")
371
+ _refresh_status()
372
+
373
+
374
+ def single_test_click():
375
+ pos = pyautogui.position()
376
+ _set_message(f"test {pos[0]},{pos[1]}")
377
+ perform_click(with_jitter=False)
378
+ _refresh_status()
379
+
380
+
381
+ def toggle_debug():
382
+ global debug
383
+ debug = not debug
384
+ _set_message("debug:on" if debug else "debug:off")
385
+ _refresh_status()
386
+
387
+
388
+ def _is_ctrl(key):
389
+ return key in (Key.ctrl, Key.ctrl_l, Key.ctrl_r)
390
+
391
+
392
+ def _is_alt(key):
393
+ return key in (Key.alt, Key.alt_l, Key.alt_r, Key.alt_gr)
394
+
395
+
396
+ def on_press(key):
397
+ # Record key
398
+ _pressed_keys.add(key)
399
+
400
+ # Detect global combo Ctrl+Alt+K (works always)
401
+ try:
402
+ if any(_is_ctrl(k) for k in _pressed_keys) and any(
403
+ _is_alt(k) for k in _pressed_keys
404
+ ):
405
+ if getattr(key, "char", None) == "k": # final key in combo
406
+ global_toggle_keys()
407
+ return
408
+ except Exception:
409
+ pass
410
+
411
+ # If local keys locked, ignore all other keys
412
+ if not local_keys_enabled:
413
+ return
414
+
415
+ try:
416
+ ch = getattr(key, "char", None)
417
+ if ch == "s":
418
+ toggle_running()
419
+ elif ch == "+": # shift + '=' on most keyboards
420
+ increase_speed()
421
+ elif ch == "-":
422
+ decrease_speed()
423
+ elif ch == "]": # increase hold/"pressure"
424
+ increase_hold()
425
+ elif ch == "[": # decrease hold/"pressure"
426
+ decrease_hold()
427
+ elif ch == "m": # toggle click method
428
+ toggle_method()
429
+ elif ch == "c": # single test click
430
+ single_test_click()
431
+ elif ch == "d": # toggle debug logs
432
+ toggle_debug()
433
+ elif ch == "j": # toggle jitter move
434
+ global jitter_enabled
435
+ jitter_enabled = not jitter_enabled
436
+ state = "enabled" if jitter_enabled else "disabled"
437
+ _set_message(f"jitter:{state}")
438
+ _refresh_status()
439
+ except Exception:
440
+ pass
441
+
442
+
443
+ def on_release(key):
444
+ # Safely remove released key
445
+ try:
446
+ _pressed_keys.discard(key)
447
+ except Exception:
448
+ pass
449
+
450
+
451
+ def main(): # pragma: no cover - interactive app
452
+ # Safety: honor simple flags here too, so that even if the CLI wrapper is
453
+ # bypassed (or an older entry point is used), we don't start the loop.
454
+ try:
455
+ import sys as _sys
456
+
457
+ _argv = _sys.argv[1:]
458
+ if any(a in ("-h", "--help") for a in _argv):
459
+ _help_lines = [
460
+ "autoclicker — macOS-friendly Python autoclicker",
461
+ "",
462
+ "Usage:",
463
+ " autoclicker [--help] [--version]",
464
+ "",
465
+ "Notes:",
466
+ " - Grant Accessibility and Input Monitoring",
467
+ " permissions to your terminal app in macOS settings,",
468
+ " then restart the terminal.",
469
+ " - Hotkeys:",
470
+ " s start/pause • +/− speed • ]/[ hold • m method •",
471
+ " c test",
472
+ " j jitter • d debug • Ctrl+Alt+K lock",
473
+ ]
474
+ print("\n".join(_help_lines))
475
+ return
476
+ if any(a in ("-V", "--version") for a in _argv):
477
+ from . import __version__ as _ver
478
+
479
+ print(_ver)
480
+ return
481
+ except Exception:
482
+ # Never block startup if flag parsing fails; continue normally.
483
+ pass
484
+
485
+ # Single listener handles both local keys and global combo
486
+ listener = keyboard.Listener(on_press=on_press, on_release=on_release)
487
+ listener.start()
488
+
489
+ print("Autoclicker ready (status line below). Press Ctrl+C to exit.")
490
+ print(
491
+ "Keys: 's' start/pause, '+' faster, '-' slower, '['/']' hold time,\n"
492
+ " 'm' method, 'c' test, 'j' jitter, 'd' debug,\n"
493
+ " Ctrl+Alt+K lock keys"
494
+ )
495
+ _refresh_status()
496
+
497
+ try:
498
+ while True:
499
+ if running:
500
+ if debug:
501
+ pos = pyautogui.position()
502
+ print(
503
+ f"Clicking at {pos} via {click_method} "
504
+ f"(delay {delay}s, hold {hold_time}s)"
505
+ )
506
+ perform_click(with_jitter=True)
507
+ nd = random.uniform(
508
+ delay * MIN_DELAY_MULTIPLIER, delay * MAX_DELAY_MULTIPLIER
509
+ )
510
+ status.next_delay = nd
511
+ status_renderer.mark()
512
+ status_renderer.maybe_render(status)
513
+ time.sleep(nd)
514
+ status.next_delay = None
515
+ status_renderer.mark()
516
+ else:
517
+ time.sleep(0.05)
518
+ status_renderer.maybe_render(status)
519
+ except KeyboardInterrupt:
520
+ print("Autoclicker stopped.")
521
+ finally:
522
+ try:
523
+ listener.stop()
524
+ except Exception:
525
+ pass
526
+ try:
527
+ status_renderer.finalize(status)
528
+ except Exception:
529
+ pass
@@ -0,0 +1,45 @@
1
+ """CLI entrypoint for autoclicker.
2
+
3
+ Usage:
4
+ autoclicker [--help] [--version]
5
+
6
+ Without flags, starts the interactive autoclicker. See README for hotkeys
7
+ and macOS permission requirements.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+
13
+ def main() -> None:
14
+ # Parse minimal flags without importing heavy modules.
15
+ import sys
16
+
17
+ argv = sys.argv[1:]
18
+ if any(a in ("-h", "--help") for a in argv):
19
+ print(
20
+ """
21
+ autoclicker — macOS-friendly Python autoclicker
22
+
23
+ Usage:
24
+ autoclicker [--help] [--version]
25
+
26
+ Notes:
27
+ - Grant Accessibility and Input Monitoring permissions to your terminal app
28
+ in macOS settings, then restart the terminal.
29
+ - Hotkeys:
30
+ s start/pause • +/− speed • ]/[ hold • m method • c test
31
+ j jitter • d debug • Ctrl+Alt+K lock
32
+ """.strip()
33
+ )
34
+ return
35
+
36
+ if any(a in ("-V", "--version") for a in argv):
37
+ from . import __version__
38
+
39
+ print(__version__)
40
+ return
41
+
42
+ # Start the application only when not handling flags.
43
+ from . import app
44
+
45
+ app.main()