better compatibility by injecting into a dedicated devnode

This commit is contained in:
sezanzeb 2021-02-18 20:38:14 +01:00
parent 5af4ba9532
commit 1ad5c82cb0
14 changed files with 387 additions and 195 deletions

View File

@ -649,6 +649,7 @@ Don't hold down any keys while the injection starts.</property>
<item id="mouse" translatable="yes">Mouse</item>
<item id="wheel" translatable="yes">Wheel</item>
<item id="buttons" translatable="yes">Buttons</item>
<item id="none" translatable="yes">Joystick</item>
</items>
<signal name="changed" handler="on_left_joystick_changed" swapped="no"/>
</object>
@ -692,6 +693,7 @@ Don't hold down any keys while the injection starts.</property>
<item id="mouse" translatable="yes">Mouse</item>
<item id="wheel" translatable="yes">Wheel</item>
<item id="buttons" translatable="yes">Buttons</item>
<item id="none" translatable="yes">Joystick</item>
</items>
<signal name="changed" handler="on_right_joystick_changed" swapped="no"/>
</object>

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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')

View File

@ -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],

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,118 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2021 sezanzeb <proxima@hip70890b.de>
#
# 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 <https://www.gnu.org/licenses/>.
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()

View File

@ -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"""

View File

@ -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)

View File

@ -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)

View File

@ -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)