Skip to content

Commit 1fe75ff

Browse files
committed
first commit
0 parents  commit 1fe75ff

7 files changed

Lines changed: 337 additions & 0 deletions

File tree

.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Environment folders
2+
.env
3+
.venv
4+
env/
5+
venv/
6+
ENV/
7+
env.bak/
8+
venv.bak/
9+
10+
# Python specific
11+
*.pyc
12+
__pycache__/
13+
*.pyo
14+
*.pyd
15+
*.log
16+
17+
# Build specific
18+
output/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Timo Inglin
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# SimpleKeyClicker
2+
3+
**SimpleKeyClicker** is a simple automated keyboard and mouse action tool. It allows you to configure a sequence of key presses and mouse clicks (with a delay in between) and then repeatedly execute them until stopped. Ideal for repetitive tasks like data entry, testing workflows, or gaming macros.
4+
5+
![Tool Screenshot](images/screenshot.png)
6+
7+
## Features
8+
9+
- Create a sequence of keyboard and mouse actions.
10+
- Configure delays between each action.
11+
- Add or remove rows of actions dynamically.
12+
- Start/Stop the automation with a button click or via hotkeys:
13+
- **Start:** Ctrl+F2
14+
- **Stop:** Ctrl+F3
15+
- View possible keys/actions through an "Info" dialog.
16+
- Modern, themed UI using `ttkbootstrap`.
17+
18+
## Download
19+
20+
**[Download the latest Windows EXE here](https://github.com/timoinglin/SimpleKeyClicker/releases/latest)**
21+
22+
23+
## Usage
24+
25+
1. **Run from Source:**
26+
- Make sure you have Python 3.8+ installed.
27+
- Install dependencies:
28+
```bash
29+
pip install pydirectinput ttkbootstrap keyboard
30+
```
31+
- Run the tool:
32+
```bash
33+
python main.py
34+
```
35+
36+
2. **Run the EXE:**
37+
- Download the `SimpleKeyClicker.exe` from the [releases page](https://github.com/timoinglin/SimpleKeyClicker/releases/latest).
38+
- Double-click `SimpleKeyClicker.exe` to start.
39+
40+
## Customizing / Editing the Code
41+
42+
You can clone the repo and modify `main.py` to adjust behavior, add new keys, or change the UI.
43+
44+
```bash
45+
git clone https://github.com/timoinglin/SimpleKeyClicker.git
46+
cd SimpleKeyClicker
47+
python main.py

images/screenshot.png

26.7 KB
Loading

logo.ico

145 KB
Binary file not shown.

logo.png

73.9 KB
Loading

main.py

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import threading
2+
import time
3+
import sys
4+
import pydirectinput
5+
import keyboard # for hotkey start/stop
6+
import ttkbootstrap as tb
7+
from ttkbootstrap.constants import *
8+
from tkinter import Toplevel, PhotoImage
9+
from tkinter import Frame, LEFT, BOTH, YES, X, Y, RIGHT, TOP, BOTTOM, HORIZONTAL, VERTICAL
10+
11+
# -----------------------------
12+
# Configuration
13+
# -----------------------------
14+
TOOL_NAME = "SimpleKeyClicker"
15+
POSSIBLE_KEYS = """
16+
Possible Keys/Mouse Actions:
17+
- Single keys: a, b, c, ..., z
18+
- Digits: 0-9
19+
- Special keys: TAB, SPACE, ENTER, ESC, SHIFT, CTRL, ALT, BACKSPACE
20+
- Mouse clicks: click (left click), rclick (right click), mclick (middle click)
21+
- Other keys: up, down, left, right, home, end, pageup, pagedown
22+
- Characters: !, @, #, $, %, ^, &, *, (, ), -, _, =, +, [, ], {, }, ;, :, ', ", \\, |, ,, <, ., >, /, ?
23+
"""
24+
25+
ICON_PATH = "logo.ico"
26+
LOGO_PATH = "logo.png" # For the UI banner and error windows
27+
28+
class KeyClickerApp:
29+
def __init__(self, root):
30+
self.root = root
31+
self.root.title(TOOL_NAME)
32+
self.root.geometry("500x250")
33+
self.root.resizable(False, True)
34+
self.root.minsize(500, 250)
35+
# Set window icon if available
36+
try:
37+
self.root.iconbitmap(ICON_PATH)
38+
except:
39+
pass
40+
41+
# Style and theme
42+
self.style = tb.Style("sandstone") # use any ttkbootstrap theme you prefer
43+
44+
# Main frame
45+
self.main_frame = tb.Frame(self.root, padding=10)
46+
self.main_frame.pack(fill=BOTH, expand=YES)
47+
48+
# -----------------------------
49+
# Top Frame (Row 1: Logo & Buttons)
50+
# -----------------------------
51+
self.top_frame = tb.Frame(self.main_frame)
52+
self.top_frame.pack(fill=X, pady=(0,5))
53+
54+
# Create a horizontal container for the logo and text/buttons
55+
logo_btn_frame = tb.Frame(self.top_frame)
56+
logo_btn_frame.pack(fill=X, pady=5)
57+
58+
# Logo
59+
try:
60+
self.logo_image = PhotoImage(file=LOGO_PATH)
61+
logo_label = tb.Label(logo_btn_frame, image=self.logo_image)
62+
logo_label.pack(side=LEFT, padx=(5, 20))
63+
except:
64+
pass
65+
66+
# Info button
67+
self.info_button = tb.Button(logo_btn_frame, text="Info (Keys)", bootstyle=INFO, command=self.show_info)
68+
self.info_button.pack(side=LEFT, padx=5)
69+
70+
# Start/Stop Buttons
71+
self.start_button = tb.Button(logo_btn_frame, text="Start", padding=(40, 6), bootstyle=PRIMARY, command=self.start_action)
72+
self.start_button.pack(side=LEFT, padx=5)
73+
74+
self.stop_button = tb.Button(logo_btn_frame, text="Stop", padding=(40, 6), bootstyle=DANGER, command=self.stop_action)
75+
self.stop_button.pack(side=LEFT, padx=5)
76+
77+
# Hint label
78+
tb.Label(self.top_frame, text="Hint: Press Ctrl+F2 to Start, Ctrl+F3 to Stop",
79+
font=("Helvetica", 10, "italic")).pack(pady=(5,0))
80+
81+
# -----------------------------
82+
# Bottom Frame (Row 2: Scrollable Action Rows)
83+
# -----------------------------
84+
self.bottom_frame = tb.Frame(self.main_frame)
85+
self.bottom_frame.pack(fill=BOTH, expand=YES, pady=(10, 0))
86+
87+
# Add Row button (at the top of bottom frame)
88+
add_row_frame = tb.Frame(self.bottom_frame)
89+
add_row_frame.pack(fill=X)
90+
tb.Button(add_row_frame, text="Add Row", bootstyle=SUCCESS, command=self._add_row).pack(side=LEFT, padx=5, pady=5)
91+
92+
# Scrollable area
93+
self.canvas = tb.Canvas(self.bottom_frame, highlightthickness=0)
94+
self.canvas.pack(side=LEFT, fill=BOTH, expand=YES)
95+
96+
self.scrollbar = tb.Scrollbar(self.bottom_frame, orient=VERTICAL, command=self.canvas.yview)
97+
self.scrollbar.pack(side=RIGHT, fill=Y)
98+
99+
self.canvas.configure(yscrollcommand=self.scrollbar.set)
100+
101+
# A frame inside the canvas
102+
self.rows_container = tb.Frame(self.canvas)
103+
self.canvas.create_window((0,0), window=self.rows_container, anchor="nw")
104+
105+
self.rows_container.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
106+
107+
# Store rows
108+
self.rows = []
109+
110+
# Add the first mandatory row
111+
self._add_row(mandatory=True)
112+
113+
# State variables
114+
self.running = False
115+
self.thread = None
116+
117+
# Setup hotkeys
118+
keyboard.add_hotkey('ctrl+f2', self.start_action)
119+
keyboard.add_hotkey('ctrl+f3', self.stop_action)
120+
121+
def _add_row(self, mandatory=False):
122+
"""Add a new row for key/sleep input and center it."""
123+
row_frame = tb.Frame(self.rows_container)
124+
row_frame.pack(fill=X, pady=2)
125+
126+
sub_frame = tb.Frame(row_frame)
127+
sub_frame.pack(anchor='center')
128+
129+
key_var = tb.StringVar(value="")
130+
sleep_var = tb.StringVar(value="0.5") # default sleep time
131+
132+
tb.Label(sub_frame, text="Key/Button:", width=12).pack(side=LEFT)
133+
tb.Entry(sub_frame, textvariable=key_var, width=20).pack(side=LEFT, padx=5)
134+
135+
tb.Label(sub_frame, text="Delay (s):", width=10).pack(side=LEFT)
136+
tb.Entry(sub_frame, textvariable=sleep_var, width=10).pack(side=LEFT, padx=5)
137+
138+
remove_btn = None
139+
if not mandatory:
140+
remove_btn = tb.Button(sub_frame, text="Remove", bootstyle=DANGER, command=lambda f=row_frame: self._remove_row(f))
141+
remove_btn.pack(side=LEFT, padx=5)
142+
143+
self.rows.append({
144+
'frame': row_frame,
145+
'key_var': key_var,
146+
'sleep_var': sleep_var,
147+
'remove_btn': remove_btn,
148+
'mandatory': mandatory
149+
})
150+
151+
def _remove_row(self, frame):
152+
"""Remove a row from the UI and the list."""
153+
for r in self.rows:
154+
if r['frame'] == frame:
155+
r['frame'].destroy()
156+
self.rows.remove(r)
157+
break
158+
159+
def show_info(self):
160+
info_win = Toplevel(self.root)
161+
info_win.title("Info - Possible Keys")
162+
try:
163+
info_win.iconbitmap(ICON_PATH)
164+
except:
165+
pass
166+
info_win.grab_set()
167+
tb.Label(info_win, text=POSSIBLE_KEYS, font=("Helvetica", 13), padding=20, justify=LEFT).pack()
168+
tb.Button(info_win, text="Close", bootstyle=PRIMARY, command=info_win.destroy).pack(pady=10)
169+
170+
def show_custom_error(self, title, message):
171+
"""Show a custom error window with the logo instead of the default icon."""
172+
error_win = Toplevel(self.root)
173+
error_win.title(title)
174+
try:
175+
error_win.iconbitmap(ICON_PATH)
176+
except:
177+
pass
178+
error_win.grab_set()
179+
180+
frm = tb.Frame(error_win, padding=10)
181+
frm.pack()
182+
183+
# Load the logo image
184+
try:
185+
logo = PhotoImage(file=LOGO_PATH)
186+
logo_label = tb.Label(frm, image=logo)
187+
logo_label.image = logo # keep a reference so it's not garbage-collected
188+
logo_label.pack()
189+
except:
190+
pass
191+
192+
tb.Label(frm, text=message, padding=10, justify=LEFT, foreground="red", font=("Helvetica", 12)).pack()
193+
tb.Button(frm, text="OK", bootstyle=PRIMARY, command=error_win.destroy).pack(pady=10)
194+
195+
# Center the error window relative to the main window
196+
error_win.update_idletasks()
197+
x = self.root.winfo_x() + (self.root.winfo_width() // 2) - (error_win.winfo_width() // 2)
198+
y = self.root.winfo_y() + (self.root.winfo_height() // 2) - (error_win.winfo_height() // 2)
199+
error_win.geometry(f"+{x}+{y}")
200+
201+
def start_action(self):
202+
if self.running:
203+
return
204+
# Validate input
205+
for r in self.rows:
206+
if r['key_var'].get().strip() == "":
207+
self.show_custom_error("Error", "Please specify a key/button in all rows.")
208+
return
209+
try:
210+
float(r['sleep_var'].get())
211+
except ValueError:
212+
self.show_custom_error("Error", f"Invalid delay value: {r['sleep_var'].get()}")
213+
return
214+
215+
self.running = True
216+
self.thread = threading.Thread(target=self._run_loop, daemon=True)
217+
self.thread.start()
218+
219+
def stop_action(self):
220+
self.running = False
221+
222+
def _run_loop(self):
223+
# Loop indefinitely until stopped
224+
while self.running:
225+
for r in self.rows:
226+
if not self.running:
227+
break
228+
key = r['key_var'].get().strip()
229+
delay = float(r['sleep_var'].get())
230+
self._perform_action(key)
231+
time.sleep(delay)
232+
233+
def _perform_action(self, key):
234+
# Determine if it's a mouse action or a key press
235+
k = key.lower()
236+
if k == "click":
237+
pydirectinput.click()
238+
elif k == "rclick":
239+
pydirectinput.rightClick()
240+
elif k == "mclick":
241+
pydirectinput.middleClick()
242+
else:
243+
pydirectinput.press(key)
244+
245+
# -----------------------------
246+
# Main
247+
# -----------------------------
248+
if __name__ == "__main__":
249+
root = tb.Window(themename="cosmo")
250+
app = KeyClickerApp(root)
251+
root.mainloop()

0 commit comments

Comments
 (0)