diff --git a/.pylintrc b/.pylintrc index e865c706..bc7d52ec 100644 --- a/.pylintrc +++ b/.pylintrc @@ -2,8 +2,8 @@ disable= # that is the standard way to import GTK afaik - wrong-import-position + wrong-import-position, # using """ for comments highlights them in green for me and makes it # a great way to separate stuff into multiple sections - pointless-string-statement \ No newline at end of file + pointless-string-statement diff --git a/keymapper/config.py b/keymapper/config.py index 7af9cadd..d2a683bc 100644 --- a/keymapper/config.py +++ b/keymapper/config.py @@ -23,9 +23,7 @@ import os -import sys import json -import shutil import copy from keymapper.paths import CONFIG_PATH, USER, touch diff --git a/keymapper/getdevices.py b/keymapper/getdevices.py index afbba21a..abfdc31b 100644 --- a/keymapper/getdevices.py +++ b/keymapper/getdevices.py @@ -28,8 +28,7 @@ import time import asyncio import evdev -from evdev.ecodes import EV_KEY, EV_ABS, KEY_CAMERA, EV_REL, ABS_PRESSURE, \ - BTN_STYLUS, BTN_A +from evdev.ecodes import EV_KEY, EV_ABS, KEY_CAMERA, EV_REL, BTN_STYLUS, BTN_A from keymapper.logger import logger diff --git a/keymapper/gui/reader.py b/keymapper/gui/reader.py index e31c1fcb..a772ce43 100644 --- a/keymapper/gui/reader.py +++ b/keymapper/gui/reader.py @@ -34,7 +34,7 @@ from evdev.ecodes import EV_KEY, EV_ABS, ABS_MISC, EV_REL from keymapper.logger import logger from keymapper.key import Key from keymapper.state import custom_mapping -from keymapper.getdevices import get_devices +from keymapper.getdevices import get_devices, is_gamepad from keymapper import utils CLOSE = 1 @@ -119,6 +119,11 @@ class _KeycodeReader: If read is called without prior start_reading, no keycodes will be available. + + Parameters + ---------- + device_name : string + As indexed in get_devices() """ if self._pipe is not None: self.stop_reading() @@ -126,33 +131,39 @@ class _KeycodeReader: self.virtual_devices = [] - for name, group in get_devices().items(): - if device_name not in name: + group = get_devices()[device_name] + + # Watch over each one of the potentially multiple devices per hardware + for path in group['paths']: + try: + device = evdev.InputDevice(path) + except FileNotFoundError: continue - # Watch over each one of the potentially multiple devices per - # hardware - for path in group['paths']: - try: - device = evdev.InputDevice(path) - except FileNotFoundError: - continue + if evdev.ecodes.EV_KEY in device.capabilities(): + self.virtual_devices.append(device) - if evdev.ecodes.EV_KEY in device.capabilities(): - self.virtual_devices.append(device) - - logger.debug( - 'Starting reading keycodes from "%s"', - '", "'.join([device.name for device in self.virtual_devices]) - ) + logger.debug( + 'Starting reading keycodes from "%s"', + '", "'.join([device.name for device in self.virtual_devices]) + ) pipe = multiprocessing.Pipe() self._pipe = pipe self._process = threading.Thread(target=self._read_worker) self._process.start() - def _pipe_event(self, event, device): - """Write the event into the pipe to the main process.""" + def _pipe_event(self, event, device, gamepad): + """Write the event into the pipe to the main process. + + Parameters + ---------- + event : evdev.InputEvent + device : evdev.InputDevice + gamepad : bool + If true, ABS_X and ABS_Y might be mapped to buttons as well + depending on the purpose configuration + """ # value: 1 for down, 0 for up, 2 for hold. if self._pipe is None or self._pipe[1].closed: logger.debug('Pipe closed, reader stops.') @@ -173,7 +184,7 @@ class _KeycodeReader: # which breaks the current workflow. return - if not utils.should_map_event_as_btn(event, custom_mapping): + if not utils.should_map_event_as_btn(event, custom_mapping, gamepad): return max_abs = utils.get_max_abs(device) @@ -186,13 +197,18 @@ class _KeycodeReader: # using a thread that blocks instead of read_one made it easier # to debug via the logs, because the UI was not polling properly # at some point which caused logs for events not to be written. - rlist = {device.fd: device for device in self.virtual_devices} + rlist = {} + gamepad = {} + for device in self.virtual_devices: + rlist[device.fd] = device + gamepad[device.fd] = is_gamepad(device) + rlist[self._pipe[1]] = self._pipe[1] while True: ready = select.select(rlist, [], [])[0] for fd in ready: - readable = rlist[fd] # a device or a pipe + readable = rlist[fd] # an InputDevice or a pipe if isinstance(readable, multiprocessing.connection.Connection): msg = readable.recv() if msg == CLOSE: @@ -202,7 +218,11 @@ class _KeycodeReader: try: for event in rlist[fd].read(): - self._pipe_event(event, readable) + self._pipe_event( + event, + readable, + gamepad.get(fd, False) + ) except OSError: logger.debug( 'Device "%s" disappeared from the reader', diff --git a/keymapper/injection/context.py b/keymapper/injection/context.py index 7da0cf89..e8ba2495 100644 --- a/keymapper/injection/context.py +++ b/keymapper/injection/context.py @@ -59,6 +59,9 @@ class Context: macros : dict mapping of ((type, code, value),) to _Macro objects. Combinations work similar as in key_to_code + is_gamepad : bool + if key-mapper considers this device to be a gamepad. If yes, ABS_X + and ABS_Y events can be treated as buttons. """ def __init__(self, mapping): self.mapping = mapping @@ -67,6 +70,7 @@ class Context: # might be a bit expensive self.key_to_code = self._map_keys_to_codes() self.macros = self._parse_macros() + self.left_purpose = None self.right_purpose = None self.update_purposes() diff --git a/keymapper/injection/injector.py b/keymapper/injection/injector.py index 90b1b93f..108001d4 100644 --- a/keymapper/injection/injector.py +++ b/keymapper/injection/injector.py @@ -406,6 +406,8 @@ class Injector(multiprocessing.Process): source.path, source.fd ) + gamepad = is_gamepad(source) + keycode_handler = KeycodeMapper(self.context, source, uinput) async for event in source.async_read_loop(): @@ -415,7 +417,7 @@ class Injector(multiprocessing.Process): continue # for mapped stuff - if utils.should_map_event_as_btn(event, self.context.mapping): + if utils.should_map_event_as_btn(event, self.context.mapping, gamepad): will_report_key_up = utils.will_report_key_up(event) keycode_handler.handle_keycode(event) diff --git a/keymapper/logger.py b/keymapper/logger.py index ab18ae9e..bd6ea56c 100644 --- a/keymapper/logger.py +++ b/keymapper/logger.py @@ -54,6 +54,7 @@ def key_spam(self, key, msg, *args): anything that can be string formatted, but usually a tuple of (type, code, value) tuples """ + # pylint: disable=protected-access if not self.isEnabledFor(SPAM): return diff --git a/keymapper/utils.py b/keymapper/utils.py index e9579ecb..49db786f 100644 --- a/keymapper/utils.py +++ b/keymapper/utils.py @@ -95,7 +95,7 @@ def will_report_key_up(event): return not is_wheel(event) -def should_map_event_as_btn(event, mapping): +def should_map_event_as_btn(event, mapping, gamepad): """Does this event describe a button. If it does, this function will make sure its value is one of [-1, 0, 1], @@ -106,6 +106,13 @@ def should_map_event_as_btn(event, mapping): Especially important for gamepad events, some of the buttons require special rules. + + Parameters + ---------- + event : evdev.InputEvent + mapping : Mapping + gamepad : bool + If the device is treated as gamepad """ if (event.type, event.code) in STYLUS: return False @@ -121,6 +128,9 @@ def should_map_event_as_btn(event, mapping): return False if event.code in JOYSTICK: + if not gamepad: + return False + l_purpose = mapping.get('gamepad.joystick.left_purpose') r_purpose = mapping.get('gamepad.joystick.right_purpose') @@ -130,6 +140,8 @@ def should_map_event_as_btn(event, mapping): if event.code in [ABS_RX, ABS_RY] and r_purpose == BUTTONS: return True else: + # for non-joystick buttons just always offer mapping them to + # buttons return True if is_wheel(event): diff --git a/readme/development.md b/readme/development.md index 29357376..1f04520d 100644 --- a/readme/development.md +++ b/readme/development.md @@ -33,7 +33,7 @@ requests. - [x] map keys using a `modifier + modifier + ... + key` syntax - [ ] injecting keys that aren't available in the systems keyboard layout - [ ] injecting keys while abs capabilities are present. e.g. stylus buttons -- [ ] ship with a list of all keys known to xkb and validate input in gui +- [ ] ship with a list of all keys known to xkb and validate input in the gui ## Tests diff --git a/readme/pylint.svg b/readme/pylint.svg index a8977660..5ff69b25 100644 --- a/readme/pylint.svg +++ b/readme/pylint.svg @@ -17,7 +17,7 @@ pylint - 9.81 - 9.81 + 9.84 + 9.84 \ No newline at end of file diff --git a/tests/testcases/test_dev_utils.py b/tests/testcases/test_dev_utils.py index 0993d139..bdc17137 100644 --- a/tests/testcases/test_dev_utils.py +++ b/tests/testcases/test_dev_utils.py @@ -56,57 +56,76 @@ class TestDevUtils(unittest.TestCase): mapping = Mapping() # the function name is so horribly long - def do(event): - return utils.should_map_event_as_btn(event, mapping) + def do(gamepad, event): + return utils.should_map_event_as_btn(event, mapping, gamepad) """D-Pad""" - self.assertTrue(do(new_event(EV_ABS, ABS_HAT0X, 1))) - self.assertTrue(do(new_event(EV_ABS, ABS_HAT0X, -1))) + self.assertTrue(do(1, new_event(EV_ABS, ABS_HAT0X, 1))) + self.assertTrue(do(0, new_event(EV_ABS, ABS_HAT0X, -1))) """Mouse movements""" - self.assertTrue(do(new_event(EV_REL, REL_WHEEL, 1))) - self.assertTrue(do(new_event(EV_REL, REL_WHEEL, -1))) - self.assertTrue(do(new_event(EV_REL, REL_HWHEEL, 1))) - self.assertTrue(do(new_event(EV_REL, REL_HWHEEL, -1))) - self.assertFalse(do(new_event(EV_REL, REL_X, -1))) + self.assertTrue(do(1, new_event(EV_REL, REL_WHEEL, 1))) + self.assertTrue(do(0, new_event(EV_REL, REL_WHEEL, -1))) + self.assertTrue(do(1, new_event(EV_REL, REL_HWHEEL, 1))) + self.assertTrue(do(0, new_event(EV_REL, REL_HWHEEL, -1))) + self.assertFalse(do(1, new_event(EV_REL, REL_X, -1))) """regular keys and buttons""" - self.assertTrue(do(new_event(EV_KEY, KEY_A, 1))) - self.assertTrue(do(new_event(EV_ABS, ABS_HAT0X, -1))) + self.assertTrue(do(1, new_event(EV_KEY, KEY_A, 1))) + self.assertTrue(do(0, new_event(EV_KEY, KEY_A, 1))) + self.assertTrue(do(1, new_event(EV_ABS, ABS_HAT0X, -1))) + self.assertTrue(do(0, new_event(EV_ABS, ABS_HAT0X, -1))) """mousepad events""" - self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_MT_SLOT, 1))) - self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_MT_TOOL_Y, 1))) - self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_MT_POSITION_X, 1))) - self.assertFalse(do(new_event(EV_KEY, ecodes.BTN_TOUCH, 1))) + self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_MT_SLOT, 1))) + self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_MT_SLOT, 1))) + self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_MT_TOOL_Y, 1))) + self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_MT_TOOL_Y, 1))) + self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_MT_POSITION_X, 1))) + self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_MT_POSITION_X, 1))) + self.assertFalse(do(1, new_event(EV_KEY, ecodes.BTN_TOUCH, 1))) + self.assertFalse(do(0, new_event(EV_KEY, ecodes.BTN_TOUCH, 1))) """stylus movements""" - self.assertFalse(do(new_event(EV_KEY, ecodes.BTN_DIGI, 1))) - self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_TILT_X, 1))) - self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_TILT_Y, 1))) - self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_DISTANCE, 1))) - self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_PRESSURE, 1))) + self.assertFalse(do(0, new_event(EV_KEY, ecodes.BTN_DIGI, 1))) + self.assertFalse(do(1, new_event(EV_KEY, ecodes.BTN_DIGI, 1))) + self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_TILT_X, 1))) + self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_TILT_X, 1))) + self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_TILT_Y, 1))) + self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_TILT_Y, 1))) + self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_DISTANCE, 1))) + self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_DISTANCE, 1))) + self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_PRESSURE, 1))) + self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_PRESSURE, 1))) """joysticks""" - self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_RX, 1234))) - self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_Y, -1))) - self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_RY, -1))) - - """weird events""" - - self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_MISC, -1))) + # without a purpose of BUTTONS it won't map any button, even for + # gamepads + self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_RX, 1234))) + self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_RX, 1234))) + self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_Y, -1))) + self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_Y, -1))) + self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_RY, -1))) + self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_RY, -1))) mapping.set('gamepad.joystick.right_purpose', BUTTONS) config.set('gamepad.joystick.left_purpose', BUTTONS) + # but only for gamepads + self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_Y, -1))) + self.assertTrue(do(1, new_event(EV_ABS, ecodes.ABS_Y, -1))) + self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_RY, -1))) + self.assertTrue(do(1, new_event(EV_ABS, ecodes.ABS_RY, -1))) - self.assertTrue(do(new_event(EV_ABS, ecodes.ABS_Y, -1))) - self.assertTrue(do(new_event(EV_ABS, ecodes.ABS_RY, -1))) + """weird events""" + + self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_MISC, -1))) + self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_MISC, -1))) def test_normalize_value(self): def do(event):