Hook and simulate keyboard events on Windows and Linux
—
Capture sequences of keyboard events and replay them later with precise timing control and selective event filtering. The keyboard package provides comprehensive recording capabilities for automation, testing, and user interface scripting.
Capture keyboard events for later replay with flexible start/stop control.
def record(until='escape', suppress=False, trigger_on_release=False):
"""
Records all keyboard events from all keyboards until the user presses the
given hotkey. Then returns the list of events recorded.
Parameters:
- until: Hotkey to stop recording (default: 'escape')
- suppress: If True, suppress recorded events from reaching other applications
- trigger_on_release: If True, stop on key release instead of press
Returns:
list[KeyboardEvent]: List of recorded keyboard events
Note: This is a blocking function that waits for the stop condition.
"""
def start_recording(recorded_events_queue=None):
"""
Starts recording all keyboard events into a global variable, or the given
queue if any. Returns the queue of events and the hooked function.
Parameters:
- recorded_events_queue: Optional queue for events (creates new if None)
Returns:
tuple: (event_queue, hook_function) for manual control
Use stop_recording() or unhook(hooked_function) to stop.
"""
def stop_recording():
"""
Stops the global recording of events and returns a list of the events
captured.
Returns:
list[KeyboardEvent]: List of recorded events
Raises:
ValueError: If start_recording() was not called first
"""Replay recorded events with timing and speed control.
def play(events, speed_factor=1.0):
"""
Plays a sequence of recorded events, maintaining the relative time
intervals. If speed_factor is <= 0 then the actions are replayed as fast
as the OS allows.
Parameters:
- events: List of KeyboardEvent objects to replay
- speed_factor: Speed multiplier (1.0 = normal, 2.0 = double speed, 0.5 = half speed)
Notes:
- The current keyboard state is cleared at the beginning and restored at the end
- Events are replayed with original timing relationships preserved
- Pairs well with record()
"""
def replay(events, speed_factor=1.0):
"""Alias for play()."""import keyboard
print('Recording keyboard events. Press ESC to stop.')
recorded_events = keyboard.record(until='esc')
print(f'Recorded {len(recorded_events)} events.')
print('Press SPACE to replay, or ESC to exit.')
keyboard.wait('space')
print('Replaying...')
keyboard.play(recorded_events)
print('Replay complete.')import keyboard
import time
# Start recording manually
print('Starting manual recording...')
queue, hook_func = keyboard.start_recording()
# Let recording run for 5 seconds
time.sleep(5)
# Stop recording
recorded_events = keyboard.stop_recording()
print(f'Recorded {len(recorded_events)} events in 5 seconds.')
# Replay at double speed
print('Replaying at 2x speed...')
keyboard.play(recorded_events, speed_factor=2.0)import keyboard
# Record some events
print('Record some typing, then press ESC.')
events = keyboard.record()
# Replay at different speeds
print('Normal speed:')
keyboard.play(events, speed_factor=1.0)
keyboard.wait('space')
print('Double speed:')
keyboard.play(events, speed_factor=2.0)
keyboard.wait('space')
print('Half speed:')
keyboard.play(events, speed_factor=0.5)
keyboard.wait('space')
print('Maximum speed:')
keyboard.play(events, speed_factor=0) # As fast as possibleimport keyboard
def filter_recording():
"""Record only specific types of events."""
# Custom recording with filtering
recorded_events = []
def record_filter(event):
# Only record letter keys and space
if event.name and (len(event.name) == 1 or event.name == 'space'):
if event.event_type == 'down': # Only key presses
recorded_events.append(event)
print('Recording only letter keys and space. Press ESC to stop.')
# Install custom hook
hook_func = keyboard.hook(record_filter)
keyboard.wait('esc')
keyboard.unhook(hook_func)
return recorded_events
# Use filtered recording
filtered_events = filter_recording()
print(f'Recorded {len(filtered_events)} filtered events.')
print('Replaying filtered events...')
keyboard.play(filtered_events)import keyboard
import json
import os
class MacroRecorder:
def __init__(self):
self.macros = {}
self.recording = False
self.current_recording = []
def start_macro_recording(self, name):
"""Start recording a named macro."""
if self.recording:
print('Already recording a macro!')
return
self.recording = True
self.current_recording = []
print(f'Recording macro "{name}". Press F9 to stop.')
def record_event(event):
if not self.recording:
return
self.current_recording.append({
'event_type': event.event_type,
'name': event.name,
'scan_code': event.scan_code,
'time': event.time
})
self.hook_func = keyboard.hook(record_event)
def stop_recording():
self.stop_macro_recording(name)
keyboard.add_hotkey('f9', stop_recording)
def stop_macro_recording(self, name):
"""Stop recording and save the macro."""
if not self.recording:
return
self.recording = False
keyboard.unhook(self.hook_func)
keyboard.remove_hotkey('f9')
self.macros[name] = self.current_recording.copy()
print(f'Macro "{name}" recorded with {len(self.current_recording)} events.')
def play_macro(self, name):
"""Play a recorded macro."""
if name not in self.macros:
print(f'Macro "{name}" not found!')
return
events = self.macros[name]
print(f'Playing macro "{name}" with {len(events)} events...')
# Convert back to KeyboardEvent objects for playback
keyboard_events = []
base_time = events[0]['time'] if events else 0
for event_data in events:
# Create a mock event for playback
if event_data['event_type'] == 'down':
keyboard.press(event_data['name'] or event_data['scan_code'])
else:
keyboard.release(event_data['name'] or event_data['scan_code'])
def save_macros(self, filename):
"""Save macros to file."""
with open(filename, 'w') as f:
json.dump(self.macros, f, indent=2)
print(f'Macros saved to {filename}')
def load_macros(self, filename):
"""Load macros from file."""
if os.path.exists(filename):
with open(filename, 'r') as f:
self.macros = json.load(f)
print(f'Loaded {len(self.macros)} macros from {filename}')
# Usage example
recorder = MacroRecorder()
def start_login_macro():
recorder.start_macro_recording('login')
def start_email_macro():
recorder.start_macro_recording('email_signature')
def play_login():
recorder.play_macro('login')
def play_email():
recorder.play_macro('email_signature')
# Set up hotkeys for macro system
keyboard.add_hotkey('ctrl+f1', start_login_macro)
keyboard.add_hotkey('ctrl+f2', start_email_macro)
keyboard.add_hotkey('f1', play_login)
keyboard.add_hotkey('f2', play_email)
print('Macro system ready!')
print('Ctrl+F1: Record login macro')
print('Ctrl+F2: Record email macro')
print('F1: Play login macro')
print('F2: Play email macro')
print('ESC: Exit')
keyboard.wait('esc')
keyboard.unhook_all_hotkeys()
# Save macros before exit
recorder.save_macros('my_macros.json')import keyboard
import time
def test_application_workflow():
"""Record and replay a complete application workflow."""
print('=== Application Workflow Test ===')
# Step 1: Record the workflow
print('Step 1: Record your workflow')
print('Perform the complete workflow, then press F12 to finish.')
workflow_events = keyboard.record(until='f12')
print(f'Recorded workflow with {len(workflow_events)} events.')
# Step 2: Replay for testing
print('Step 2: Replaying workflow for testing...')
print('Starting in 3 seconds...')
time.sleep(3)
# Replay the workflow
keyboard.play(workflow_events)
print('Workflow replay complete!')
# Step 3: Stress test with multiple replays
choice = input('Run stress test with 5 replays? (y/n): ')
if choice.lower() == 'y':
for i in range(5):
print(f'Stress test run {i+1}/5...')
time.sleep(2) # Brief pause between runs
keyboard.play(workflow_events, speed_factor=1.5) # Slightly faster
print('Stress test complete!')
# Run the test
test_application_workflow()import keyboard
from collections import Counter
def analyze_recording():
"""Analyze a keyboard recording for patterns."""
print('Record some typing for analysis. Press ESC when done.')
events = keyboard.record()
# Analyze the recording
print(f'\n=== Recording Analysis ===')
print(f'Total events: {len(events)}')
# Count key presses vs releases
press_events = [e for e in events if e.event_type == 'down']
release_events = [e for e in events if e.event_type == 'up']
print(f'Key presses: {len(press_events)}')
print(f'Key releases: {len(release_events)}')
# Most common keys
key_counts = Counter(e.name for e in press_events if e.name)
print(f'\nMost common keys:')
for key, count in key_counts.most_common(10):
print(f' {key}: {count} times')
# Typing speed analysis
if len(press_events) > 1:
duration = events[-1].time - events[0].time
keys_per_second = len(press_events) / duration
print(f'\nTyping speed: {keys_per_second:.1f} keys/second')
# Time between events
if len(events) > 1:
intervals = [events[i+1].time - events[i].time for i in range(len(events)-1)]
avg_interval = sum(intervals) / len(intervals)
print(f'Average time between events: {avg_interval:.3f} seconds')
return events
# Run analysis
analyzed_events = analyze_recording()Recorded events contain complete timing and key information:
class KeyboardEvent:
"""Recorded keyboard event with complete metadata."""
event_type: str # 'down' or 'up'
scan_code: int # Hardware scan code
name: str # Key name (e.g., 'a', 'space', 'ctrl')
time: float # Timestamp in seconds since epoch
device: int # Device identifier (platform-specific)
modifiers: list # Active modifier keys at time of event
is_keypad: bool # True if from numeric keypad
def to_json(self, ensure_ascii=False) -> str:
"""Convert event to JSON for serialization."""Recording and playback may encounter:
The package will raise ValueError for invalid recording states and may silently fail to capture or replay events in restricted environments.
Install with Tessl CLI
npx tessl i tessl/pypi-keyboard