clickmate 0.1.2.dev0__tar.gz → 0.1.3__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.3
4
4
  Summary: Add your description here
5
5
  Keywords: autoclicker,clickmate,automation,macos,quartz,mouse,cli
6
6
  Author: ivand890
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "clickmate"
3
- version = "0.1.2-dev"
3
+ version = "0.1.3"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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.3"
@@ -1,9 +1,14 @@
1
1
  import time
2
2
  import random
3
+ import threading
3
4
  import pyautogui
4
5
  from pynput import keyboard
5
6
  from pynput.keyboard import Key
6
- from pynput.mouse import Controller as MouseController, Button
7
+ from pynput.mouse import (
8
+ Controller as MouseController,
9
+ Button,
10
+ Listener as MouseListener,
11
+ )
7
12
  import importlib
8
13
  import sys
9
14
  import shutil
@@ -32,7 +37,13 @@ debug = False
32
37
  jitter_enabled = False # default jitter disabled (enable with 'j' if needed)
33
38
  jitter_px = 1
34
39
  local_keys_enabled = True # when False, only the global hotkey works
40
+ cursor_lock_enabled = False # Lock cursor position when running
41
+ locked_position = None # (x, y) tuple
42
+ _last_lock_alert = 0.0 # track last alert time for throttling
35
43
  _pressed_keys = set() # track currently pressed keys for combo detection
44
+ _cursor_lock_thread = None
45
+ _cursor_lock_active = threading.Event()
46
+ _cursor_lock_shutdown = threading.Event()
36
47
 
37
48
 
38
49
  # ---------------- Status line support -----------------
@@ -46,6 +57,7 @@ class Status:
46
57
  jitter_enabled: bool
47
58
  debug: bool
48
59
  local_keys_enabled: bool
60
+ cursor_lock_enabled: bool
49
61
  click_count: int = 0
50
62
  last_click_ts: float | None = None
51
63
  next_delay: float | None = None
@@ -85,6 +97,7 @@ class StatusRenderer:
85
97
  state_sym = "▶" if st.running else "⏸"
86
98
  # lock shows yes/no for compatibility (yes = locked)
87
99
  lock_val = "no" if st.local_keys_enabled else "yes"
100
+ clock_val = "yes" if st.cursor_lock_enabled else "no"
88
101
  jitter_sym = "J" if st.jitter_enabled else "-"
89
102
  debug_sym = "D" if st.debug else "-"
90
103
  method_map = {"quartz": "QTZ", "pynput": "PNP", "pyautogui": "PGA"}
@@ -117,6 +130,7 @@ class StatusRenderer:
117
130
  f"jitter:{jitter_sym}",
118
131
  f"debug:{debug_sym}",
119
132
  f"lock:{lock_val}",
133
+ f"clock:{clock_val}",
120
134
  f"clicks:{clicks_val}",
121
135
  f"last:{last_val}",
122
136
  f"next:{next_val}",
@@ -135,6 +149,8 @@ class StatusRenderer:
135
149
  line = line.replace(state_sym, self._color(state_sym, "33"))
136
150
  if not st.local_keys_enabled:
137
151
  line = line.replace("lock:yes", self._color("lock:yes", "31"))
152
+ if st.cursor_lock_enabled:
153
+ line = line.replace("clock:yes", self._color("clock:yes", "36"))
138
154
  width = shutil.get_terminal_size(fallback=(120, 20)).columns
139
155
  if len(line) > width:
140
156
  line = line[: max(0, width - 1)] + "…"
@@ -172,6 +188,7 @@ status = Status(
172
188
  jitter_enabled=jitter_enabled,
173
189
  debug=debug,
174
190
  local_keys_enabled=local_keys_enabled,
191
+ cursor_lock_enabled=cursor_lock_enabled,
175
192
  )
176
193
  status_renderer = StatusRenderer()
177
194
 
@@ -185,6 +202,7 @@ def _refresh_status(mark: bool = True):
185
202
  status.jitter_enabled = jitter_enabled
186
203
  status.debug = debug
187
204
  status.local_keys_enabled = local_keys_enabled
205
+ status.cursor_lock_enabled = cursor_lock_enabled
188
206
  if mark:
189
207
  status_renderer.mark()
190
208
 
@@ -315,8 +333,11 @@ def perform_click(with_jitter: bool = True):
315
333
 
316
334
 
317
335
  def toggle_running():
318
- global running
336
+ global running, locked_position
319
337
  running = not running
338
+ if running:
339
+ locked_position = _mouse.position
340
+ _sync_cursor_lock_worker_state()
320
341
  _set_message("started" if running else "paused")
321
342
  _refresh_status()
322
343
 
@@ -330,6 +351,17 @@ def global_toggle_keys():
330
351
  _refresh_status()
331
352
 
332
353
 
354
+ def toggle_cursor_lock():
355
+ global cursor_lock_enabled, locked_position
356
+ cursor_lock_enabled = not cursor_lock_enabled
357
+ if cursor_lock_enabled:
358
+ locked_position = _mouse.position
359
+ _sync_cursor_lock_worker_state()
360
+ state = "enabled" if cursor_lock_enabled else "disabled"
361
+ _set_message(f"cursor_lock:{state}")
362
+ _refresh_status()
363
+
364
+
333
365
  def increase_speed():
334
366
  global delay
