From ef6710877c3e85c027eee301470eb554b8ff9577 Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Mon, 30 Nov 2020 16:22:17 +0100 Subject: [PATCH] support for linux kernel keycode constants --- README.md | 10 +++ data/key-mapper.glade | 3 +- keymapper/dev/injector.py | 112 +++++++++++++++++++++++++-------- keymapper/dev/reader.py | 5 +- keymapper/state.py | 36 +++++++---- tests/testcases/daemon.py | 7 ++- tests/testcases/injector.py | 13 ++-- tests/testcases/integration.py | 11 ++-- tests/testcases/mapping.py | 6 +- 9 files changed, 144 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index fb47aca5..41becfd2 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,19 @@ Documentation: - `m` holds a modifier while executing the second parameter - `.` executes two actions behind each other +##### Names + For a list of supported keystrokes and their names, check the output of `xmodmap -pke` +- Alphanumeric `a` to `z` and `0` to `9` +- Modifiers `Alt_L` `Control_L` `Control_R` `Shift_L` `Shift_R` + +If you can't find what you need, consult [linux/input-event-codes.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h) + +- Mouse buttons `BTN_LEFT` `BTN_RIGHT` `BTN_MIDDLE` `BTN_SIDE` +- Macro special keys `KEY_MACRO1` `KEY_MACRO2` ... + ## Installation After your installation, independent of the method, you should add yourself diff --git a/data/key-mapper.glade b/data/key-mapper.glade index ec53d623..d0e5f141 100644 --- a/data/key-mapper.glade +++ b/data/key-mapper.glade @@ -688,7 +688,8 @@ "KP_0" - "KP_9" "Shift_L", "Shift_R" "Alt_L", "Alt_R" -"Control_R" +"Control_R" +"Mouse_1" - "Mouse_5" 5 5 Mapping diff --git a/keymapper/dev/injector.py b/keymapper/dev/injector.py index 8a54e6b4..c004303b 100644 --- a/keymapper/dev/injector.py +++ b/keymapper/dev/injector.py @@ -32,7 +32,7 @@ import evdev from keymapper.logger import logger from keymapper.getdevices import get_devices -from keymapper.state import system_mapping +from keymapper.state import system_mapping, KEYCODE_OFFSET from keymapper.dev.macros import parse @@ -41,9 +41,6 @@ DEVICE_CREATED = 1 FAILED = 2 DEVICE_SKIPPED = 3 -# offset between xkb and linux keycodes. linux keycodes are lower -KEYCODE_OFFSET = 8 - def is_numlock_on(): """Get the current state of the numlock.""" @@ -112,6 +109,10 @@ class KeycodeInjector: self.mapping = mapping self._process = None + def __del__(self): + if self._process is not None: + self._process.terminate() + def start_injecting(self): """Start injecting keycodes.""" self._process = multiprocessing.Process(target=self._start_injecting) @@ -124,13 +125,16 @@ class KeycodeInjector: if device is None: return None - capabilities = device.capabilities(absinfo=False)[evdev.ecodes.EV_KEY] + capabilities = device.capabilities(absinfo=False) needed = False for keycode, _ in self.mapping: - if keycode - KEYCODE_OFFSET in capabilities: + if keycode - KEYCODE_OFFSET in capabilities[evdev.ecodes.EV_KEY]: needed = True break + # TODO only if map ABS to REL keep ABS devics + if capabilities.get(evdev.ecodes.EV_REL) is not None: + needed = True if not needed: # skipping reading and checking on events from those devices @@ -162,6 +166,10 @@ class KeycodeInjector: return device + def map_abs_to_rel(self): + # TODO offer configuration via the UI if a gamepad is elected + return True + def _modify_capabilities(self, input_device): """Adds all keycode into a copy of a devices capabilities. @@ -169,19 +177,32 @@ class KeycodeInjector: --------- input_device : evdev.InputDevice """ + ecodes = evdev.ecodes + # copy the capabilities because the keymapper_device is going # to act like the device. capabilities = input_device.capabilities(absinfo=False) - # However, make sure that it supports all keycodes, not just some - # random ones, because the mapping could contain anything. - # That's why I avoid from_device for this - capabilities[evdev.ecodes.EV_KEY] = list(evdev.ecodes.keys.keys()) + # Furthermore, support all injected keycodes + for _, character in self.mapping: + keycode = system_mapping.get(character) + if keycode is not None: + capabilities[ecodes.EV_KEY].append(keycode - KEYCODE_OFFSET) + + if self.map_abs_to_rel(): + if capabilities.get(ecodes.EV_ABS): + del capabilities[ecodes.EV_ABS] + capabilities[ecodes.EV_REL] = [ + evdev.ecodes.REL_X, + evdev.ecodes.REL_Y, + # for my system to recognize it as mouse, WHEEL is also needed: + evdev.ecodes.REL_WHEEL, + ] # just like what python-evdev does in from_device - if evdev.ecodes.EV_SYN in capabilities: - del capabilities[evdev.ecodes.EV_SYN] - if evdev.ecodes.EV_FF in capabilities: - del capabilities[evdev.ecodes.EV_FF] + if ecodes.EV_SYN in capabilities: + del capabilities[ecodes.EV_SYN] + if ecodes.EV_FF in capabilities: + del capabilities[ecodes.EV_FF] return capabilities @@ -191,24 +212,29 @@ class KeycodeInjector: Stuff is non-blocking by using asyncio in order to do multiple things somewhat concurrently. """ + # TODO do select.select insted of async_read_loop loop = asyncio.get_event_loop() coroutines = [] - paths = get_devices()[self.device]['paths'] - logger.info('Starting injecting the mapping for %s', self.device) + paths = get_devices()[self.device]['paths'] + devices = [self._prepare_device(path) for path in paths] + # Watch over each one of the potentially multiple devices per hardware - for path in paths: - input_device = self._prepare_device(path) + for input_device in devices: if input_device is None: continue + # certain capabilities can have side effects apparently. with an + # EV_ABS capability, EV_REL won't move the mouse pointer anymore. + # so don't merge all InputDevices into one UInput device. uinput = evdev.UInput( - name=f'key-mapper {input_device.name}', + name=f'key-mapper {self.device}', phys='key-mapper', events=self._modify_capabilities(input_device) ) + coroutine = self._injection_loop(input_device, uinput) coroutines.append(coroutine) @@ -217,19 +243,24 @@ class KeycodeInjector: loop.run_until_complete(asyncio.gather(*coroutines)) - def _write(self, device, keycode, value): + def _write(self, device, type, keycode, value): """Actually inject.""" - device.write(evdev.ecodes.EV_KEY, keycode - KEYCODE_OFFSET, value) + device.write(type, keycode, value) device.syn() def _macro_write(self, character, value, keymapper_device): """Handler for macros.""" - keycode = system_mapping.get_keycode(character) + keycode = system_mapping[character] logger.spam( 'macro writes code:%s value:%d char:%s', keycode, value, character ) - self._write(keymapper_device, keycode, value) + self._write( + keymapper_device, + evdev.ecodes.EV_KEY - KEYCODE_OFFSET, + keycode, + value + ) async def _injection_loop(self, device, keymapper_device): """Inject keycodes for one of the virtual devices. @@ -241,6 +272,7 @@ class KeycodeInjector: keymapper_device : evdev.UInput where to write keycodes to """ + # TODO this function is too long # Parse all macros beforehand logger.debug('Parsing macros') macros = {} @@ -258,6 +290,29 @@ class KeycodeInjector: ) async for event in device.async_read_loop(): + if self.map_abs_to_rel() and event.type == evdev.ecodes.EV_ABS: + if event.code not in [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y]: + continue + # TODO somehow the injector has to keep injecting EV_REL + # codes to keep the mouse moving + # code 0:X, 1:Y + # TODO get absinfo beforehand + value = event.value // 2000 + if value == 0: + continue + print( + evdev.ecodes.EV_REL, + event.code, + value + ) + self._write( + keymapper_device, + evdev.ecodes.EV_REL, + event.code, + value + ) + continue + if event.type != evdev.ecodes.EV_KEY: keymapper_device.write(event.type, event.code, event.value) # this already includes SYN events, so need to syn here again @@ -289,7 +344,9 @@ class KeycodeInjector: asyncio.ensure_future(macro.run()) continue else: - target_keycode = system_mapping.get_keycode(character) + # TODO compile int-int mapping instead of going this route. + # I think that makes the reverse mapping obsolete. + target_keycode = system_mapping[character] if target_keycode is None: logger.error( 'Cannot find character %s in the internal mapping', @@ -305,7 +362,12 @@ class KeycodeInjector: character ) - self._write(keymapper_device, target_keycode, event.value) + self._write( + keymapper_device, + event.type, + target_keycode - KEYCODE_OFFSET, + event.value + ) # this should only ever happen in tests to avoid blocking them # forever, as soon as all events are consumed. In normal operation diff --git a/keymapper/dev/reader.py b/keymapper/dev/reader.py index be8dfcff..062d794c 100644 --- a/keymapper/dev/reader.py +++ b/keymapper/dev/reader.py @@ -28,10 +28,7 @@ import multiprocessing from keymapper.logger import logger from keymapper.getdevices import get_devices, refresh_devices - - -# offset between xkb and linux keycodes. linux keycodes are lower -KEYCODE_OFFSET = 8 +from keymapper.state import KEYCODE_OFFSET class _KeycodeReader: diff --git a/keymapper/state.py b/keymapper/state.py index dc851c1b..2700bc46 100644 --- a/keymapper/state.py +++ b/keymapper/state.py @@ -25,22 +25,35 @@ import stat import re import subprocess +import evdev from keymapper.mapping import Mapping -def parse_xmodmap(mapping): - """Read the output of xmodmap into a mapping.""" +# offset between xkb and linux keycodes. linux keycodes are lower +KEYCODE_OFFSET = 8 + + +def populate_system_mapping(): + """Get a mapping of all available names to their keycodes.""" + mapping = {} + xmodmap = subprocess.check_output(['xmodmap', '-pke']).decode() + '\n' mappings = re.findall(r'(\d+) = (.+)\n', xmodmap) - for keycode, characters in mappings: - # this is the "array" format needed for symbols files - character = ', '.join(characters.split()) - mapping.change( - previous_keycode=None, - new_keycode=int(keycode), - character=character - ) + for keycode, names in mappings: + for name in names.split(): + mapping[name] = int(keycode) + + for name, ecode in evdev.ecodes.ecodes.items(): + mapping[name] = ecode + KEYCODE_OFFSET + + return mapping + + +def clear_system_mapping(): + """Remove all mapped keys. Only needed for tests.""" + for key in system_mapping: + del system_mapping[key] # one mapping object for the whole application that holds all @@ -48,8 +61,7 @@ def parse_xmodmap(mapping): custom_mapping = Mapping() # this mapping represents the xmodmap output, which stays constant -system_mapping = Mapping() -parse_xmodmap(system_mapping) +system_mapping = populate_system_mapping() # permissions for files created in /usr _PERMISSIONS = stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IROTH diff --git a/tests/testcases/daemon.py b/tests/testcases/daemon.py index 4b66deeb..4361ef3b 100644 --- a/tests/testcases/daemon.py +++ b/tests/testcases/daemon.py @@ -34,7 +34,8 @@ import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk -from keymapper.state import custom_mapping, system_mapping +from keymapper.state import custom_mapping, system_mapping, \ + clear_system_mapping from keymapper.config import config from keymapper.daemon import Daemon, get_dbus_interface @@ -89,8 +90,8 @@ class TestDaemon(unittest.TestCase): keycode_to = 100 custom_mapping.change(keycode_from, 'a') - system_mapping.empty() - system_mapping.change(keycode_to, 'a') + clear_system_mapping() + system_mapping['a'] = keycode_to custom_mapping.save('device 2', 'foo') config.set_autoload_preset('device 2', 'foo') diff --git a/tests/testcases/injector.py b/tests/testcases/injector.py index 296c0a4d..17738195 100644 --- a/tests/testcases/injector.py +++ b/tests/testcases/injector.py @@ -24,8 +24,9 @@ import unittest import evdev from keymapper.dev.injector import is_numlock_on, toggle_numlock,\ - ensure_numlock, KeycodeInjector, KEYCODE_OFFSET -from keymapper.state import custom_mapping, system_mapping + ensure_numlock, KeycodeInjector +from keymapper.state import custom_mapping, system_mapping, \ + clear_system_mapping, KEYCODE_OFFSET from keymapper.mapping import Mapping from test import uinput_write_history, Event, pending_events, fixtures, \ @@ -134,13 +135,13 @@ class TestInjector(unittest.TestCase): # one mapping that is unknown in the system_mapping on purpose custom_mapping.change(10, 'b') - system_mapping.empty() + clear_system_mapping() code_a = 100 code_q = 101 code_w = 102 - system_mapping.change(code_a, 'a') - system_mapping.change(code_q, 'q') - system_mapping.change(code_w, 'w') + system_mapping['a'] = code_a + system_mapping['q'] = code_q + system_mapping['w'] = code_w # the second arg of those event objects is 8 lower than the # keycode used in X and in the mappings diff --git a/tests/testcases/integration.py b/tests/testcases/integration.py index 754c34da..09a0c218 100644 --- a/tests/testcases/integration.py +++ b/tests/testcases/integration.py @@ -34,7 +34,8 @@ import shutil gi.require_version('Gtk', '3.0') from gi.repository import Gtk -from keymapper.state import custom_mapping, system_mapping +from keymapper.state import custom_mapping, system_mapping, \ + clear_system_mapping from keymapper.paths import CONFIG, get_config_path from keymapper.config import config @@ -395,8 +396,8 @@ class TestIntegration(unittest.TestCase): keycode_to = 200 self.change_empty_row(keycode_from, 'a') - system_mapping.empty() - system_mapping.change(keycode_to, 'a') + clear_system_mapping() + system_mapping['a'] = keycode_to pending_events['device 2'] = [ Event(evdev.events.EV_KEY, keycode_from - 8, 1), @@ -432,8 +433,8 @@ class TestIntegration(unittest.TestCase): keycode_to = 90 self.change_empty_row(keycode_from, 't') - system_mapping.empty() - system_mapping.change(keycode_to, 't') + clear_system_mapping() + system_mapping['t'] = keycode_to # not all of those events should be processed, since that takes some # time due to time.sleep in the fakes and the injection is stopped. diff --git a/tests/testcases/mapping.py b/tests/testcases/mapping.py index 1c6a390b..ac8dfec7 100644 --- a/tests/testcases/mapping.py +++ b/tests/testcases/mapping.py @@ -22,7 +22,7 @@ import unittest from keymapper.mapping import Mapping -from keymapper.state import parse_xmodmap +from keymapper.state import populate_system_mapping class TestMapping(unittest.TestCase): @@ -30,8 +30,8 @@ class TestMapping(unittest.TestCase): self.mapping = Mapping() self.assertFalse(self.mapping.changed) - def test_parse_xmodmap(self): - parse_xmodmap(self.mapping) + def test_populate_system_mapping(self): + populate_system_mapping(self.mapping) self.assertGreater(len(self.mapping), 100) # keycode 10 is typically mapped to '1' self.assertEqual(self.mapping.get_keycode('1'), 10)