# -*- coding: utf-8 -*-
"""
keyboard
== == == ==
Take full control of your keyboard with this small Python library . Hook global events , register hotkeys , simulate key presses and much more .
## Features
- * * Global event hook * * on all keyboards ( captures keys regardless of focus ) .
- * * Listen * * and * * send * * keyboard events .
- Works with * * Windows * * and * * Linux * * ( requires sudo ) , with experimental * * OS X * * support ( thanks @glitchassassin ! ) .
- * * Pure Python * * , no C modules to be compiled .
- * * Zero dependencies * * . Trivial to install and deploy , just copy the files .
- * * Python 2 and 3 * * .
- Complex hotkey support ( e . g . ` ctrl + shift + m , ctrl + space ` ) with controllable timeout .
- Includes * * high level API * * ( e . g . [ record ] ( #keyboard.record) and [play](#keyboard.play), [add_abbreviation](#keyboard.add_abbreviation)).
- Maps keys as they actually are in your layout , with * * full internationalization support * * ( e . g . ` Ctrl + ç ` ) .
- Events automatically captured in separate thread , doesn ' t block main program.
- Tested and documented .
- Doesn ' t break accented dead keys (I ' m looking at you , pyHook ) .
- Mouse support available via project [ mouse ] ( https : / / github . com / boppreh / mouse ) ( ` pip install mouse ` ) .
## Usage
Install the [ PyPI package ] ( https : / / pypi . python . org / pypi / keyboard / ) :
pip install keyboard
or clone the repository ( no installation required , source files are sufficient ) :
git clone https : / / github . com / boppreh / keyboard
or [ download and extract the zip ] ( https : / / github . com / boppreh / keyboard / archive / master . zip ) into your project folder .
Then check the [ API docs below ] ( https : / / github . com / boppreh / keyboard #api) to see what features are available.
## Example
` ` ` py
import keyboard
keyboard . press_and_release ( ' shift+s, space ' )
keyboard . write ( ' The quick brown fox jumps over the lazy dog. ' )
keyboard . add_hotkey ( ' ctrl+shift+a ' , print , args = ( ' triggered ' , ' hotkey ' ) )
# Press PAGE UP then PAGE DOWN to type "foobar".
keyboard . add_hotkey ( ' page up, page down ' , lambda : keyboard . write ( ' foobar ' ) )
# Blocks until you press esc.
keyboard . wait ( ' esc ' )
# Record events until 'esc' is pressed.
recorded = keyboard . record ( until = ' esc ' )
# Then replay back at three times the speed.
keyboard . play ( recorded , speed_factor = 3 )
# Type @@ then press space to replace with abbreviation.
keyboard . add_abbreviation ( ' @@ ' , ' my.long.email@example.com ' )
# Block forever, like `while True`.
keyboard . wait ( )
` ` `
## Known limitations:
- Events generated under Windows don ' t report device id (`event.device == None`). [#21](https://github.com/boppreh/keyboard/issues/21)
- Media keys on Linux may appear nameless ( scan - code only ) or not at all . [ #20](https://github.com/boppreh/keyboard/issues/20)
- Key suppression / blocking only available on Windows . [ #22](https://github.com/boppreh/keyboard/issues/22)
- To avoid depending on X , the Linux parts reads raw device files ( ` / dev / input / input * ` )
but this requries root .
- Other applications , such as some games , may register hooks that swallow all
key events . In this case ` keyboard ` will be unable to report events .
- This program makes no attempt to hide itself , so don ' t use it for keyloggers or online gaming bots. Be responsible.
"""
from __future__ import print_function as _print_function
import re as _re
import itertools as _itertools
import collections as _collections
from threading import Thread as _Thread , Lock as _Lock
import time as _time
# Python2... Buggy on time changes and leap seconds, but no other good option (https://stackoverflow.com/questions/1205722/how-do-i-get-monotonic-time-durations-in-python).
_time . monotonic = getattr ( _time , ' monotonic ' , None ) or _time . time
try :
# Python2
long , basestring
_is_str = lambda x : isinstance ( x , basestring )
_is_number = lambda x : isinstance ( x , ( int , long ) )
import Queue as _queue
# threading.Event is a function in Python2 wrappin _Event (?!).
from threading import _Event as _UninterruptibleEvent
except NameError :
# Python3
_is_str = lambda x : isinstance ( x , str )
_is_number = lambda x : isinstance ( x , int )
import queue as _queue
from threading import Event as _UninterruptibleEvent
_is_list = lambda x : isinstance ( x , ( list , tuple ) )
# Just a dynamic object to store attributes for the closures.
class _State ( object ) : pass
# The "Event" class from `threading` ignores signals when waiting and is
# impossible to interrupt with Ctrl+C. So we rewrite `wait` to wait in small,
# interruptible intervals.
class _Event ( _UninterruptibleEvent ) :
def wait ( self ) :
while True :
if _UninterruptibleEvent . wait ( self , 0.5 ) :
break
import platform as _platform
if _platform . system ( ) == ' Windows ' :
from . import _winkeyboard as _os_keyboard
elif _platform . system ( ) == ' Linux ' :
from . import _nixkeyboard as _os_keyboard
elif _platform . system ( ) == ' Darwin ' :
from . import _darwinkeyboard as _os_keyboard
else :
raise OSError ( " Unsupported platform ' {} ' " . format ( _platform . system ( ) ) )
from . _keyboard_event import KEY_DOWN , KEY_UP , KeyboardEvent
from . _generic import GenericListener as _GenericListener
from . _canonical_names import all_modifiers , sided_modifiers , normalize_name
_modifier_scan_codes = set ( )
def is_modifier ( key ) :
"""
Returns True if ` key ` is a scan code or name of a modifier key .
"""
if _is_str ( key ) :
return key in all_modifiers
else :
if not _modifier_scan_codes :
scan_codes = ( key_to_scan_codes ( name , False ) for name in all_modifiers )
_modifier_scan_codes . update ( * scan_codes )
return key in _modifier_scan_codes
_pressed_events_lock = _Lock ( )
_pressed_events = { }
_physically_pressed_keys = _pressed_events
_logically_pressed_keys = { }
class _KeyboardListener ( _GenericListener ) :
transition_table = {
#Current state of the modifier, per `modifier_states`.
#|
#| Type of event that triggered this modifier update.
#| |
#| | Type of key that triggered this modiier update.
#| | |
#| | | Should we send a fake key press?
#| | | |
#| | | => | Accept the event?
#| | | | |
#| | | | | Next state.
#v v v v v v
( ' free ' , KEY_UP , ' modifier ' ) : ( False , True , ' free ' ) ,
( ' free ' , KEY_DOWN , ' modifier ' ) : ( False , False , ' pending ' ) ,
( ' pending ' , KEY_UP , ' modifier ' ) : ( True , True , ' free ' ) ,
( ' pending ' , KEY_DOWN , ' modifier ' ) : ( False , True , ' allowed ' ) ,
( ' suppressed ' , KEY_UP , ' modifier ' ) : ( False , False , ' free ' ) ,
( ' suppressed ' , KEY_DOWN , ' modifier ' ) : ( False , False , ' suppressed ' ) ,
( ' allowed ' , KEY_UP , ' modifier ' ) : ( False , True , ' free ' ) ,
( ' allowed ' , KEY_DOWN , ' modifier ' ) : ( False , True , ' allowed ' ) ,
( ' free ' , KEY_UP , ' hotkey ' ) : ( False , None , ' free ' ) ,
( ' free ' , KEY_DOWN , ' hotkey ' ) : ( False , None , ' free ' ) ,
( ' pending ' , KEY_UP , ' hotkey ' ) : ( False , None , ' suppressed ' ) ,
( ' pending ' , KEY_DOWN , ' hotkey ' ) : ( False , None , ' suppressed ' ) ,
( ' suppressed ' , KEY_UP , ' hotkey ' ) : ( False , None , ' suppressed ' ) ,
( ' suppressed ' , KEY_DOWN , ' hotkey ' ) : ( False , None , ' suppressed ' ) ,
( ' allowed ' , KEY_UP , ' hotkey ' ) : ( False , None , ' allowed ' ) ,
( ' allowed ' , KEY_DOWN , ' hotkey ' ) : ( False , None , ' allowed ' ) ,
( ' free ' , KEY_UP , ' other ' ) : ( False , True , ' free ' ) ,
( ' free ' , KEY_DOWN , ' other ' ) : ( False , True , ' free ' ) ,
( ' pending ' , KEY_UP , ' other ' ) : ( True , True , ' allowed ' ) ,
( ' pending ' , KEY_DOWN , ' other ' ) : ( True , True , ' allowed ' ) ,
# Necessary when hotkeys are removed after beign triggered, such as
# TestKeyboard.test_add_hotkey_multistep_suppress_modifier.
( ' suppressed ' , KEY_UP , ' other ' ) : ( False , False , ' allowed ' ) ,
( ' suppressed ' , KEY_DOWN , ' other ' ) : ( True , True , ' allowed ' ) ,
( ' allowed ' , KEY_UP , ' other ' ) : ( False , True , ' allowed ' ) ,
( ' allowed ' , KEY_DOWN , ' other ' ) : ( False , True , ' allowed ' ) ,
}
def init ( self ) :
_os_keyboard . init ( )
self . active_modifiers = set ( )
self . blocking_hooks = [ ]
self . blocking_keys = _collections . defaultdict ( list )
self . nonblocking_keys = _collections . defaultdict ( list )
self . blocking_hotkeys = _collections . defaultdict ( list )
self . nonblocking_hotkeys = _collections . defaultdict ( list )
self . filtered_modifiers = _collections . Counter ( )
self . is_replaying = False
# Supporting hotkey suppression is harder than it looks. See
# https://github.com/boppreh/keyboard/issues/22
self . modifier_states = { } # "alt" -> "allowed"
def pre_process_event ( self , event ) :
for key_hook in self . nonblocking_keys [ event . scan_code ] :
key_hook ( event )
with _pressed_events_lock :
hotkey = tuple ( sorted ( _pressed_events ) )
for callback in self . nonblocking_hotkeys [ hotkey ] :
callback ( event )
return event . scan_code or ( event . name and event . name != ' unknown ' )
def direct_callback ( self , event ) :
"""
This function is called for every OS keyboard event and decides if the
event should be blocked or not , and passes a copy of the event to
other , non - blocking , listeners .
There are two ways to block events : remapped keys , which translate
events by suppressing and re - emitting ; and blocked hotkeys , which
suppress specific hotkeys .
"""
# Pass through all fake key events, don't even report to other handlers.
if self . is_replaying :
return True
if not all ( hook ( event ) for hook in self . blocking_hooks ) :
return False
event_type = event . event_type
scan_code = event . scan_code
# Update tables of currently pressed keys and modifiers.
with _pressed_events_lock :
if event_type == KEY_DOWN :
if is_modifier ( scan_code ) : self . active_modifiers . add ( scan_code )
_pressed_events [ scan_code ] = event
hotkey = tuple ( sorted ( _pressed_events ) )
if event_type == KEY_UP :
self . active_modifiers . discard ( scan_code )
if scan_code in _pressed_events : del _pressed_events [ scan_code ]
# Mappings based on individual keys instead of hotkeys.
for key_hook in self . blocking_keys [ scan_code ] :
if not key_hook ( event ) :
return False
# Default accept.
accept = True
if self . blocking_hotkeys :
if self . filtered_modifiers [ scan_code ] :
origin = ' modifier '
modifiers_to_update = set ( [ scan_code ] )
else :
modifiers_to_update = self . active_modifiers
if is_modifier ( scan_code ) :
modifiers_to_update = modifiers_to_update | { scan_code }
callback_results = [ callback ( event ) for callback in self . blocking_hotkeys [ hotkey ] ]
if callback_results :
accept = all ( callback_results )
origin = ' hotkey '
else :
origin = ' other '
for key in sorted ( modifiers_to_update ) :
transition_tuple = ( self . modifier_states . get ( key , ' free ' ) , event_type , origin )
should_press , new_accept , new_state = self . transition_table [ transition_tuple ]
if should_press : press ( key )
if new_accept is not None : accept = new_accept
self . modifier_states [ key ] = new_state
if accept :
if event_type == KEY_DOWN :
_logically_pressed_keys [ scan_code ] = event
elif event_type == KEY_UP and scan_code in _logically_pressed_keys :
del _logically_pressed_keys [ scan_code ]
# Queue for handlers that won't block the event.
self . queue . put ( event )
return accept
def listen ( self ) :
_os_keyboard . listen ( self . direct_callback )
_listener = _KeyboardListener ( )
def key_to_scan_codes ( key , error_if_missing = True ) :
"""
Returns a list of scan codes associated with this key ( name or scan code ) .
"""
if _is_number ( key ) :
return ( key , )
elif _is_list ( key ) :
return sum ( ( key_to_scan_codes ( i ) for i in key ) , ( ) )
elif not _is_str ( key ) :
raise ValueError ( ' Unexpected key type ' + str ( type ( key ) ) + ' , value ( ' + repr ( key ) + ' ) ' )
normalized = normalize_name ( key )
if normalized in sided_modifiers :
left_scan_codes = key_to_scan_codes ( ' left ' + normalized , False )
right_scan_codes = key_to_scan_codes ( ' right ' + normalized , False )
return left_scan_codes + tuple ( c for c in right_scan_codes if c not in left_scan_codes )
try :
# Put items in ordered dict to remove duplicates.
t = tuple ( _collections . OrderedDict ( ( scan_code , True ) for scan_code , modifier in _os_keyboard . map_name ( normalized ) ) )
e = None
except ( KeyError , ValueError ) as exception :
t = ( )
e = exception
if not t and error_if_missing :
raise ValueError ( ' Key {} is not mapped to any known key. ' . format ( repr ( key ) ) , e )
else :
return t
def parse_hotkey ( hotkey ) :
"""
Parses a user - provided hotkey into nested tuples representing the
parsed structure , with the bottom values being lists of scan codes .
Also accepts raw scan codes , which are then wrapped in the required
number of nestings .
Example :
parse_hotkey ( " alt+shift+a, alt+b, c " )
# Keys: ^~^ ^~~~^ ^ ^~^ ^ ^
# Steps: ^~~~~~~~~~^ ^~~~^ ^
# ((alt_codes, shift_codes, a_codes), (alt_codes, b_codes), (c_codes,))
"""
if _is_number ( hotkey ) or len ( hotkey ) == 1 :
scan_codes = key_to_scan_codes ( hotkey )
step = ( scan_codes , )
steps = ( step , )
return steps
elif _is_list ( hotkey ) :
if not any ( map ( _is_list , hotkey ) ) :
step = tuple ( key_to_scan_codes ( k ) for k in hotkey )
steps = ( step , )
return steps
return hotkey
steps = [ ]
for step in _re . split ( r ' , \ s? ' , hotkey ) :
keys = _re . split ( r ' \ s? \ + \ s? ' , step )
steps . append ( tuple ( key_to_scan_codes ( key ) for key in keys ) )
return tuple ( steps )
def send ( hotkey , do_press = True , do_release = True ) :
"""
Sends OS events that perform the given * hotkey * hotkey .
- ` hotkey ` can be either a scan code ( e . g . 57 for space ) , single key
( e . g . ' space ' ) or multi - key , multi - step hotkey ( e . g . ' alt+F4, enter ' ) .
- ` do_press ` if true then press events are sent . Defaults to True .
- ` do_release ` if true then release events are sent . Defaults to True .
send ( 57 )
send ( ' ctrl+alt+del ' )
send ( ' alt+F4, enter ' )
send ( ' shift+s ' )
Note : keys are released in the opposite order they were pressed .
"""
_listener . is_replaying = True
parsed = parse_hotkey ( hotkey )
for step in parsed :
if do_press :
for scan_codes in step :
_os_keyboard . press ( scan_codes [ 0 ] )
if do_release :
for scan_codes in reversed ( step ) :
_os_keyboard . release ( scan_codes [ 0 ] )
_listener . is_replaying = False
# Alias.
press_and_release = send
def press ( hotkey ) :
""" Presses and holds down a hotkey (see `send`). """
send ( hotkey , True , False )
def release ( hotkey ) :
""" Releases a hotkey (see `send`). """
send ( hotkey , False , True )
def is_pressed ( hotkey ) :
"""
Returns True if the key is pressed .
is_pressed ( 57 ) #-> True
is_pressed ( ' space ' ) #-> True
is_pressed ( ' ctrl+space ' ) #-> True
"""
_listener . start_if_necessary ( )
if _is_number ( hotkey ) :
# Shortcut.
with _pressed_events_lock :
return hotkey in _pressed_events
steps = parse_hotkey ( hotkey )
if len ( steps ) > 1 :
raise ValueError ( " Impossible to check if multi-step hotkeys are pressed (`a+b` is ok, `a, b` isn ' t). " )
# Convert _pressed_events into a set
with _pressed_events_lock :
pressed_scan_codes = set ( _pressed_events )
for scan_codes in steps [ 0 ] :
if not any ( scan_code in pressed_scan_codes for scan_code in scan_codes ) :
return False
return True
def call_later ( fn , args = ( ) , delay = 0.001 ) :
"""
Calls the provided function in a new thread after waiting some time .
Useful for giving the system some time to process an event , without blocking
the current execution flow .
"""
thread = _Thread ( target = lambda : ( _time . sleep ( delay ) , fn ( * args ) ) )
thread . start ( )
_hooks = { }
def hook ( callback , suppress = False , on_remove = lambda : None ) :
"""
Installs a global listener on all available keyboards , invoking ` callback `
each time a key is pressed or released .
The event passed to the callback is of type ` keyboard . KeyboardEvent ` ,
with the following attributes :
- ` name ` : an Unicode representation of the character ( e . g . " & " ) or
description ( e . g . " space " ) . The name is always lower - case .
- ` scan_code ` : number representing the physical key , e . g . 55.
- ` time ` : timestamp of the time the event occurred , with as much precision
as given by the OS .
Returns the given callback for easier development .
"""
if suppress :
_listener . start_if_necessary ( )
append , remove = _listener . blocking_hooks . append , _listener . blocking_hooks . remove
else :
append , remove = _listener . add_handler , _listener . remove_handler
append ( callback )
def remove_ ( ) :
del _hooks [ callback ]
del _hooks [ remove_ ]
remove ( callback )
on_remove ( )
_hooks [ callback ] = _hooks [ remove_ ] = remove_
return remove_
def on_press ( callback , suppress = False ) :
"""
Invokes ` callback ` for every KEY_DOWN event . For details see ` hook ` .
"""
return hook ( lambda e : e . event_type == KEY_UP or callback ( e ) , suppress = suppress )
def on_release ( callback , suppress = False ) :
"""
Invokes ` callback ` for every KEY_UP event . For details see ` hook ` .
"""
return hook ( lambda e : e . event_type == KEY_DOWN or callback ( e ) , suppress = suppress )
def hook_key ( key , callback , suppress = False ) :
"""
Hooks key up and key down events for a single key . Returns the event handler
created . To remove a hooked key use ` unhook_key ( key ) ` or
` unhook_key ( handler ) ` .
Note : this function shares state with hotkeys , so ` clear_all_hotkeys `
affects it aswell .
"""
_listener . start_if_necessary ( )
store = _listener . blocking_keys if suppress else _listener . nonblocking_keys
scan_codes = key_to_scan_codes ( key )
for scan_code in scan_codes :
store [ scan_code ] . append ( callback )
def remove_ ( ) :
del _hooks [ callback ]
del _hooks [ key ]
del _hooks [ remove_ ]
for scan_code in scan_codes :
store [ scan_code ] . remove ( callback )
_hooks [ callback ] = _hooks [ key ] = _hooks [ remove_ ] = remove_
return remove_
def on_press_key ( key , callback , suppress = False ) :
"""
Invokes ` callback ` for KEY_DOWN event related to the given key . For details see ` hook ` .
"""
return hook_key ( key , lambda e : e . event_type == KEY_UP or callback ( e ) , suppress = suppress )
def on_release_key ( key , callback , suppress = False ) :
"""
Invokes ` callback ` for KEY_UP event related to the given key . For details see ` hook ` .
"""
return hook_key ( key , lambda e : e . event_type == KEY_DOWN or callback ( e ) , suppress = suppress )
def unhook ( remove ) :
"""
Removes a previously added hook , either by callback or by the return value
of ` hook ` .
"""
_hooks [ remove ] ( )
unhook_key = unhook
def unhook_all ( ) :
"""
Removes all keyboard hooks in use , including hotkeys , abbreviations , word
listeners , ` record ` ers and ` wait ` s .
"""
_listener . start_if_necessary ( )
_listener . blocking_keys . clear ( )
_listener . nonblocking_keys . clear ( )
del _listener . blocking_hooks [ : ]
del _listener . handlers [ : ]
unhook_all_hotkeys ( )
def block_key ( key ) :
"""
Suppresses all key events of the given key , regardless of modifiers .
"""
return hook_key ( key , lambda e : False , suppress = True )
unblock_key = unhook_key
def remap_key ( src , dst ) :
"""
Whenever the key ` src ` is pressed or released , regardless of modifiers ,
press or release the hotkey ` dst ` instead .
"""
def handler ( event ) :
if event . event_type == KEY_DOWN :
press ( dst )
else :
release ( dst )
return False
return hook_key ( src , handler , suppress = True )
unremap_key = unhook_key
def parse_hotkey_combinations ( hotkey ) :
"""
Parses a user - provided hotkey . Differently from ` parse_hotkey ` ,
instead of each step being a list of the different scan codes for each key ,
each step is a list of all possible combinations of those scan codes .
"""
def combine_step ( step ) :
# A single step may be composed of many keys, and each key can have
# multiple scan codes. To speed up hotkey matching and avoid introducing
# event delays, we list all possible combinations of scan codes for these
# keys. Hotkeys are usually small, and there are not many combinations, so
# this is not as insane as it sounds.
return ( tuple ( sorted ( scan_codes ) ) for scan_codes in _itertools . product ( * step ) )
return tuple ( tuple ( combine_step ( step ) ) for step in parse_hotkey ( hotkey ) )
def _add_hotkey_step ( handler , combinations , suppress ) :
"""
Hooks a single - step hotkey ( e . g . ' shift+a ' ) .
"""
container = _listener . blocking_hotkeys if suppress else _listener . nonblocking_hotkeys
# Register the scan codes of every possible combination of
# modfiier + main key. Modifiers have to be registered in
# filtered_modifiers too, so suppression and replaying can work.
for scan_codes in combinations :
for scan_code in scan_codes :
if is_modifier ( scan_code ) :
_listener . filtered_modifiers [ scan_code ] + = 1
container [ scan_codes ] . append ( handler )
def remove ( ) :
for scan_codes in combinations :
for scan_code in scan_codes :
if is_modifier ( scan_code ) :
_listener . filtered_modifiers [ scan_code ] - = 1
container [ scan_codes ] . remove ( handler )
return remove
_hotkeys = { }
def add_hotkey ( hotkey , callback , args = ( ) , suppress = False , timeout = 1 , trigger_on_release = False ) :
"""
Invokes a callback every time a hotkey is pressed . The hotkey must
be in the format ` ctrl + shift + a , s ` . This would trigger when the user holds
ctrl , shift and " a " at once , releases , and then presses " s " . To represent
literal commas , pluses , and spaces , use their names ( ' comma ' , ' plus ' ,
' space ' ) .
- ` args ` is an optional list of arguments to passed to the callback during
each invocation .
- ` suppress ` defines if successful triggers should block the keys from being
sent to other programs .
- ` timeout ` is the amount of seconds allowed to pass between key presses .
- ` trigger_on_release ` if true , the callback is invoked on key release instead
of key press .
The event handler function is returned . To remove a hotkey call
` remove_hotkey ( hotkey ) ` or ` remove_hotkey ( handler ) ` .
before the hotkey state is reset .
Note : hotkeys are activated when the last key is * pressed * , not released .
Note : the callback is executed in a separate thread , asynchronously . For an
example of how to use a callback synchronously , see ` wait ` .
Examples :
# Different but equivalent ways to listen for a spacebar key press.
add_hotkey ( ' ' , print , args = [ ' space was pressed ' ] )
add_hotkey ( ' space ' , print , args = [ ' space was pressed ' ] )
add_hotkey ( ' Space ' , print , args = [ ' space was pressed ' ] )
# Here 57 represents the keyboard code for spacebar; so you will be
# pressing 'spacebar', not '57' to activate the print function.
add_hotkey ( 57 , print , args = [ ' space was pressed ' ] )
add_hotkey ( ' ctrl+q ' , quit )
add_hotkey ( ' ctrl+alt+enter, space ' , some_callback )
"""
if args :
callback = lambda callback = callback : callback ( * args )
_listener . start_if_necessary ( )
steps = parse_hotkey_combinations ( hotkey )
event_type = KEY_UP if trigger_on_release else KEY_DOWN
if len ( steps ) == 1 :
# Deciding when to allow a KEY_UP event is far harder than I thought,
# and any mistake will make that key "sticky". Therefore just let all
# KEY_UP events go through as long as that's not what we are listening
# for.
handler = lambda e : ( event_type == KEY_DOWN and e . event_type == KEY_UP and e . scan_code in _logically_pressed_keys ) or ( event_type == e . event_type and callback ( ) )
remove_step = _add_hotkey_step ( handler , steps [ 0 ] , suppress )
def remove_ ( ) :
remove_step ( )
del _hotkeys [ hotkey ]
del _hotkeys [ remove_ ]
del _hotkeys [ callback ]
# TODO: allow multiple callbacks for each hotkey without overwriting the
# remover.
_hotkeys [ hotkey ] = _hotkeys [ remove_ ] = _hotkeys [ callback ] = remove_
return remove_
state = _State ( )
state . remove_catch_misses = None
state . remove_last_step = None
state . suppressed_events = [ ]
state . last_update = float ( ' -inf ' )
def catch_misses ( event , force_fail = False ) :
if (
event . event_type == event_type
and state . index
and event . scan_code not in allowed_keys_by_step [ state . index ]
) or (
timeout
and _time . monotonic ( ) - state . last_update > = timeout
) or force_fail : # Weird formatting to ensure short-circuit.
state . remove_last_step ( )
for event in state . suppressed_events :
if event . event_type == KEY_DOWN :
press ( event . scan_code )
else :
release ( event . scan_code )
del state . suppressed_events [ : ]
index = 0
set_index ( 0 )
return True
def set_index ( new_index ) :
state . index = new_index
if new_index == 0 :
# This is done for performance reasons, avoiding a global key hook
# that is always on.
state . remove_catch_misses = lambda : None
elif new_index == 1 :
state . remove_catch_misses ( )
# Must be `suppress=True` to ensure `send` has priority.
state . remove_catch_misses = hook ( catch_misses , suppress = True )
if new_index == len ( steps ) - 1 :
def handler ( event ) :
if event . event_type == KEY_UP :
remove ( )
set_index ( 0 )
accept = event . event_type == event_type and callback ( )
if accept :
return catch_misses ( event , force_fail = True )
else :
state . suppressed_events [ : ] = [ event ]
return False
remove = _add_hotkey_step ( handler , steps [ state . index ] , suppress )
else :
# Fix value of next_index.
def handler ( event , new_index = state . index + 1 ) :
if event . event_type == KEY_UP :
remove ( )
set_index ( new_index )
state . suppressed_events . append ( event )
return False
remove = _add_hotkey_step ( handler , steps [ state . index ] , suppress )
state . remove_last_step = remove
state . last_update = _time . monotonic ( )
return False
set_index ( 0 )
allowed_keys_by_step = [
set ( ) . union ( * step )
for step in steps
]
def remove_ ( ) :
state . remove_catch_misses ( )
state . remove_last_step ( )
del _hotkeys [ hotkey ]
del _hotkeys [ remove_ ]
del _hotkeys [ callback ]
# TODO: allow multiple callbacks for each hotkey without overwriting the
# remover.
_hotkeys [ hotkey ] = _hotkeys [ remove_ ] = _hotkeys [ callback ] = remove_
return remove_
register_hotkey = add_hotkey
def remove_hotkey ( hotkey_or_callback ) :
"""
Removes a previously hooked hotkey . Must be called wtih the value returned
by ` add_hotkey ` .
"""
_hotkeys [ hotkey_or_callback ] ( )
unregister_hotkey = clear_hotkey = remove_hotkey
def unhook_all_hotkeys ( ) :
"""
Removes all keyboard hotkeys in use , including abbreviations , word listeners ,
` record ` ers and ` wait ` s .
"""
# Because of "alises" some hooks may have more than one entry, all of which
# are removed together.
_listener . blocking_hotkeys . clear ( )
_listener . nonblocking_hotkeys . clear ( )
unregister_all_hotkeys = remove_all_hotkeys = clear_all_hotkeys = unhook_all_hotkeys
def remap_hotkey ( src , dst , suppress = True , trigger_on_release = False ) :
"""
Whenever the hotkey ` src ` is pressed , suppress it and send
` dst ` instead .
Example :
remap ( ' alt+w ' , ' ctrl+up ' )
"""
def handler ( ) :
active_modifiers = sorted ( modifier for modifier , state in _listener . modifier_states . items ( ) if state == ' allowed ' )
for modifier in active_modifiers :
release ( modifier )
send ( dst )
for modifier in reversed ( active_modifiers ) :
press ( modifier )
return False
return add_hotkey ( src , handler , suppress = suppress , trigger_on_release = trigger_on_release )
unremap_hotkey = remove_hotkey
def stash_state ( ) :
"""
Builds a list of all currently pressed scan codes , releases them and returns
the list . Pairs well with ` restore_state ` and ` restore_modifiers ` .
"""
# TODO: stash caps lock / numlock /scrollock state.
with _pressed_events_lock :
state = sorted ( _pressed_events )
for scan_code in state :
_os_keyboard . release ( scan_code )
return state
def restore_state ( scan_codes ) :
"""
Given a list of scan_codes ensures these keys , and only these keys , are
pressed . Pairs well with ` stash_state ` , alternative to ` restore_modifiers ` .
"""
_listener . is_replaying = True
with _pressed_events_lock :
current = set ( _pressed_events )
target = set ( scan_codes )
for scan_code in current - target :
_os_keyboard . release ( scan_code )
for scan_code in target - current :
_os_keyboard . press ( scan_code )
_listener . is_replaying = False
def restore_modifiers ( scan_codes ) :
"""
Like ` restore_state ` , but only restores modifier keys .
"""
restore_state ( ( scan_code for scan_code in scan_codes if is_modifier ( scan_code ) ) )
def write ( text , delay = 0 , restore_state_after = True , exact = None ) :
"""
Sends artificial keyboard events to the OS , simulating the typing of a given
text . Characters not available on the keyboard are typed as explicit unicode
characters using OS - specific functionality , such as alt + codepoint .
To ensure text integrity , all currently pressed keys are released before
the text is typed , and modifiers are restored afterwards .
- ` delay ` is the number of seconds to wait between keypresses , defaults to
no delay .
- ` restore_state_after ` can be used to restore the state of pressed keys
after the text is typed , i . e . presses the keys that were released at the
beginning . Defaults to True .
- ` exact ` forces typing all characters as explicit unicode ( e . g .
alt + codepoint or special events ) . If None , uses platform - specific suggested
value .
"""
if exact is None :
exact = _platform . system ( ) == ' Windows '
state = stash_state ( )
# Window's typing of unicode characters is quite efficient and should be preferred.
if exact :
for letter in text :
if letter in ' \n \b ' :
send ( letter )
else :
_os_keyboard . type_unicode ( letter )
if delay : _time . sleep ( delay )
else :
for letter in text :
try :
entries = _os_keyboard . map_name ( normalize_name ( letter ) )
scan_code , modifiers = next ( iter ( entries ) )
except ( KeyError , ValueError ) :
_os_keyboard . type_unicode ( letter )
continue
for modifier in modifiers :
press ( modifier )
_os_keyboard . press ( scan_code )
_os_keyboard . release ( scan_code )
for modifier in modifiers :
release ( modifier )
if delay :
_time . sleep ( delay )
if restore_state_after :
restore_modifiers ( state )
def wait ( hotkey = None , suppress = False , trigger_on_release = False ) :
"""
Blocks the program execution until the given hotkey is pressed or ,
if given no parameters , blocks forever .
"""
if hotkey :
lock = _Event ( )
remove = add_hotkey ( hotkey , lambda : lock . set ( ) , suppress = suppress , trigger_on_release = trigger_on_release )
lock . wait ( )
remove_hotkey ( remove )
else :
while True :
_time . sleep ( 1e6 )
def get_hotkey_name ( names = None ) :
"""
Returns a string representation of hotkey from the given key names , or
the currently pressed keys if not given . This function :
- normalizes names ;
- removes " left " and " right " prefixes ;
- replaces the " + " key name with " plus " to avoid ambiguity ;
- puts modifier keys first , in a standardized order ;
- sort remaining keys ;
- finally , joins everything with " + " .
Example :
get_hotkey_name ( [ ' + ' , ' left ctrl ' , ' shift ' ] )
# "ctrl+shift+plus"
"""
if names is None :
_listener . start_if_necessary ( )
with _pressed_events_lock :
names = [ e . name for e in _pressed_events . values ( ) ]
else :
names = [ normalize_name ( name ) for name in names ]
clean_names = set ( e . replace ( ' left ' , ' ' ) . replace ( ' right ' , ' ' ) . replace ( ' + ' , ' plus ' ) for e in names )
# https://developer.apple.com/macos/human-interface-guidelines/input-and-output/keyboard/
# > List modifier keys in the correct order. If you use more than one modifier key in a
# > hotkey, always list them in this order: Control, Option, Shift, Command.
modifiers = [ ' ctrl ' , ' alt ' , ' shift ' , ' windows ' ]
sorting_key = lambda k : ( modifiers . index ( k ) if k in modifiers else 5 , str ( k ) )
return ' + ' . join ( sorted ( clean_names , key = sorting_key ) )
def read_event ( suppress = False ) :
"""
Blocks until a keyboard event happens , then returns that event .
"""
queue = _queue . Queue ( maxsize = 1 )
hooked = hook ( queue . put , suppress = suppress )
while True :
event = queue . get ( )
unhook ( hooked )
return event
def read_key ( suppress = False ) :
"""
Blocks until a keyboard event happens , then returns that event ' s name or,
if missing , its scan code .
"""
event = read_event ( suppress )
return event . name or event . scan_code
def read_hotkey ( suppress = True ) :
"""
Similar to ` read_key ( ) ` , but blocks until the user presses and releases a
hotkey ( or single key ) , then returns a string representing the hotkey
pressed .
Example :
read_hotkey ( )
# "ctrl+shift+p"
"""
queue = _queue . Queue ( )
fn = lambda e : queue . put ( e ) or e . event_type == KEY_DOWN
hooked = hook ( fn , suppress = suppress )
while True :
event = queue . get ( )
if event . event_type == KEY_UP :
unhook ( hooked )
with _pressed_events_lock :
names = [ e . name for e in _pressed_events . values ( ) ] + [ event . name ]
return get_hotkey_name ( names )
def get_typed_strings ( events , allow_backspace = True ) :
"""
Given a sequence of events , tries to deduce what strings were typed .
Strings are separated when a non - textual key is pressed ( such as tab or
enter ) . Characters are converted to uppercase according to shift and
capslock status . If ` allow_backspace ` is True , backspaces remove the last
character typed .
This function is a generator , so you can pass an infinite stream of events
and convert them to strings in real time .
Note this functions is merely an heuristic . Windows for example keeps per -
process keyboard state such as keyboard layout , and this information is not
available for our hooks .
get_type_strings ( record ( ) ) #-> ['This is what', 'I recorded', '']
"""
backspace_name = ' delete ' if _platform . system ( ) == ' Darwin ' else ' backspace '
shift_pressed = False
capslock_pressed = False
string = ' '
for event in events :
name = event . name
# Space is the only key that we _parse_hotkey to the spelled out name
# because of legibility. Now we have to undo that.
if event . name == ' space ' :
name = ' '
if ' shift ' in event . name :
shift_pressed = event . event_type == ' down '
elif event . name == ' caps lock ' and event . event_type == ' down ' :
capslock_pressed = not capslock_pressed
elif allow_backspace and event . name == backspace_name and event . event_type == ' down ' :
string = string [ : - 1 ]
elif event . event_type == ' down ' :
if len ( name ) == 1 :
if shift_pressed ^ capslock_pressed :
name = name . upper ( )
string = string + name
else :
yield string
string = ' '
yield string
_recording = None
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 .
Use ` stop_recording ( ) ` or ` unhook ( hooked_function ) ` to stop .
"""
recorded_events_queue = recorded_events_queue or _queue . Queue ( )
global _recording
_recording = ( recorded_events_queue , hook ( recorded_events_queue . put ) )
return _recording
def stop_recording ( ) :
"""
Stops the global recording of events and returns a list of the events
captured .
"""
global _recording
if not _recording :
raise ValueError ( ' Must call " start_recording " before. ' )
recorded_events_queue , hooked = _recording
unhook ( hooked )
return list ( recorded_events_queue . queue )
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 , of type
` keyboard . KeyboardEvent ` . Pairs well with
` play ( events ) ` .
Note : this is a blocking function .
Note : for more details on the keyboard hook and events see ` hook ` .
"""
start_recording ( )
wait ( until , suppress = suppress , trigger_on_release = trigger_on_release )
return stop_recording ( )
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 . Pairs well with ` record ( ) ` .
Note : the current keyboard state is cleared at the beginning and restored at
the end of the function .
"""
state = stash_state ( )
last_time = None
for event in events :
if speed_factor > 0 and last_time is not None :
_time . sleep ( ( event . time - last_time ) / speed_factor )
last_time = event . time
key = event . scan_code or event . name
press ( key ) if event . event_type == KEY_DOWN else release ( key )
restore_modifiers ( state )
replay = play
_word_listeners = { }
def add_word_listener ( word , callback , triggers = [ ' space ' ] , match_suffix = False , timeout = 2 ) :
"""
Invokes a callback every time a sequence of characters is typed ( e . g . ' pet ' )
and followed by a trigger key ( e . g . space ) . Modifiers ( e . g . alt , ctrl ,
shift ) are ignored .
- ` word ` the typed text to be matched . E . g . ' pet ' .
- ` callback ` is an argument - less function to be invoked each time the word
is typed .
- ` triggers ` is the list of keys that will cause a match to be checked . If
the user presses some key that is not a character ( len > 1 ) and not in
triggers , the characters so far will be discarded . By default the trigger
is only ` space ` .
- ` match_suffix ` defines if endings of words should also be checked instead
of only whole words . E . g . if true , typing ' carpet ' + space will trigger the
listener for ' pet ' . Defaults to false , only whole words are checked .
- ` timeout ` is the maximum number of seconds between typed characters before
the current word is discarded . Defaults to 2 seconds .
Returns the event handler created . To remove a word listener use
` remove_word_listener ( word ) ` or ` remove_word_listener ( handler ) ` .
Note : all actions are performed on key down . Key up events are ignored .
Note : word mathes are * * case sensitive * * .
"""
state = _State ( )
state . current = ' '
state . time = - 1
def handler ( event ) :
name = event . name
if event . event_type == KEY_UP or name in all_modifiers : return
if timeout and event . time - state . time > timeout :
state . current = ' '
state . time = event . time
matched = state . current == word or ( match_suffix and state . current . endswith ( word ) )
if name in triggers and matched :
callback ( )
state . current = ' '
elif len ( name ) > 1 :
state . current = ' '
else :
state . current + = name
hooked = hook ( handler )
def remove ( ) :
hooked ( )
del _word_listeners [ word ]
del _word_listeners [ handler ]
del _word_listeners [ remove ]
_word_listeners [ word ] = _word_listeners [ handler ] = _word_listeners [ remove ] = remove
# TODO: allow multiple word listeners and removing them correctly.
return remove
def remove_word_listener ( word_or_handler ) :
"""
Removes a previously registered word listener . Accepts either the word used
during registration ( exact string ) or the event handler returned by the
` add_word_listener ` or ` add_abbreviation ` functions .
"""
_word_listeners [ word_or_handler ] ( )
def add_abbreviation ( source_text , replacement_text , match_suffix = False , timeout = 2 ) :
"""
Registers a hotkey that replaces one typed text with another . For example
add_abbreviation ( ' tm ' , u ' ™ ' )
Replaces every " tm " followed by a space with a ™ symbol ( and no space ) . The
replacement is done by sending backspace events .
- ` match_suffix ` defines if endings of words should also be checked instead
of only whole words . E . g . if true , typing ' carpet ' + space will trigger the
listener for ' pet ' . Defaults to false , only whole words are checked .
- ` timeout ` is the maximum number of seconds between typed characters before
the current word is discarded . Defaults to 2 seconds .
For more details see ` add_word_listener ` .
"""
replacement = ' \b ' * ( len ( source_text ) + 1 ) + replacement_text
callback = lambda : write ( replacement )
return add_word_listener ( source_text , callback , match_suffix = match_suffix , timeout = timeout )
# Aliases.
register_word_listener = add_word_listener
register_abbreviation = add_abbreviation
remove_abbreviation = remove_word_listener