clickmate 0.1.2.dev0__tar.gz → 0.1.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: clickmate
3
- Version: 0.1.2.dev0
3
+ Version: 0.1.4
4
4
  Summary: Add your description here
5
5
  Keywords: autoclicker,clickmate,automation,macos,quartz,mouse,cli
6
6
  Author: ivand890
@@ -37,7 +37,6 @@ Classifier: Programming Language :: Python :: 3 :: Only
37
37
  Classifier: Programming Language :: Python :: 3.11
38
38
  Classifier: Programming Language :: Python :: 3.12
39
39
  Classifier: Programming Language :: Python :: 3.13
40
- Requires-Dist: pyautogui>=0.9.54
41
40
  Requires-Dist: pynput>=1.8.1
42
41
  Requires-Dist: pyobjc-framework-quartz>=10.3 ; sys_platform == 'darwin' and extra == 'mac'
43
42
  Requires-Python: >=3.11
@@ -50,7 +49,7 @@ Description-Content-Type: text/markdown
50
49
 
51
50
  # Clickmate
52
51
 
53
- Clickmate is a macOS-friendly Python autoclicker. It prefers native Quartz CoreGraphics events on macOS (when available) and falls back to `pynput` or `pyautogui`. It features randomized delays, adjustable hold time (“pressure”), and a clean single-line status UI.
52
+ Clickmate is a macOS-friendly Python autoclicker. It prefers native Quartz CoreGraphics events on macOS (when available) and falls back to `pynput`. It features randomized delays, adjustable hold time (“pressure”), and a clean single-line status UI.
54
53
 
55
54
  ## Features
56
55
 
@@ -58,7 +57,7 @@ Clickmate is a macOS-friendly Python autoclicker. It prefers native Quartz CoreG
58
57
  - Randomized delay per click (0.5×–1.5× of base)
59
58
  - Adjust speed with `+` (faster) / `-` (slower)
60
59
  - Adjust hold time with `]` (increase) / `[` (decrease)
61
- - Toggle click engine with `m` (`pynput` → `quartz` → `pyautogui`)
60
+ - Toggle click engine with `m` (`pynput` ↔ `quartz`, macOS only)
62
61
  - Single test click with `c`
63
62
  - Optional jitter movement `j` and debug logs `d`
64
63
  - Global hotkey to lock/unlock local keys: `Ctrl+Alt+K`
@@ -115,7 +114,7 @@ uv tool install .
115
114
  - `s` — start/pause
116
115
  - `+` / `-` — faster / slower
117
116
  - `]` / `[` — increase / decrease hold time
118
- - `m` — toggle engine (`pynput` → `quartz` → `pyautogui`)
117
+ - `m` — toggle engine (`pynput` ↔ `quartz`, macOS only)
119
118
  - `c` — single test click
120
119
  - `j` — toggle jitter
121
120
  - `d` — toggle debug logs
@@ -133,7 +132,7 @@ Example:
133
132
  Key fields:
134
133
  - `delay` base delay; actual delays are randomized within `rng` (0.5×–1.5×)
135
134
  - `hold` button press duration (“pressure”)
136
- - `meth` engine: `QTZ` (Quartz), `PNP` (pynput), `PGA` (pyautogui)
135
+ - `meth` engine: `QTZ` (Quartz), `PNP` (pynput)
137
136
  - `lock` `yes` when local keys are locked (only `Ctrl+Alt+K` works)
138
137
  - `fb:N` fallback count when switching engines was required
139
138
 
@@ -171,6 +170,5 @@ MIT — see `LICENSE`.
171
170
 
172
171
  ## Dependencies
173
172
 
174
- - `pyautogui` — automation
175
- - `pynput` — input handling and alternative click path
173
+ - `pynput` — input handling and clicks
176
174
  - Optional: `pyobjc` (provides `Quartz`) — enables Quartz engine on macOS
@@ -1,6 +1,6 @@
1
1
  # Clickmate
2
2
 
3
- Clickmate is a macOS-friendly Python autoclicker. It prefers native Quartz CoreGraphics events on macOS (when available) and falls back to `pynput` or `pyautogui`. It features randomized delays, adjustable hold time (“pressure”), and a clean single-line status UI.
3
+ Clickmate is a macOS-friendly Python autoclicker. It prefers native Quartz CoreGraphics events on macOS (when available) and falls back to `pynput`. It features randomized delays, adjustable hold time (“pressure”), and a clean single-line status UI.
4
4
 
5
5
  ## Features
6
6
 
