Skip to content

Instantly share code, notes, and snippets.

@gary23w
Last active February 5, 2025 17:19
Show Gist options
  • Save gary23w/60aa0526e4690909856fd5f64f108a4a to your computer and use it in GitHub Desktop.
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.
# 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