├── .gitignore ├── bin └── xkeysnail ├── setup.py ├── xkeysnail ├── info.py ├── __init__.py ├── output.py ├── input.py ├── transform.py └── key.py ├── example └── config.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .pyc 3 | -------------------------------------------------------------------------------- /bin/xkeysnail: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | if __name__ == '__main__': 5 | from xkeysnail import cli_main 6 | cli_main() 7 | 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | exec(open("xkeysnail/info.py").read()) 5 | 6 | setup(name = "xkeysnail", 7 | version = __version__, 8 | author = "Masafumi Oyamada", 9 | url = "https://github.com/mooz/xkeysnail", 10 | description = __description__, 11 | long_description = __doc__, 12 | packages = ["xkeysnail"], 13 | scripts = ["bin/xkeysnail"], 14 | license = "GPL", 15 | install_requires = ["evdev", "python-xlib", "inotify_simple", "appdirs"] 16 | ) 17 | -------------------------------------------------------------------------------- /xkeysnail/info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __version__ = "0.4.0" 4 | 5 | __logo__ = """ 6 | ██╗ ██╗██╗ ██╗███████╗██╗ ██╗ 7 | ╚██╗██╔╝██║ ██╔╝██╔════╝╚██╗ ██╔╝ 8 | ╚███╔╝ █████╔╝ █████╗ ╚████╔╝ 9 | ██╔██╗ ██╔═██╗ ██╔══╝ ╚██╔╝ 10 | ██╔╝ ██╗██║ ██╗███████╗ ██║ 11 | ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═╝ 12 | ███████╗███╗ ██╗ █████╗ ██╗██╗ 13 | ██╔════╝████╗ ██║██╔══██╗██║██║ 14 | ███████╗██╔██╗ ██║███████║██║██║ 15 | ╚════██║██║╚██╗██║██╔══██║██║██║ 16 | ███████║██║ ╚████║██║ ██║██║███████╗ 17 | ╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝╚══════╝ 18 | """ 19 | 20 | __description__ = "Yet another keyboard remapping tool for X environment." 21 | 22 | __doc__ = """ 23 | ``xkeysnail`` is yet another keyboard remapping tool for X environment. 24 | It's like ``xmodmap`` but allows more flexible remappings. 25 | 26 | - Has high-level and flexible remapping mechanisms, such as 27 | 28 | - **per-application keybindings can be defined** 29 | - **multiple stroke keybindings can be defined** such as 30 | ``Ctrl+x Ctrl+c`` to ``Ctrl+q`` 31 | - **not only key remapping but arbitrary commands defined by 32 | Python can be bound to a key** 33 | 34 | - Runs in low-level layer (``evdev`` and ``uinput``), making 35 | **remapping work in almost all the places** 36 | """ 37 | -------------------------------------------------------------------------------- /xkeysnail/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def eval_file(path): 5 | with open(path, "rb") as file: 6 | exec(compile(file.read(), path, 'exec'), globals()) 7 | 8 | 9 | def uinput_device_exists(): 10 | from os.path import exists 11 | return exists('/dev/uinput') 12 | 13 | 14 | def has_access_to_uinput(): 15 | from evdev.uinput import UInputError 16 | try: 17 | from xkeysnail.output import _uinput # noqa: F401 18 | return True 19 | except UInputError: 20 | return False 21 | 22 | 23 | def cli_main(): 24 | from .info import __logo__, __version__ 25 | print("") 26 | print(__logo__.strip()) 27 | print(" v{}".format(__version__)) 28 | print("") 29 | 30 | # Parse args 31 | import argparse 32 | from appdirs import user_config_dir 33 | parser = argparse.ArgumentParser(description='Yet another keyboard remapping tool for X environment.') 34 | parser.add_argument('config', metavar='config.py', type=str, default=user_config_dir('xkeysnail/config.py'), nargs='?', 35 | help='configuration file (See README.md for syntax)') 36 | parser.add_argument('--devices', dest="devices", metavar='device', type=str, nargs='+', 37 | help='keyboard devices to remap (if omitted, xkeysnail choose proper keyboard devices)') 38 | parser.add_argument('--watch', dest='watch', action='store_true', 39 | help='watch keyboard devices plug in ') 40 | parser.add_argument('-q', '--quiet', dest='quiet', action='store_true', 41 | help='suppress output of key events') 42 | args = parser.parse_args() 43 | 44 | # Make sure that the /dev/uinput device exists 45 | if not uinput_device_exists(): 46 | print("""The '/dev/uinput' device does not exist. 47 | Please check your kernel configuration.""") 48 | import sys 49 | sys.exit(1) 50 | 51 | # Make sure that user have root privilege 52 | if not has_access_to_uinput(): 53 | print("""Failed to open `uinput` in write mode. 54 | Make sure that you have executed xkeysnail with root privilege such as 55 | 56 | $ sudo xkeysnail config.py 57 | """) 58 | import sys 59 | sys.exit(1) 60 | 61 | # Load configuration file 62 | eval_file(args.config) 63 | 64 | # Enter event loop 65 | from xkeysnail.input import loop 66 | loop(args.devices, args.watch, args.quiet) 67 | -------------------------------------------------------------------------------- /xkeysnail/output.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from evdev import ecodes 4 | from evdev.uinput import UInput 5 | from .key import Action, Combo, Modifier 6 | 7 | __author__ = 'zh' 8 | 9 | 10 | # Remove all buttons so udev doesn't think xkeysnail is a joystick 11 | _keyboard_codes = ecodes.keys.keys() - ecodes.BTN 12 | 13 | # But we want mouse buttons, so let's enumerate those and add them 14 | # back into the set of buttons we'll watch and use 15 | mouse_btns = {256: ['BTN_0', 'BTN_MISC'], 16 | 257: 'BTN_1', 17 | 258: 'BTN_2', 18 | 259: 'BTN_3', 19 | 260: 'BTN_4', 20 | 261: 'BTN_5', 21 | 262: 'BTN_6', 22 | 263: 'BTN_7', 23 | 264: 'BTN_8', 24 | 265: 'BTN_9', 25 | 272: ['BTN_LEFT', 'BTN_MOUSE'], 26 | 274: 'BTN_MIDDLE', 27 | 273: 'BTN_RIGHT'} 28 | _keyboard_codes.update(mouse_btns) 29 | 30 | _uinput = UInput(events={ecodes.EV_KEY: _keyboard_codes, 31 | ecodes.EV_REL: set([0,1,6,8,9]), 32 | }) 33 | 34 | _pressed_modifier_keys = set() 35 | _pressed_keys = set() 36 | 37 | def update_modifier_key_pressed(key, action): 38 | if key in Modifier.get_all_keys(): 39 | if action.is_pressed(): 40 | _pressed_modifier_keys.add(key) 41 | else: 42 | _pressed_modifier_keys.discard(key) 43 | 44 | def update_pressed_keys(key, action): 45 | if action.is_pressed(): 46 | _pressed_keys.add(key) 47 | else: 48 | _pressed_keys.discard(key) 49 | 50 | def is_pressed(key): 51 | return key in _pressed_keys 52 | 53 | def send_sync(): 54 | _uinput.syn() 55 | 56 | 57 | def send_event(event): 58 | _uinput.write_event(event) 59 | send_sync() 60 | 61 | 62 | def send_key_action(key, action): 63 | update_modifier_key_pressed(key, action) 64 | update_pressed_keys(key, action) 65 | _uinput.write(ecodes.EV_KEY, key, action) 66 | send_sync() 67 | 68 | 69 | def send_combo(combo): 70 | 71 | released_modifiers_keys = [] 72 | 73 | extra_modifier_keys = _pressed_modifier_keys.copy() 74 | missing_modifiers = combo.modifiers.copy() 75 | for pressed_key in _pressed_modifier_keys: 76 | for modifier in combo.modifiers: 77 | if pressed_key in modifier.get_keys(): 78 | extra_modifier_keys.remove(pressed_key) 79 | missing_modifiers.remove(modifier) 80 | 81 | for modifier_key in extra_modifier_keys: 82 | send_key_action(modifier_key, Action.RELEASE) 83 | released_modifiers_keys.append(modifier_key) 84 | 85 | pressed_modifier_keys = [] 86 | for modifier in missing_modifiers: 87 | modifier_key = modifier.get_key() 88 | send_key_action(modifier_key, Action.PRESS) 89 | pressed_modifier_keys.append(modifier_key) 90 | 91 | send_key_action(combo.key, Action.PRESS) 92 | 93 | send_key_action(combo.key, Action.RELEASE) 94 | 95 | for modifier in reversed(pressed_modifier_keys): 96 | send_key_action(modifier, Action.RELEASE) 97 | 98 | for modifier in reversed(released_modifiers_keys): 99 | send_key_action(modifier, Action.PRESS) 100 | 101 | 102 | def send_key(key): 103 | send_combo(Combo(None, key)) 104 | -------------------------------------------------------------------------------- /example/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | from xkeysnail.transform import * 5 | 6 | # define timeout for multipurpose_modmap 7 | define_timeout(1) 8 | 9 | # [Global modemap] Change modifier keys as in xmodmap 10 | define_modmap({ 11 | Key.CAPSLOCK: Key.LEFT_CTRL 12 | }) 13 | 14 | # [Conditional modmap] Change modifier keys in certain applications 15 | define_conditional_modmap(re.compile(r'Emacs'), { 16 | Key.RIGHT_CTRL: Key.ESC, 17 | }) 18 | 19 | # [Multipurpose modmap] Give a key two meanings. A normal key when pressed and 20 | # released, and a modifier key when held down with another key. See Xcape, 21 | # Carabiner and caps2esc for ideas and concept. 22 | define_multipurpose_modmap( 23 | # Enter is enter when pressed and released. Control when held down. 24 | {Key.ENTER: [Key.ENTER, Key.RIGHT_CTRL]} 25 | 26 | # Capslock is escape when pressed and released. Control when held down. 27 | # {Key.CAPSLOCK: [Key.ESC, Key.LEFT_CTRL] 28 | # To use this example, you can't remap capslock with define_modmap. 29 | ) 30 | 31 | # [Conditional multipurpose modmap] Multipurpose modmap in certain conditions, 32 | # such as for a particular device. 33 | define_conditional_multipurpose_modmap(lambda wm_class, device_name: device_name.startswith("Microsoft"), { 34 | # Left shift is open paren when pressed and released. 35 | # Left shift when held down. 36 | Key.LEFT_SHIFT: [Key.KPLEFTPAREN, Key.LEFT_SHIFT], 37 | 38 | # Right shift is close paren when pressed and released. 39 | # Right shift when held down. 40 | Key.RIGHT_SHIFT: [Key.KPRIGHTPAREN, Key.RIGHT_SHIFT] 41 | }) 42 | 43 | 44 | # Keybindings for Firefox/Chrome 45 | define_keymap(re.compile("Firefox|Google-chrome"), { 46 | # Ctrl+Alt+j/k to switch next/previous tab 47 | K("C-M-j"): K("C-TAB"), 48 | K("C-M-k"): K("C-Shift-TAB"), 49 | # Type C-j to focus to the content 50 | K("C-j"): K("C-f6"), 51 | # very naive "Edit in editor" feature (just an example) 52 | K("C-o"): [K("C-a"), K("C-c"), launch(["gedit"]), sleep(0.5), K("C-v")] 53 | }, "Firefox and Chrome") 54 | 55 | # Keybindings for Zeal https://github.com/zealdocs/zeal/ 56 | define_keymap(re.compile("Zeal"), { 57 | # Ctrl+s to focus search area 58 | K("C-s"): K("C-k"), 59 | }, "Zeal") 60 | 61 | # Emacs-like keybindings in non-Emacs applications 62 | define_keymap(lambda wm_class: wm_class not in ("Emacs", "URxvt"), { 63 | # Cursor 64 | K("C-b"): with_mark(K("left")), 65 | K("C-f"): with_mark(K("right")), 66 | K("C-p"): with_mark(K("up")), 67 | K("C-n"): with_mark(K("down")), 68 | K("C-h"): with_mark(K("backspace")), 69 | # Forward/Backward word 70 | K("M-b"): with_mark(K("C-left")), 71 | K("M-f"): with_mark(K("C-right")), 72 | # Beginning/End of line 73 | K("C-a"): with_mark(K("home")), 74 | K("C-e"): with_mark(K("end")), 75 | # Page up/down 76 | K("M-v"): with_mark(K("page_up")), 77 | K("C-v"): with_mark(K("page_down")), 78 | # Beginning/End of file 79 | K("M-Shift-comma"): with_mark(K("C-home")), 80 | K("M-Shift-dot"): with_mark(K("C-end")), 81 | # Newline 82 | K("C-m"): K("enter"), 83 | K("C-j"): K("enter"), 84 | K("C-o"): [K("enter"), K("left")], 85 | # Copy 86 | K("C-w"): [K("C-x"), set_mark(False)], 87 | K("M-w"): [K("C-c"), set_mark(False)], 88 | K("C-y"): [K("C-v"), set_mark(False)], 89 | # Delete 90 | K("C-d"): [K("delete"), set_mark(False)], 91 | K("M-d"): [K("C-delete"), set_mark(False)], 92 | # Kill line 93 | K("C-k"): [K("Shift-end"), K("C-x"), set_mark(False)], 94 | # Undo 95 | K("C-slash"): [K("C-z"), set_mark(False)], 96 | K("C-Shift-ro"): K("C-z"), 97 | # Mark 98 | K("C-space"): set_mark(True), 99 | K("C-M-space"): with_or_set_mark(K("C-right")), 100 | # Search 101 | K("C-s"): K("F3"), 102 | K("C-r"): K("Shift-F3"), 103 | K("M-Shift-key_5"): K("C-h"), 104 | # Cancel 105 | K("C-g"): [K("esc"), set_mark(False)], 106 | # Escape 107 | K("C-q"): escape_next_key, 108 | # C-x YYY 109 | K("C-x"): { 110 | # C-x h (select all) 111 | K("h"): [K("C-home"), K("C-a"), set_mark(True)], 112 | # C-x C-f (open) 113 | K("C-f"): K("C-o"), 114 | # C-x C-s (save) 115 | K("C-s"): K("C-s"), 116 | # C-x k (kill tab) 117 | K("k"): K("C-f4"), 118 | # C-x C-c (exit) 119 | K("C-c"): K("C-q"), 120 | # cancel 121 | K("C-g"): pass_through_key, 122 | # C-x u (undo) 123 | K("u"): [K("C-z"), set_mark(False)], 124 | } 125 | }, "Emacs-like keys") 126 | -------------------------------------------------------------------------------- /xkeysnail/input.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from evdev import ecodes, InputDevice, list_devices 4 | from select import select 5 | from sys import exit 6 | from .transform import on_event 7 | from .output import send_event 8 | from .key import Key 9 | 10 | __author__ = 'zh' 11 | 12 | 13 | def get_devices_list(): 14 | return [InputDevice(device_fn) for device_fn in reversed(list_devices())] 15 | 16 | 17 | def is_keyboard_device(device): 18 | """Guess the device is a keyboard or not""" 19 | capabilities = device.capabilities(verbose=False) 20 | if 1 not in capabilities: 21 | return False 22 | supported_keys = capabilities[1] 23 | if Key.SPACE not in supported_keys or \ 24 | Key.A not in supported_keys or \ 25 | Key.Z not in supported_keys: 26 | # Not support common keys. Not keyboard. 27 | return False 28 | if Key.BTN_MOUSE in supported_keys: 29 | # Mouse. 30 | return False 31 | # Otherwise, its keyboard! 32 | return True 33 | 34 | 35 | def print_device_list(devices): 36 | device_format = '{1.fn:<20} {1.name:<35} {1.phys}' 37 | device_lines = [device_format.format(n, d) for n, d in enumerate(devices)] 38 | print('-' * len(max(device_lines, key=len))) 39 | print('{:<20} {:<35} {}'.format('Device', 'Name', 'Phys')) 40 | print('-' * len(max(device_lines, key=len))) 41 | print('\n'.join(device_lines)) 42 | print('') 43 | 44 | 45 | def get_devices_from_paths(device_paths): 46 | return [InputDevice(device_fn) for device_fn in device_paths] 47 | 48 | 49 | class DeviceFilter(object): 50 | def __init__(self, matches): 51 | self.matches = matches 52 | 53 | def __call__(self, device): 54 | # Match by device path or name, if no keyboard devices specified, picks up keyboard-ish devices. 55 | if self.matches: 56 | for match in self.matches: 57 | if device.fn == match or device.name == match: 58 | return True 59 | return False 60 | # Exclude none keyboard devices 61 | if not is_keyboard_device(device): 62 | return False 63 | # Exclude evdev device, we use for output emulation, from input monitoring list 64 | if device.name == "py-evdev-uinput": 65 | return False 66 | return True 67 | 68 | 69 | def select_device(device_matches=None, interactive=True): 70 | """Select a device from the list of accessible input devices.""" 71 | devices = get_devices_from_paths(reversed(list_devices())) 72 | 73 | if interactive: 74 | if not device_matches: 75 | print("""No keyboard devices specified via (--devices) option. 76 | xkeysnail picks up keyboard-ish devices from the list below: 77 | """) 78 | print_device_list(devices) 79 | 80 | devices = list(filter(DeviceFilter(device_matches), devices)) 81 | 82 | if interactive: 83 | if not devices: 84 | print('error: no input devices found (do you have rw permission on /dev/input/*?)') 85 | exit(1) 86 | 87 | print("Okay, now enable remapping on the following device(s):\n") 88 | print_device_list(devices) 89 | 90 | return devices 91 | 92 | 93 | def in_device_list(fn, devices): 94 | for device in devices: 95 | if device.fn == fn: 96 | return True 97 | return False 98 | 99 | 100 | def loop(device_matches, device_watch, quiet): 101 | devices = select_device(device_matches, True) 102 | try: 103 | for device in devices: 104 | device.grab() 105 | except IOError: 106 | print("IOError when grabbing device. Maybe, another xkeysnail instance is running?") 107 | exit(1) 108 | 109 | if device_watch: 110 | from inotify_simple import INotify, flags 111 | inotify = INotify() 112 | inotify.add_watch("/dev/input", flags.CREATE | flags.ATTRIB) 113 | print("Watching keyboard devices plug in") 114 | device_filter = DeviceFilter(device_matches) 115 | 116 | if quiet: 117 | print("No key event will be output since quiet option was specified.") 118 | 119 | try: 120 | while True: 121 | try: 122 | waitables = devices[:] 123 | if device_watch: 124 | waitables.append(inotify.fd) 125 | r, w, x = select(waitables, [], []) 126 | 127 | for waitable in r: 128 | if isinstance(waitable, InputDevice): 129 | for event in waitable.read(): 130 | if event.type == ecodes.EV_KEY: 131 | on_event(event, waitable.name, quiet) 132 | else: 133 | send_event(event) 134 | else: 135 | new_devices = add_new_device(devices, device_filter, inotify) 136 | if new_devices: 137 | print("Okay, now enable remapping on the following new device(s):\n") 138 | print_device_list(new_devices) 139 | except OSError: 140 | if isinstance(waitable, InputDevice): 141 | remove_device(devices, waitable) 142 | print("Device removed: " + str(device.name)) 143 | except KeyboardInterrupt: 144 | print("Received an interrupt, exiting.") 145 | break 146 | finally: 147 | for device in devices: 148 | try: 149 | device.ungrab() 150 | except OSError as e: 151 | pass 152 | if device_watch: 153 | inotify.close() 154 | 155 | 156 | def add_new_device(devices, device_filter, inotify): 157 | new_devices = [] 158 | for event in inotify.read(): 159 | new_device = InputDevice("/dev/input/" + event.name) 160 | if device_filter(new_device) and not in_device_list(new_device.fn, devices): 161 | try: 162 | new_device.grab() 163 | except IOError: 164 | # Ignore errors on new devices 165 | print("IOError when grabbing new device: " + str(new_device.name)) 166 | continue 167 | devices.append(new_device) 168 | new_devices.append(new_device) 169 | return new_devices 170 | 171 | 172 | def remove_device(devices, device): 173 | devices.remove(device) 174 | try: 175 | device.ungrab() 176 | except OSError as e: 177 | pass 178 | 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xkeysnail 2 | 3 | `xkeysnail` is yet another keyboard remapping tool for X environment written in Python. It's like 4 | `xmodmap` but allows more flexible remappings. 5 | 6 | ![screenshot](http://mooz.github.io/image/xkeysnail_screenshot.png) 7 | 8 | - **Pros** 9 | - Has high-level and flexible remapping mechanisms, such as 10 | - **per-application keybindings can be defined** 11 | - **multiple stroke keybindings can be defined** such as `Ctrl+x Ctrl+c` to `Ctrl+q` 12 | - **not only key remapping but arbitrary commands defined by Python can be bound to a key** 13 | - Runs in low-level layer (`evdev` and `uinput`), making **remapping work in almost all the places** 14 | - **Cons** 15 | - Runs in root-mode (requires `sudo`) 16 | 17 | The key remapping mechanism of `xkeysnail` is based on `pykeymacs` 18 | (https://github.com/DreaminginCodeZH/pykeymacs). 19 | 20 | ## Installation 21 | 22 | Requires root privilege and **Python 3**. 23 | 24 | ### Ubuntu 25 | 26 | sudo apt install python3-pip 27 | sudo pip3 install xkeysnail 28 | 29 | # If you plan to compile from source 30 | sudo apt install python3-dev 31 | 32 | ### Fedora 33 | 34 | sudo dnf install python3-pip 35 | sudo pip3 install xkeysnail 36 | # Add your user to input group if you don't want to run xkeysnail 37 | # with sudo (log out and log in again to apply group change) 38 | sudo usermod -a -G input $USER 39 | 40 | # If you plan to compile from source 41 | sudo dnf install python3-devel 42 | 43 | ### Manjaro/Arch 44 | 45 | # Some distros will need to compile evdev components 46 | # and may fail to do so if gcc is not installed. 47 | sudo pacman -Syy 48 | sudo pacman -S gcc 49 | 50 | ### Solus 51 | 52 | # Some distros will need to compile evdev components 53 | # and may fail to do so if gcc is not installed. 54 | sudo eopkg install gcc 55 | sudo eopkg install -c system.devel 56 | 57 | ### From source 58 | 59 | git clone --depth 1 https://github.com/mooz/xkeysnail.git 60 | cd xkeysnail 61 | sudo pip3 install --upgrade . 62 | 63 | ## Usage 64 | 65 | sudo xkeysnail config.py 66 | 67 | When you encounter the errors like `Xlib.error.DisplayConnectionError: Can't connect to display ":0.0": b'No protocol specified\n' 68 | `, try 69 | 70 | xhost +SI:localuser:root 71 | sudo xkeysnail config.py 72 | 73 | If you want to specify keyboard devices, use `--devices` option: 74 | 75 | sudo xkeysnail config.py --devices /dev/input/event3 'Topre Corporation HHKB Professional' 76 | 77 | If you have hot-plugging keyboards, use `--watch` option. 78 | 79 | If you want to suppress output of key events, use `-q` / `--quiet` option especially when running as a daemon. 80 | 81 | ## How to prepare `config.py`? 82 | 83 | (**If you just need Emacs-like keybindings, consider to 84 | use 85 | [`example/config.py`](https://github.com/mooz/xkeysnail/blob/master/example/config.py), 86 | which contains Emacs-like keybindings)**. 87 | 88 | Configuration file is a Python script that consists of several keymaps defined 89 | by `define_keymap(condition, mappings, name)` 90 | 91 | ### `define_keymap(condition, mappings, name)` 92 | 93 | Defines a keymap consists of `mappings`, which is activated when the `condition` 94 | is satisfied. 95 | 96 | Argument `condition` specifies the condition of activating the `mappings` on an 97 | application and takes one of the following forms: 98 | - Regular expression (e.g., `re.compile("YYY")`) 99 | - Activates the `mappings` if the pattern `YYY` matches the `WM_CLASS` of the application. 100 | - Case Insensitivity matching against `WM_CLASS` via `re.IGNORECASE` (e.g. `re.compile('Gnome-terminal', re.IGNORECASE)`) 101 | - `lambda wm_class: some_condition(wm_class)` 102 | - Activates the `mappings` if the `WM_CLASS` of the application satisfies the condition specified by the `lambda` function. 103 | - Case Insensitivity matching via `casefold()` or `lambda wm_class: wm_class.casefold()` (see example below to see how to compare to a list of names) 104 | - `None`: Refers to no condition. `None`-specified keymap will be a global keymap and is always enabled. 105 | 106 | Argument `mappings` is a dictionary in the form of `{key: command, key2: 107 | command2, ...}` where `key` and `command` take following forms: 108 | - `key`: Key to override specified by `K("YYY")` 109 | - For the syntax of key specification, please refer to the [key specification section](#key-specification). 110 | - `command`: one of the followings 111 | - `K("YYY")`: Dispatch custom key to the application. 112 | - `[command1, command2, ...]`: Execute commands sequentially. 113 | - `{ ... }`: Sub-keymap. Used to define multiple stroke keybindings. See [multiple stroke keys](#multiple-stroke-keys) for details. 114 | - `pass_through_key`: Pass through `key` to the application. Useful to override the global mappings behavior on certain applications. 115 | - `escape_next_key`: Escape next key. 116 | - Arbitrary function: The function is executed and the returned value is used as a command. 117 | - Can be used to invoke UNIX commands. 118 | 119 | Argument `name` specifies the keymap name. This is an optional argument. 120 | 121 | #### Key Specification 122 | 123 | Key specification in a keymap is in a form of `K("(-)*")` where 124 | 125 | `` is one of the followings 126 | - `C` or `Ctrl` -> Control key 127 | - `M` or `Alt` -> Alt key 128 | - `Shift` -> Shift key 129 | - `Super` or `Win` -> Super/Windows key 130 | 131 | You can specify left/right modifiers by adding any one of prefixes `L`/`R`. 132 | 133 | And `` is a key whose name is defined 134 | in [`key.py`](https://github.com/mooz/xkeysnail/blob/master/xkeysnail/key.py). 135 | 136 | Here is a list of key specification examples: 137 | 138 | - `K("C-M-j")`: `Ctrl` + `Alt` + `j` 139 | - `K("Ctrl-m")`: `Ctrl` + `m` 140 | - `K("Win-o")`: `Super/Windows` + `o` 141 | - `K("M-Shift-comma")`: `Alt` + `Shift` + `comma` (= `Alt` + `>`) 142 | 143 | #### Multiple stroke keys 144 | 145 | When you needs multiple stroke keys, define nested keymap. For example, the 146 | following example remaps `C-x C-c` to `C-q`. 147 | 148 | ```python 149 | define_keymap(None, { 150 | K("C-x"): { 151 | K("C-c"): K("C-q"), 152 | K("C-f"): K("C-q"), 153 | } 154 | }) 155 | ``` 156 | 157 | #### Checking an application's `WM_CLASS` with `xprop` 158 | 159 | To check `WM_CLASS` of the application you want to have custom keymap, use 160 | `xprop` command: 161 | 162 | xprop WM_CLASS 163 | 164 | and then click the application. `xprop` tells `WM_CLASS` of the application as follows. 165 | 166 | WM_CLASS(STRING) = "Navigator", "Firefox" 167 | 168 | Use the second value (in this case `Firefox`) as the `WM_CLASS` value in your 169 | `config.py`. 170 | 171 | ### Example `config.py` 172 | 173 | See [`example/config.py`](https://github.com/mooz/xkeysnail/blob/master/example/config.py). 174 | 175 | Here is an excerpt of `example/config.py`. 176 | 177 | ```python 178 | from xkeysnail.transform import * 179 | 180 | define_keymap(re.compile("Firefox|Google-chrome"), { 181 | # Ctrl+Alt+j/k to switch next/previous tab 182 | K("C-M-j"): K("C-TAB"), 183 | K("C-M-k"): K("C-Shift-TAB"), 184 | }, "Firefox and Chrome") 185 | 186 | define_keymap(re.compile("Zeal"), { 187 | # Ctrl+s to focus search area 188 | K("C-s"): K("C-k"), 189 | }, "Zeal") 190 | 191 | define_keymap(lambda wm_class: wm_class not in ("Emacs", "URxvt"), { 192 | # Cancel 193 | K("C-g"): [K("esc"), set_mark(False)], 194 | # Escape 195 | K("C-q"): escape_next_key, 196 | # C-x YYY 197 | K("C-x"): { 198 | # C-x h (select all) 199 | K("h"): [K("C-home"), K("C-a"), set_mark(True)], 200 | # C-x C-f (open) 201 | K("C-f"): K("C-o"), 202 | # C-x C-s (save) 203 | K("C-s"): K("C-s"), 204 | # C-x k (kill tab) 205 | K("k"): K("C-f4"), 206 | # C-x C-c (exit) 207 | K("C-c"): K("M-f4"), 208 | # cancel 209 | K("C-g"): pass_through_key, 210 | # C-x u (undo) 211 | K("u"): [K("C-z"), set_mark(False)], 212 | } 213 | }, "Emacs-like keys") 214 | ``` 215 | 216 | ### Example of Case Insensitivity Matching 217 | 218 | ``` 219 | terminals = ["gnome-terminal","konsole","io.elementary.terminal","sakura"] 220 | terminals = [term.casefold() for term in terminals] 221 | termStr = "|".join(str(x) for x in terminals) 222 | 223 | # [Conditional modmap] Change modifier keys in certain applications 224 | define_conditional_modmap(lambda wm_class: wm_class.casefold() not in terminals,{ 225 | # Default Mac/Win 226 | Key.LEFT_ALT: Key.RIGHT_CTRL, # WinMac 227 | Key.LEFT_META: Key.LEFT_ALT, # WinMac 228 | Key.LEFT_CTRL: Key.LEFT_META, # WinMac 229 | Key.RIGHT_ALT: Key.RIGHT_CTRL, # WinMac 230 | Key.RIGHT_META: Key.RIGHT_ALT, # WinMac 231 | Key.RIGHT_CTRL: Key.RIGHT_META, # WinMac 232 | }) 233 | 234 | # [Conditional modmap] Change modifier keys in certain applications 235 | define_conditional_modmap(re.compile(termStr, re.IGNORECASE), { 236 | 237 | # Default Mac/Win 238 | Key.LEFT_ALT: Key.RIGHT_CTRL, # WinMac 239 | Key.LEFT_META: Key.LEFT_ALT, # WinMac 240 | Key.LEFT_CTRL: Key.LEFT_CTRL, # WinMac 241 | Key.RIGHT_ALT: Key.RIGHT_CTRL, # WinMac 242 | Key.RIGHT_META: Key.RIGHT_ALT, # WinMac 243 | Key.RIGHT_CTRL: Key.LEFT_CTRL, # WinMac 244 | }) 245 | ``` 246 | 247 | ## FAQ 248 | 249 | ### How do I fix Firefox capturing Alt before xkeysnail? 250 | 251 | In the Firefox location bar, go to `about:config`, search for `ui.key.menuAccessKeyFocuses`, and set the Value to `false`. 252 | 253 | 254 | ## License 255 | 256 | `xkeysnail` is distributed under GPL. 257 | 258 | xkeysnail 259 | Copyright (C) 2018 Masafumi Oyamada 260 | 261 | This program is free software: you can redistribute it and/or modify 262 | it under the terms of the GNU General Public License as published by 263 | the Free Software Foundation, either version 3 of the License, or 264 | (at your option) any later version. 265 | 266 | This program is distributed in the hope that it will be useful, 267 | but WITHOUT ANY WARRANTY; without even the implied warranty of 268 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 269 | GNU General Public License for more details. 270 | 271 | You should have received a copy of the GNU General Public License 272 | along with this program. If not, see . 273 | 274 | `xkeysnail` is based on `pykeymacs` 275 | (https://github.com/DreaminginCodeZH/pykeymacs), which is distributed under 276 | GPL. 277 | 278 | pykeymacs 279 | Copyright (C) 2015 Zhang Hai 280 | 281 | This program is free software: you can redistribute it and/or modify 282 | it under the terms of the GNU General Public License as published by 283 | the Free Software Foundation, either version 3 of the License, or 284 | (at your option) any later version. 285 | 286 | This program is distributed in the hope that it will be useful, 287 | but WITHOUT ANY WARRANTY; without even the implied warranty of 288 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 289 | GNU General Public License for more details. 290 | 291 | You should have received a copy of the GNU General Public License 292 | along with this program. If not, see . 293 | -------------------------------------------------------------------------------- /xkeysnail/transform.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import itertools 4 | from time import time 5 | from inspect import signature 6 | from .key import Action, Combo, Key, Modifier 7 | from .output import send_combo, send_key_action, send_key, is_pressed 8 | 9 | __author__ = 'zh' 10 | 11 | # ============================================================ # 12 | 13 | import Xlib.display 14 | 15 | 16 | def get_active_window_wm_class(display=Xlib.display.Display()): 17 | """Get active window's WM_CLASS""" 18 | current_window = display.get_input_focus().focus 19 | pair = get_class_name(current_window) 20 | if pair: 21 | # (process name, class name) 22 | return str(pair[1]) 23 | else: 24 | return "" 25 | 26 | 27 | def get_class_name(window): 28 | """Get window's class name (recursively checks parents)""" 29 | try: 30 | wmname = window.get_wm_name() 31 | wmclass = window.get_wm_class() 32 | # workaround for Java app 33 | # https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/classes/sun/awt/X11/XFocusProxyWindow.java#L35 34 | if (wmclass is None and wmname is None) or "FocusProxy" in wmclass: 35 | parent_window = window.query_tree().parent 36 | if parent_window: 37 | return get_class_name(parent_window) 38 | return None 39 | return wmclass 40 | except: 41 | return None 42 | 43 | # ============================================================ # 44 | 45 | 46 | _pressed_modifier_keys = set() 47 | 48 | 49 | def update_pressed_modifier_keys(key, action): 50 | if action.is_pressed(): 51 | _pressed_modifier_keys.add(key) 52 | else: 53 | _pressed_modifier_keys.discard(key) 54 | 55 | 56 | def get_pressed_modifiers(): 57 | return {Modifier.from_key(key) for key in _pressed_modifier_keys} 58 | 59 | 60 | # ============================================================ # 61 | 62 | 63 | _pressed_keys = set() 64 | 65 | 66 | def update_pressed_keys(key, action): 67 | if action.is_pressed(): 68 | _pressed_keys.add(key) 69 | else: 70 | _pressed_keys.discard(key) 71 | 72 | 73 | # ============================================================ # 74 | # Mark 75 | # ============================================================ # 76 | 77 | _mark_set = False 78 | 79 | 80 | def with_mark(combo): 81 | if isinstance(combo, Key): 82 | combo = Combo(None, combo) 83 | 84 | def _with_mark(): 85 | return combo.with_modifier(Modifier.SHIFT) if _mark_set else combo 86 | 87 | return _with_mark 88 | 89 | 90 | def set_mark(mark_set): 91 | def _set_mark(): 92 | global _mark_set 93 | _mark_set = mark_set 94 | return _set_mark 95 | 96 | 97 | def with_or_set_mark(combo): 98 | if isinstance(combo, Key): 99 | combo = Combo(None, combo) 100 | 101 | def _with_or_set_mark(): 102 | global _mark_set 103 | _mark_set = True 104 | return combo.with_modifier(Modifier.SHIFT) 105 | 106 | return _with_or_set_mark 107 | 108 | 109 | # ============================================================ # 110 | # Utility functions for keymap 111 | # ============================================================ # 112 | 113 | 114 | def launch(command): 115 | """Launch command""" 116 | def launcher(): 117 | from subprocess import Popen 118 | Popen(command) 119 | return launcher 120 | 121 | 122 | def sleep(sec): 123 | """Sleep sec in commands""" 124 | def sleeper(): 125 | import time 126 | time.sleep(sec) 127 | return sleeper 128 | 129 | # ============================================================ # 130 | 131 | 132 | def K(exp): 133 | "Helper function to specify keymap" 134 | import re 135 | modifier_strs = [] 136 | while True: 137 | m = re.match(r"\A(LC|LCtrl|RC|RCtrl|C|Ctrl|LM|LAlt|RM|RAlt|M|Alt|LShift|RShift|Shift|LSuper|LWin|RSuper|RWin|Super|Win)-", exp) 138 | if m is None: 139 | break 140 | modifier = m.group(1) 141 | modifier_strs.append(modifier) 142 | exp = re.sub(r"\A{}-".format(modifier), "", exp) 143 | key_str = exp.upper() 144 | key = getattr(Key, key_str) 145 | return Combo(create_modifiers_from_strings(modifier_strs), key) 146 | 147 | 148 | def create_modifiers_from_strings(modifier_strs): 149 | modifiers = set() 150 | for modifier_str in modifier_strs: 151 | if modifier_str == 'LC' or modifier_str == 'LCtrl': 152 | modifiers.add(Modifier.L_CONTROL) 153 | elif modifier_str == 'RC' or modifier_str == 'RCtrl': 154 | modifiers.add(Modifier.R_CONTROL) 155 | elif modifier_str == 'C' or modifier_str == 'Ctrl': 156 | modifiers.add(Modifier.CONTROL) 157 | elif modifier_str == 'LM' or modifier_str == 'LAlt': 158 | modifiers.add(Modifier.L_ALT) 159 | elif modifier_str == 'RM' or modifier_str == 'RAlt': 160 | modifiers.add(Modifier.R_ALT) 161 | elif modifier_str == 'M' or modifier_str == 'Alt': 162 | modifiers.add(Modifier.ALT) 163 | elif modifier_str == 'LSuper' or modifier_str == 'LWin': 164 | modifiers.add(Modifier.L_SUPER) 165 | pass 166 | elif modifier_str == 'RSuper' or modifier_str == 'RWin': 167 | modifiers.add(Modifier.R_SUPER) 168 | pass 169 | elif modifier_str == 'Super' or modifier_str == 'Win': 170 | modifiers.add(Modifier.SUPER) 171 | pass 172 | elif modifier_str == 'LShift': 173 | modifiers.add(Modifier.L_SHIFT) 174 | elif modifier_str == 'RShift': 175 | modifiers.add(Modifier.R_SHIFT) 176 | elif modifier_str == 'Shift': 177 | modifiers.add(Modifier.SHIFT) 178 | return modifiers 179 | 180 | # ============================================================ 181 | # Keymap 182 | # ============================================================ 183 | 184 | 185 | _toplevel_keymaps = [] 186 | _mode_maps = None 187 | 188 | escape_next_key = {} 189 | pass_through_key = {} 190 | 191 | 192 | def define_keymap(condition, mappings, name="Anonymous keymap"): 193 | global _toplevel_keymaps 194 | 195 | # Expand not L/R-specified modifiers 196 | # Suppose a nesting is not so deep 197 | # {K("C-a"): Key.A, 198 | # K("C-b"): { 199 | # K("LC-c"): Key.B, 200 | # K("C-d"): Key.C}} 201 | # -> 202 | # {K("LC-a"): Key.A, K("RC-a"): Key.A, 203 | # K("LC-b"): { 204 | # K("LC-c"): Key.B, 205 | # K("LC-d"): Key.C, 206 | # K("RC-d"): Key.C}, 207 | # K("RC-b"): { 208 | # K("LC-c"): Key.B, 209 | # K("LC-d"): Key.C, 210 | # K("RC-d"): Key.C}} 211 | def expand(target): 212 | if isinstance(target, dict): 213 | expanded_mappings = {} 214 | keys_for_deletion = [] 215 | for k, v in target.items(): 216 | # Expand children 217 | expand(v) 218 | 219 | if isinstance(k, Combo): 220 | expanded_modifiers = [] 221 | for modifier in k.modifiers: 222 | if not modifier.is_specified(): 223 | expanded_modifiers.append([modifier.to_left(), modifier.to_right()]) 224 | else: 225 | expanded_modifiers.append([modifier]) 226 | 227 | # Create a Cartesian product of expanded modifiers 228 | expanded_modifier_lists = itertools.product(*expanded_modifiers) 229 | # Create expanded mappings 230 | for modifiers in expanded_modifier_lists: 231 | expanded_mappings[Combo(set(modifiers), k.key)] = v 232 | keys_for_deletion.append(k) 233 | 234 | # Delete original mappings whose key was expanded into expanded_mappings 235 | for key in keys_for_deletion: 236 | del target[key] 237 | # Merge expanded mappings into original mappings 238 | target.update(expanded_mappings) 239 | 240 | expand(mappings) 241 | 242 | _toplevel_keymaps.append((condition, mappings, name)) 243 | return mappings 244 | 245 | 246 | # ============================================================ 247 | # Key handler 248 | # ============================================================ 249 | 250 | # keycode translation 251 | # e.g., { Key.CAPSLOCK: Key.LEFT_CTRL } 252 | _mod_map = None 253 | _conditional_mod_map = [] 254 | 255 | # multipurpose keys 256 | # e.g, {Key.LEFT_CTRL: [Key.ESC, Key.LEFT_CTRL, Action.RELEASE]} 257 | _multipurpose_map = None 258 | _conditional_multipurpose_map = [] 259 | 260 | # last key that sent a PRESS event or a non-mod or non-multi key that sent a RELEASE 261 | # or REPEAT 262 | _last_key = None 263 | 264 | # last key time record time when execute multi press 265 | _last_key_time = time() 266 | _timeout = 1 267 | def define_timeout(seconds=1): 268 | global _timeout 269 | _timeout = seconds 270 | 271 | 272 | 273 | def define_modmap(mod_remappings): 274 | """Defines modmap (keycode translation) 275 | 276 | Example: 277 | 278 | define_modmap({ 279 | Key.CAPSLOCK: Key.LEFT_CTRL 280 | }) 281 | """ 282 | global _mod_map 283 | _mod_map = mod_remappings 284 | 285 | 286 | def define_conditional_modmap(condition, mod_remappings): 287 | """Defines conditional modmap (keycode translation) 288 | 289 | Example: 290 | 291 | define_conditional_modmap(re.compile(r'Emacs'), { 292 | Key.CAPSLOCK: Key.LEFT_CTRL 293 | }) 294 | """ 295 | if hasattr(condition, 'search'): 296 | condition = condition.search 297 | if not callable(condition): 298 | raise ValueError('condition must be a function or compiled regexp') 299 | _conditional_mod_map.append((condition, mod_remappings)) 300 | 301 | 302 | def define_multipurpose_modmap(multipurpose_remappings): 303 | """Defines multipurpose modmap (multi-key translations) 304 | 305 | Give a key two different meanings. One when pressed and released alone and 306 | one when it's held down together with another key (making it a modifier 307 | key). 308 | 309 | Example: 310 | 311 | define_multipurpose_modmap( 312 | {Key.CAPSLOCK: [Key.ESC, Key.LEFT_CTRL] 313 | }) 314 | """ 315 | global _multipurpose_map 316 | for key, value in multipurpose_remappings.items(): 317 | value.append(Action.RELEASE) 318 | _multipurpose_map = multipurpose_remappings 319 | 320 | 321 | def define_conditional_multipurpose_modmap(condition, multipurpose_remappings): 322 | """Defines conditional multipurpose modmap (multi-key translation) 323 | 324 | Example: 325 | 326 | define_conditional_multipurpose_modmap(lambda wm_class, device_name: device_name.startswith("Microsoft"), { 327 | {Key.CAPSLOCK: [Key.ESC, Key.LEFT_CTRL] 328 | }) 329 | """ 330 | if hasattr(condition, 'search'): 331 | condition = condition.search 332 | if not callable(condition): 333 | raise ValueError('condition must be a function or compiled regexp') 334 | for key, value in multipurpose_remappings.items(): 335 | value.append(Action.RELEASE) 336 | _conditional_multipurpose_map.append((condition, multipurpose_remappings)) 337 | 338 | 339 | def multipurpose_handler(multipurpose_map, key, action): 340 | 341 | def maybe_press_modifiers(multipurpose_map): 342 | """Search the multipurpose map for keys that are pressed. If found and 343 | we have not yet sent it's modifier translation we do so.""" 344 | for k, [ _, mod_key, state ] in multipurpose_map.items(): 345 | if k in _pressed_keys and mod_key not in _pressed_modifier_keys: 346 | on_key(mod_key, Action.PRESS) 347 | 348 | # we need to register the last key presses so we know if a multipurpose key 349 | # was a single press and release 350 | global _last_key 351 | global _last_key_time 352 | 353 | if key in multipurpose_map: 354 | single_key, mod_key, key_state = multipurpose_map[key] 355 | key_is_down = key in _pressed_keys 356 | mod_is_down = mod_key in _pressed_modifier_keys 357 | key_was_last_press = key == _last_key 358 | 359 | update_pressed_keys(key, action) 360 | if action == Action.RELEASE and key_is_down: 361 | # it is a single press and release 362 | if key_was_last_press and _last_key_time + _timeout > time(): 363 | maybe_press_modifiers(multipurpose_map) # maybe other multipurpose keys are down 364 | on_key(single_key, Action.PRESS) 365 | on_key(single_key, Action.RELEASE) 366 | # it is the modifier in a combo 367 | elif mod_is_down: 368 | on_key(mod_key, Action.RELEASE) 369 | elif action == Action.PRESS and not key_is_down: 370 | _last_key_time = time() 371 | # if key is not a multipurpose or mod key we want eventual modifiers down 372 | elif (key not in Modifier.get_all_keys()) and action == Action.PRESS: 373 | maybe_press_modifiers(multipurpose_map) 374 | 375 | # we want to register all key-presses 376 | if action == Action.PRESS: 377 | _last_key = key 378 | 379 | 380 | def on_event(event, device_name, quiet): 381 | key = Key(event.code) 382 | action = Action(event.value) 383 | wm_class = None 384 | # translate keycode (like xmodmap) 385 | active_mod_map = _mod_map 386 | if _conditional_mod_map: 387 | wm_class = get_active_window_wm_class() 388 | for condition, mod_map in _conditional_mod_map: 389 | params = [wm_class] 390 | if len(signature(condition).parameters) == 2: 391 | params = [wm_class, device_name] 392 | 393 | if condition(*params): 394 | active_mod_map = mod_map 395 | break 396 | if active_mod_map and key in active_mod_map: 397 | key = active_mod_map[key] 398 | 399 | active_multipurpose_map = _multipurpose_map 400 | if _conditional_multipurpose_map: 401 | wm_class = get_active_window_wm_class() 402 | for condition, mod_map in _conditional_multipurpose_map: 403 | params = [wm_class] 404 | if len(signature(condition).parameters) == 2: 405 | params = [wm_class, device_name] 406 | 407 | if condition(*params): 408 | active_multipurpose_map = mod_map 409 | break 410 | if active_multipurpose_map: 411 | multipurpose_handler(active_multipurpose_map, key, action) 412 | if key in active_multipurpose_map: 413 | return 414 | 415 | on_key(key, action, wm_class=wm_class, quiet=quiet) 416 | update_pressed_keys(key, action) 417 | 418 | 419 | def on_key(key, action, wm_class=None, quiet=False): 420 | if key in Modifier.get_all_keys(): 421 | update_pressed_modifier_keys(key, action) 422 | send_key_action(key, action) 423 | elif not action.is_pressed(): 424 | if is_pressed(key): 425 | send_key_action(key, action) 426 | else: 427 | transform_key(key, action, wm_class=wm_class, quiet=quiet) 428 | 429 | 430 | def transform_key(key, action, wm_class=None, quiet=False): 431 | global _mode_maps 432 | global _toplevel_keymaps 433 | 434 | combo = Combo(get_pressed_modifiers(), key) 435 | 436 | if _mode_maps is escape_next_key: 437 | print("Escape key: {}".format(combo)) 438 | send_key_action(key, action) 439 | _mode_maps = None 440 | return 441 | 442 | is_top_level = False 443 | if _mode_maps is None: 444 | # Decide keymap(s) 445 | is_top_level = True 446 | _mode_maps = [] 447 | if wm_class is None: 448 | wm_class = get_active_window_wm_class() 449 | keymap_names = [] 450 | for condition, mappings, name in _toplevel_keymaps: 451 | if (callable(condition) and condition(wm_class)) \ 452 | or (hasattr(condition, "search") and condition.search(wm_class)) \ 453 | or condition is None: 454 | _mode_maps.append(mappings) 455 | keymap_names.append(name) 456 | if not quiet: 457 | print("WM_CLASS '{}' | active keymaps = [{}]".format(wm_class, ", ".join(keymap_names))) 458 | 459 | if not quiet: 460 | print(combo) 461 | 462 | # _mode_maps: [global_map, local_1, local_2, ...] 463 | for mappings in _mode_maps: 464 | if combo not in mappings: 465 | continue 466 | # Found key in "mappings". Execute commands defined for the key. 467 | reset_mode = handle_commands(mappings[combo], key, action) 468 | if reset_mode: 469 | _mode_maps = None 470 | return 471 | 472 | # Not found in all keymaps 473 | if is_top_level: 474 | # If it's top-level, pass through keys 475 | send_key_action(key, action) 476 | 477 | _mode_maps = None 478 | 479 | 480 | def handle_commands(commands, key, action): 481 | """ 482 | returns: reset_mode (True/False) if this is True, _mode_maps will be reset 483 | """ 484 | global _mode_maps 485 | 486 | if not isinstance(commands, list): 487 | commands = [commands] 488 | 489 | # Execute commands 490 | for command in commands: 491 | if callable(command): 492 | reset_mode = handle_commands(command(), key, action) 493 | if reset_mode: 494 | return True 495 | 496 | if isinstance(command, Key): 497 | send_key(command) 498 | elif isinstance(command, Combo): 499 | send_combo(command) 500 | elif command is escape_next_key: 501 | _mode_maps = escape_next_key 502 | return False 503 | # Go to next keymap 504 | elif isinstance(command, dict): 505 | _mode_maps = [command] 506 | return False 507 | elif command is pass_through_key: 508 | send_key_action(key, action) 509 | return True 510 | # Reset keymap in ordinary flow 511 | return True 512 | -------------------------------------------------------------------------------- /xkeysnail/key.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from enum import Enum, unique, IntEnum 4 | 5 | __author__ = 'zh' 6 | 7 | 8 | class Key(IntEnum): 9 | RESERVED = 0 10 | ESC = 1 11 | KEY_1 = 2 12 | KEY_2 = 3 13 | KEY_3 = 4 14 | KEY_4 = 5 15 | KEY_5 = 6 16 | KEY_6 = 7 17 | KEY_7 = 8 18 | KEY_8 = 9 19 | KEY_9 = 10 20 | KEY_0 = 11 21 | MINUS = 12 22 | EQUAL = 13 23 | BACKSPACE = 14 24 | TAB = 15 25 | Q = 16 26 | W = 17 27 | E = 18 28 | R = 19 29 | T = 20 30 | Y = 21 31 | U = 22 32 | I = 23 33 | O = 24 34 | P = 25 35 | LEFT_BRACE = 26 36 | RIGHT_BRACE = 27 37 | ENTER = 28 38 | LEFT_CTRL = 29 39 | A = 30 40 | S = 31 41 | D = 32 42 | F = 33 43 | G = 34 44 | H = 35 45 | J = 36 46 | K = 37 47 | L = 38 48 | SEMICOLON = 39 49 | APOSTROPHE = 40 50 | GRAVE = 41 51 | LEFT_SHIFT = 42 52 | BACKSLASH = 43 53 | Z = 44 54 | X = 45 55 | C = 46 56 | V = 47 57 | B = 48 58 | N = 49 59 | M = 50 60 | COMMA = 51 61 | DOT = 52 62 | SLASH = 53 63 | RIGHT_SHIFT = 54 64 | KPASTERISK = 55 65 | LEFT_ALT = 56 66 | SPACE = 57 67 | CAPSLOCK = 58 68 | F1 = 59 69 | F2 = 60 70 | F3 = 61 71 | F4 = 62 72 | F5 = 63 73 | F6 = 64 74 | F7 = 65 75 | F8 = 66 76 | F9 = 67 77 | F10 = 68 78 | NUMLOCK = 69 79 | SCROLLLOCK = 70 80 | KP7 = 71 81 | KP8 = 72 82 | KP9 = 73 83 | KPMINUS = 74 84 | KP4 = 75 85 | KP5 = 76 86 | KP6 = 77 87 | KPPLUS = 78 88 | KP1 = 79 89 | KP2 = 80 90 | KP3 = 81 91 | KP0 = 82 92 | KPDOT = 83 93 | 94 | ZENKAKUHANKAKU = 85 95 | KEY_102ND = 86 96 | F11 = 87 97 | F12 = 88 98 | RO = 89 99 | KATAKANA = 90 100 | HIRAGANA = 91 101 | HENKAN = 92 102 | KATAKANAHIRAGANA = 93 103 | MUHENKAN = 94 104 | KPJPCOMMA = 95 105 | KPENTER = 96 106 | RIGHT_CTRL = 97 107 | KPSLASH = 98 108 | SYSRQ = 99 109 | RIGHT_ALT = 100 110 | LINEFEED = 101 111 | HOME = 102 112 | UP = 103 113 | PAGE_UP = 104 114 | LEFT = 105 115 | RIGHT = 106 116 | END = 107 117 | DOWN = 108 118 | PAGE_DOWN = 109 119 | INSERT = 110 120 | DELETE = 111 121 | MACRO = 112 122 | MUTE = 113 123 | VOLUMEDOWN = 114 124 | VOLUMEUP = 115 125 | POWER = 116 126 | KPEQUAL = 117 127 | KPPLUSMINUS = 118 128 | PAUSE = 119 129 | SCALE = 120 130 | 131 | KPCOMMA = 121 132 | HANGEUL = 122 133 | HANGUEL = HANGEUL 134 | HANJA = 123 135 | YEN = 124 136 | LEFT_META = 125 137 | RIGHT_META = 126 138 | COMPOSE = 127 139 | 140 | STOP = 128 141 | AGAIN = 129 142 | PROPS = 130 143 | UNDO = 131 144 | FRONT = 132 145 | COPY = 133 146 | OPEN = 134 147 | PASTE = 135 148 | FIND = 136 149 | CUT = 137 150 | HELP = 138 151 | MENU = 139 152 | CALC = 140 153 | SETUP = 141 154 | SLEEP = 142 155 | WAKEUP = 143 156 | FILE = 144 157 | SENDFILE = 145 158 | DELETEFILE = 146 159 | XFER = 147 160 | PROG1 = 148 161 | PROG2 = 149 162 | WWW = 150 163 | MSDOS = 151 164 | COFFEE = 152 165 | SCREENLOCK = COFFEE 166 | DIRECTION = 153 167 | CYCLEWINDOWS = 154 168 | MAIL = 155 169 | BOOKMARKS = 156 170 | COMPUTER = 157 171 | BACK = 158 172 | FORWARD = 159 173 | CLOSECD = 160 174 | EJECTCD = 161 175 | EJECTCLOSECD = 162 176 | NEXTSONG = 163 177 | PLAYPAUSE = 164 178 | PREVIOUSSONG = 165 179 | STOPCD = 166 180 | RECORD = 167 181 | REWIND = 168 182 | PHONE = 169 183 | ISO = 170 184 | CONFIG = 171 185 | HOMEPAGE = 172 186 | REFRESH = 173 187 | EXIT = 174 188 | MOVE = 175 189 | EDIT = 176 190 | SCROLLUP = 177 191 | SCROLLDOWN = 178 192 | KPLEFTPAREN = 179 193 | KPRIGHTPAREN = 180 194 | NEW = 181 195 | REDO = 182 196 | 197 | F13 = 183 198 | F14 = 184 199 | F15 = 185 200 | F16 = 186 201 | F17 = 187 202 | F18 = 188 203 | F19 = 189 204 | F20 = 190 205 | F21 = 191 206 | F22 = 192 207 | F23 = 193 208 | F24 = 194 209 | 210 | PLAYCD = 200 211 | PAUSECD = 201 212 | PROG3 = 202 213 | PROG4 = 203 214 | DASHBOARD = 204 215 | SUSPEND = 205 216 | CLOSE = 206 217 | PLAY = 207 218 | FASTFORWARD = 208 219 | BASSBOOST = 209 220 | PRINT = 210 221 | HP = 211 222 | CAMERA = 212 223 | SOUND = 213 224 | QUESTION = 214 225 | EMAIL = 215 226 | CHAT = 216 227 | SEARCH = 217 228 | CONNECT = 218 229 | FINANCE = 219 230 | SPORT = 220 231 | SHOP = 221 232 | ALTERASE = 222 233 | CANCEL = 223 234 | BRIGHTNESSDOWN = 224 235 | BRIGHTNESSUP = 225 236 | MEDIA = 226 237 | 238 | SWITCHVIDEOMODE = 227 239 | KBDILLUMTOGGLE = 228 240 | KBDILLUMDOWN = 229 241 | KBDILLUMUP = 230 242 | 243 | SEND = 231 244 | REPLY = 232 245 | FORWARDMAIL = 233 246 | SAVE = 234 247 | DOCUMENTS = 235 248 | 249 | BATTERY = 236 250 | 251 | BLUETOOTH = 237 252 | WLAN = 238 253 | UWB = 239 254 | 255 | UNKNOWN = 240 256 | 257 | VIDEO_NEXT = 241 258 | VIDEO_PREV = 242 259 | BRIGHTNESS_CYCLE = 243 260 | BRIGHTNESS_AUTO = 244 261 | BRIGHTNESS_ZERO = BRIGHTNESS_AUTO 262 | DISPLAY_OFF = 245 263 | 264 | WWAN = 246 265 | WIMAX = WWAN 266 | RFKILL = 247 267 | 268 | MICMUTE = 248 269 | 270 | BTN_MISC = 0x100 271 | BTN_0 = 0x100 272 | BTN_1 = 0x101 273 | BTN_2 = 0x102 274 | BTN_3 = 0x103 275 | BTN_4 = 0x104 276 | BTN_5 = 0x105 277 | BTN_6 = 0x106 278 | BTN_7 = 0x107 279 | BTN_8 = 0x108 280 | BTN_9 = 0x109 281 | 282 | BTN_MOUSE = 0x110 283 | BTN_LEFT = 0x110 284 | BTN_RIGHT = 0x111 285 | BTN_MIDDLE = 0x112 286 | BTN_SIDE = 0x113 287 | BTN_EXTRA = 0x114 288 | BTN_FORWARD = 0x115 289 | BTN_BACK = 0x116 290 | BTN_TASK = 0x117 291 | 292 | BTN_JOYSTICK = 0x120 293 | BTN_TRIGGER = 0x120 294 | BTN_THUMB = 0x121 295 | BTN_THUMB2 = 0x122 296 | BTN_TOP = 0x123 297 | BTN_TOP2 = 0x124 298 | BTN_PINKIE = 0x125 299 | BTN_BASE = 0x126 300 | BTN_BASE2 = 0x127 301 | BTN_BASE3 = 0x128 302 | BTN_BASE4 = 0x129 303 | BTN_BASE5 = 0x12a 304 | BTN_BASE6 = 0x12b 305 | BTN_DEAD = 0x12f 306 | 307 | BTN_GAMEPAD = 0x130 308 | BTN_SOUTH = 0x130 309 | BTN_A = BTN_SOUTH 310 | BTN_EAST = 0x131 311 | BTN_B = BTN_EAST 312 | BTN_C = 0x132 313 | BTN_NORTH = 0x133 314 | BTN_X = BTN_NORTH 315 | BTN_WEST = 0x134 316 | BTN_Y = BTN_WEST 317 | BTN_Z = 0x135 318 | BTN_TL = 0x136 319 | BTN_TR = 0x137 320 | BTN_TL2 = 0x138 321 | BTN_TR2 = 0x139 322 | BTN_SELECT = 0x13a 323 | BTN_START = 0x13b 324 | BTN_MODE = 0x13c 325 | BTN_THUMBL = 0x13d 326 | BTN_THUMBR = 0x13e 327 | 328 | BTN_DIGI = 0x140 329 | BTN_TOOL_PEN = 0x140 330 | BTN_TOOL_RUBBER = 0x141 331 | BTN_TOOL_BRUSH = 0x142 332 | BTN_TOOL_PENCIL = 0x143 333 | BTN_TOOL_AIRBRUSH = 0x144 334 | BTN_TOOL_FINGER = 0x145 335 | BTN_TOOL_MOUSE = 0x146 336 | BTN_TOOL_LENS = 0x147 337 | BTN_TOOL_QUINTTAP = 0x148 # Five fingers on trackpad 338 | BTN_STYLUS3 = 0x149 339 | BTN_TOUCH = 0x14a 340 | BTN_STYLUS = 0x14b 341 | BTN_STYLUS2 = 0x14c 342 | BTN_TOOL_DOUBLETAP = 0x14d 343 | BTN_TOOL_TRIPLETAP = 0x14e 344 | BTN_TOOL_QUADTAP = 0x14f #Four fingers on trackpad 345 | 346 | BTN_WHEEL = 0x150 347 | BTN_GEAR_DOWN = 0x150 348 | BTN_GEAR_UP = 0x151 349 | 350 | KEY_OK = 0x160 351 | KEY_SELECT = 0x161 352 | KEY_GOTO = 0x162 353 | KEY_CLEAR = 0x163 354 | KEY_POWER2 = 0x164 355 | KEY_OPTION = 0x165 356 | KEY_INFO = 0x166 # AL OEM Features/Tips/Tutorial 357 | KEY_TIME = 0x167 358 | KEY_VENDOR = 0x168 359 | KEY_ARCHIVE = 0x169 360 | KEY_PROGRAM = 0x16a # Media Select Program Guide 361 | KEY_CHANNEL = 0x16b 362 | KEY_FAVORITES = 0x16c 363 | KEY_EPG = 0x16d 364 | KEY_PVR = 0x16e # Media Select Home 365 | KEY_MHP = 0x16f 366 | KEY_LANGUAGE = 0x170 367 | KEY_TITLE = 0x171 368 | KEY_SUBTITLE = 0x172 369 | KEY_ANGLE = 0x173 370 | KEY_ZOOM = 0x174 371 | KEY_MODE = 0x175 372 | KEY_KEYBOARD = 0x176 373 | KEY_SCREEN = 0x177 374 | KEY_PC = 0x178 # Media Select Computer 375 | KEY_TV = 0x179 # Media Select TV 376 | KEY_TV2 = 0x17a # Media Select Cable 377 | KEY_VCR = 0x17b # Media Select VCR 378 | KEY_VCR2 = 0x17c # VCR Plus 379 | KEY_SAT = 0x17d # Media Select Satellite 380 | KEY_SAT2 = 0x17e 381 | KEY_CD = 0x17f # Media Select CD */ 382 | KEY_TAPE = 0x180 # Media Select Tape */ 383 | KEY_RADIO = 0x181 384 | KEY_TUNER = 0x182 # Media Select Tuner */ 385 | KEY_PLAYER = 0x183 386 | KEY_TEXT = 0x184 387 | KEY_DVD = 0x185 # Media Select DVD */ 388 | KEY_AUX = 0x186 389 | KEY_MP3 = 0x187 390 | KEY_AUDIO = 0x188 # AL Audio Browser */ 391 | KEY_VIDEO = 0x189 # AL Movie Browser */ 392 | KEY_DIRECTORY = 0x18a 393 | KEY_LIST = 0x18b 394 | KEY_MEMO = 0x18c # Media Select Messages */ 395 | KEY_CALENDAR = 0x18d 396 | KEY_RED = 0x18e 397 | KEY_GREEN = 0x18f 398 | KEY_YELLOW = 0x190 399 | KEY_BLUE = 0x191 400 | KEY_CHANNELUP = 0x192 # Channel Increment */ 401 | KEY_CHANNELDOWN = 0x193 # Channel Decrement */ 402 | KEY_FIRST = 0x194 403 | KEY_LAST = 0x195 # Recall Last */ 404 | KEY_AB = 0x196 405 | KEY_NEXT = 0x197 406 | KEY_RESTART = 0x198 407 | KEY_SLOW = 0x199 408 | KEY_SHUFFLE = 0x19a 409 | KEY_BREAK = 0x19b 410 | KEY_PREVIOUS = 0x19c 411 | KEY_DIGITS = 0x19d 412 | KEY_TEEN = 0x19e 413 | KEY_TWEN = 0x19f 414 | KEY_VIDEOPHONE = 0x1a0 # Media Select Video Phone */ 415 | KEY_GAMES = 0x1a1 # Media Select Games */ 416 | KEY_ZOOMIN = 0x1a2 # AC Zoom In */ 417 | KEY_ZOOMOUT = 0x1a3 # AC Zoom Out */ 418 | KEY_ZOOMRESET = 0x1a4 # AC Zoom */ 419 | KEY_WORDPROCESSOR = 0x1a5 # AL Word Processor */ 420 | KEY_EDITOR = 0x1a6 # AL Text Editor */ 421 | KEY_SPREADSHEET = 0x1a7 # AL Spreadsheet */ 422 | KEY_GRAPHICSEDITOR = 0x1a8 # AL Graphics Editor */ 423 | KEY_PRESENTATION = 0x1a9 # AL Presentation App */ 424 | KEY_DATABASE = 0x1aa # AL Database App */ 425 | KEY_NEWS = 0x1ab # AL Newsreader */ 426 | KEY_VOICEMAIL = 0x1ac # AL Voicemail */ 427 | KEY_ADDRESSBOOK = 0x1ad # AL Contacts/Address Book */ 428 | KEY_MESSENGER = 0x1ae # AL Instant Messaging */ 429 | KEY_DISPLAYTOGGLE = 0x1af # Turn display (LCD) on and off */ 430 | KEY_BRIGHTNESS_TOGGLE = KEY_DISPLAYTOGGLE 431 | KEY_SPELLCHECK = 0x1b0 # AL Spell Check */ 432 | KEY_LOGOFF = 0x1b1 # AL Logoff */ 433 | 434 | KEY_DOLLAR = 0x1b2 435 | KEY_EURO = 0x1b3 436 | 437 | KEY_FRAMEBACK = 0x1b4 # Consumer - transport controls */ 438 | KEY_FRAMEFORWARD = 0x1b5 439 | KEY_CONTEXT_MENU = 0x1b6 # GenDesc - system context menu */ 440 | KEY_MEDIA_REPEAT = 0x1b7 # Consumer - transport control */ 441 | KEY_10CHANNELSUP = 0x1b8 # 10 channels up (10+) */ 442 | KEY_10CHANNELSDOWN = 0x1b9 # 10 channels down (10-) */ 443 | KEY_IMAGES = 0x1ba # AL Image Browser */ 444 | 445 | KEY_DEL_EOL = 0x1c0 446 | KEY_DEL_EOS = 0x1c1 447 | KEY_INS_LINE = 0x1c2 448 | KEY_DEL_LINE = 0x1c3 449 | 450 | KEY_FN = 0x1d0 451 | KEY_FN_ESC = 0x1d1 452 | KEY_FN_F1 = 0x1d2 453 | KEY_FN_F2 = 0x1d3 454 | KEY_FN_F3 = 0x1d4 455 | KEY_FN_F4 = 0x1d5 456 | KEY_FN_F5 = 0x1d6 457 | KEY_FN_F6 = 0x1d7 458 | KEY_FN_F7 = 0x1d8 459 | KEY_FN_F8 = 0x1d9 460 | KEY_FN_F9 = 0x1da 461 | KEY_FN_F10 = 0x1db 462 | KEY_FN_F11 = 0x1dc 463 | KEY_FN_F12 = 0x1dd 464 | KEY_FN_1 = 0x1de 465 | KEY_FN_2 = 0x1df 466 | KEY_FN_D = 0x1e0 467 | KEY_FN_E = 0x1e1 468 | KEY_FN_F = 0x1e2 469 | KEY_FN_S = 0x1e3 470 | KEY_FN_B = 0x1e4 471 | 472 | KEY_BRL_DOT1 = 0x1f1 473 | KEY_BRL_DOT2 = 0x1f2 474 | KEY_BRL_DOT3 = 0x1f3 475 | KEY_BRL_DOT4 = 0x1f4 476 | KEY_BRL_DOT5 = 0x1f5 477 | KEY_BRL_DOT6 = 0x1f6 478 | KEY_BRL_DOT7 = 0x1f7 479 | KEY_BRL_DOT8 = 0x1f8 480 | KEY_BRL_DOT9 = 0x1f9 481 | KEY_BRL_DOT10 = 0x1fa 482 | 483 | KEY_NUMERIC_0 = 0x200 # used by phones, remote controls, */ 484 | KEY_NUMERIC_1 = 0x201 # and other keypads */ 485 | KEY_NUMERIC_2 = 0x202 486 | KEY_NUMERIC_3 = 0x203 487 | KEY_NUMERIC_4 = 0x204 488 | KEY_NUMERIC_5 = 0x205 489 | KEY_NUMERIC_6 = 0x206 490 | KEY_NUMERIC_7 = 0x207 491 | KEY_NUMERIC_8 = 0x208 492 | KEY_NUMERIC_9 = 0x209 493 | KEY_NUMERIC_STAR = 0x20a 494 | KEY_NUMERIC_POUND = 0x20b 495 | KEY_NUMERIC_A = 0x20c # Phone key A - HUT Telephony = 0xb9 */ 496 | KEY_NUMERIC_B = 0x20d 497 | KEY_NUMERIC_C = 0x20e 498 | KEY_NUMERIC_D = 0x20f 499 | 500 | KEY_CAMERA_FOCUS = 0x210 501 | KEY_WPS_BUTTON = 0x211 # WiFi Protected Setup key */ 502 | 503 | KEY_TOUCHPAD_TOGGLE = 0x212 # Request switch touchpad on or off */ 504 | KEY_TOUCHPAD_ON = 0x213 505 | KEY_TOUCHPAD_OFF = 0x214 506 | 507 | KEY_CAMERA_ZOOMIN = 0x215 508 | KEY_CAMERA_ZOOMOUT = 0x216 509 | KEY_CAMERA_UP = 0x217 510 | KEY_CAMERA_DOWN = 0x218 511 | KEY_CAMERA_LEFT = 0x219 512 | KEY_CAMERA_RIGHT = 0x21a 513 | 514 | KEY_ATTENDANT_ON = 0x21b 515 | KEY_ATTENDANT_OFF = 0x21c 516 | KEY_ATTENDANT_TOGGLE = 0x21d # Attendant call on or off */ 517 | KEY_LIGHTS_TOGGLE = 0x21e # Reading light on or off */ 518 | 519 | BTN_DPAD_UP = 0x220 520 | BTN_DPAD_DOWN = 0x221 521 | BTN_DPAD_LEFT = 0x222 522 | BTN_DPAD_RIGHT = 0x223 523 | 524 | KEY_ALS_TOGGLE = 0x230 # Ambient light sensor */ 525 | 526 | KEY_BUTTONCONFIG = 0x240 # AL Button Configuration */ 527 | KEY_TASKMANAGER = 0x241 # AL Task/Project Manager */ 528 | KEY_JOURNAL = 0x242 # AL Log/Journal/Timecard */ 529 | KEY_CONTROLPANEL = 0x243 # AL Control Panel */ 530 | KEY_APPSELECT = 0x244 # AL Select Task/Application */ 531 | KEY_SCREENSAVER = 0x245 # AL Screen Saver */ 532 | KEY_VOICECOMMAND = 0x246 # Listening Voice Command */ 533 | KEY_ASSISTANT = 0x247 # AL Context-aware desktop assistant */ 534 | 535 | KEY_BRIGHTNESS_MIN = 0x250 # Set Brightness to Minimum */ 536 | KEY_BRIGHTNESS_MAX = 0x251 # Set Brightness to Maximum */ 537 | 538 | KEY_KBDINPUTASSIST_PREV = 0x260 539 | KEY_KBDINPUTASSIST_NEXT = 0x261 540 | KEY_KBDINPUTASSIST_PREVGROUP = 0x262 541 | KEY_KBDINPUTASSIST_NEXTGROUP = 0x263 542 | KEY_KBDINPUTASSIST_ACCEPT = 0x264 543 | KEY_KBDINPUTASSIST_CANCEL = 0x265 544 | 545 | KEY_RIGHT_UP = 0x266 546 | KEY_RIGHT_DOWN = 0x267 547 | KEY_LEFT_UP = 0x268 548 | KEY_LEFT_DOWN = 0x269 549 | 550 | KEY_ROOT_MENU = 0x26a 551 | KEY_MEDIA_TOP_MENU = 0x26b 552 | KEY_NUMERIC_11 = 0x26c 553 | KEY_NUMERIC_12 = 0x26d 554 | 555 | KEY_AUDIO_DESC = 0x26e 556 | KEY_3D_MODE = 0x26f 557 | KEY_NEXT_FAVORITE = 0x270 558 | KEY_STOP_RECORD = 0x271 559 | KEY_PAUSE_RECORD = 0x272 560 | KEY_VOD = 0x273 # Video on Demand */ 561 | KEY_UNMUTE = 0x274 562 | KEY_FASTREVERSE = 0x275 563 | KEY_SLOWREVERSE = 0x276 564 | 565 | KEY_DATA = 0x277 566 | KEY_ONSCREEN_KEYBOARD = 0x278 567 | 568 | BTN_TRIGGER_HAPPY = 0x2c0 569 | BTN_TRIGGER_HAPPY1 = 0x2c0 570 | BTN_TRIGGER_HAPPY2 = 0x2c1 571 | BTN_TRIGGER_HAPPY3 = 0x2c2 572 | BTN_TRIGGER_HAPPY4 = 0x2c3 573 | BTN_TRIGGER_HAPPY5 = 0x2c4 574 | BTN_TRIGGER_HAPPY6 = 0x2c5 575 | BTN_TRIGGER_HAPPY7 = 0x2c6 576 | BTN_TRIGGER_HAPPY8 = 0x2c7 577 | BTN_TRIGGER_HAPPY9 = 0x2c8 578 | BTN_TRIGGER_HAPPY10 = 0x2c9 579 | BTN_TRIGGER_HAPPY11 = 0x2ca 580 | BTN_TRIGGER_HAPPY12 = 0x2cb 581 | BTN_TRIGGER_HAPPY13 = 0x2cc 582 | BTN_TRIGGER_HAPPY14 = 0x2cd 583 | BTN_TRIGGER_HAPPY15 = 0x2ce 584 | BTN_TRIGGER_HAPPY16 = 0x2cf 585 | BTN_TRIGGER_HAPPY17 = 0x2d0 586 | BTN_TRIGGER_HAPPY18 = 0x2d1 587 | BTN_TRIGGER_HAPPY19 = 0x2d2 588 | BTN_TRIGGER_HAPPY20 = 0x2d3 589 | BTN_TRIGGER_HAPPY21 = 0x2d4 590 | BTN_TRIGGER_HAPPY22 = 0x2d5 591 | BTN_TRIGGER_HAPPY23 = 0x2d6 592 | BTN_TRIGGER_HAPPY24 = 0x2d7 593 | BTN_TRIGGER_HAPPY25 = 0x2d8 594 | BTN_TRIGGER_HAPPY26 = 0x2d9 595 | BTN_TRIGGER_HAPPY27 = 0x2da 596 | BTN_TRIGGER_HAPPY28 = 0x2db 597 | BTN_TRIGGER_HAPPY29 = 0x2dc 598 | BTN_TRIGGER_HAPPY30 = 0x2dd 599 | BTN_TRIGGER_HAPPY31 = 0x2de 600 | BTN_TRIGGER_HAPPY32 = 0x2df 601 | BTN_TRIGGER_HAPPY33 = 0x2e0 602 | BTN_TRIGGER_HAPPY34 = 0x2e1 603 | BTN_TRIGGER_HAPPY35 = 0x2e2 604 | BTN_TRIGGER_HAPPY36 = 0x2e3 605 | BTN_TRIGGER_HAPPY37 = 0x2e4 606 | BTN_TRIGGER_HAPPY38 = 0x2e5 607 | BTN_TRIGGER_HAPPY39 = 0x2e6 608 | BTN_TRIGGER_HAPPY40 = 0x2e7 609 | 610 | # We avoid low common keys in module aliases so they don't get huge. */ 611 | KEY_MIN_INTERESTING = MUTE 612 | KEY_MAX = 0x2ff 613 | KEY_CNT = (KEY_MAX+1) 614 | 615 | REL_X = 0x00 616 | REL_Y = 0x01 617 | REL_Z = 0x02 618 | REL_RX = 0x03 619 | REL_RY = 0x04 620 | REL_RZ = 0x05 621 | REL_HWHEEL = 0x06 622 | REL_DIAL = 0x07 623 | REL_WHEEL = 0x08 624 | REL_MISC = 0x09 625 | REL_MAX = 0x0f 626 | REL_CNT = (REL_MAX+1) 627 | 628 | ABS_X = 0x00 629 | ABS_Y = 0x01 630 | ABS_Z = 0x02 631 | ABS_RX = 0x03 632 | ABS_RY = 0x04 633 | ABS_RZ = 0x05 634 | ABS_THROTTLE = 0x06 635 | ABS_RUDDER = 0x07 636 | ABS_WHEEL = 0x08 637 | ABS_GAS = 0x09 638 | ABS_BRAKE = 0x0a 639 | ABS_HAT0X = 0x10 640 | ABS_HAT0Y = 0x11 641 | ABS_HAT1X = 0x12 642 | ABS_HAT1Y = 0x13 643 | ABS_HAT2X = 0x14 644 | ABS_HAT2Y = 0x15 645 | ABS_HAT3X = 0x16 646 | ABS_HAT3Y = 0x17 647 | ABS_PRESSURE = 0x18 648 | ABS_DISTANCE = 0x19 649 | ABS_TILT_X = 0x1a 650 | ABS_TILT_Y = 0x1b 651 | ABS_TOOL_WIDTH = 0x1c 652 | 653 | ABS_VOLUME = 0x20 654 | ABS_MISC = 0x28 655 | 656 | ABS_MT_SLOT = 0x2f # MT slot being modified */ 657 | ABS_MT_TOUCH_MAJOR = 0x30 # Major axis of touching ellipse */ 658 | ABS_MT_TOUCH_MINOR = 0x31 # Minor axis (omit if circular) */ 659 | ABS_MT_WIDTH_MAJOR = 0x32 # Major axis of approaching ellipse */ 660 | ABS_MT_WIDTH_MINOR = 0x33 # Minor axis (omit if circular) */ 661 | ABS_MT_ORIENTATION = 0x34 # Ellipse orientation */ 662 | ABS_MT_POSITION_X = 0x35 # Center X touch position */ 663 | ABS_MT_POSITION_Y = 0x36 # Center Y touch position */ 664 | ABS_MT_TOOL_TYPE = 0x37 # Type of touching device */ 665 | ABS_MT_BLOB_ID = 0x38 # Group a set of packets as a blob */ 666 | ABS_MT_TRACKING_ID = 0x39 # Unique ID of initiated contact */ 667 | ABS_MT_PRESSURE = 0x3a # Pressure on contact area */ 668 | ABS_MT_DISTANCE = 0x3b # Contact hover distance */ 669 | ABS_MT_TOOL_X = 0x3c # Center X tool position */ 670 | ABS_MT_TOOL_Y = 0x3d # Center Y tool position */ 671 | 672 | ABS_MAX = 0x3f 673 | ABS_CNT = (ABS_MAX+1) 674 | 675 | SW_LID = 0x00 # set = lid shut */ 676 | SW_TABLET_MODE = 0x01 # set = tablet mode */ 677 | SW_HEADPHONE_INSERT = 0x02 # set = inserted */ 678 | SW_RFKILL_ALL = 0x03 # rfkill master switch, type "any" set = radio enabled */ 679 | SW_RADIO = SW_RFKILL_ALL # deprecated */ 680 | SW_MICROPHONE_INSERT = 0x04 # set = inserted */ 681 | SW_DOCK = 0x05 # set = plugged into dock */ 682 | SW_LINEOUT_INSERT = 0x06 # set = inserted */ 683 | SW_JACK_PHYSICAL_INSERT = 0x07 # set = mechanical switch set */ 684 | SW_VIDEOOUT_INSERT = 0x08 # set = inserted */ 685 | SW_CAMERA_LENS_COVER = 0x09 # set = lens covered */ 686 | SW_KEYPAD_SLIDE = 0x0a # set = keypad slide out */ 687 | SW_FRONT_PROXIMITY = 0x0b # set = front proximity sensor active */ 688 | SW_ROTATE_LOCK = 0x0c # set = rotate locked/disabled */ 689 | SW_LINEIN_INSERT = 0x0d # set = inserted */ 690 | SW_MUTE_DEVICE = 0x0e # set = device disabled */ 691 | SW_PEN_INSERTED = 0x0f # set = pen inserted */ 692 | SW_MAX = 0x0f 693 | SW_CNT = (SW_MAX+1) 694 | 695 | MSC_SERIAL = 0x00 696 | MSC_PULSELED = 0x01 697 | MSC_GESTURE = 0x02 698 | MSC_RAW = 0x03 699 | MSC_SCAN = 0x04 700 | MSC_TIMESTAMP = 0x05 701 | MSC_MAX = 0x07 702 | MSC_CNT = (MSC_MAX+1) 703 | 704 | LED_NUML = 0x00 705 | LED_CAPSL = 0x01 706 | LED_SCROLLL = 0x02 707 | LED_COMPOSE = 0x03 708 | LED_KANA = 0x04 709 | LED_SLEEP = 0x05 710 | LED_SUSPEND = 0x06 711 | LED_MUTE = 0x07 712 | LED_MISC = 0x08 713 | LED_MAIL = 0x09 714 | LED_CHARGING = 0x0a 715 | LED_MAX = 0x0f 716 | LED_CNT = (LED_MAX+1) 717 | 718 | REP_DELAY = 0x00 719 | REP_PERIOD = 0x01 720 | REP_MAX = 0x01 721 | REP_CNT = (REP_MAX+1) 722 | 723 | SND_CLICK = 0x00 724 | SND_BELL = 0x01 725 | SND_TONE = 0x02 726 | SND_MAX = 0x07 727 | SND_CNT = (SND_MAX+1) 728 | 729 | 730 | @unique 731 | class Action(IntEnum): 732 | 733 | RELEASE, PRESS, REPEAT = range(3) 734 | 735 | def is_pressed(self): 736 | return self == Action.PRESS or self == Action.REPEAT 737 | 738 | 739 | @unique 740 | class Modifier(Enum): 741 | 742 | L_CONTROL, R_CONTROL, CONTROL, \ 743 | L_ALT, R_ALT, ALT, \ 744 | L_SHIFT, R_SHIFT, SHIFT, \ 745 | L_SUPER, R_SUPER, SUPER = range(12) 746 | 747 | @classmethod 748 | def _get_modifier_map(cls): 749 | return { 750 | cls.L_CONTROL: {Key.LEFT_CTRL}, 751 | cls.R_CONTROL: {Key.RIGHT_CTRL}, 752 | cls.CONTROL: {Key.LEFT_CTRL, Key.RIGHT_CTRL}, 753 | cls.L_ALT: {Key.LEFT_ALT}, 754 | cls.R_ALT: {Key.RIGHT_ALT}, 755 | cls.ALT: {Key.LEFT_ALT, Key.RIGHT_ALT}, 756 | cls.L_SHIFT: {Key.LEFT_SHIFT}, 757 | cls.R_SHIFT: {Key.RIGHT_SHIFT}, 758 | cls.SHIFT: {Key.LEFT_SHIFT, Key.RIGHT_SHIFT}, 759 | cls.L_SUPER: {Key.LEFT_META}, 760 | cls.R_SUPER: {Key.RIGHT_META}, 761 | cls.SUPER: {Key.LEFT_META, Key.RIGHT_META} 762 | } 763 | 764 | def __str__(self): 765 | if self.value == self.L_CONTROL.value: return "LC" 766 | if self.value == self.R_CONTROL.value: return "RC" 767 | if self.value == self.CONTROL.value: return "C" 768 | if self.value == self.L_ALT.value: return "LM" 769 | if self.value == self.R_ALT.value: return "RM" 770 | if self.value == self.ALT.value: return "M" 771 | if self.value == self.L_SHIFT.value: return "LShift" 772 | if self.value == self.R_SHIFT.value: return "RShift" 773 | if self.value == self.SHIFT.value: return "Shift" 774 | if self.value == self.L_SUPER.value: return "LSuper" 775 | if self.value == self.R_SUPER.value: return "RSuper" 776 | if self.value == self.SUPER.value: return "Super" 777 | return None 778 | 779 | def is_specified(self): 780 | return self.value == self.L_CONTROL.value or \ 781 | self.value == self.R_CONTROL.value or \ 782 | self.value == self.L_ALT.value or \ 783 | self.value == self.R_ALT.value or \ 784 | self.value == self.L_SHIFT.value or \ 785 | self.value == self.R_SHIFT.value or \ 786 | self.value == self.L_SUPER.value or \ 787 | self.value == self.R_SUPER.value 788 | 789 | def to_left(self): 790 | if self.value == self.CONTROL.value: 791 | return self.L_CONTROL 792 | elif self.value == self.ALT.value: 793 | return self.L_ALT 794 | elif self.value == self.SHIFT.value: 795 | return self.L_SHIFT 796 | elif self.value == self.SUPER.value: 797 | return self.L_SUPER 798 | 799 | def to_right(self): 800 | if self.value == self.CONTROL.value: 801 | return self.R_CONTROL 802 | elif self.value == self.ALT.value: 803 | return self.R_ALT 804 | elif self.value == self.SHIFT.value: 805 | return self.R_SHIFT 806 | elif self.value == self.SUPER.value: 807 | return self.R_SUPER 808 | 809 | def get_keys(self): 810 | return self._get_modifier_map()[self] 811 | 812 | def get_key(self): 813 | return next(iter(self.get_keys())) 814 | 815 | @classmethod 816 | def get_all_keys(cls): 817 | return {key for keys in cls._get_modifier_map().values() for key in keys} 818 | 819 | @staticmethod 820 | def from_key(key): 821 | for modifier in Modifier: 822 | if key in modifier.get_keys(): 823 | return modifier 824 | 825 | 826 | class Combo: 827 | 828 | def __init__(self, modifiers, key): 829 | 830 | if isinstance(modifiers, list): 831 | raise ValueError("modifiers should be a set instead of a list") 832 | elif modifiers is None: 833 | modifiers = set() 834 | elif isinstance(modifiers, Modifier): 835 | modifiers = {modifiers} 836 | elif not isinstance(modifiers, set): 837 | raise ValueError("modifiers should be a set") 838 | 839 | if not isinstance(key, Key): 840 | raise ValueError("key should be a Key") 841 | 842 | self.modifiers = modifiers 843 | self.key = key 844 | 845 | def __eq__(self, other): 846 | if isinstance(other, Combo): 847 | return self.modifiers == other.modifiers and self.key == other.key 848 | else: 849 | return NotImplemented 850 | 851 | def __hash__(self): 852 | return hash((frozenset(self.modifiers), self.key)) 853 | 854 | def __str__(self): 855 | return "-".join([str(mod) for mod in self.modifiers] + [self.key.name]) 856 | 857 | def with_modifier(self, modifiers): 858 | if isinstance(modifiers, Modifier): 859 | modifiers = {modifiers} 860 | return Combo(self.modifiers | modifiers, self.key) 861 | --------------------------------------------------------------------------------