Skip to content

Instantly share code, notes, and snippets.

@s4lt3d
Last active December 25, 2025 01:23
Show Gist options
  • Select an option

  • Save s4lt3d/12c07cfec79bdcb4c31ae500b81fe776 to your computer and use it in GitHub Desktop.

Select an option

Save s4lt3d/12c07cfec79bdcb4c31ae500b81fe776 to your computer and use it in GitHub Desktop.
from pynput import mouse, keyboard
import threading
import time
import json
from pathlib import Path
from datetime import datetime
DEFAULT_RECORDING_FILE = "input_recording"
class Recorder:
def __init__(self):
self.recording = False
self.actions = []
self.start_time = None
self.playback_thread = None
self.stop_loop = threading.Event()
self.available_recordings = []
self.awaiting_selection = False
def start_recording(self):
self.recording = True
self.actions = []
self.start_time = time.time()
print("Recording started.")
def stop_recording(self):
self.recording = False
print("Recording stopped.")
def record_click(self, x, y, button, pressed):
if self.recording:
action_time = time.time() - self.start_time
self.actions.append(('click', action_time, x, y, str(button), pressed))
def record_move(self, x, y):
if self.recording:
action_time = time.time() - self.start_time
self.actions.append(('move', action_time, x, y))
def record_keystroke(self, key, modifiers):
if self.recording:
action_time = time.time() - self.start_time
self.actions.append((
'keystroke',
action_time,
str(key),
[str(m) for m in modifiers]
))
# -------------------------
# Save / Load
# -------------------------
def save_to_file(self):
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{DEFAULT_RECORDING_FILE}_{timestamp}.json"
Path(filename).write_text(json.dumps(self.actions))
print(f"Saved recording to {filename}")
def list_recordings(self):
self.available_recordings = sorted(
Path(".").glob(f"{DEFAULT_RECORDING_FILE}_*.json"),
key=lambda p: p.stat().st_mtime,
reverse=True
)
if not self.available_recordings:
print("No recordings found.")
return False
print()
print("Available recordings:")
for i, f in enumerate(self.available_recordings):
print(f"{i}: {f.name}")
print("Press digit 0–9 to select a recording.")
print()
self.awaiting_selection = True
return True
def load_by_index(self, index):
try:
path = self.available_recordings[index]
except IndexError:
print("Invalid selection.")
return
self.actions = json.loads(path.read_text())
print(f"Loaded recording from {path.name}")
self.awaiting_selection = False
self.start_playback(loop=False)
# -------------------------
# Playback
# -------------------------
def playback(self, loop=False):
self.stop_loop.clear()
while True:
print("Playback starting...")
start_time = time.time()
for action in self.actions:
if self.stop_loop.is_set():
print("Playback interrupted.")
return
while time.time() - start_time < action[1]:
time.sleep(0.01)
if action[0] == 'move':
mouse_controller.position = (action[2], action[3])
elif action[0] == 'click':
btn = parse_mouse_button(action[4])
if action[5]:
mouse_controller.press(btn)
else:
mouse_controller.release(btn)
elif action[0] == 'keystroke':
key = parse_key(action[2])
mods = [parse_key(m) for m in action[3]]
for m in mods:
keyboard_controller.press(m)
keyboard_controller.tap(key)
for m in mods:
keyboard_controller.release(m)
if not loop:
break
print("Playback finished.")
def start_playback(self, loop=False):
if self.playback_thread is None or not self.playback_thread.is_alive():
self.playback_thread = threading.Thread(
target=self.playback,
args=(loop,)
)
self.playback_thread.start()
def stop_playback(self):
self.stop_loop.set()
if self.playback_thread:
self.playback_thread.join()
# -------------------------
# Helpers
# -------------------------
def parse_mouse_button(s):
if "Button.left" in s:
return mouse.Button.left
if "Button.right" in s:
return mouse.Button.right
if "Button.middle" in s:
return mouse.Button.middle
raise ValueError(f"Unknown mouse button {s}")
def parse_key(s):
if s.startswith("Key."):
return getattr(keyboard.Key, s.split(".")[1])
if s.startswith("'") and s.endswith("'"):
return s[1:-1]
return s
# -------------------------
# Global state
# -------------------------
recorder = Recorder()
mouse_controller = mouse.Controller()
keyboard_controller = keyboard.Controller()
current_keys = set()
modifiers = {
keyboard.Key.shift, keyboard.Key.shift_l, keyboard.Key.shift_r,
keyboard.Key.ctrl, keyboard.Key.ctrl_l, keyboard.Key.ctrl_r,
keyboard.Key.alt, keyboard.Key.alt_l, keyboard.Key.alt_r,
keyboard.Key.cmd, keyboard.Key.cmd_l, keyboard.Key.cmd_r
}
# -------------------------
# Input hooks
# -------------------------
def on_click(x, y, button, pressed):
recorder.record_click(x, y, button, pressed)
def on_move(x, y):
recorder.record_move(x, y)
def on_press(key):
# Handle numeric selection
if recorder.awaiting_selection and hasattr(key, 'char') and key.char.isdigit():
recorder.load_by_index(int(key.char))
return
if hasattr(key, 'char'):
current_keys.add(key.char)
else:
current_keys.add(key)
active_modifiers = [k for k in current_keys if k in modifiers]
if key not in modifiers and key not in [
keyboard.Key.f1, keyboard.Key.f2, keyboard.Key.f3,
keyboard.Key.f4, keyboard.Key.f5, keyboard.Key.f6,
keyboard.Key.f7
]:
recorder.record_keystroke(key, active_modifiers)
if {keyboard.Key.shift, keyboard.Key.f2} <= current_keys:
recorder.start_recording()
elif {keyboard.Key.shift, keyboard.Key.f3} <= current_keys:
recorder.stop_recording()
recorder.save_to_file()
elif {keyboard.Key.shift, keyboard.Key.f4} <= current_keys:
recorder.list_recordings()
elif {keyboard.Key.shift, keyboard.Key.f6} <= current_keys:
if recorder.playback_thread and recorder.playback_thread.is_alive():
recorder.stop_playback()
else:
recorder.start_playback(loop=True)
elif {keyboard.Key.shift, keyboard.Key.f7} <= current_keys:
print("Quit requested.")
recorder.stop_playback()
return False # stop keyboard listener
def on_release(key):
if hasattr(key, 'char'):
current_keys.discard(key.char)
else:
current_keys.discard(key)
# -------------------------
# Startup help
# -------------------------
def print_help():
print()
print("Input Recorder Controls")
print("----------------------")
print("Shift + F2 : Start recording")
print("Shift + F3 : Stop recording and save to timestamped file")
print("Shift + F4 : Select a recording to play")
print(" Digit 0–9: Choose recording when prompted")
print("Shift + F6 : Toggle looping playback")
print("Shift + F7 : Quit program")
print()
# -------------------------
# Run
# -------------------------
print_help()
with mouse.Listener(on_click=on_click, on_move=on_move) as mouse_listener, \
keyboard.Listener(on_press=on_press, on_release=on_release) as key_listener:
key_listener.join()
@s4lt3d
Copy link
Author

s4lt3d commented May 3, 2024

Records most mouse and keyboard strokes with time.

Shift + F2 to record
Shift + F3 to stop
Shift + F4 to playback once
Shift + F6 to playback on loop
Shift + F6 again to stop loop playback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment