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)