CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-keyboard

Hook and simulate keyboard events on Windows and Linux

Pending
Overview
Eval results
Files

recording-playback.mddocs/

Recording and Playback

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.

Capabilities

Event Recording

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
    """

Event Playback

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()."""

Usage Examples

Basic Recording and Playback

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.')

Manual Recording Control

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)

Speed-Controlled Playback

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 possible

Filtered Recording

import 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)

Macro Recording System

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')

Automation Testing

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()

Event Analysis

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()

Event Data Structure

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 Considerations

Performance

  • Recording captures all keyboard events, which can generate large event lists
  • Long recordings consume significant memory
  • High-frequency typing creates many events

Timing Accuracy

  • Event timestamps preserve original timing relationships
  • Playback maintains relative timing between events
  • System load can affect playback timing precision

Platform Differences

  • Event suppression during recording may not work on all platforms
  • Some keys may not be recordable on certain systems
  • Device information varies by platform

Security and Privacy

  • Recordings capture all keystrokes including passwords
  • Store recordings securely and clear sensitive data appropriately
  • Be aware of keylogger-like behavior in security-conscious environments

Error Handling

Recording and playback may encounter:

  • Insufficient privileges for global keyboard access
  • Platform limitations on event suppression
  • Invalid event data during playback
  • System security restrictions

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

docs

hotkeys.md

index.md

interactive-input.md

key-simulation.md

keyboard-events.md

recording-playback.md

text-processing.md

tile.json