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.
- {clickmate-0.1.2.dev0 → clickmate-0.1.3}/PKG-INFO +1 -1
- {clickmate-0.1.2.dev0 → clickmate-0.1.3}/pyproject.toml +1 -1
- {clickmate-0.1.2.dev0 → clickmate-0.1.3}/src/autoclicker/__init__.py +1 -1
- {clickmate-0.1.2.dev0 → clickmate-0.1.3}/src/autoclicker/app.py +119 -4
- {clickmate-0.1.2.dev0 → clickmate-0.1.3}/LICENSE +0 -0
- {clickmate-0.1.2.dev0 → clickmate-0.1.3}/README.md +0 -0
- {clickmate-0.1.2.dev0 → clickmate-0.1.3}/src/autoclicker/__main__.py +0 -0
- {clickmate-0.1.2.dev0 → clickmate-0.1.3}/src/autoclicker/cli.py +0 -0
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|