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.
- {clickmate-0.1.2.dev0 → clickmate-0.1.4}/PKG-INFO +6 -8
- {clickmate-0.1.2.dev0 → clickmate-0.1.4}/README.md +5 -6
- {clickmate-0.1.2.dev0 → clickmate-0.1.4}/pyproject.toml +1 -2
- {clickmate-0.1.2.dev0 → clickmate-0.1.4}/src/autoclicker/__init__.py +1 -1
- {clickmate-0.1.2.dev0 → clickmate-0.1.4}/src/autoclicker/app.py +146 -66
- {clickmate-0.1.2.dev0 → clickmate-0.1.4}/LICENSE +0 -0
- {clickmate-0.1.2.dev0 → clickmate-0.1.4}/src/autoclicker/__main__.py +0 -0
- {clickmate-0.1.2.dev0 → clickmate-0.1.4}/src/autoclicker/cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: clickmate
|
|
3
|
-
Version: 0.1.
|
|
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
|
|
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`
|
|
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`
|
|
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)
|
|
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
|
-
- `
|
|
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
|
|
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`
|
|
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`
|
|
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)
|
|
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
|
-
- `
|
|
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.
|
|
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" }
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import time
|
|
2
2
|
import random
|
|
3
|
-
import
|
|
3
|
+
import threading
|
|
4
4
|
from pynput import keyboard
|
|
5
5
|
from pynput.keyboard import Key
|
|
6
|
-
from pynput.mouse import
|
|
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"
|
|
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"
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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 =
|
|
283
|
+
idx = _ENGINES.index(click_method)
|
|
297
284
|
except ValueError:
|
|
298
|
-
idx =
|
|
299
|
-
other =
|
|
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
|
-
|
|
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 =
|
|
363
|
+
idx = _ENGINES.index(click_method)
|
|
367
364
|
except ValueError:
|
|
368
365
|
idx = 0
|
|
369
|
-
click_method =
|
|
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
|
-
|
|
376
|
-
_set_message(f"test {
|
|
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 =
|
|
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
|
|
File without changes
|
|
File without changes
|