Last active
February 5, 2025 17:19
-
-
Save gary23w/60aa0526e4690909856fd5f64f108a4a to your computer and use it in GitHub Desktop.
Couldn't find a good clicker macro app, so I decided to create one myself.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Couldn't find a good clicker macro app. so I decided to create one myself. | |
import tkinter as tk | |
from tkinter import Toplevel, Label, Button, Checkbutton, IntVar | |
import pyautogui | |
import time | |
import threading | |
import logging | |
from pynput import keyboard, mouse | |
import signal | |
import sys | |
class ClickMacroApp: | |
def __init__(self, root): | |
self.root = root | |
self.root.title("Click Macro Recorder") | |
self.root.attributes("-topmost", True) | |
self.root.attributes("-alpha", 0.85) | |
self.root.overrideredirect(True) | |
self.root.geometry("250x350+10+10") | |
logging.basicConfig( | |
filename="log.gary23w", | |
level=logging.INFO, | |
format="%(asctime)s - %(levelname)s - %(message)s" | |
) | |
logging.info("Application started.") | |
self.clicks = [] | |
self.recording = False | |
self.running_macro = False | |
self.unlimited_loops = IntVar(value=0) | |
self.loop_count = 0 | |
self.offset_x = 0 | |
self.offset_y = 0 | |
self.root.bind("<Button-1>", self.start_drag) | |
self.root.bind("<B1-Motion>", self.do_drag) | |
self.instructions = tk.Label( | |
root, | |
text="1. Record clicks.\n" | |
"2. Set interval.\n" | |
"3. Set loops/unlimited.\n" | |
"4. Play or stop macro.\n" | |
"5. Ctrl + C to exit.\n" | |
"6. Press Q to close the app.", | |
justify="left", | |
font=("Arial", 8), | |
wraplength=230 | |
) | |
self.instructions.pack(pady=5) | |
self.interval_label = tk.Label(root, text="Interval (seconds):", font=("Arial", 8)) | |
self.interval_label.pack() | |
self.interval_spinbox = tk.Spinbox(root, from_=0.1, to=10, increment=0.1, width=5, font=("Arial", 8)) | |
self.interval_spinbox.pack(pady=5) | |
self.loop_label = tk.Label(root, text="Number of loops:", font=("Arial", 8)) | |
self.loop_label.pack() | |
self.loop_spinbox = tk.Spinbox(root, from_=1, to=100, increment=1, width=5, font=("Arial", 8)) | |
self.loop_spinbox.pack(pady=5) | |
self.unlimited_checkbox = Checkbutton( | |
root, | |
text="Unlimited Loops", | |
variable=self.unlimited_loops, | |
onvalue=1, | |
offvalue=0, | |
font=("Arial", 8) | |
) | |
self.unlimited_checkbox.pack(pady=5) | |
self.play_button = tk.Button(root, text="Play Macro", command=self.start_macro, font=("Arial", 8)) | |
self.play_button.pack(pady=5) | |
self.record_button = tk.Button(root, text="Record", command=self.start_recording, font=("Arial", 8)) | |
self.record_button.pack(pady=5) | |
self.status_label = tk.Label(root, text="", fg="blue", font=("Arial", 8)) | |
self.status_label.pack(pady=5) | |
self.loop_counter_label = tk.Label(root, text="Loop Count: 0", fg="green", font=("Arial", 8)) | |
self.loop_counter_label.pack(pady=5) | |
self.keyboard_listener = keyboard.Listener(on_press=self.on_key_press) | |
self.keyboard_listener.start() | |
self.mouse_listener = None | |
self.root.protocol("WM_DELETE_WINDOW", self.close_application) | |
signal.signal(signal.SIGINT, self.terminate) | |
def start_drag(self, event): | |
"""Store the offset of the mouse pointer relative to the window.""" | |
self.offset_x = event.x | |
self.offset_y = event.y | |
def do_drag(self, event): | |
"""Reposition the window based on mouse movement.""" | |
x = event.x_root - self.offset_x | |
y = event.y_root - self.offset_y | |
self.root.geometry(f"+{x}+{y}") | |
def show_centered_message(self, title, message): | |
"""Display a message box centered on the screen.""" | |
popup = Toplevel(self.root) | |
popup.title(title) | |
screen_width = self.root.winfo_screenwidth() | |
screen_height = self.root.winfo_screenheight() | |
window_width = 200 | |
window_height = 100 | |
x = (screen_width // 2) - (window_width // 2) | |
y = (screen_height // 2) - (window_height // 2) | |
popup.geometry(f"{window_width}x{window_height}+{x}+{y}") | |
popup.attributes("-topmost", True) | |
label = Label(popup, text=message, wraplength=180, justify="center", font=("Arial", 8)) | |
label.pack(pady=10) | |
button = Button(popup, text="OK", command=lambda: self.close_popup(popup), font=("Arial", 8)) | |
button.pack(pady=5) | |
popup.transient(self.root) | |
popup.grab_set() | |
def close_popup(self, popup): | |
"""Safely close the pop-up.""" | |
popup.grab_release() | |
popup.destroy() | |
def start_recording(self): | |
self.clicks = [] | |
self.recording = True | |
self.status_label.config(text="Recording clicks... Press 'Ctrl + ,' to stop.") | |
logging.info("Started recording clicks.") | |
self.mouse_listener = mouse.Listener(on_click=self.record_mouse_click) | |
self.mouse_listener.start() | |
def record_mouse_click(self, x, y, button, pressed): | |
"""Record mouse clicks.""" | |
if not self.recording or not pressed: | |
return | |
self.clicks.append((x, y)) | |
logging.info(f"Recorded click at position: ({x}, {y})") | |
self.status_label.config(text=f"Click recorded at ({x}, {y})") | |
def stop_recording(self): | |
self.recording = False | |
if self.mouse_listener: | |
self.mouse_listener.stop() | |
self.status_label.config(text=f"Recording complete. {len(self.clicks)} clicks recorded.") | |
logging.info(f"Recording complete. Total clicks recorded: {len(self.clicks)}") | |
self.show_centered_message("Recording Complete", f"Recorded {len(self.clicks)} clicks.") | |
def start_macro(self): | |
if not self.clicks: | |
logging.warning("Attempted to play macro without any recorded clicks.") | |
self.show_centered_message("No Clicks", "No clicks recorded. Please record clicks first.") | |
return | |
try: | |
interval = float(self.interval_spinbox.get()) | |
loops = int(self.loop_spinbox.get()) if not self.unlimited_loops.get() else float('inf') | |
except ValueError: | |
logging.error("Invalid interval or loop amount entered.") | |
self.show_centered_message("Invalid Input", "Please enter valid numbers for the interval and loop amount.") | |
return | |
self.running_macro = True | |
self.loop_count = 0 | |
self.status_label.config(text="Running macro... Press 'Ctrl + .' to stop.") | |
logging.info("Started macro playback.") | |
threading.Thread(target=self.play_macro, args=(interval, loops), daemon=True).start() | |
def play_macro(self, interval, loops): | |
while self.running_macro and self.loop_count < loops: | |
for index, (x, y) in enumerate(self.clicks): | |
if not self.running_macro: | |
logging.info("Macro playback stopped manually.") | |
break | |
self.status_label.config(text=f"Executing click {index + 1} at ({x}, {y})...") | |
self.root.update() | |
logging.info(f"Executing click {index + 1} at ({x}, {y})") | |
pyautogui.click(x, y) | |
time.sleep(interval) | |
if not self.running_macro: | |
break | |
self.loop_count += 1 | |
self.loop_counter_label.config(text=f"Loop Count: {self.loop_count}") | |
self.root.update() | |
self.running_macro = False | |
self.status_label.config(text="Macro playback complete.") | |
logging.info("Macro playback completed.") | |
screenshot_path = "screenshot.png" | |
pyautogui.screenshot(screenshot_path) | |
logging.info(f"Screenshot saved at {screenshot_path}") | |
self.show_centered_message("Macro Complete", f"Macro completed. Screenshot saved as {screenshot_path}.") | |
def stop_macro(self): | |
self.running_macro = False | |
self.status_label.config(text="Macro stopped.") | |
logging.info("Macro playback stopped.") | |
def on_key_press(self, key): | |
try: | |
if key == keyboard.Key.ctrl_l or key == keyboard.Key.ctrl_r: | |
return | |
if key.char == ',': | |
self.stop_recording() | |
elif key.char == '.': | |
self.stop_macro() | |
elif key.char == 'q': | |
self.close_application() | |
except AttributeError: | |
pass | |
def close_application(self): | |
logging.info("Application closing.") | |
if self.keyboard_listener.running: | |
self.keyboard_listener.stop() | |
if self.mouse_listener and self.mouse_listener.running: | |
self.mouse_listener.stop() | |
self.root.destroy() | |
logging.info("Application closed successfully.") | |
def terminate(self, signum, frame): | |
logging.info("Termination signal received (Ctrl+C).") | |
self.close_application() | |
sys.exit(0) | |
if __name__ == "__main__": | |
root = tk.Tk() | |
app = ClickMacroApp(root) | |
root.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment