|
| 1 | +import time |
| 2 | +import threading |
| 3 | +import random |
| 4 | +import sys |
| 5 | +import subprocess |
| 6 | +import tkinter as tk |
| 7 | +from tkinter import messagebox, ttk |
| 8 | + |
| 9 | +# --- AUTO-INSTALL DEPENDENCIES --- |
| 10 | +def install_and_import(package, import_name=None): |
| 11 | + import_name = import_name or package |
| 12 | + try: |
| 13 | + __import__(import_name) |
| 14 | + except ImportError: |
| 15 | + print(f"Installing {package}...") |
| 16 | + subprocess.check_call([sys.executable, "-m", "pip", "install", package]) |
| 17 | + try: |
| 18 | + __import__(import_name) |
| 19 | + except ImportError: |
| 20 | + messagebox.showerror("Error", f"Failed to install {package}. Please run 'pip install -r requirements.txt' manually.") |
| 21 | + sys.exit(1) |
| 22 | + |
| 23 | +install_and_import("pynput") |
| 24 | +install_and_import("pygetwindow") |
| 25 | +install_and_import("pyrect") |
| 26 | + |
| 27 | +from pynput.mouse import Button, Controller, Listener as MouseListener |
| 28 | +from pynput.keyboard import Listener as KeyListener, KeyCode, Key |
| 29 | +import pygetwindow as gw |
| 30 | + |
| 31 | +class AutoclickerApp: |
| 32 | + def __init__(self, root): |
| 33 | + self.root = root |
| 34 | + self.root.title("Agalar911 AutoClicker") |
| 35 | + self.root.geometry("400x780") |
| 36 | + self.root.resizable(False, False) |
| 37 | + |
| 38 | + # --- PROFESSIONAL DARK THEME --- |
| 39 | + self.bg_black = "#0b0c10" |
| 40 | + self.bg_card = "#1f2833" |
| 41 | + self.accent_blue = "#66fcf1" |
| 42 | + self.accent_dim = "#45a29e" |
| 43 | + self.fg_white = "#c5c6c7" |
| 44 | + self.btn_bg = "#121212" |
| 45 | + self.kill_red = "#f7768e" |
| 46 | + |
| 47 | + self.root.configure(bg=self.bg_black) |
| 48 | + |
| 49 | + self.mouse = Controller() |
| 50 | + self.running = False |
| 51 | + self.binding_mode = None |
| 52 | + self.picking_pos = False |
| 53 | + |
| 54 | + # --- STATE VARIABLES --- |
| 55 | + self.unit_var = tk.StringVar(value="ms") |
| 56 | + self.button_var = tk.StringVar(value="left") |
| 57 | + self.click_type_var = tk.StringVar(value="single") |
| 58 | + |
| 59 | + self.jitter_var = tk.BooleanVar(value=False) |
| 60 | + self.always_on_top_var = tk.BooleanVar(value=False) |
| 61 | + self.click_limit_enabled = tk.BooleanVar(value=False) |
| 62 | + self.timer_enabled = tk.BooleanVar(value=False) |
| 63 | + self.lock_pos_enabled = tk.BooleanVar(value=False) |
| 64 | + self.lock_window_enabled = tk.BooleanVar(value=False) |
| 65 | + |
| 66 | + self.click_limit_val = tk.StringVar(value="1000") |
| 67 | + self.timer_min_val = tk.StringVar(value="10") |
| 68 | + self.target_pos = None |
| 69 | + self.target_window_title = tk.StringVar(value="No Window Captured") |
| 70 | + |
| 71 | + self.hotkey_start_stop = Key.f6 |
| 72 | + self.hotkey_kill = Key.esc |
| 73 | + |
| 74 | + self.clicks_done = 0 |
| 75 | + self.start_time = 0 |
| 76 | + |
| 77 | + self.setup_ui() |
| 78 | + |
| 79 | + # Separate listeners |
| 80 | + self.key_listener = KeyListener(on_press=self.on_press) |
| 81 | + self.mouse_listener = MouseListener(on_click=self.on_click_global) |
| 82 | + self.key_listener.start() |
| 83 | + self.mouse_listener.start() |
| 84 | + |
| 85 | + self.root.protocol("WM_DELETE_WINDOW", self.on_closing) |
| 86 | + |
| 87 | + def setup_ui(self): |
| 88 | + title_font = ("Segoe UI", 24, "bold") |
| 89 | + header_font = ("Segoe UI", 10, "bold") |
| 90 | + label_font = ("Segoe UI", 10) |
| 91 | + |
| 92 | + def create_card(parent, text): |
| 93 | + frame = tk.LabelFrame(parent, text=f" {text} ", bg=self.bg_black, fg=self.accent_blue, |
| 94 | + font=header_font, padx=15, pady=10, borderwidth=1, relief="flat", highlightthickness=1) |
| 95 | + frame.config(highlightbackground="#1f2833", highlightcolor=self.accent_blue) |
| 96 | + return frame |
| 97 | + |
| 98 | + # Header |
| 99 | + tk.Label(self.root, text="AGALAR911", font=title_font, bg=self.bg_black, fg="#ffffff").pack(pady=(25, 10)) |
| 100 | + |
| 101 | + # Main Layout Container |
| 102 | + main_container = tk.Frame(self.root, bg=self.bg_black) |
| 103 | + main_container.pack(fill="both", expand=True, padx=25) |
| 104 | + |
| 105 | + radio_style = {"bg": self.bg_black, "fg": self.fg_white, "selectcolor": self.bg_black, |
| 106 | + "activebackground": self.bg_black, "activeforeground": self.accent_blue, |
| 107 | + "font": label_font, "padx": 5} |
| 108 | + |
| 109 | + # --- INTERVAL SECTION --- |
| 110 | + frame_speed = create_card(main_container, "CLICK INTERVAL") |
| 111 | + frame_speed.pack(fill="x", pady=5) |
| 112 | + |
| 113 | + speed_row = tk.Frame(frame_speed, bg=self.bg_black) |
| 114 | + speed_row.pack(fill="x") |
| 115 | + self.interval_entry = tk.Entry(speed_row, width=8, justify='center', bg=self.btn_bg, fg="#ffffff", borderwidth=0, font=("Segoe UI", 12)) |
| 116 | + self.interval_entry.insert(0, "100") |
| 117 | + self.interval_entry.pack(side=tk.LEFT, padx=(0, 10)) |
| 118 | + |
| 119 | + tk.Radiobutton(speed_row, text="ms", variable=self.unit_var, value="ms", **radio_style).pack(side=tk.LEFT) |
| 120 | + tk.Radiobutton(speed_row, text="sec", variable=self.unit_var, value="sec", **radio_style).pack(side=tk.LEFT) |
| 121 | + tk.Radiobutton(speed_row, text="CPS", variable=self.unit_var, value="cps", **radio_style).pack(side=tk.LEFT) |
| 122 | + |
| 123 | + # --- MOUSE SECTION --- |
| 124 | + frame_mouse = create_card(main_container, "MOUSE SETTINGS") |
| 125 | + frame_mouse.pack(fill="x", pady=5) |
| 126 | + |
| 127 | + m_row1 = tk.Frame(frame_mouse, bg=self.bg_black) |
| 128 | + m_row1.pack(fill="x") |
| 129 | + tk.Label(m_row1, text="Button:", bg=self.bg_black, fg=self.accent_dim, width=8, anchor="w").pack(side=tk.LEFT) |
| 130 | + tk.Radiobutton(m_row1, text="Left", variable=self.button_var, value="left", **radio_style).pack(side=tk.LEFT) |
| 131 | + tk.Radiobutton(m_row1, text="Right", variable=self.button_var, value="right", **radio_style).pack(side=tk.LEFT) |
| 132 | + tk.Radiobutton(m_row1, text="Mid", variable=self.button_var, value="middle", **radio_style).pack(side=tk.LEFT) |
| 133 | + |
| 134 | + m_row2 = tk.Frame(frame_mouse, bg=self.bg_black) |
| 135 | + m_row2.pack(fill="x", pady=(5,0)) |
| 136 | + tk.Label(m_row2, text="Type:", bg=self.bg_black, fg=self.accent_dim, width=8, anchor="w").pack(side=tk.LEFT) |
| 137 | + tk.Radiobutton(m_row2, text="Single", variable=self.click_type_var, value="single", **radio_style).pack(side=tk.LEFT) |
| 138 | + tk.Radiobutton(m_row2, text="Double", variable=self.click_type_var, value="double", **radio_style).pack(side=tk.LEFT) |
| 139 | + |
| 140 | + # --- AUTOMATION SECTION --- |
| 141 | + frame_auto = create_card(main_container, "AUTO-STOP CONDITIONS") |
| 142 | + frame_auto.pack(fill="x", pady=5) |
| 143 | + |
| 144 | + limit_row = tk.Frame(frame_auto, bg=self.bg_black) |
| 145 | + limit_row.pack(fill="x") |
| 146 | + tk.Checkbutton(limit_row, text="Max Clicks:", variable=self.click_limit_enabled, **radio_style).pack(side=tk.LEFT) |
| 147 | + tk.Entry(limit_row, textvariable=self.click_limit_val, width=10, bg=self.btn_bg, fg="#ffffff", borderwidth=0).pack(side=tk.RIGHT) |
| 148 | + |
| 149 | + timer_row = tk.Frame(frame_auto, bg=self.bg_black) |
| 150 | + timer_row.pack(fill="x", pady=(5,0)) |
| 151 | + tk.Checkbutton(timer_row, text="Stop Timer (min):", variable=self.timer_enabled, **radio_style).pack(side=tk.LEFT) |
| 152 | + tk.Entry(timer_row, textvariable=self.timer_min_val, width=10, bg=self.btn_bg, fg="#ffffff", borderwidth=0).pack(side=tk.RIGHT) |
| 153 | + |
| 154 | + # --- LOCKS SECTION --- |
| 155 | + frame_locks = create_card(main_container, "POSITIONAL LOCKS") |
| 156 | + frame_locks.pack(fill="x", pady=5) |
| 157 | + |
| 158 | + l_row1 = tk.Frame(frame_locks, bg=self.bg_black) |
| 159 | + l_row1.pack(fill="x") |
| 160 | + tk.Checkbutton(l_row1, text="Lock Coordinates", variable=self.lock_pos_enabled, **radio_style).pack(side=tk.LEFT) |
| 161 | + tk.Button(l_row1, text="Select Spot", command=self.start_picking_pos, bg=self.bg_card, fg=self.accent_blue, borderwidth=0, padx=8).pack(side=tk.RIGHT) |
| 162 | + |
| 163 | + l_row2 = tk.Frame(frame_locks, bg=self.bg_black) |
| 164 | + l_row2.pack(fill="x", pady=(5,0)) |
| 165 | + tk.Checkbutton(l_row2, text="Lock Target Window", variable=self.lock_window_enabled, **radio_style).pack(side=tk.LEFT) |
| 166 | + tk.Button(l_row2, text="Capture", command=self.pick_active_window, bg=self.bg_card, fg=self.accent_blue, borderwidth=0, padx=8).pack(side=tk.RIGHT) |
| 167 | + |
| 168 | + tk.Label(frame_locks, textvariable=self.target_window_title, bg=self.bg_black, fg=self.accent_dim, font=("Segoe UI", 7), wraplength=300).pack(fill="x", pady=(5,0)) |
| 169 | + |
| 170 | + # --- HOTKEYS SECTION --- |
| 171 | + frame_keys = create_card(main_container, "HOTKEYS") |
| 172 | + frame_keys.pack(fill="x", pady=5) |
| 173 | + |
| 174 | + self.btn_bind_start = tk.Button(frame_keys, text=f"Toggle: {self.format_key(self.hotkey_start_stop)}", |
| 175 | + command=lambda: self.start_binding('start_stop'), bg=self.btn_bg, fg=self.accent_blue, borderwidth=1, relief="solid", pady=6) |
| 176 | + self.btn_bind_start.pack(fill="x", pady=2) |
| 177 | + |
| 178 | + self.btn_bind_kill = tk.Button(frame_keys, text=f"Exit App: {self.format_key(self.hotkey_kill)}", |
| 179 | + command=lambda: self.start_binding('kill'), bg=self.btn_bg, fg=self.kill_red, borderwidth=1, relief="solid", pady=6) |
| 180 | + self.btn_bind_kill.pack(fill="x") |
| 181 | + |
| 182 | + # --- FOOTER OPTIONS --- |
| 183 | + tk.Checkbutton(main_container, text="Humanize (Random Jitter/Offset)", variable=self.jitter_var, **radio_style).pack(anchor="w", pady=(10, 0)) |
| 184 | + tk.Checkbutton(main_container, text="Always On Top", variable=self.always_on_top_var, command=self.toggle_on_top, **radio_style).pack(anchor="w") |
| 185 | + |
| 186 | + # --- STATUS FOOTER --- |
| 187 | + self.status_bar = tk.Frame(self.root, bg=self.btn_bg, pady=12) |
| 188 | + self.status_bar.pack(fill="x", side="bottom") |
| 189 | + self.status_label = tk.Label(self.status_bar, text="STATUS: READY", font=("Segoe UI", 10, "bold"), bg=self.btn_bg, fg=self.accent_blue) |
| 190 | + self.status_label.pack() |
| 191 | + |
| 192 | + tk.Label(self.root, text="Developed by KenzoNight", font=("Segoe UI", 7), bg=self.bg_black, fg="#313244").pack(side="bottom", pady=5) |
| 193 | + |
| 194 | + def toggle_on_top(self): |
| 195 | + self.root.attributes("-topmost", self.always_on_top_var.get()) |
| 196 | + |
| 197 | + def start_picking_pos(self): |
| 198 | + self.picking_pos = True |
| 199 | + self.status_label.config(text="STATUS: CLICK TO LOCK POS", fg="#ffffff") |
| 200 | + |
| 201 | + def pick_active_window(self): |
| 202 | + def countdown(count): |
| 203 | + if count > 0: |
| 204 | + self.status_label.config(text=f"STATUS: FOCUS TARGET IN {count}s...", fg=self.kill_red) |
| 205 | + self.root.after(1000, lambda: countdown(count - 1)) |
| 206 | + else: |
| 207 | + try: |
| 208 | + active = gw.getActiveWindow() |
| 209 | + if active: |
| 210 | + self.target_window_title.set(active.title) |
| 211 | + self.status_label.config(text="STATUS: WINDOW CAPTURED", fg=self.accent_blue) |
| 212 | + else: |
| 213 | + self.status_label.config(text="STATUS: CAPTURE FAILED", fg=self.kill_red) |
| 214 | + except: |
| 215 | + self.status_label.config(text="STATUS: CAPTURE ERROR", fg=self.kill_red) |
| 216 | + |
| 217 | + countdown(3) |
| 218 | + |
| 219 | + def start_binding(self, mode): |
| 220 | + self.binding_mode = mode |
| 221 | + btn = self.btn_bind_start if mode == 'start_stop' else self.btn_bind_kill |
| 222 | + btn.config(text="... PRESS ANY KEY ...", fg="#ffffff") |
| 223 | + self.status_label.config(text="STATUS: LISTENING FOR KEY", fg="#ffffff") |
| 224 | + |
| 225 | + def format_key(self, key): |
| 226 | + if isinstance(key, Key): return key.name.upper() |
| 227 | + if isinstance(key, KeyCode): return str(key.char).upper() if key.char else f"K:{key.vk}" |
| 228 | + return str(key) |
| 229 | + |
| 230 | + def get_sleep_interval(self): |
| 231 | + try: |
| 232 | + val = float(self.interval_entry.get()) |
| 233 | + unit = self.unit_var.get() |
| 234 | + interval = val if unit == "sec" else (val / 1000.0 if unit == "ms" else 1.0 / max(0.1, val)) |
| 235 | + except: interval = 0.1 |
| 236 | + if self.jitter_var.get(): interval += random.uniform(-interval*0.15, interval*0.15) |
| 237 | + return max(0.001, interval) |
| 238 | + |
| 239 | + def clicker_loop(self): |
| 240 | + button_map = {"left": Button.left, "right": Button.right, "middle": Button.middle} |
| 241 | + btn = button_map.get(self.button_var.get(), Button.left) |
| 242 | + is_double = self.click_type_var.get() == "double" |
| 243 | + |
| 244 | + while self.running: |
| 245 | + try: |
| 246 | + if self.click_limit_enabled.get() and self.clicks_done >= int(self.click_limit_val.get()): |
| 247 | + self.root.after(0, self.stop_clicking) |
| 248 | + break |
| 249 | + if self.timer_enabled.get() and (time.time() - self.start_time) >= float(self.timer_min_val.get()) * 60: |
| 250 | + self.root.after(0, self.stop_clicking) |
| 251 | + break |
| 252 | + if self.lock_window_enabled.get(): |
| 253 | + active = gw.getActiveWindow() |
| 254 | + if not active or self.target_window_title.get() not in active.title: |
| 255 | + time.sleep(0.1) |
| 256 | + continue |
| 257 | + except: break |
| 258 | + |
| 259 | + interval = self.get_sleep_interval() |
| 260 | + st = time.perf_counter() |
| 261 | + |
| 262 | + if self.lock_pos_enabled.get() and self.target_pos: |
| 263 | + # If "Humanize" is on, add slight jitter to the locked position |
| 264 | + if self.jitter_var.get(): |
| 265 | + jx = self.target_pos[0] + random.randint(-3, 3) |
| 266 | + jy = self.target_pos[1] + random.randint(-3, 3) |
| 267 | + self.mouse.position = (jx, jy) |
| 268 | + else: |
| 269 | + self.mouse.position = self.target_pos |
| 270 | + elif self.jitter_var.get(): |
| 271 | + # If humanize is on but NOT locked, subtle shake in place |
| 272 | + cur_x, cur_y = self.mouse.position |
| 273 | + self.mouse.position = (cur_x + random.randint(-1, 1), cur_y + random.randint(-1, 1)) |
| 274 | + |
| 275 | + # Execution |
| 276 | + self.mouse.press(btn) |
| 277 | + time.sleep(random.uniform(0.01, 0.03) if self.jitter_var.get() else 0.01) |
| 278 | + self.mouse.release(btn) |
| 279 | + self.clicks_done += 1 |
| 280 | + |
| 281 | + if is_double: |
| 282 | + time.sleep(random.uniform(0.01, 0.03) if self.jitter_var.get() else 0.01) |
| 283 | + self.mouse.press(btn) |
| 284 | + time.sleep(random.uniform(0.01, 0.03) if self.jitter_var.get() else 0.01) |
| 285 | + self.mouse.release(btn) |
| 286 | + self.clicks_done += 1 |
| 287 | + |
| 288 | + # Precise Idle |
| 289 | + rem = interval - (time.perf_counter() - st) |
| 290 | + if rem > 0: |
| 291 | + end = time.perf_counter() + rem |
| 292 | + while self.running and time.perf_counter() < end: |
| 293 | + diff = end - time.perf_counter() |
| 294 | + if diff > 0.015: time.sleep(diff - 0.010) |
| 295 | + |
| 296 | + def start_clicking(self): |
| 297 | + if not self.running: |
| 298 | + self.clicks_done = 0 |
| 299 | + self.start_time = time.time() |
| 300 | + self.running = True |
| 301 | + self.status_label.config(text="STATUS: RUNNING", fg="#a6e3a1") |
| 302 | + threading.Thread(target=self.clicker_loop, daemon=True).start() |
| 303 | + |
| 304 | + def stop_clicking(self): |
| 305 | + if self.running: |
| 306 | + self.running = False |
| 307 | + self.status_label.config(text="STATUS: STOPPED", fg=self.kill_red) |
| 308 | + |
| 309 | + def on_press(self, key): |
| 310 | + if self.binding_mode: |
| 311 | + if self.binding_mode == 'start_stop': |
| 312 | + self.hotkey_start_stop = key |
| 313 | + self.btn_bind_start.config(text=f"Toggle: {self.format_key(key)}", fg=self.accent_blue) |
| 314 | + else: |
| 315 | + self.hotkey_kill = key |
| 316 | + self.btn_bind_kill.config(text=f"Exit App: {self.format_key(key)}", fg=self.kill_red) |
| 317 | + self.binding_mode = None |
| 318 | + self.status_label.config(text="STATUS: KEY BOUND", fg=self.accent_blue) |
| 319 | + return |
| 320 | + |
| 321 | + if key == self.hotkey_start_stop: |
| 322 | + self.stop_clicking() if self.running else self.start_clicking() |
| 323 | + elif key == self.hotkey_kill: |
| 324 | + self.on_closing() |
| 325 | + |
| 326 | + def on_click_global(self, x, y, button, pressed): |
| 327 | + if self.picking_pos and pressed: |
| 328 | + self.target_pos = (x, y) |
| 329 | + self.picking_pos = False |
| 330 | + def update_ui(): |
| 331 | + messagebox.showinfo("Selection", f"Position locked to {x}, {y}") |
| 332 | + self.status_label.config(text="STATUS: POSITION LOCKED", fg=self.accent_blue) |
| 333 | + self.root.after(0, update_ui) |
| 334 | + |
| 335 | + def on_closing(self): |
| 336 | + self.running = False |
| 337 | + if hasattr(self, 'key_listener'): self.key_listener.stop() |
| 338 | + if hasattr(self, 'mouse_listener'): self.mouse_listener.stop() |
| 339 | + self.root.destroy() |
| 340 | + sys.exit() |
| 341 | + |
| 342 | +if __name__ == "__main__": |
| 343 | + root = tk.Tk() |
| 344 | + app = AutoclickerApp(root) |
| 345 | + root.mainloop() |
0 commit comments