aboutsummaryrefslogtreecommitdiff
path: root/Python/venv1/Lib/site-packages/keyboard/__init__.py
diff options
context:
space:
mode:
authorSyndamia <kami02882@gmail.com>2019-07-29 11:46:36 +0300
committerSyndamia <kami02882@gmail.com>2019-07-29 11:46:36 +0300
commitbc09da5a7b65b08b5d5dcd1e90173ad3b6081c23 (patch)
treec66cebc02aac30ff859c06ca462f3dd58b6809b0 /Python/venv1/Lib/site-packages/keyboard/__init__.py
parent65edf7296baf48aad1b4e0c09b57f1a7f48791a8 (diff)
downloadSelf-learning-bc09da5a7b65b08b5d5dcd1e90173ad3b6081c23.tar
Self-learning-bc09da5a7b65b08b5d5dcd1e90173ad3b6081c23.tar.gz
Self-learning-bc09da5a7b65b08b5d5dcd1e90173ad3b6081c23.zip
Did some more work in Python and started officially learning Java
Diffstat (limited to 'Python/venv1/Lib/site-packages/keyboard/__init__.py')
-rw-r--r--Python/venv1/Lib/site-packages/keyboard/__init__.py1155
1 files changed, 1155 insertions, 0 deletions
diff --git a/Python/venv1/Lib/site-packages/keyboard/__init__.py b/Python/venv1/Lib/site-packages/keyboard/__init__.py
new file mode 100644
index 0000000..5d8f305
--- /dev/null
+++ b/Python/venv1/Lib/site-packages/keyboard/__init__.py
@@ -0,0 +1,1155 @@
+# -*- 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 \ No newline at end of file