335
367
  delay = max(min_delay, round(delay - 0.1, 3))
@@ -440,6 +472,8 @@ def on_press(key):
440
472
  state = "enabled" if jitter_enabled else "disabled"
441
473
  _set_message(f"jitter:{state}")
442
474
  _refresh_status()
475
+ elif ch == "l": # toggle cursor lock
476
+ toggle_cursor_lock()
443
477
  except Exception:
444
478
  pass
445
479
 
@@ -452,6 +486,75 @@ def on_release(key):
452
486
  pass
453
487
 
454
488
 
489
+ def on_mouse_move(x, y):
490
+ # We now use a dedicated thread for more aggressive locking on Windows,
491
+ # but we keep the listener for potential future use or other platforms.
492
+ pass
493
+
494
+
495
+ def _enforce_cursor_lock():
496
+ """Logic to enforce cursor lock. Returns True if alert was triggered."""
497
+ global _last_lock_alert
498
+ if running and cursor_lock_enabled and locked_position:
499
+ curr = _mouse.position
500
+ # Check distance (squared)
501
+ dx = curr[0] - locked_position[0]
502
+ dy = curr[1] - locked_position[1]
503
+ if (dx * dx + dy * dy) > 25: # > 5 pixels
504
+ _mouse.position = locked_position
505
+ now = time.time()
506
+ if now - _last_lock_alert > 2.0:
507
+ _set_message("Cursor locked! Movement detected.")
508
+ _last_lock_alert = now
509
+ return True
510
+ return False
511
+
512
+
513
+ def _ensure_cursor_lock_worker():
514
+ global _cursor_lock_thread
515
+ if _cursor_lock_thread and _cursor_lock_thread.is_alive():
516
+ return
517
+ _cursor_lock_shutdown.clear()
518
+ _cursor_lock_thread = threading.Thread(
519
+ target=_cursor_lock_worker,
520
+ daemon=True,
521
+ )
522
+ _cursor_lock_thread.start()
523
+
524
+
525
+ def _sync_cursor_lock_worker_state():
526
+ if running and cursor_lock_enabled:
527
+ _ensure_cursor_lock_worker()
528
+ _cursor_lock_active.set()
529
+ else:
530
+ _cursor_lock_active.clear()
531
+
532
+
533
+ def _stop_cursor_lock_worker():
534
+ global _cursor_lock_thread
535
+ thread = _cursor_lock_thread
536
+ _cursor_lock_shutdown.set()
537
+ _cursor_lock_active.set()
538
+ if thread and thread.is_alive():
539
+ thread.join(timeout=0.1)
540
+ _cursor_lock_active.clear()
541
+ _cursor_lock_thread = None
542
+
543
+
544
+ def _cursor_lock_worker():
545
+ """Dedicated thread to enforce cursor lock with high frequency."""
546
+ while not _cursor_lock_shutdown.is_set():
547
+ _cursor_lock_active.wait()
548
+ if _cursor_lock_shutdown.is_set():
549
+ break
550
+ while _cursor_lock_active.is_set() and not _cursor_lock_shutdown.is_set():
551
+ try:
552
+ _enforce_cursor_lock()
553
+ except Exception:
554
+ pass
555
+ time.sleep(0.005) # 200Hz for smooth locking
556
+
557
+
455
558
  def main(): # pragma: no cover - interactive app
456
559
  # Safety: honor simple flags here too, so that even if the CLI wrapper is
457
560
  # bypassed (or an older entry point is used), we don't start the loop.
@@ -473,7 +576,7 @@ def main(): # pragma: no cover - interactive app
473
576
  " - Hotkeys:",
474
577
  " s start/pause • +/− speed • ]/[ hold • m method •",
475
578
  " c test",
476
- " j jitter • d debug • Ctrl+Alt+K lock",
579
+ " j jitter • d debug • l lock cursor • Ctrl+Alt+K lock keys",
477
580
  ]
478
581
  print("\n".join(_help_lines))
479
582
  return
@@ -490,11 +593,15 @@ def main(): # pragma: no cover - interactive app
490
593
  listener = keyboard.Listener(on_press=on_press, on_release=on_release)
491
594
  listener.start()
492
595
 
596
+ # Mouse listener for future use
597
+ mouse_listener = MouseListener(on_move=on_mouse_move)
598
+ mouse_listener.start()
599
+
493
600
  print("Autoclicker ready (status line below). Press Ctrl+C to exit.")
494
601
  print(
495
602
  "Keys: 's' start/pause, '+' faster, '-' slower, '['/']' hold time,\n"
496
603
  " 'm' method, 'c' test, 'j' jitter, 'd' debug,\n"
497
- " Ctrl+Alt+K lock keys"
604
+ " Ctrl+Alt+K lock keys, 'l' cursor lock"
498
605
  )
499
606
  _refresh_status()
500
607
 
@@ -527,6 +634,14 @@ def main(): # pragma: no cover - interactive app
527
634
  listener.stop()
528
635
  except Exception:
529
636
  pass
637
+ try:
638
+ mouse_listener.stop()
639
+ except Exception:
640
+ pass
641
+ try:
642
+ _stop_cursor_lock_worker()
643
+ except Exception:
644
+ pass
530
645
  try:
531
646
  status_renderer.finalize(status)
532
647
  except Exception:
File without changes
File without changes