@@ -8,7 +8,7 @@ Clickmate is a macOS-friendly Python autoclicker. It prefers native Quartz CoreG
8
8
  - Randomized delay per click (0.5×–1.5× of base)
9
9
  - Adjust speed with `+` (faster) / `-` (slower)
10
10
  - Adjust hold time with `]` (increase) / `[` (decrease)
11
- - Toggle click engine with `m` (`pynput` → `quartz` → `pyautogui`)
11
+ - Toggle click engine with `m` (`pynput` ↔ `quartz`, macOS only)
12
12
  - Single test click with `c`
13
13
  - Optional jitter movement `j` and debug logs `d`
14
14
  - Global hotkey to lock/unlock local keys: `Ctrl+Alt+K`
@@ -65,7 +65,7 @@ uv tool install .
65
65
  - `s` — start/pause
66
66
  - `+` / `-` — faster / slower
67
67
  - `]` / `[` — increase / decrease hold time
68
- - `m` — toggle engine (`pynput` → `quartz` → `pyautogui`)
68
+ - `m` — toggle engine (`pynput` ↔ `quartz`, macOS only)
69
69
  - `c` — single test click
70
70
  - `j` — toggle jitter
71
71
  - `d` — toggle debug logs
@@ -83,7 +83,7 @@ Example:
83
83
  Key fields:
84
84
  - `delay` base delay; actual delays are randomized within `rng` (0.5×–1.5×)
85
85
  - `hold` button press duration (“pressure”)
86
- - `meth` engine: `QTZ` (Quartz), `PNP` (pynput), `PGA` (pyautogui)
86
+ - `meth` engine: `QTZ` (Quartz), `PNP` (pynput)
87
87
  - `lock` `yes` when local keys are locked (only `Ctrl+Alt+K` works)
88
88
  - `fb:N` fallback count when switching engines was required
89
89
 
@@ -121,6 +121,5 @@ MIT — see `LICENSE`.
121
121
 
122
122
  ## Dependencies
123
123
 
124
- - `pyautogui` — automation
125
- - `pynput` — input handling and alternative click path
124
+ - `pynput` — input handling and clicks
126
125
  - Optional: `pyobjc` (provides `Quartz`) — enables Quartz engine on macOS
@@ -1,11 +1,10 @@
1
1
  [project]
2
2
  name = "clickmate"
3
- version = "0.1.2-dev"
3
+ version = "0.1.4"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
7
7
  dependencies = [
8
- "pyautogui>=0.9.54",
9
8
  "pynput>=1.8.1",
10
9
  ]
11
10
  license = { file = "LICENSE" }
