clickmate 0.1.0__py3-none-any.whl
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.
- autoclicker/__init__.py +7 -0
- autoclicker/__main__.py +4 -0
- autoclicker/app.py +529 -0
- autoclicker/cli.py +45 -0
- clickmate-0.1.0.dist-info/METADATA +235 -0
- clickmate-0.1.0.dist-info/RECORD +8 -0
- clickmate-0.1.0.dist-info/WHEEL +4 -0
- clickmate-0.1.0.dist-info/entry_points.txt +3 -0
autoclicker/__init__.py
ADDED
autoclicker/__main__.py
ADDED
autoclicker/app.py
ADDED
|
@@ -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
|
autoclicker/cli.py
ADDED
|
@@ -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()
|
|
@@ -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,8 @@
|
|
|
1
|
+
autoclicker/__init__.py,sha256=mp1C5FeZ9eoErv5K-UMBf2l7n-yaY9yLkTqF2sHTKig,135
|
|
2
|
+
autoclicker/__main__.py,sha256=jUDiCGRIhJvfwEslxaIxuuBq7RQwE_t_z0CFJRVfNl8,81
|
|
3
|
+
autoclicker/app.py,sha256=6Hje0-gJrvoRfIDuW3JPB4uS-Jysc_6U3dMEdod-kdY,16630
|
|
4
|
+
autoclicker/cli.py,sha256=lVdMmvrlW4ca8fNx5UryJpOtdOqj02_0Mu4Y0pINRuE,1069
|
|
5
|
+
clickmate-0.1.0.dist-info/WHEEL,sha256=F3mArEuDT3LDFEqo9fCiUx6ISLN64aIhcGSiIwtu4r8,79
|
|
6
|
+
clickmate-0.1.0.dist-info/entry_points.txt,sha256=7B0IeGyfAKk77m6JxL7Xj6lMwwpw8vKE_b2grvgn7o8,52
|
|
7
|
+
clickmate-0.1.0.dist-info/METADATA,sha256=1wxwlwrSkPBuuxSSY3Bj2tjD8gtZeu2oUrPfrt9I08Y,9588
|
|
8
|
+
clickmate-0.1.0.dist-info/RECORD,,
|