diff --git a/data/key-mapper.glade b/data/key-mapper.glade index 3a33bb3e..6e2240d9 100644 --- a/data/key-mapper.glade +++ b/data/key-mapper.glade @@ -649,6 +649,7 @@ Don't hold down any keys while the injection starts. Mouse Wheel Buttons + Joystick @@ -692,6 +693,7 @@ Don't hold down any keys while the injection starts. Mouse Wheel Buttons + Joystick diff --git a/keymapper/gui/reader.py b/keymapper/gui/reader.py index a772ce43..b9222e08 100644 --- a/keymapper/gui/reader.py +++ b/keymapper/gui/reader.py @@ -184,7 +184,7 @@ class _KeycodeReader: # which breaks the current workflow. return - if not utils.should_map_event_as_btn(event, custom_mapping, gamepad): + if not utils.should_map_as_btn(event, custom_mapping, gamepad): return max_abs = utils.get_max_abs(device) diff --git a/keymapper/injection/context.py b/keymapper/injection/context.py index e8ba2495..52be551b 100644 --- a/keymapper/injection/context.py +++ b/keymapper/injection/context.py @@ -47,21 +47,30 @@ class Context: Members ------- mapping : Mapping - the mapping that is the source of key_to_code and macros, + The mapping that is the source of key_to_code and macros, only used to query config values. key_to_code : dict - mapping of ((type, code, value),) to linux-keycode - or multiple of those like ((...), (...), ...) for combinations - combinations need to be present in every possible valid ordering. + Mapping of ((type, code, value),) to linux-keycode + or multiple of those like ((...), (...), ...) for combinations. + Combinations need to be present in every possible valid ordering. e.g. shift + alt + a and alt + shift + a. This is needed to query keycodes more efficiently without having to search mapping each time. macros : dict - mapping of ((type, code, value),) to _Macro objects. + 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. + uinput : evdev.UInput + Where to inject stuff to. This is an extra node in /dev so that + existing capabilities won't clash. + For example a gamepad can keep being a gamepad, while injected + keycodes appear as keyboard input. + This way the stylus buttons of a graphics tablet can also be mapped + to keys, while the stylus keeps being a stylus. + The main issue is, that the window manager handles events differently + depending on the overall capabilities, and with EV_ABS capabilities + keycodes are pretty much ignored and not written to the desktop. + So this uinput should not have EV_ABS capabilities. Only EV_REL + and EV_KEY is allowed. """ def __init__(self, mapping): self.mapping = mapping @@ -75,6 +84,8 @@ class Context: self.right_purpose = None self.update_purposes() + self.uinput = None + def update_purposes(self): """Read joystick purposes from the configuration.""" self.left_purpose = self.mapping.get('gamepad.joystick.left_purpose') @@ -151,4 +162,4 @@ class Context: def writes_keys(self): """Check if anything is being mapped to keys.""" - return len(self.macros) == 0 and len(self.key_to_code) == 0 + return len(self.macros) > 0 and len(self.key_to_code) > 0 diff --git a/keymapper/injection/event_producer.py b/keymapper/injection/event_producer.py index eaa4d150..587777fb 100644 --- a/keymapper/injection/event_producer.py +++ b/keymapper/injection/event_producer.py @@ -55,7 +55,6 @@ class EventProducer: """Construct the event producer without it doing anything yet.""" self.context = context - self.mouse_uinput = None self.max_abs = None # events only take ints, so a movement of 0.3 needs to add # up to 1.2 to affect the cursor, with 0.2 remaining @@ -74,13 +73,13 @@ class EventProducer: if event.type == EV_ABS and event.code in self.abs_state: self.abs_state[event.code] = event.value - def _write(self, device, ev_type, keycode, value): + def _write(self, ev_type, keycode, value): """Inject.""" # if the mouse won't move even though correct stuff is written here, # the capabilities are probably wrong try: - device.write(ev_type, keycode, value) - device.syn() + self.context.uinput.write(ev_type, keycode, value) + self.context.uinput.syn() except OverflowError: # screwed up the calculation of mouse movements logger.error('OverflowError (%s, %s, %s)', ev_type, keycode, value) @@ -113,11 +112,6 @@ class EventProducer: self.pending_rel[code] -= output_value return output_value - def set_mouse_uinput(self, uinput): - """Set where to write mouse movements to.""" - logger.debug('Going to inject mouse movements to "%s"', uinput.name) - self.mouse_uinput = uinput - def set_max_abs_from(self, device): """Update the maximum value joysticks will report. @@ -254,19 +248,19 @@ class EventProducer: rel_x = self.accumulate(REL_X, rel_x) rel_y = self.accumulate(REL_Y, rel_y) if rel_x != 0: - self._write(self.mouse_uinput, EV_REL, REL_X, rel_x) + self._write(EV_REL, REL_X, rel_x) if rel_y != 0: - self._write(self.mouse_uinput, EV_REL, REL_Y, rel_y) + self._write(EV_REL, REL_Y, rel_y) # wheel movements if abs(wheel_x) > 0: change = wheel_x * x_scroll_speed / max_abs value = self.accumulate(REL_WHEEL, change) if abs(change) > WHEEL_THRESHOLD * x_scroll_speed: - self._write(self.mouse_uinput, EV_REL, REL_HWHEEL, value) + self._write(EV_REL, REL_HWHEEL, value) if abs(wheel_y) > 0: change = wheel_y * y_scroll_speed / max_abs value = self.accumulate(REL_HWHEEL, change) if abs(change) > WHEEL_THRESHOLD * y_scroll_speed: - self._write(self.mouse_uinput, EV_REL, REL_WHEEL, -value) + self._write(EV_REL, REL_WHEEL, -value) diff --git a/keymapper/injection/injector.py b/keymapper/injection/injector.py index 108001d4..0e44a901 100644 --- a/keymapper/injection/injector.py +++ b/keymapper/injection/injector.py @@ -196,7 +196,29 @@ class Injector(multiprocessing.Process): return device - def _modify_capabilities(self, input_device, gamepad): + def _copy_capabilities(self, input_device): + """Copy capabilities for a new device.""" + ecodes = evdev.ecodes + + # copy the capabilities because the uinput is going + # to act like the device. + capabilities = input_device.capabilities(absinfo=True) + + # just like what python-evdev does in from_device + if ecodes.EV_SYN in capabilities: + del capabilities[ecodes.EV_SYN] + if ecodes.EV_FF in capabilities: + del capabilities[ecodes.EV_FF] + + if ecodes.ABS_VOLUME in capabilities.get(ecodes.EV_ABS, []): + # For some reason an ABS_VOLUME capability likes to appear + # for some users. It prevents mice from moving around and + # keyboards from writing characters + capabilities[ecodes.EV_ABS].remove(ecodes.ABS_VOLUME) + + return capabilities + + def _construct_capabilities(self, gamepad): """Adds all used keycodes into a copy of a devices capabilities. Sometimes capabilities are a bit tricky and change how the system @@ -204,28 +226,21 @@ class Injector(multiprocessing.Process): Parameters ---------- - input_device : evdev.InputDevice gamepad : bool - If ABS capabilities should be removed in favor of REL. - This parameter is somewhat redundant and could be derived - from input_device, but it is very useful to control this in - tests. + If gamepad events can be translated to mouse events. (also + depends on the configured purpose) Returns ------- a mapping of int event type to an array of int event codes. - Without absinfo. """ ecodes = evdev.ecodes - # copy the capabilities because the uinput is going - # to act like the device. - capabilities = input_device.capabilities(absinfo=True) + capabilities = { + EV_KEY: [] + } - if self.context.writes_keys and capabilities.get(EV_KEY) is None: - capabilities[EV_KEY] = [] - - # Furthermore, support all injected keycodes + # support all injected keycodes for code in self.context.key_to_code.values(): if code == DISABLE_CODE: continue @@ -255,24 +270,6 @@ class Injector(multiprocessing.Process): # needed capabilities[EV_KEY].append(ecodes.BTN_MOUSE) - # just like what python-evdev does in from_device - if ecodes.EV_SYN in capabilities: - del capabilities[ecodes.EV_SYN] - if ecodes.EV_FF in capabilities: - del capabilities[ecodes.EV_FF] - if gamepad and not self.context.forwards_joystick(): - # Key input to text inputs and such only works without ABS - # events in the capabilities, possibly due to some intentional - # constraints in wayland/X. So if the joysticks are not used - # as joysticks remove ABS. - del capabilities[ecodes.EV_ABS] - - if ecodes.ABS_VOLUME in capabilities.get(ecodes.EV_ABS, []): - # For some reason an ABS_VOLUME capability likes to appear - # for some users. It prevents mice from moving around and - # keyboards from writing characters - capabilities[ecodes.EV_ABS].remove(ecodes.ABS_VOLUME) - return capabilities async def _msg_listener(self): @@ -304,6 +301,8 @@ class Injector(multiprocessing.Process): logger.error('Cannot inject for unknown device "%s"', self.device) return + group = get_devices()[self.device] + logger.info('Starting injecting the mapping for "%s"', self.device) # create a new event loop, because somehow running an infinite loop @@ -318,43 +317,40 @@ class Injector(multiprocessing.Process): numlock_state = is_numlock_on() coroutines = [] + # where mapped events go to. + # See the Context docstring on why this is needed. + self.context.uinput = evdev.UInput( + name=f'{DEV_NAME} {self.device} mapped', + phys=DEV_NAME, + events=self._construct_capabilities(group['gamepad']) + ) + # Watch over each one of the potentially multiple devices per hardware - for path in get_devices()[self.device]['paths']: + for path in group['paths']: source = self._grab_device(path) if source is None: # this path doesn't need to be grabbed for injection, because # it doesn't provide the events needed to execute the mapping continue - logger.spam( - 'Original capabilities for "%s": %s', - path, source.capabilities(verbose=True) - ) - # 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. gamepad = is_gamepad(source) - uinput = evdev.UInput( - name=f'{DEV_NAME} {self.device}', + forward_to = evdev.UInput( + name=f'{DEV_NAME} {source.name} forwarded', phys=DEV_NAME, - events=self._modify_capabilities(source, gamepad) - ) - - logger.spam( - 'Injected capabilities for "%s": %s', - path, uinput.capabilities(verbose=True) + events=self._copy_capabilities(source) ) # actual reading of events - coroutines.append(self._event_consumer(source, uinput)) + coroutines.append(self._event_consumer(source, forward_to)) # The event source of the current iteration will deliver events # that are needed for this. It is that one that will be mapped # to a mouse-like devnode. if gamepad and self.context.joystick_as_mouse(): self._event_producer.set_max_abs_from(source) - self._event_producer.set_mouse_uinput(uinput) if len(coroutines) == 0: logger.error('Did not grab any device') @@ -386,7 +382,7 @@ class Injector(multiprocessing.Process): # reached otherwise. logger.debug('asyncio coroutines ended') - async def _event_consumer(self, source, uinput): + async def _event_consumer(self, source, forward_to): """Reads input events to inject keycodes or talk to the event_producer. Can be stopped by stopping the asyncio loop. This loop @@ -398,8 +394,10 @@ class Injector(multiprocessing.Process): ---------- source : evdev.InputDevice where to read keycodes from - uinput : evdev.UInput - where to write keycodes to + forward_to : evdev.UInput + where to write keycodes to that were not mapped to anything. + Should be an UInput with capabilities that work for all forwarded + events, so ideally they should be copied from source. """ logger.debug( 'Started consumer to inject to %s, fd %s', @@ -408,7 +406,7 @@ class Injector(multiprocessing.Process): gamepad = is_gamepad(source) - keycode_handler = KeycodeMapper(self.context, source, uinput) + keycode_handler = KeycodeMapper(self.context, source, forward_to) async for event in source.async_read_loop(): if self._event_producer.is_handled(event): @@ -417,7 +415,7 @@ class Injector(multiprocessing.Process): continue # for mapped stuff - if utils.should_map_event_as_btn(event, self.context.mapping, gamepad): + if utils.should_map_as_btn(event, self.context.mapping, gamepad): will_report_key_up = utils.will_report_key_up(event) keycode_handler.handle_keycode(event) @@ -436,7 +434,10 @@ class Injector(multiprocessing.Process): continue # forward the rest - uinput.write(event.type, event.code, event.value) + forward_to.write(event.type, event.code, event.value) # this already includes SYN events, so need to syn here again + # This happens all the time in tests because the async_read_loop + # stops when there is nothing to read anymore. Otherwise tests + # would block. logger.error('The consumer for "%s" stopped early', source.path) diff --git a/keymapper/injection/keycode_mapper.py b/keymapper/injection/keycode_mapper.py index c449d3bd..85676bce 100644 --- a/keymapper/injection/keycode_mapper.py +++ b/keymapper/injection/keycode_mapper.py @@ -72,12 +72,6 @@ def is_key_up(value): return 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 @@ -195,7 +189,7 @@ def print_unreleased(): class KeycodeMapper: """Injects keycodes and starts macros.""" - def __init__(self, context, source, uinput): + def __init__(self, context, source, forward_to): """Create a keycode mapper for one virtual device. There may be multiple KeycodeMappers for one hardware device. They @@ -207,13 +201,13 @@ class KeycodeMapper: the configuration of the Injector process source : InputDevice where events used in handle_keycode come from - uinput : UInput: - where to inject events to + forward_to : UInput + where forwarded/unhandled events should be written to """ self.source = source self.max_abs = utils.get_max_abs(source) self.context = context - self.uinput = uinput + self.forward_to = forward_to # some type checking, prevents me from forgetting what that stuff # is supposed to be when writing tests. @@ -227,8 +221,17 @@ class KeycodeMapper: def macro_write(self, code, value): """Handler for macros.""" - self.uinput.write(EV_KEY, code, value) - self.uinput.syn() + self.context.uinput.write(EV_KEY, code, value) + self.context.uinput.syn() + + def write(self, key): + """Shorthand to write stuff.""" + self.context.uinput.write(*key) + self.context.uinput.syn() + + def forward(self, key): + """Shorthand to forwards an event.""" + self.forward_to.write(*key) def _get_key(self, key): """If the event triggers stuff, get the key for that. @@ -358,11 +361,11 @@ class KeycodeMapper: elif type_code != (target_type, target_code): # release what the input is mapped to logger.key_spam(key, 'releasing %s', target_code) - write(self.uinput, (target_type, target_code, 0)) + self.write((target_type, target_code, 0)) elif forward: # forward the release event logger.key_spam((original_tuple,), 'forwarding release') - write(self.uinput, original_tuple) + self.forward(original_tuple) else: logger.key_spam(key, 'not forwarding release') elif event.type != EV_ABS: @@ -425,12 +428,12 @@ class KeycodeMapper: return logger.key_spam(key, 'maps to %s', target_code) - write(self.uinput, (EV_KEY, target_code, 1)) + self.write((EV_KEY, target_code, 1)) return if forward: logger.key_spam((original_tuple,), 'forwarding') - write(self.uinput, original_tuple) + self.forward(original_tuple) else: logger.key_spam((event_tuple,), 'not forwarding') diff --git a/keymapper/utils.py b/keymapper/utils.py index 49db786f..f3eaa15b 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, gamepad): +def should_map_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], diff --git a/readme/development.md b/readme/development.md index 5676f1f2..37dcc8b0 100644 --- a/readme/development.md +++ b/readme/development.md @@ -32,7 +32,7 @@ requests. - [x] automatically load presets when devices get plugged in after login (udev) - [x] map keys using a `modifier + modifier + ... + key` syntax - [ ] injecting keys that aren't available in the systems keyboard layout -- [ ] inject in an additional device instead to avoid clashing capabilities +- [x] inject in an additional device instead to avoid clashing capabilities - [ ] ship with a list of all keys known to xkb and validate input in the gui ## Tests @@ -136,27 +136,16 @@ sudo evtest **It tries or doesn't try to map ABS_X/ABS_Y** Is the device a gamepad? Does the GUI show joystick configurations? + - if yes, no: adjust `is_gamepad` to loosen up the constraints - if no, yes: adjust `is_gamepad` to tighten up the constraints + Try to do it in such a way that other devices won't break. Also see readme/capabilities.md **It won't offer mapping a button** -Modify `should_map_event_as_btn` - -**The cursor won't move anymore** - -Can be difficult. Depending on capabilities the display server might not -treat events as cursor movements anymore. e.g. mice with EV_ABS capabilities -won't move the cursor. Or key-mapper removed the EV_ABS capabilities. -Or due to weird stuff a new capability appears out of nowhere (ABS_VOLUME). - -At some point this won't be a problem anymore when key-mapper creates a new -device for all injected keys for non-keyboards, as well as for generated -EV_REL events for gamepads. - -Modify `_modify_capabilities` to get it to work. +Modify `should_map_as_btn` ## Resources diff --git a/tests/test.py b/tests/test.py index 40c83d8e..fbfd2b3d 100644 --- a/tests/test.py +++ b/tests/test.py @@ -325,6 +325,9 @@ class InputDevice: return result +uinputs = {} + + class UInput: def __init__(self, events=None, name='unnamed', *args, **kwargs): self.fd = 0 @@ -334,6 +337,9 @@ class UInput: self.events = events self.write_history = [] + global uinputs + uinputs[name] = self + def capabilities(self, *args, **kwargs): return self.events diff --git a/tests/testcases/test_context.py b/tests/testcases/test_context.py new file mode 100644 index 00000000..cb095db3 --- /dev/null +++ b/tests/testcases/test_context.py @@ -0,0 +1,118 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# key-mapper - GUI for device specific keyboard mappings +# Copyright (C) 2021 sezanzeb +# +# This file is part of key-mapper. +# +# key-mapper is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# key-mapper is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with key-mapper. If not, see . + + +import unittest + +from keymapper.injection.context import Context +from keymapper.mapping import Mapping +from keymapper.key import Key +from keymapper.config import NONE, MOUSE, WHEEL, BUTTONS +from keymapper.state import system_mapping + + +class TestContext(unittest.TestCase): + def setUp(self): + self.mapping = Mapping() + self.mapping.set('gamepad.joystick.left_purpose', WHEEL) + self.mapping.set('gamepad.joystick.right_purpose', WHEEL) + self.mapping.change(Key(1, 31, 1), 'k(a)') + self.mapping.change(Key(1, 32, 1), 'b') + self.mapping.change(Key((1, 33, 1), (1, 34, 1), (1, 35, 1)), 'c') + self.context = Context(self.mapping) + + def test_update_purposes(self): + self.mapping.set('gamepad.joystick.left_purpose', BUTTONS) + self.mapping.set('gamepad.joystick.right_purpose', MOUSE) + self.context.update_purposes() + self.assertEqual(self.context.left_purpose, BUTTONS) + self.assertEqual(self.context.right_purpose, MOUSE) + + def test_parse_macros(self): + self.assertEqual(len(self.context.macros), 1) + self.assertEqual(self.context.macros[((1, 31, 1),)].code, 'k(a)') + + def test_map_keys_to_codes(self): + b = system_mapping.get('b') + c = system_mapping.get('c') + self.assertEqual(len(self.context.key_to_code), 3) + self.assertEqual(self.context.key_to_code[((1, 32, 1),)], b) + self.assertEqual(self.context.key_to_code[(1, 33, 1), (1, 34, 1), (1, 35, 1)], c) + self.assertEqual(self.context.key_to_code[(1, 34, 1), (1, 33, 1), (1, 35, 1)], c) + + def test_is_mapped(self): + self.assertTrue(self.context.is_mapped( + ((1, 32, 1),) + )) + self.assertTrue(self.context.is_mapped( + ((1, 33, 1), (1, 34, 1), (1, 35, 1)) + )) + self.assertTrue(self.context.is_mapped( + ((1, 34, 1), (1, 33, 1), (1, 35, 1)) + )) + + self.assertFalse(self.context.is_mapped( + ((1, 34, 1), (1, 35, 1), (1, 33, 1)) + )) + self.assertFalse(self.context.is_mapped( + ((1, 36, 1),) + )) + + def test_forwards_joystick(self): + self.assertFalse(self.context.forwards_joystick()) + self.mapping.set('gamepad.joystick.left_purpose', NONE) + self.mapping.set('gamepad.joystick.right_purpose', BUTTONS) + self.assertFalse(self.context.forwards_joystick()) + + # I guess the whole purpose of update_purposes is that the config + # doesn't need to get resolved many times during operation + self.context.update_purposes() + self.assertTrue(self.context.forwards_joystick()) + + def test_maps_joystick(self): + self.assertTrue(self.context.maps_joystick()) + self.mapping.set('gamepad.joystick.left_purpose', NONE) + self.mapping.set('gamepad.joystick.right_purpose', NONE) + self.context.update_purposes() + self.assertFalse(self.context.maps_joystick()) + + def test_joystick_as_mouse(self): + self.assertTrue(self.context.maps_joystick()) + + self.mapping.set('gamepad.joystick.right_purpose', MOUSE) + self.context.update_purposes() + self.assertTrue(self.context.joystick_as_mouse()) + + self.mapping.set('gamepad.joystick.left_purpose', NONE) + self.mapping.set('gamepad.joystick.right_purpose', NONE) + self.context.update_purposes() + self.assertFalse(self.context.joystick_as_mouse()) + + self.mapping.set('gamepad.joystick.right_purpose', BUTTONS) + self.context.update_purposes() + self.assertFalse(self.context.joystick_as_mouse()) + + def test_writes_keys(self): + self.assertTrue(self.context.writes_keys()) + self.assertFalse(Context(Mapping()).writes_keys()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/testcases/test_dev_utils.py b/tests/testcases/test_dev_utils.py index bdc17137..6eae5268 100644 --- a/tests/testcases/test_dev_utils.py +++ b/tests/testcases/test_dev_utils.py @@ -52,12 +52,11 @@ class TestDevUtils(unittest.TestCase): self.assertFalse(utils.is_wheel(new_event(EV_KEY, KEY_A, 1))) self.assertFalse(utils.is_wheel(new_event(EV_ABS, ABS_HAT0X, -1))) - def test_should_map_event_as_btn(self): + def test_should_map_as_btn(self): mapping = Mapping() - # the function name is so horribly long def do(gamepad, event): - return utils.should_map_event_as_btn(event, mapping, gamepad) + return utils.should_map_as_btn(event, mapping, gamepad) """D-Pad""" diff --git a/tests/testcases/test_event_producer.py b/tests/testcases/test_event_producer.py index 785dead8..2e4ab1fc 100644 --- a/tests/testcases/test_event_producer.py +++ b/tests/testcases/test_event_producer.py @@ -45,11 +45,12 @@ class TestEventProducer(unittest.TestCase): self.mapping = Mapping() self.context = Context(self.mapping) - device = InputDevice('/dev/input/event30') uinput = UInput() + self.context.uinput = uinput + + device = InputDevice('/dev/input/event30') self.event_producer = EventProducer(self.context) self.event_producer.set_max_abs_from(device) - self.event_producer.set_mouse_uinput(uinput) asyncio.ensure_future(self.event_producer.run()) config.set('gamepad.joystick.x_scroll_speed', 1) diff --git a/tests/testcases/test_injector.py b/tests/testcases/test_injector.py index 94f506c1..92f97946 100644 --- a/tests/testcases/test_injector.py +++ b/tests/testcases/test_injector.py @@ -26,7 +26,7 @@ import copy import evdev from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X, BTN_LEFT, KEY_A, \ REL_X, REL_Y, REL_WHEEL, REL_HWHEEL, BTN_A, ABS_X, ABS_Y, \ - ABS_Z, ABS_RZ, ABS_VOLUME + ABS_Z, ABS_RZ, ABS_VOLUME, KEY_B from keymapper.injection.injector import Injector, is_in_capabilities, \ STARTING, RUNNING, STOPPED, NO_GRAB, UNKNOWN @@ -42,10 +42,10 @@ from keymapper.getdevices import get_devices, is_gamepad from tests.test import new_event, pending_events, fixtures, \ EVENT_READ_TIMEOUT, uinput_write_history_pipe, \ - MAX_ABS, quick_cleanup, read_write_history_pipe, InputDevice + MAX_ABS, quick_cleanup, read_write_history_pipe, InputDevice, uinputs -original_smeab = utils.should_map_event_as_btn +original_smeab = utils.should_map_as_btn class TestInjector(unittest.TestCase): @@ -68,7 +68,7 @@ class TestInjector(unittest.TestCase): evdev.InputDevice.grab = grab_fail_twice def tearDown(self): - utils.should_map_event_as_btn = original_smeab + utils.should_map_as_btn = original_smeab if self.injector is not None: self.injector.stop_injecting() @@ -137,7 +137,7 @@ class TestInjector(unittest.TestCase): self.assertIsNotNone(device) self.assertTrue(gamepad) - capabilities = self.injector._modify_capabilities(device, gamepad) + capabilities = self.injector._construct_capabilities(gamepad) self.assertNotIn(EV_ABS, capabilities) self.assertIn(EV_REL, capabilities) @@ -165,8 +165,8 @@ class TestInjector(unittest.TestCase): self.assertIsNotNone(device) gamepad = is_gamepad(device) self.assertTrue(gamepad) - capabilities = self.injector._modify_capabilities(device, gamepad) - self.assertIn(EV_ABS, capabilities) + capabilities = self.injector._construct_capabilities(gamepad) + self.assertNotIn(EV_ABS, capabilities) def test_gamepad_purpose_none_2(self): # forward abs joystick events for the left joystick only @@ -182,8 +182,8 @@ class TestInjector(unittest.TestCase): self.assertIsNotNone(device) gamepad = is_gamepad(device) self.assertTrue(gamepad) - capabilities = self.injector._modify_capabilities(device, gamepad) - self.assertIn(EV_ABS, capabilities) + capabilities = self.injector._construct_capabilities(gamepad) + self.assertNotIn(EV_ABS, capabilities) self.assertIn(EV_REL, capabilities) custom_mapping.change(Key(EV_KEY, BTN_A, 1), 'a') @@ -191,8 +191,8 @@ class TestInjector(unittest.TestCase): gamepad = is_gamepad(device) self.assertIsNotNone(device) self.assertTrue(gamepad) - capabilities = self.injector._modify_capabilities(device, gamepad) - self.assertIn(EV_ABS, capabilities) + capabilities = self.injector._construct_capabilities(gamepad) + self.assertNotIn(EV_ABS, capabilities) self.assertIn(EV_REL, capabilities) self.assertIn(EV_KEY, capabilities) @@ -227,7 +227,7 @@ class TestInjector(unittest.TestCase): fixtures[path]['capabilities'][EV_KEY].append(KEY_A) device = self.injector._grab_device(path) gamepad = is_gamepad(device) - capabilities = self.injector._modify_capabilities(device, gamepad) + capabilities = self.injector._construct_capabilities(gamepad) self.assertIn(EV_KEY, capabilities) self.assertIn(evdev.ecodes.BTN_MOUSE, capabilities[EV_KEY]) self.assertIn(evdev.ecodes.KEY_A, capabilities[EV_KEY]) @@ -239,7 +239,7 @@ class TestInjector(unittest.TestCase): gamepad = is_gamepad(device) self.assertIn(EV_KEY, device.capabilities()) self.assertNotIn(evdev.ecodes.BTN_MOUSE, device.capabilities()[EV_KEY]) - capabilities = self.injector._modify_capabilities(device, gamepad) + capabilities = self.injector._construct_capabilities(gamepad) self.assertIn(EV_KEY, capabilities) self.assertGreater(len(capabilities), 1) self.assertIn(evdev.ecodes.BTN_MOUSE, capabilities[EV_KEY]) @@ -411,7 +411,6 @@ class TestInjector(unittest.TestCase): self.injector.run() # not in a process, so the event_producer state can be checked self.assertEqual(self.injector._event_producer.max_abs, MAX_ABS) - self.assertIsNotNone(self.injector._event_producer.mouse_uinput) def test_gamepad_to_buttons_event_producer(self): custom_mapping.set('gamepad.joystick.left_purpose', BUTTONS) @@ -420,7 +419,6 @@ class TestInjector(unittest.TestCase): self.injector.stop_injecting() self.injector.run() self.assertIsNone(self.injector._event_producer.max_abs, MAX_ABS) - self.assertIsNone(self.injector._event_producer.mouse_uinput) def test_device1_event_producer(self): custom_mapping.set('gamepad.joystick.left_purpose', MOUSE) @@ -431,7 +429,41 @@ class TestInjector(unittest.TestCase): # not a gamepad, so _event_producer is not initialized for that. # it can still debounce stuff though self.assertIsNone(self.injector._event_producer.max_abs) - self.assertIsNone(self.injector._event_producer.mouse_uinput) + + def test_capabilities_and_uinput_presence(self): + custom_mapping.change(Key(EV_KEY, KEY_A, 1), 'a') + custom_mapping.change(Key(EV_REL, REL_HWHEEL, 1), 'k(b)') + self.injector = Injector('device 1', custom_mapping) + self.injector.stop_injecting() + self.injector.run() + + forwarded_foo = uinputs.get('key-mapper device 1 foo forwarded') + forwarded = uinputs.get('key-mapper device 1 forwarded') + mapped = uinputs.get('key-mapper device 1 mapped') + + # reading and preventing original events from reaching the + # display server + self.assertIsNotNone(forwarded_foo) + self.assertIsNotNone(forwarded) + # injection + self.assertIsNotNone(mapped) + self.assertEqual(len(uinputs), 3) + + # puts the needed capabilities into the new key-mapper device + self.assertIn(EV_KEY, mapped.capabilities()) + self.assertEqual(len(mapped.capabilities()[EV_KEY]), 2) + self.assertIn(KEY_A, mapped.capabilities()[EV_KEY]) + self.assertIn(KEY_B, mapped.capabilities()[EV_KEY]) + # not a gamepad that maps joysticks to mouse movements + self.assertNotIn(EV_REL, mapped.capabilities()) + + # copies capabilities for all other forwarded devices + self.assertIn(EV_REL, forwarded_foo.capabilities()) + self.assertIn(EV_KEY, forwarded.capabilities()) + self.assertEqual( + len(forwarded.capabilities()[EV_KEY]), + len(evdev.ecodes.keys) + ) def test_injector(self): # the tests in test_keycode_mapper.py test this stuff in detail @@ -537,7 +569,7 @@ class TestInjector(unittest.TestCase): self.assertEqual(self.injector.get_state(), RUNNING) def test_any_funky_event_as_button(self): - # as long as should_map_event_as_btn says it should be a button, + # as long as should_map_as_btn says it should be a button, # it will be. EV_TYPE = 4531 CODE_1 = 754 @@ -595,7 +627,7 @@ class TestInjector(unittest.TestCase): """yes""" - utils.should_map_event_as_btn = lambda *args: True + utils.should_map_as_btn = lambda *args: True history = do_stuff() self.assertEqual(history.count((EV_KEY, code_w, 1)), 1) self.assertEqual(history.count((EV_KEY, code_d, 1)), 1) @@ -694,12 +726,12 @@ class TestInjector(unittest.TestCase): class Stop(Exception): pass - def _modify_capabilities(*args): + def _construct_capabilities(*args): history.append(args) # avoid going into any mainloop raise Stop() - self.injector._modify_capabilities = _modify_capabilities + self.injector._construct_capabilities = _construct_capabilities try: self.injector.run() except Stop: @@ -817,17 +849,14 @@ class TestModifyCapabilities(unittest.TestCase): def tearDown(self): quick_cleanup() - def test_modify_capabilities(self): + def test_construct_capabilities(self): self.mapping.change(Key(EV_KEY, 60, 1), self.macro.code) self.injector = Injector('foo', self.mapping) - capabilities = self.injector._modify_capabilities( - self.fake_device, - gamepad=False - ) + capabilities = self.injector._construct_capabilities(gamepad=False) + self.assertNotIn(EV_ABS, capabilities) - self.assertIn(EV_ABS, capabilities) self.check_keys(capabilities) keys = capabilities[EV_KEY] # mouse capabilities were not present in the fake_device and are @@ -838,34 +867,29 @@ class TestModifyCapabilities(unittest.TestCase): self.assertNotIn(evdev.ecodes.EV_FF, capabilities) self.assertNotIn(EV_REL, capabilities) - # keeps that stuff since modify_capabilities is told that it is not - # a gamepad, so it probably serves some special purpose for that - # device type. For example drawing tablets need that information in - # order to move the cursor around. Since it keeps ABS, the AbsInfo - # should also be still intact - self.assertIn(EV_ABS, capabilities) - self.assertEqual(capabilities[EV_ABS][0][1].max, 1234) - self.assertEqual(capabilities[EV_ABS][1][1].max, 2345) - self.assertEqual(capabilities[EV_ABS][1][1].min, 50) - self.assertEqual(capabilities[EV_ABS][2], 3) - - def test_no_abs_volume(self): + def test_copy_capabilities(self): self.mapping.change(Key(EV_KEY, 60, 1), self.macro.code) # I don't know what ABS_VOLUME is, for now I would like to just always - # remove it until somebody complains + # remove it until somebody complains, since its presence broke stuff self.injector = Injector('foo', self.mapping) self.fake_device._capabilities = { - EV_ABS: [ABS_Y, ABS_VOLUME, ABS_X] + EV_ABS: [ABS_VOLUME, (ABS_X, evdev.AbsInfo(0, 0, 500, 0, 0, 0))], + EV_KEY: [1, 2, 3], + EV_REL: [11, 12, 13], + evdev.ecodes.EV_SYN: [1], + evdev.ecodes.EV_FF: [2], } - capabilities = self.injector._modify_capabilities( - self.fake_device, - gamepad=False - ) + capabilities = self.injector._copy_capabilities(self.fake_device) self.assertNotIn(ABS_VOLUME, capabilities[EV_ABS]) + self.assertNotIn(evdev.ecodes.EV_SYN, capabilities) + self.assertNotIn(evdev.ecodes.EV_FF, capabilities) + self.assertListEqual(capabilities[EV_KEY], [1, 2, 3]) + self.assertListEqual(capabilities[EV_REL], [11, 12, 13]) + self.assertEqual(capabilities[EV_ABS][0][1].max, 500) - def test_modify_capabilities_gamepad(self): + def test_construct_capabilities_gamepad(self): self.mapping.change(Key((EV_KEY, 60, 1)), self.macro.code) config.set('gamepad.joystick.left_purpose', MOUSE) @@ -876,11 +900,7 @@ class TestModifyCapabilities(unittest.TestCase): self.assertTrue(self.injector.context.maps_joystick()) self.assertTrue(self.injector.context.joystick_as_mouse()) - capabilities = self.injector._modify_capabilities( - self.fake_device, - gamepad=True - ) - # because ABS is translated to REL, ABS is not a capability anymore + capabilities = self.injector._construct_capabilities(gamepad=True) self.assertNotIn(EV_ABS, capabilities) self.check_keys(capabilities) @@ -890,7 +910,7 @@ class TestModifyCapabilities(unittest.TestCase): # to ensure the operating system interprets it as mouse. self.assertIn(self.left, keys) - def test_modify_capabilities_gamepad_none_none(self): + def test_construct_capabilities_gamepad_none_none(self): self.mapping.change(Key(EV_KEY, 60, 1), self.macro.code) config.set('gamepad.joystick.left_purpose', NONE) @@ -901,15 +921,12 @@ class TestModifyCapabilities(unittest.TestCase): self.assertFalse(self.injector.context.maps_joystick()) self.assertFalse(self.injector.context.joystick_as_mouse()) - capabilities = self.injector._modify_capabilities( - self.fake_device, - gamepad=True - ) + capabilities = self.injector._construct_capabilities(gamepad=True) + self.assertNotIn(EV_ABS, capabilities) self.check_keys(capabilities) - self.assertIn(EV_ABS, capabilities) - def test_modify_capabilities_gamepad_buttons_buttons(self): + def test_construct_capabilities_gamepad_buttons_buttons(self): self.mapping.change(Key((EV_KEY, 60, 1)), self.macro.code) config.set('gamepad.joystick.left_purpose', BUTTONS) @@ -920,16 +937,13 @@ class TestModifyCapabilities(unittest.TestCase): self.assertTrue(self.injector.context.maps_joystick()) self.assertFalse(self.injector.context.joystick_as_mouse()) - capabilities = self.injector._modify_capabilities( - self.fake_device, - gamepad=True - ) + capabilities = self.injector._construct_capabilities(gamepad=True) self.check_keys(capabilities) self.assertNotIn(EV_ABS, capabilities) self.assertNotIn(EV_REL, capabilities) - def test_modify_capabilities_buttons_buttons(self): + def test_construct_capabilities_buttons_buttons(self): self.mapping.change(Key(EV_KEY, 60, 1), self.macro.code) # those settings shouldn't have an effect with gamepad=False @@ -938,15 +952,10 @@ class TestModifyCapabilities(unittest.TestCase): self.injector = Injector('foo', self.mapping) - capabilities = self.injector._modify_capabilities( - self.fake_device, - gamepad=False - ) + capabilities = self.injector._construct_capabilities(gamepad=False) self.check_keys(capabilities) - # not a gamepad, keeps EV_ABS because it probably has some special - # purpose - self.assertIn(EV_ABS, capabilities) + self.assertNotIn(EV_ABS, capabilities) self.assertNotIn(EV_REL, capabilities) diff --git a/tests/testcases/test_keycode_mapper.py b/tests/testcases/test_keycode_mapper.py index 541ffe0a..8a68cd2f 100644 --- a/tests/testcases/test_keycode_mapper.py +++ b/tests/testcases/test_keycode_mapper.py @@ -113,6 +113,7 @@ class TestKeycodeMapper(unittest.TestCase): uinput = UInput() context = Context(self.mapping) + context.uinput = uinput context.key_to_code = { (ev_1,): 51, (ev_2,): 52, @@ -192,15 +193,16 @@ class TestKeycodeMapper(unittest.TestCase): uinput = UInput() context = Context(self.mapping) + context.uinput = uinput keycode_mapper = KeycodeMapper(context, self.source, uinput) - keycode_mapper.handle_keycode(new_event(*down), False) + keycode_mapper.handle_keycode(new_event(*down), forward=False) self.assertEqual(unreleased[(EV_KEY, 91)].input_event_tuple, down) self.assertEqual(unreleased[(EV_KEY, 91)].target_type_code, down[:2]) self.assertEqual(len(unreleased), 1) self.assertEqual(uinput.write_count, 0) - keycode_mapper.handle_keycode(new_event(*up), False) + keycode_mapper.handle_keycode(new_event(*up), forward=False) self.assertEqual(len(unreleased), 0) self.assertEqual(uinput.write_count, 0) @@ -222,6 +224,7 @@ class TestKeycodeMapper(unittest.TestCase): source = InputDevice('/dev/input/event30') context = Context(self.mapping) + context.uinput = uinput context.key_to_code = _key_to_code keycode_mapper = KeycodeMapper(context, source, uinput) @@ -238,29 +241,34 @@ class TestKeycodeMapper(unittest.TestCase): self.assertEqual(history.count((EV_KEY, 73, 0)), 1) def test_dont_filter_unmapped(self): - # if an event is not used at all, it should be written into - # unmapped but not furthermore modified. For example wheel events + # if an event is not used at all, it should be written but not + # furthermore modified. For example wheel events # keep reporting events of the same value without a release inbetween, # they should be forwarded. down = (EV_KEY, 91, 1) up = (EV_KEY, 91, 0) uinput = UInput() + forward_to = UInput() context = Context(self.mapping) - keycode_mapper = KeycodeMapper(context, self.source, uinput) + context.uinput = uinput + keycode_mapper = KeycodeMapper(context, self.source, forward_to) for _ in range(10): + # don't filter duplicate events if not mapped keycode_mapper.handle_keycode(new_event(*down)) self.assertEqual(unreleased[(EV_KEY, 91)].input_event_tuple, down) self.assertEqual(unreleased[(EV_KEY, 91)].target_type_code, down[:2]) self.assertEqual(len(unreleased), 1) - self.assertEqual(uinput.write_count, 10) + self.assertEqual(forward_to.write_count, 10) + self.assertEqual(uinput.write_count, 0) keycode_mapper.handle_keycode(new_event(*up)) self.assertEqual(len(unreleased), 0) - self.assertEqual(uinput.write_count, 11) + self.assertEqual(forward_to.write_count, 11) + self.assertEqual(uinput.write_count, 0) def test_filter_combi_mapped_duplicate_down(self): # the opposite of the other test, but don't map the key directly @@ -278,6 +286,7 @@ class TestKeycodeMapper(unittest.TestCase): } context = Context(self.mapping) + context.uinput = uinput context.key_to_code = key_to_code keycode_mapper = KeycodeMapper(context, self.source, uinput) @@ -313,6 +322,7 @@ class TestKeycodeMapper(unittest.TestCase): uinput = UInput() context = Context(self.mapping) + context.uinput = uinput context.key_to_code = _key_to_code keycode_mapper = KeycodeMapper(context, self.source, uinput) @@ -349,6 +359,7 @@ class TestKeycodeMapper(unittest.TestCase): uinput = UInput() context = Context(self.mapping) + context.uinput = uinput context.key_to_code = _key_to_code keycode_mapper = KeycodeMapper(context, self.source, uinput) @@ -370,6 +381,7 @@ class TestKeycodeMapper(unittest.TestCase): uinput = UInput() context = Context(self.mapping) + context.uinput = uinput context.key_to_code = _key_to_code keycode_mapper = KeycodeMapper(context, self.source, uinput) @@ -433,6 +445,7 @@ class TestKeycodeMapper(unittest.TestCase): source = InputDevice('/dev/input/event30') context = Context(self.mapping) + context.uinput = uinput context.key_to_code = _key_to_code keycode_mapper = KeycodeMapper(context, source, uinput) @@ -470,6 +483,29 @@ class TestKeycodeMapper(unittest.TestCase): self.assertEqual(uinput_write_history[7].t, (EV_KEY, 101, 0)) self.assertEqual(uinput_write_history[8].t, (EV_KEY, 103, 0)) + def test_macro_writes_to_context_uinput(self): + macro_mapping = { + ((EV_KEY, 1, 1),): parse('k(a)', self.mapping) + } + + context = Context(self.mapping) + context.macros = macro_mapping + context.uinput = UInput() + forward_to = UInput() + keycode_mapper = KeycodeMapper(context, self.source, forward_to) + + keycode_mapper.handle_keycode(new_event(EV_KEY, 1, 1)) + + loop = asyncio.get_event_loop() + sleeptime = config.get('macros.keystroke_sleep_ms', 10) * 12 + loop.run_until_complete(asyncio.sleep(sleeptime / 1000 + 0.1)) + + self.assertEqual(context.uinput.write_count, 2) # down and up + self.assertEqual(forward_to.write_count, 0) + + keycode_mapper.handle_keycode(new_event(EV_KEY, 2, 1)) + self.assertEqual(forward_to.write_count, 1) + def test_handle_keycode_macro(self): history = [] @@ -814,6 +850,7 @@ class TestKeycodeMapper(unittest.TestCase): context.macros = macro_mapping uinput_1 = UInput() + context.uinput = uinput_1 keycode_mapper = KeycodeMapper(context, self.source, uinput_1) @@ -829,6 +866,7 @@ class TestKeycodeMapper(unittest.TestCase): """start macros""" uinput_2 = UInput() + context.uinput = uinput_2 keycode_mapper = KeycodeMapper(context, self.source, uinput_2) @@ -966,6 +1004,7 @@ class TestKeycodeMapper(unittest.TestCase): uinput = UInput() context = Context(self.mapping) + context.uinput = uinput context.key_to_code = _key_to_code keycode_mapper = KeycodeMapper(context, self.source, uinput) @@ -998,7 +1037,8 @@ class TestKeycodeMapper(unittest.TestCase): def test_ignore_hold(self): # hold as in event-value 2, not in macro-hold. # linux will generate events with value 2 after key-mapper injected - # the key-press, so key-mapper doesn't need to forward them. + # the key-press, so key-mapper doesn't need to forward them. That + # would cause duplicate events of those values otherwise. key = (EV_KEY, KEY_A) ev_1 = (*key, 1) ev_2 = (*key, 2) @@ -1011,6 +1051,7 @@ class TestKeycodeMapper(unittest.TestCase): uinput = UInput() context = Context(self.mapping) + context.uinput = uinput context.key_to_code = _key_to_code keycode_mapper = KeycodeMapper(context, self.source, uinput) @@ -1048,10 +1089,16 @@ class TestKeycodeMapper(unittest.TestCase): } uinput = UInput() + forward_to = UInput() context = Context(self.mapping) + context.uinput = uinput context.key_to_code = _key_to_code - keycode_mapper = KeycodeMapper(context, self.source, uinput) + keycode_mapper = KeycodeMapper(context, self.source, forward_to) + + def expect_writecounts(uinput_count, forwarded_count): + self.assertEqual(uinput.write_count, uinput_count) + self.assertEqual(forward_to.write_count, forwarded_count) """single keys""" @@ -1060,9 +1107,11 @@ class TestKeycodeMapper(unittest.TestCase): keycode_mapper.handle_keycode(new_event(*ev_3)) self.assertIn(ev_1[:2], unreleased) self.assertIn(ev_3[:2], unreleased) + expect_writecounts(1, 0) # up keycode_mapper.handle_keycode(new_event(*ev_2)) keycode_mapper.handle_keycode(new_event(*ev_4)) + expect_writecounts(2, 0) self.assertNotIn(ev_1[:2], unreleased) self.assertNotIn(ev_3[:2], unreleased) @@ -1073,8 +1122,9 @@ class TestKeycodeMapper(unittest.TestCase): """a combination that ends in a disabled key""" # ev_5 should be forwarded and the combination triggered - keycode_mapper.handle_keycode(new_event(*combi_1[0])) - keycode_mapper.handle_keycode(new_event(*combi_1[1])) + keycode_mapper.handle_keycode(new_event(*combi_1[0])) # ev_5 + keycode_mapper.handle_keycode(new_event(*combi_1[1])) # ev_3 + expect_writecounts(3, 1) self.assertEqual(len(uinput_write_history), 4) self.assertEqual(uinput_write_history[2].t, (EV_KEY, KEY_A, 1)) self.assertEqual(uinput_write_history[3].t, (EV_KEY, 62, 1)) @@ -1089,6 +1139,7 @@ class TestKeycodeMapper(unittest.TestCase): # release what the combination maps to event = new_event(combi_1[1][0], combi_1[1][1], 0) keycode_mapper.handle_keycode(event) + expect_writecounts(4, 1) self.assertEqual(len(uinput_write_history), 5) self.assertEqual(uinput_write_history[-1].t, (EV_KEY, 62, 0)) self.assertIn(combi_1[0][:2], unreleased) @@ -1096,6 +1147,7 @@ class TestKeycodeMapper(unittest.TestCase): event = new_event(combi_1[0][0], combi_1[0][1], 0) keycode_mapper.handle_keycode(event) + expect_writecounts(4, 2) self.assertEqual(len(uinput_write_history), 6) self.assertEqual(uinput_write_history[-1].t, (EV_KEY, KEY_A, 0)) self.assertNotIn(combi_1[0][:2], unreleased) @@ -1106,6 +1158,7 @@ class TestKeycodeMapper(unittest.TestCase): # only the combination should get triggered keycode_mapper.handle_keycode(new_event(*combi_2[0])) keycode_mapper.handle_keycode(new_event(*combi_2[1])) + expect_writecounts(5, 2) self.assertEqual(len(uinput_write_history), 7) self.assertEqual(uinput_write_history[-1].t, (EV_KEY, 63, 1)) @@ -1115,12 +1168,14 @@ class TestKeycodeMapper(unittest.TestCase): keycode_mapper.handle_keycode(event) self.assertEqual(len(uinput_write_history), 8) self.assertEqual(uinput_write_history[-1].t, (EV_KEY, 63, 0)) + expect_writecounts(6, 2) # the first key of combi_2 is disabled, so it won't write another # key-up event event = new_event(combi_2[0][0], combi_2[0][1], 0) keycode_mapper.handle_keycode(event) self.assertEqual(len(uinput_write_history), 8) + expect_writecounts(6, 2) def test_combination_keycode_macro_mix(self): # ev_1 triggers macro, ev_1 + ev_2 triggers key while the macro is @@ -1138,16 +1193,19 @@ class TestKeycodeMapper(unittest.TestCase): macro_history = [] def handler(*args): + # handler prevents uinput_write_history form growing macro_history.append(args) uinput = UInput() + forward_to = UInput() loop = asyncio.get_event_loop() context = Context(self.mapping) + context.uinput = uinput context.key_to_code = _key_to_code context.macros = macro_mapping - keycode_mapper = KeycodeMapper(context, self.source, uinput) + keycode_mapper = KeycodeMapper(context, self.source, forward_to) keycode_mapper.macro_write = handler @@ -1214,6 +1272,7 @@ class TestKeycodeMapper(unittest.TestCase): uinput = UInput() context = Context(self.mapping) + context.uinput = uinput context.key_to_code = k2c keycode_mapper = KeycodeMapper(context, self.source, uinput)