From 27a35981725578c0a211818f9da3ff9e93486915 Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Fri, 1 Jan 2021 00:57:04 +0100 Subject: [PATCH] improved, cleaner keycode injection --- README.md | 2 +- keymapper/dev/injector.py | 17 ++++- keymapper/dev/keycode_mapper.py | 109 ++++++++++++++++++------------- keymapper/gtk/window.py | 4 +- keymapper/key.py | 1 - readme/pylint.svg | 4 +- tests/testcases/test_injector.py | 6 +- 7 files changed, 86 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 1b41e7d4..1b111434 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ sudo apt install git python3-setuptools git clone https://github.com/sezanzeb/key-mapper.git cd key-mapper && ./scripts/build.sh sudo dpkg -i ./dist/key-mapper-0.4.0.deb -sudo apt -f install +sudo apt -f install # fixes missing dependency ``` ##### pip diff --git a/keymapper/dev/injector.py b/keymapper/dev/injector.py index c96dbd13..5fa414c1 100644 --- a/keymapper/dev/injector.py +++ b/keymapper/dev/injector.py @@ -277,8 +277,8 @@ class KeycodeInjector: evdev.ecodes.REL_WHEEL, evdev.ecodes.REL_HWHEEL, ] - keys = capabilities.get(EV_KEY) - if keys is None: + + if capabilities.get(EV_KEY) is None: capabilities[EV_KEY] = [] if ecodes.BTN_MOUSE not in capabilities[EV_KEY]: @@ -335,6 +335,19 @@ class KeycodeInjector: paths = get_devices()[self.device]['paths'] + """# in order to map a combination of shift + a to xf86audiomute, + # key-mapper has to find a way around the systems xkb configs, + # because X11 won't do shift + xf86audiomute. + # This special input can write every possible EV_KEY character + # below 272 (which is mouse-left) without being bothered by + # modifiers. It has its own special xkb symbols and keycodes files + # and + self.special_uinput = evdev.UInput( + name=f'{DEV_NAME} special {self.device}', + phys=DEV_NAME, + events=self._modify_capabilities(macros, source, abs_to_rel) + )""" + # Watch over each one of the potentially multiple devices per hardware for path in paths: source, abs_to_rel = self._prepare_device(path) diff --git a/keymapper/dev/keycode_mapper.py b/keymapper/dev/keycode_mapper.py index e1824a25..5eec4514 100644 --- a/keymapper/dev/keycode_mapper.py +++ b/keymapper/dev/keycode_mapper.py @@ -87,6 +87,12 @@ def is_key_up(event): return event.value == 0 +def write(uinput, key): + """Shorthand to write stuff.""" + uinput.write(*key) + uinput.syn() + + COMBINATION_INCOMPLETE = 1 # not all keys of the combination are pressed NOT_COMBINED = 2 # this key is not part of a combination @@ -115,7 +121,7 @@ def handle_keycode(key_to_code, macros, event, uinput): # normalize event numbers to one of -1, 0, +1. Otherwise mapping # trigger values that are between 1 and 255 is not possible, because # they might skip the 1 when pressed fast enough. - # The key used to index the mappings + # The key used to index the mappings `key_to_code` and `macros` key = (event.type, event.code, sign(event.value)) # the tuple of the actual input event. Used to forward the event if it is @@ -123,69 +129,78 @@ def handle_keycode(key_to_code, macros, event, uinput): event_tuple = (event.type, event.code, sign(event.value)) type_code = (event.type, event.code) - # the finishing key has to be the last element in combination, all + # the triggering key-down has to be the last element in combination, all # others can have any arbitrary order. By checking all unreleased keys, # a + b + c takes priority over b + c, if both mappings exist. + # WARNING! the combination-down triggers, but a single key-up releases. + # Do not check if key in macros and such, if it is an up event. It's + # going to be False. combination = tuple([value[1] for value in unreleased.values()] + [key]) if combination in macros or combination in key_to_code: key = combination - existing_macro = active_macros.get(type_code) - if existing_macro is not None: - if is_key_up(event) and not existing_macro.running: - # key was released, but macro already stopped - return + """Releasing keys and macros""" - if is_key_up(event) and existing_macro.holding: + active_macro = active_macros.get(type_code) + if is_key_up(event): + if active_macro is not None and active_macro.holding: # Tell the macro for that keycode that the key is released and - # let it decide what to with that information. - existing_macro.release_key() - return + # let it decide what to do with that information. + active_macro.release_key() + logger.spam('%s, releasing macro', key) + + if type_code in unreleased: + target_type, target_code = unreleased[type_code][0] + logger.spam('%s, releasing %s', key, target_code) + del unreleased[type_code] + write(uinput, (target_type, target_code, 0)) + else: + logger.spam('%s, unexpected key up', key) + + # everything that can be released is released now + return + + """Filtering duplicate key downs""" - if is_key_down(event) and existing_macro.running: + if is_key_down(event): + # it would start a macro usually + if key in macros and active_macro is not None and active_macro.running: # for key-down events and running macros, don't do anything. # This avoids spawning a second macro while the first one is not # finished, especially since gamepad-triggers report a ton of # events with a positive value. + logger.spam('%s, macro already running', key) return - if key in macros: - macro = macros[key] - active_macros[type_code] = macro - macro.press_key() - logger.spam('got %s, maps to macro %s', key, macro.code) - asyncio.ensure_future(macro.run()) - return - - if is_key_down(event) and type_code in unreleased: - # duplicate key-down. skip this event. Avoid writing millions of - # key-down events when a continuous value is reported, for example - # for gamepad triggers - logger.spam('%s, duplicate key down', key) - return + # it would write a key usually + if key in key_to_code and type_code in unreleased: + # duplicate key-down. skip this event. Avoid writing millions of + # key-down events when a continuous value is reported, for example + # for gamepad triggers + logger.spam('%s, duplicate key down', key) + return - if is_key_up(event) and type_code in unreleased: - target_type, target_code = unreleased[type_code][0] - target_value = 0 - logger.spam('%s, releasing %s', key, target_code) - elif key in key_to_code and is_key_down(event): - target_type = EV_KEY - target_code = key_to_code[key] - target_value = 1 - logger.spam('%s, maps to %s', key, target_code) - else: - target_type = event_tuple[0] - target_code = event_tuple[1] - target_value = event_tuple[2] - logger.spam('%s, unmapped', key) + """starting new macros or injecting new keys""" if is_key_down(event): - # for a combination, the last key that was pressed is also the - # key that releases it, so type_code is used to index this. - unreleased[type_code] = ((target_type, target_code), event_tuple) + if key in macros: + macro = macros[key] + active_macros[type_code] = macro + macro.press_key() + logger.spam('%s, maps to macro %s', key, macro.code) + asyncio.ensure_future(macro.run()) + return - if is_key_up(event) and type_code in unreleased: - del unreleased[type_code] + if key in key_to_code: + target_code = key_to_code[key] + logger.spam('%s, maps to %s', key, target_code) + unreleased[type_code] = ((EV_KEY, target_code), event_tuple) + write(uinput, (EV_KEY, target_code, 1)) + return - uinput.write(target_type, target_code, target_value) - uinput.syn() + logger.spam('%s, forwarding', key) + unreleased[type_code] = ((event_tuple[:2]), event_tuple) + write(uinput, event_tuple) + return + + logger.error('%s, unhandled. %s %s', key, unreleased, active_macros) diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py index e850fccc..394cd57b 100755 --- a/keymapper/gtk/window.py +++ b/keymapper/gtk/window.py @@ -54,6 +54,9 @@ CTX_ERROR = 3 CTX_WARNING = 4 +# TODO status warning for ctrl in key combis + + def get_selected_row_bg(): """Get the background color that a row is going to have when selected.""" # ListBoxRows can be selected, but either they are always selectable @@ -353,7 +356,6 @@ class Window: # it return the leftover key, it will continue to return None because # they have already been read. key = keycode_reader.read() - key and print(key) if isinstance(focused, Gtk.ToggleButton): if not keycode_reader.are_keys_pressed(): diff --git a/keymapper/key.py b/keymapper/key.py index 32f7b85a..7475b897 100644 --- a/keymapper/key.py +++ b/keymapper/key.py @@ -98,7 +98,6 @@ class Key: return hash(self.keys) def __eq__(self, other): - print(self, 'eq', other) if isinstance(other, tuple): if isinstance(other[0], tuple): # a combination ((1, 5, 1), (1, 3, 1)) diff --git a/readme/pylint.svg b/readme/pylint.svg index 336a35f2..a31dd1ad 100644 --- a/readme/pylint.svg +++ b/readme/pylint.svg @@ -17,7 +17,7 @@ pylint - 9.71 - 9.71 + 9.67 + 9.67 \ No newline at end of file diff --git a/tests/testcases/test_injector.py b/tests/testcases/test_injector.py index 9506e360..2be1dd9f 100644 --- a/tests/testcases/test_injector.py +++ b/tests/testcases/test_injector.py @@ -24,7 +24,7 @@ import time import copy import evdev -from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X, BTN_A +from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X from keymapper.dev.injector import is_numlock_on, set_numlock, \ ensure_numlock, KeycodeInjector, is_in_capabilities @@ -191,8 +191,8 @@ class TestInjector(unittest.TestCase): self.assertIn(evdev.ecodes.REL_WHEEL, capabilities.get(EV_REL)) self.assertIn(evdev.ecodes.REL_HWHEEL, capabilities.get(EV_REL)) - self.assertIn(evdev.ecodes.EV_KEY, capabilities) - self.assertEqual(len(capabilities[evdev.ecodes.EV_KEY]), 1) + self.assertIn(EV_KEY, capabilities) + self.assertIn(evdev.ecodes.BTN_LEFT, capabilities[EV_KEY]) def test_adds_ev_key(self): # for some reason, having any EV_KEY capability is needed to