mirror of
https://github.com/sezanzeb/input-remapper
synced 2024-11-18 03:25:52 +00:00
better compatibility by injecting into a dedicated devnode
This commit is contained in:
parent
5af4ba9532
commit
1ad5c82cb0
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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],
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
118
tests/testcases/test_context.py
Normal file
118
tests/testcases/test_context.py
Normal 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()
|
@ -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"""
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user