@@ -4,4 +4,4 @@ Exports the CLI entrypoint via ``autoclicker.cli:main``.
4
4
  """
5
5
 
6
6
  __all__ = ["__version__"]
7
- __version__ = "0.1.2-dev"
7
+ __version__ = "0.1.4"
@@ -1,9 +1,13 @@
1
1
  import time
2
2
  import random
3
- import pyautogui
3
+ import threading
4
4
  from pynput import keyboard
5
5
  from pynput.keyboard import Key
6
- from pynput.mouse import Controller as MouseController, Button
6
+ from pynput.mouse import (
7
+ Controller as MouseController,
8
+ Button,
9
+ Listener as MouseListener,
10
+ )
7
11
  import importlib
8
12
  import sys
9
13
  import shutil
@@ -15,6 +19,8 @@ try:
15
19
  except Exception:
16
20
  _HAS_QUARTZ = False
17
21
 
22
+ # Click engines available on this platform, in toggle/fallback order
23
+ _ENGINES = ["pynput", "quartz"] if _HAS_QUARTZ else ["pynput"]
18
24
 
19
25
  # Runtime state
20
26
  running = False
@@ -25,14 +31,20 @@ hold_time = 0.1 # how long to hold the button down per click
25
31
  # Randomization multipliers for per-click delay
26
32
  MIN_DELAY_MULTIPLIER = 0.5
27
33
  MAX_DELAY_MULTIPLIER = 1.5
28
- # Preferred click method: "pynput", "pyautogui", or "quartz" (macOS)
34
+ # Preferred click method: "pynput" or "quartz" (macOS)
29
35
  # Default to Quartz if available for best macOS compatibility
30
36
  click_method = "quartz" if _HAS_QUARTZ else "pynput"
31
37
  debug = False
32
38
  jitter_enabled = False # default jitter disabled (enable with 'j' if needed)
33
39
  jitter_px = 1
34
40
  local_keys_enabled = True # when False, only the global hotkey works
41
+ cursor_lock_enabled = False # Lock cursor position when running
42
+ locked_position = None # (x, y) tuple
43
+ _last_lock_alert = 0.0 # track last alert time for throttling
35
44
  _pressed_keys = set() # track currently pressed keys for combo detection
45
+ _cursor_lock_thread = None
46
+ _cursor_lock_active = threading.Event()
47
+ _cursor_lock_shutdown = threading.Event()
36
48
 
37
49
 
38
50
  # ---------------- Status line support -----------------
@@ -46,6 +58,7 @@ class Status:
46
58
  jitter_enabled: bool
47
59
  debug: bool
48
60
  local_keys_enabled: bool
61
+ cursor_lock_enabled: bool
49
62
  click_count: int = 0
50
63
  last_click_ts: float | None = None
51
64
  next_delay: float | None = None
@@ -85,9 +98,10 @@ class StatusRenderer:
85
98
  state_sym = "▶" if st.running else "⏸"
86
99
  # lock shows yes/no for compatibility (yes = locked)
87
100
  lock_val = "no" if st.local_keys_enabled else "yes"
101
+ clock_val = "yes" if st.cursor_lock_enabled else "no"
88
102
  jitter_sym = "J" if st.jitter_enabled else "-"
89
103
  debug_sym = "D" if st.debug else "-"
90
- method_map = {"quartz": "QTZ", "pynput": "PNP", "pyautogui": "PGA"}
104
+ method_map = {"quartz": "QTZ", "pynput": "PNP"}
91
105
  method_disp = method_map.get(st.click_method, st.click_method)
92
106
  # Fixed width numeric fields to avoid jitter
93
107
  delay_val = f"{st.delay:5.2f}s"
@@ -117,6 +131,7 @@ class StatusRenderer:
117
131
  f"jitter:{jitter_sym}",
118
132
  f"debug:{debug_sym}",
119
133
  f"lock:{lock_val}",
134
+ f"clock:{clock_val}",
120
135
  f"clicks:{clicks_val}",
121
136
  f"last:{last_val}",
122
137
  f"next:{next_val}",
@@ -135,6 +150,8 @@ class StatusRenderer:
135
150
  line = line.replace(state_sym, self._color(state_sym, "33"))
136
151
  if not st.local_keys_enabled:
137
152
  line = line.replace("lock:yes", self._color("lock:yes", "31"))
153
+ if st.cursor_lock_enabled:
154
+ line = line.replace("clock:yes", self._color("clock:yes", "36"))
138
155
  width = shutil.get_terminal_size(fallback=(120, 20)).columns
139
156
  if len(line) > width:
140
157
  line = line[: max(0, width - 1)] + "…"
@@ -172,6 +189,7 @@ status = Status(
172
189
  jitter_enabled=jitter_enabled,
173
190
  debug=debug,
174
191
  local_keys_enabled=local_keys_enabled,
192
+ cursor_lock_enabled=cursor_lock_enabled,
175
193
  )
176
194
  status_renderer = StatusRenderer()
177
195
 
@@ -185,6 +203,7 @@ def _refresh_status(mark: bool = True):
185
203
  status.jitter_enabled = jitter_enabled
186
204
  status.debug = debug
187
205
  status.local_keys_enabled = local_keys_enabled
206
+ status.cursor_lock_enabled = cursor_lock_enabled
188
207
  if mark:
189
208
  status_renderer.mark()
190
209
 
@@ -195,10 +214,6 @@ def _set_message(msg: str):
195
214
  status_renderer.mark()
196
215
 
197
216
 
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
217
  # Shared mouse controller for pynput method
203
218
  _mouse = MouseController()
204
219
 
@@ -211,31 +226,6 @@ def _click_with_pynput():
211
226
  _mouse.release(Button.left)
212
227
 
213
228
 
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
229
  def _click_with_quartz():
240
230
  if not _HAS_QUARTZ:
241
231
  raise RuntimeError("Quartz not available")
@@ -263,60 +253,58 @@ def _jitter_move():
263
253
  if not jitter_enabled or jitter_px <= 0:
264
254
  return
265
255
  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)
256
+ x, y = _mouse.position
257
+ _mouse.position = (x + jitter_px, y)
258
+ _mouse.position = (x, y)
273
259
  except Exception:
274
260
  # ignore jitter failures
275
261
  pass
276
262
 
277
263
 
264
+ def _click_with(method: str):
265
+ if method == "quartz":
266
+ _click_with_quartz()
267
+ else:
268
+ _click_with_pynput()
269
+
270
+
278
271
  def perform_click(with_jitter: bool = True):
279
272
  global click_method
280
273
  try:
281
274
  if with_jitter:
282
275
  _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()
276
+ _click_with(click_method)
289
277
  status.click_count += 1
290
278
  status.last_click_ts = time.time()
291
279
  status_renderer.mark()
292
280
  except Exception as e:
293
281
  # Fallback to the other method once if one fails
294
- order = ["pynput", "quartz", "pyautogui"]
295
282
  try:
296
- idx = order.index(click_method)
283
+ idx = _ENGINES.index(click_method)
297
284
  except ValueError:
298
- idx = 0
299
- other = order[(idx + 1) % len(order)]
285
+ idx = -1
286
+ other = _ENGINES[(idx + 1) % len(_ENGINES)]
287
+ if other == click_method:
288
+ # No alternative engine on this platform
289
+ raise
300
290
  if debug:
301
291
  print(f"Click via {click_method} failed: {e} -> trying {other}")
302
292
  click_method = other
303
293
  status.fallbacks += 1
304
294
  status_renderer.mark()
305
295
  _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()
296
+ _click_with(other)
312
297
  status.click_count += 1
313
298
  status.last_click_ts = time.time()
314
299
  status_renderer.mark()
315
300
 
316
301
 
317
302
  def toggle_running():
318
- global running
303
+ global running, locked_position
319
304
  running = not running
305
+ if running:
306
+ locked_position = _mouse.position
307
+ _sync_cursor_lock_worker_state()
320
308
  _set_message("started" if running else "paused")
321
309
  _refresh_status()
322
310
 
@@ -330,6 +318,17 @@ def global_toggle_keys():
330
318
  _refresh_status()
331
319
 
332
320
 
321
+ def toggle_cursor_lock():
322
+ global cursor_lock_enabled, locked_position
323
+ cursor_lock_enabled = not cursor_lock_enabled
324
+ if cursor_lock_enabled:
325
+ locked_position = _mouse.position
326
+ _sync_cursor_lock_worker_state()
327
+ state = "enabled" if cursor_lock_enabled else "disabled"
328
+ _set_message(f"cursor_lock:{state}")
329
+ _refresh_status()
330
+
331
+
333
332
  def increase_speed():
334
333
  global delay
335
334
  delay = max(min_delay, round(delay - 0.1, 3))
@@ -360,20 +359,18 @@ def decrease_hold():
360
359
 
361
360
  def toggle_method():
362
361
  global click_method
363
- order = ["pynput", "quartz" if _HAS_QUARTZ else None, "pyautogui"]
364
- order = [m for m in order if m]
365
362
  try:
366
- idx = order.index(click_method)
363
+ idx = _ENGINES.index(click_method)
367
364
  except ValueError:
368
365
  idx = 0
369
- click_method = order[(idx + 1) % len(order)]
366
+ click_method = _ENGINES[(idx + 1) % len(_ENGINES)]
370
367
  _set_message(f"method {click_method}")
371
368
  _refresh_status()
372
369
 
373
370
 
374
371
  def single_test_click():
375
- pos = pyautogui.position()
376
- _set_message(f"test {pos[0]},{pos[1]}")
372
+ x, y = _mouse.position
373
+ _set_message(f"test {int(x)},{int(y)}")
377
374
  perform_click(with_jitter=False)
378
375
  _refresh_status()
379
376
 
@@ -440,6 +437,8 @@ def on_press(key):
440
437
  state = "enabled" if jitter_enabled else "disabled"
441
438
  _set_message(f"jitter:{state}")
442
439
  _refresh_status()
440
+ elif ch == "l": # toggle cursor lock
441
+ toggle_cursor_lock()
443
442
  except Exception:
444
443
  pass
445
444
 
@@ -452,6 +451,75 @@ def on_release(key):
452
451
  pass
453
452
 
454
453
 
454
+ def on_mouse_move(x, y):
455
+ # We now use a dedicated thread for more aggressive locking on Windows,
456
+ # but we keep the listener for potential future use or other platforms.
457
+ pass
458
+
459
+
460
+ def _enforce_cursor_lock():
461
+ """Logic to enforce cursor lock. Returns True if alert was triggered."""
462
+ global _last_lock_alert
463
+ if running and cursor_lock_enabled and locked_position:
464
+ curr = _mouse.position
465
+ # Check distance (squared)
466
+ dx = curr[0] - locked_position[0]
467
+ dy = curr[1] - locked_position[1]
468
+ if (dx * dx + dy * dy) > 25: # > 5 pixels
469
+ _mouse.position = locked_position
470
+ now = time.time()
471
+ if now - _last_lock_alert > 2.0:
472
+ _set_message("Cursor locked! Movement detected.")
473
+ _last_lock_alert = now
474
+ return True
475
+ return False
476
+
477
+
478
+ def _ensure_cursor_lock_worker():
479
+ global _cursor_lock_thread
480
+ if _cursor_lock_thread and _cursor_lock_thread.is_alive():
481
+ return
482
+ _cursor_lock_shutdown.clear()
483
+ _cursor_lock_thread = threading.Thread(
484
+ target=_cursor_lock_worker,
485
+ daemon=True,
486
+ )
487
+ _cursor_lock_thread.start()
488
+
489
+
490
+ def _sync_cursor_lock_worker_state():
491
+ if running and cursor_lock_enabled:
492
+ _ensure_cursor_lock_worker()
493
+ _cursor_lock_active.set()
494
+ else:
495
+ _cursor_lock_active.clear()
496
+
497
+
498
+ def _stop_cursor_lock_worker():
499
+ global _cursor_lock_thread
500
+ thread = _cursor_lock_thread
501
+ _cursor_lock_shutdown.set()
502
+ _cursor_lock_active.set()
503
+ if thread and thread.is_alive():
504
+ thread.join(timeout=0.1)
505
+ _cursor_lock_active.clear()
506
+ _cursor_lock_thread = None
507
+
508
+
509
+ def _cursor_lock_worker():
510
+ """Dedicated thread to enforce cursor lock with high frequency."""
511
+ while not _cursor_lock_shutdown.is_set():
512
+ _cursor_lock_active.wait()
513
+ if _cursor_lock_shutdown.is_set():
514
+ break
515
+ while _cursor_lock_active.is_set() and not _cursor_lock_shutdown.is_set():
516
+ try:
517
+ _enforce_cursor_lock()
518
+ except Exception:
519
+ pass
520
+ time.sleep(0.005) # 200Hz for smooth locking
521
+
522
+
455
523
  def main(): # pragma: no cover - interactive app
456
524
  # Safety: honor simple flags here too, so that even if the CLI wrapper is
457
525
  # bypassed (or an older entry point is used), we don't start the loop.
@@ -473,7 +541,7 @@ def main(): # pragma: no cover - interactive app
473
541
  " - Hotkeys:",
474
542
  " s start/pause • +/− speed • ]/[ hold • m method •",
475
543
  " c test",
476
- " j jitter • d debug • Ctrl+Alt+K lock",
544
+ " j jitter • d debug • l lock cursor • Ctrl+Alt+K lock keys",
477
545
  ]
478
546
  print("\n".join(_help_lines))
479
547
  return
@@ -490,11 +558,15 @@ def main(): # pragma: no cover - interactive app
490
558
  listener = keyboard.Listener(on_press=on_press, on_release=on_release)
491
559
  listener.start()
492
560
 
561
+ # Mouse listener for future use
562
+ mouse_listener = MouseListener(on_move=on_mouse_move)
563
+ mouse_listener.start()
564
+
493
565
  print("Autoclicker ready (status line below). Press Ctrl+C to exit.")
494
566
  print(
495
567
  "Keys: 's' start/pause, '+' faster, '-' slower, '['/']' hold time,\n"
496
568
  " 'm' method, 'c' test, 'j' jitter, 'd' debug,\n"
497
- " Ctrl+Alt+K lock keys"
569
+ " Ctrl+Alt+K lock keys, 'l' cursor lock"
498
570
  )
499
571
  _refresh_status()
500
572
 
@@ -502,7 +574,7 @@ def main(): # pragma: no cover - interactive app
502
574
  while True:
503
575
  if running:
504
576
  if debug:
505
- pos = pyautogui.position()
577
+ pos = _mouse.position
506
578
  print(
507
579
  f"Clicking at {pos} via {click_method} "
508
580
  f"(delay {delay}s, hold {hold_time}s)"
@@ -527,6 +599,14 @@ def main(): # pragma: no cover - interactive app
527
599
  listener.stop()
528
600
  except Exception:
529
601
  pass
602
+ try:
603
+ mouse_listener.stop()
604
+ except Exception:
605
+ pass
606
+ try:
607
+ _stop_cursor_lock_worker()
608
+ except Exception:
609
+ pass
530
610
  try:
531
611
  status_renderer.finalize(status)
532
612
  except Exception:
File without changes