D-Pad direction mapping and joystick purpose configuration

xkb
sezanzeb 4 years ago committed by sezanzeb
parent 23e9fab5a8
commit 850c60ab50

@ -36,10 +36,13 @@ Syntax errors are shown in the UI on save. each `k` function adds a short
delay of 10ms between key-down, key-up and ad the end that can be configured
in `~/.config/key-mapper/config`.
Bear in mind that anti-cheat software might detect macros in games.
##### Key Names
Run `key-mapper-service --key-names` for a list of supported keys for
the middle column, or check the autocompletion on the GUI. Examples:
Check the autocompletion of the GUI for possible values. You can also
obtain a complete list of possiblities using `key-mapper-service --key-names`.
Examples:
- Alphanumeric `a` to `z` and `0` to `9`
- Modifiers `Alt_L` `Control_L` `Control_R` `Shift_L` `Shift_R`
@ -48,13 +51,16 @@ the middle column, or check the autocompletion on the GUI. Examples:
##### Gamepads
Tested with the XBOX 360 Gamepad.
- Joystick movements will be translated to mouse movements
- The second joystick acts as a mouse wheel
- Buttons can be mapped to keycodes or macros
- The D-Pad only works as two buttons - horizontal and vertical
Joystick movements will be translated to mouse movements, while the second
joystick acts as a mouse wheel. All buttons, triggers and D-Pads can be
mapped to keycodes and macros. Configuring the purpose of your joysticks
is currently done in the global configuration at `~/.config/key-mapper/config`.
The D-Pad can be mapped to W, A, S, D for example, to run around in games,
while the joystick turns the view.
On Ubuntu, gamepads worked better in Wayland than with X11 for me.
Tested with the XBOX 360 Gamepad. On Ubuntu, gamepads worked better in
Wayland than with X11 for me.
## Installation
@ -115,12 +121,15 @@ cd key-mapper && sudo python3 setup.py install
- [x] support timed macros, maybe using some sort of syntax
- [x] add to the AUR, provide .deb file
- [x] basic support for gamepads as keyboard and mouse combi
- [x] executing a macro forever while holding down the key
- [x] executing a macro forever while holding down the key using `h`
- [x] mapping D-Pad directions as buttons
- [ ] support for non-GUI TTY environments
- [ ] map D-Pad and Joystick directions as buttons, joystick purpose via config
- [ ] mapping joystick directions as buttons
- [ ] configure joystick purpose via the GUI and store it in the preset
- [ ] automatically load presets when devices get plugged in after login (udev)
- [ ] mapping a combined button press to a key
- [ ] start the daemon in the input group to not require usermod somehow
- [ ] configure locale for preset to provide a different set of possible keys
## Tests

@ -31,6 +31,9 @@ from keymapper.paths import CONFIG, USER, touch
from keymapper.logger import logger
MOUSE = 'mouse'
WHEEL = 'wheel'
CONFIG_PATH = os.path.join(CONFIG, 'config')
INITIAL_CONFIG = {
@ -48,6 +51,8 @@ INITIAL_CONFIG = {
# move the cursor.
'non_linearity': 4,
'pointer_speed': 80,
'left_purpose': MOUSE,
'right_purpose': WHEEL,
},
}
}

@ -29,7 +29,7 @@ import evdev
from evdev.ecodes import EV_ABS, EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL
from keymapper.logger import logger
from keymapper.config import config
from keymapper.config import config, MOUSE, WHEEL
# other events for ABS include buttons
@ -63,13 +63,53 @@ def accumulate(pending, current):
return pending, current
def abs_max(a, b):
"""Get the value with the higher abs value."""
if abs(a) > abs(b):
return a
return b
def get_values(abs_state, left_purpose, right_purpose):
"""Get the raw values for wheel and mouse movement.
If two joysticks have the same purpose, the one that reports higher
absolute values takes over the control.
"""
mouse_x = 0
mouse_y = 0
wheel_x = 0
wheel_y = 0
if left_purpose == MOUSE:
mouse_x = abs_max(mouse_x, abs_state[0])
mouse_y = abs_max(mouse_y, abs_state[1])
if left_purpose == WHEEL:
wheel_x = abs_max(wheel_x, abs_state[0])
wheel_y = abs_max(wheel_y, abs_state[1])
if right_purpose == MOUSE:
mouse_x = abs_max(mouse_x, abs_state[2])
mouse_y = abs_max(mouse_y, abs_state[3])
if right_purpose == WHEEL:
wheel_x = abs_max(wheel_x, abs_state[2])
wheel_y = abs_max(wheel_y, abs_state[3])
return mouse_x, mouse_y, wheel_x, wheel_y
async def ev_abs_mapper(abs_state, input_device, keymapper_device):
"""Keep writing mouse movements based on the gamepad stick position.
Parameters
----------
abs_state : [int, int]
array to read the current abs values from. Like a pointer.
abs_state : [int, int. int, int]
array to read the current abs values from for events of codes
ABS_X, ABS_Y, ABS_RX and ABS_RY
Its contents will change while this function executes its loop from
the outside.
input_device : evdev.InputDevice
keymapper_device : evdev.UInput
"""
@ -87,42 +127,56 @@ async def ev_abs_mapper(abs_state, input_device, keymapper_device):
pending_rx_rel = 0
pending_ry_rel = 0
# TODO move this stuff into the preset configuration
pointer_speed = config.get('gamepad.joystick.pointer_speed')
non_linearity = config.get('gamepad.joystick.non_linearity')
left_purpose = config.get('gamepad.joystick.left_purpose')
right_purpose = config.get('gamepad.joystick.right_purpose')
logger.info('Mapping gamepad to mouse movements')
logger.info(
'Left joystick as %s, right joystick as %s',
left_purpose,
right_purpose
)
while True:
start = time.time()
abs_x, abs_y, abs_rx, abs_ry = abs_state
mouse_x, mouse_y, wheel_x, wheel_y = get_values(
abs_state,
left_purpose,
right_purpose
)
if non_linearity != 1:
# to make small movements smaller for more precision
speed = (abs_x ** 2 + abs_y ** 2) ** 0.5
speed = (mouse_x ** 2 + mouse_y ** 2) ** 0.5
factor = (speed / max_speed) ** non_linearity
else:
factor = 1
# mouse movements
rel_x = abs_x * factor * pointer_speed / max_value
rel_y = abs_y * factor * pointer_speed / max_value
pending_x_rel, rel_x = accumulate(pending_x_rel, rel_x)
pending_y_rel, rel_y = accumulate(pending_y_rel, rel_y)
if rel_x != 0:
_write(keymapper_device, EV_REL, REL_X, rel_x)
if rel_y != 0:
_write(keymapper_device, EV_REL, REL_Y, rel_y)
if abs(mouse_x) > 0 or abs(mouse_y) > 0:
rel_x = mouse_x * factor * pointer_speed / max_value
rel_y = mouse_y * factor * pointer_speed / max_value
pending_x_rel, rel_x = accumulate(pending_x_rel, rel_x)
pending_y_rel, rel_y = accumulate(pending_y_rel, rel_y)
if rel_x != 0:
_write(keymapper_device, EV_REL, REL_X, rel_x)
if rel_y != 0:
_write(keymapper_device, EV_REL, REL_Y, rel_y)
# wheel movements
float_rel_rx = abs_rx / max_value
pending_rx_rel, rel_rx = accumulate(pending_rx_rel, float_rel_rx)
if abs(float_rel_rx) > WHEEL_THRESHOLD:
_write(keymapper_device, EV_REL, REL_HWHEEL, -rel_rx)
float_rel_ry = abs_ry / max_value
pending_ry_rel, rel_ry = accumulate(pending_ry_rel, float_rel_ry)
if abs(float_rel_ry) > WHEEL_THRESHOLD:
_write(keymapper_device, EV_REL, REL_WHEEL, -rel_ry)
if abs(wheel_x) > 0:
float_rel_rx = wheel_x / max_value
pending_rx_rel, rel_rx = accumulate(pending_rx_rel, float_rel_rx)
if abs(float_rel_rx) > WHEEL_THRESHOLD:
_write(keymapper_device, EV_REL, REL_HWHEEL, -rel_rx)
if abs(wheel_y) > 0:
float_rel_ry = wheel_y / max_value
pending_ry_rel, rel_ry = accumulate(pending_ry_rel, float_rel_ry)
if abs(float_rel_ry) > WHEEL_THRESHOLD:
_write(keymapper_device, EV_REL, REL_WHEEL, -rel_ry)
# try to do this as close to 60hz as possible
time_taken = time.time() - start

@ -105,7 +105,7 @@ class KeycodeInjector:
self.mapping = mapping
self._process = None
self._msg_pipe = multiprocessing.Pipe()
self._code_to_code = self._map_codes_to_codes()
self._key_to_code = self._map_keys_to_codes()
self.stopped = False
# when moving the joystick and then staying at a position, no
@ -114,20 +114,21 @@ class KeycodeInjector:
# position.
self.abs_state = [0, 0, 0, 0]
def _map_codes_to_codes(self):
def _map_keys_to_codes(self):
"""To quickly get target keycodes during operation."""
_code_to_code = {}
for (_, keycode), output in self.mapping:
key_to_code = {}
for key, output in self.mapping:
if is_this_a_macro(output):
continue
target_keycode = system_mapping.get(output)
if target_keycode is None:
target_code = system_mapping.get(output)
if target_code is None:
logger.error('Don\'t know what %s is', output)
continue
_code_to_code[keycode] = target_keycode
return _code_to_code
key_to_code[key] = target_code
return key_to_code
def start_injecting(self):
"""Start injecting keycodes."""
@ -166,8 +167,8 @@ class KeycodeInjector:
capabilities = device.capabilities(absinfo=False)
needed = False
for (ev_type, keycode), _ in self.mapping:
if keycode in capabilities.get(ev_type, []):
for (ev_type, code, _), _ in self.mapping:
if code in capabilities.get(ev_type, []):
needed = True
break
@ -226,14 +227,14 @@ class KeycodeInjector:
# to act like the device.
capabilities = input_device.capabilities(absinfo=False)
if len(self._code_to_code) > 0 or len(macros) > 0:
if len(self._key_to_code) > 0 or len(macros) > 0:
if capabilities.get(EV_KEY) is None:
capabilities[EV_KEY] = []
# Furthermore, support all injected keycodes
for keycode in self._code_to_code.values():
if keycode not in capabilities[EV_KEY]:
capabilities[EV_KEY].append(keycode)
for code in self._key_to_code.values():
if code not in capabilities[EV_KEY]:
capabilities[EV_KEY].append(code)
# and all keycodes that are injected by macros
for macro in macros.values():
@ -302,13 +303,13 @@ class KeycodeInjector:
# each device parses the macros with a different handler
logger.debug('Parsing macros for %s', path)
macros = {}
for (_, keycode), output in self.mapping:
for key, output in self.mapping:
if is_this_a_macro(output):
macro = parse(output)
if macro is None:
continue
macros[keycode] = macro
macros[key] = macro
# certain capabilities can have side effects apparently. with an
# EV_ABS capability, EV_REL won't move the mouse pointer anymore.
@ -358,6 +359,8 @@ class KeycodeInjector:
except RuntimeError:
# stopped event loop most likely
pass
except OSError as error:
logger.error(str(error))
if len(coroutines) > 0:
logger.debug('asyncio coroutines ended')
@ -402,7 +405,7 @@ class KeycodeInjector:
continue
if should_map_event_as_btn(event.type, event.code):
handle_keycode(self._code_to_code, macros, event, uinput)
handle_keycode(self._key_to_code, macros, event, uinput)
continue
# forward the rest

@ -24,17 +24,28 @@
import asyncio
import evdev
from evdev.ecodes import EV_KEY, EV_ABS, ABS_MISC
from evdev.ecodes import EV_KEY, EV_ABS
from keymapper.logger import logger
from keymapper.util import sign
from keymapper.dev.ev_abs_mapper import JOYSTICK
# maps mouse buttons to macro instances that have been executed. They may
# still be running or already be done.
# still be running or already be done. Just like unreleased, this is a
# mapping of (type, code). The value is not included in the key, because
# a key release event with a value of 0 needs to be able to find the
# running macro. The downside is that a d-pad cannot execute two macros at
# once, one for each direction. Only sequentially.
active_macros = {}
# mapping of input (type, code) to the output keycode that has not yet
# been released. This is needed in order to release the correct event
# mapped on a D-Pad. Both directions on each axis report the same type,
# code and value (0) when releasing, but the correct release-event for
# the mapped output needs to be triggered.
unreleased = {}
def should_map_event_as_btn(ev_type, code):
"""Does this event describe a button.
@ -60,11 +71,6 @@ def should_map_event_as_btn(ev_type, code):
def is_key_down(event):
"""Is this event a key press."""
if event.type == EV_KEY:
# might be 2 for hold
return event.value == 1
# for all other event types, just fire for anything that is not 0
return event.value != 0
@ -73,76 +79,78 @@ def is_key_up(event):
return event.value == 0
def handle_keycode(code_to_code, macros, event, uinput):
"""Write the mapped keycode or forward unmapped ones.
def handle_keycode(key_to_code, macros, event, uinput):
"""Write mapped keycodes, forward unmapped ones and manage macros.
Parameters
----------
code_to_code : dict
mapping of linux-keycode to linux-keycode
key_to_code : dict
mapping of (type, code, value) to linux-keycode
macros : dict
mapping of linux-keycode to _Macro objects
mapping of (type, code, value) to _Macro objects
event : evdev.InputEvent
"""
if event.value == 2:
# button-hold event. Linux seems to create them on its own, no need
# to inject them.
if event.type == EV_KEY and event.value == 2:
# button-hold event. Linux creates them on its own for the
# injection-fake-device if the release event won't appear,
# no need to forward or map them.
return
input_keycode = event.code
input_type = event.type
# normalize event numbers to one of -1, 0, +1. Otherwise mapping
# trigger values that are between 1 and 255 is not possible, because
# they might skip the 1 when pressed fast enough.
key = (event.type, event.code, sign(event.value))
short = (event.type, event.code)
if input_keycode in macros:
if is_key_up(event):
existing_macro = active_macros.get(short)
if existing_macro is not None:
if is_key_up(event) and not existing_macro.running:
# key was released, but macro already stopped
return
if is_key_up(event) and existing_macro.holding:
# Tell the macro for that keycode that the key is released and
# let it decide what to with that information.
macro = active_macros.get(input_keycode)
if macro is not None and macro.holding:
macro.release_key()
existing_macro.release_key()
return
if not is_key_down(event):
if is_key_down(event) and existing_macro.running:
# for key-down events and running macros, don't do anything.
# This avoids spawning a second macro while the first one is not
# finished, especially since gamepad-triggers report a ton of
# events with a positive value.
return
existing_macro = active_macros.get(input_keycode)
if existing_macro is not None:
# make sure that a duplicate key-down event won't make a
# macro with a hold function run forever. there should always
# be only one active.
# Furthermore, don't stop and rerun the macro because gamepad
# triggers report events all the time just by releasing the key.
if existing_macro.running:
return
macro = macros[input_keycode]
active_macros[input_keycode] = macro
if key in macros:
macro = macros[key]
active_macros[short] = macro
macro.press_key()
logger.spam(
'got code:%s value:%s, maps to macro %s',
input_keycode,
event.value,
macro.code
)
logger.spam('got %s, maps to macro %s', key, macro.code)
asyncio.ensure_future(macro.run())
return
if input_keycode in code_to_code:
target_keycode = code_to_code[input_keycode]
if is_key_down(event) and short in unreleased:
# duplicate key-down. skip this event. Avoid writing millions of
# key-down events when a continuous value is reported, for example
# for gamepad triggers
return
if is_key_up(event) and short in unreleased:
target_type = EV_KEY
logger.spam(
'got code:%s value:%s event:%s, maps to EV_KEY:%s',
input_keycode,
event.value,
evdev.ecodes.EV[event.type],
target_keycode
)
target_value = 0
target_code = unreleased[short]
del unreleased[short]
elif key in key_to_code and is_key_down(event):
target_type = EV_KEY
target_value = 1
target_code = key_to_code[key]
unreleased[short] = target_code
logger.spam('got %s, maps to EV_KEY:%s', key, target_code)
else:
logger.spam(
'got unmapped code:%s value:%s',
input_keycode,
event.value,
)
target_keycode = input_keycode
target_type = input_type
uinput.write(target_type, target_keycode, event.value)
target_type = key[0]
target_code = key[1]
target_value = key[2]
logger.spam('got unmapped %s', key)
uinput.write(target_type, target_code, target_value)
uinput.syn()

@ -111,7 +111,7 @@ class _Macro:
async def run(self):
"""Run the macro."""
self.running = True
for task_type, task in self.tasks:
for _, task in self.tasks:
coroutine = task()
if asyncio.iscoroutine(coroutine):
await coroutine
@ -197,11 +197,11 @@ class _Macro:
try:
repeats = int(repeats)
except ValueError:
except ValueError as error:
raise ValueError(
'Expected the first param for r (repeat) to be '
f'a number, but got "{repeats}"'
)
) from error
for _ in range(repeats):
self.tasks.append((CHILD_MACRO, macro.run))
@ -225,7 +225,7 @@ class _Macro:
code = system_mapping.get(character)
if code is None:
raise KeyError(f'Unknown key "{character}"')
raise KeyError(f'aUnknown key "{character}"')
self.capabilities.add(code)
@ -239,11 +239,11 @@ class _Macro:
"""Wait time in milliseconds."""
try:
sleeptime = int(sleeptime)
except ValueError:
except ValueError as error:
raise ValueError(
'Expected the param for w (wait) to be '
f'a number, but got "{sleeptime}"'
)
) from error
sleeptime /= 1000
@ -343,12 +343,6 @@ def _parse_recurse(macro, macro_instance=None, depth=0):
call_match = re.match(r'^(\w+)\(', macro)
call = call_match[1] if call_match else None
if call is not None:
if 'k(' not in macro and 'm(' not in macro:
# maybe this just applies a modifier for a certain amout of time.
# and maybe it's a wait in repeat or something. Don't make it
# fail here.
logger.warn('"%s" doesn\'t write any keys (using k)', macro)
# available functions in the macro and the minimum and maximum number
# of their parameters
functions = {
@ -381,16 +375,18 @@ def _parse_recurse(macro, macro_instance=None, depth=0):
if len(params) < function[1] or len(params) > function[2]:
if function[1] != function[2]:
raise ValueError(
msg = (
f'{call} takes between {function[1]} and {function[2]}, '
f'not {len(params)} parameters'
)
else:
raise ValueError(
msg = (
f'{call} takes {function[1]}, '
f'not {len(params)} parameters'
)
raise ValueError(msg)
function[0](*params)
# is after this another call? Chain it to the macro_instance

@ -30,6 +30,7 @@ import evdev
from evdev.events import EV_KEY, EV_ABS
from keymapper.logger import logger
from keymapper.util import sign
from keymapper.getdevices import get_devices, refresh_devices
from keymapper.dev.keycode_mapper import should_map_event_as_btn
@ -43,8 +44,14 @@ PRIORITIES = {
def prioritize(events):
"""Return the event that is most likely desired to be mapped."""
return sorted(events, key=lambda e: PRIORITIES[e.type])[-1]
"""Return the event that is most likely desired to be mapped.
High absolute values (down) over low values (up), KEY over ABS.
"""
return sorted(events, key=lambda e: (
PRIORITIES[e.type],
abs(e.value)
))[-1]
class _KeycodeReader:
@ -124,10 +131,10 @@ class _KeycodeReader:
if should_map_event_as_btn(event.type, event.code):
logger.spam(
'got code:%s value:%s type:%s',
'got (%s, %s, %s)',
event.type,
event.code,
event.value,
evdev.ecodes.EV[event.type]
event.value
)
self._pipe[1].send(event)
@ -171,7 +178,7 @@ class _KeycodeReader:
if self.fail_counter % 10 == 0:
# spam less
logger.debug('No pipe available to read from')
return None, None
return None
newest_event = self.newest_event
newest_time = (
@ -182,6 +189,9 @@ class _KeycodeReader:
while self._pipe[0].poll():
event = self._pipe[0].recv()
if event.value == 0:
continue
time = event.sec + event.usec / 1000000
delta = time - newest_time
@ -190,8 +200,8 @@ class _KeycodeReader:
# spam from the device. The wacom intuos 5 adds an
# ABS_MISC event to every button press, filter that out
logger.spam(
'Ignoring event code:%s, value:%s, type:%s',
evdev.ecodes.EV[event.type], event.code, event.value
'Ignoring event (%s, %s, %s)',
event.type, event.code, event.value
)
continue
@ -200,14 +210,15 @@ class _KeycodeReader:
if newest_event == self.newest_event:
# don't return the same event twice
return None, None
return None
self.newest_event = newest_event
return (
(None, None) if newest_event is None
else (newest_event.type, newest_event.code)
)
return (None if newest_event is None else (
newest_event.type,
newest_event.code,
sign(newest_event.value)
))
keycode_reader = _KeycodeReader()

@ -38,12 +38,31 @@ for name in system_mapping.list_names():
store.append([name])
def to_string(ev_type, code):
def to_string(ev_type, code, value):
"""A nice to show description of the pressed key."""
try:
name = evdev.ecodes.bytype[ev_type][code]
if isinstance(name, list):
name = name[0]
if ev_type != evdev.ecodes.EV_KEY:
direction = {
(evdev.ecodes.ABS_HAT0X, -1): 'L',
(evdev.ecodes.ABS_HAT0X, 1): 'R',
(evdev.ecodes.ABS_HAT0Y, -1): 'U',
(evdev.ecodes.ABS_HAT0Y, 1): 'D',
(evdev.ecodes.ABS_HAT1X, -1): 'L',
(evdev.ecodes.ABS_HAT1X, 1): 'R',
(evdev.ecodes.ABS_HAT1Y, -1): 'U',
(evdev.ecodes.ABS_HAT1Y, 1): 'D',
(evdev.ecodes.ABS_HAT2X, -1): 'L',
(evdev.ecodes.ABS_HAT2X, 1): 'R',
(evdev.ecodes.ABS_HAT2Y, -1): 'U',
(evdev.ecodes.ABS_HAT2Y, 1): 'D',
}.get((code, value))
if direction is not None:
name += f' {direction}'
return name.replace('KEY_', '')
except KeyError:
return 'unknown'
@ -53,11 +72,14 @@ class Row(Gtk.ListBoxRow):
"""A single, configurable key mapping."""
__gtype_name__ = 'ListBoxRow'
def __init__(
self, delete_callback, window, ev_type=None, keycode=None,
character=None
):
"""Construct a row widget."""
def __init__(self, delete_callback, window, key=None, character=None):
"""Construct a row widget.
Parameters
----------
key : int, int, int
event, code, value
"""
super().__init__()
self.device = window.selected_device
self.window = window
@ -66,13 +88,12 @@ class Row(Gtk.ListBoxRow):
self.character_input = None
self.keycode_input = None
self.ev_type = ev_type
self.keycode = keycode
self.key = key
self.put_together(character)
def get_keycode(self):
"""Get a tuple of event_type and keycode from the left column.
"""Get a tuple of type, code and value from the left column.
Or None if no codes are mapped on this row.
"""
@ -80,45 +101,39 @@ class Row(Gtk.ListBoxRow):
if not keycode:
return None
return self.ev_type, self.keycode
return self.key
def get_character(self):
"""Get the assigned character from the middle column."""
character = self.character_input.get_text()
return character if character else None
def set_new_keycode(self, ev_type, new_keycode):
def set_new_keycode(self, new_key):
"""Check if a keycode has been pressed and if so, display it."""
# the newest_keycode is populated since the ui regularly polls it
# in order to display it in the status bar.
key = self.get_keycode()
previous_type = key[0] if key else None
previous_keycode = key[1] if key else None
character = self.get_character()
previous_key = self.get_keycode()
# no input
if new_keycode is None:
if new_key is None:
return
# keycode didn't change, do nothing
if new_keycode == previous_keycode:
if new_key == previous_key:
return
# keycode is already set by some other row
existing = custom_mapping.get_character(ev_type, new_keycode)
existing = custom_mapping.get_character(new_key)
if existing is not None:
name = to_string(ev_type, new_keycode)
msg = f'"{name}" already mapped to {existing}"'
msg = f'"{to_string(*new_key)}" already mapped to {existing}"'
logger.info(msg)
self.window.get('status_bar').push(CTX_KEYCODE, msg)
return
# it's legal to display the keycode
self.window.get('status_bar').remove_all(CTX_KEYCODE)
self.keycode_input.set_label(to_string(ev_type, new_keycode))
self.ev_type = ev_type
self.keycode = new_keycode
self.keycode_input.set_label(to_string(*new_key))
self.key = new_key
# switch to the character, don't require mouse input because
# that would overwrite the key with the mouse-button key if
# the current device is a mouse. idle_add this so that the
@ -127,15 +142,17 @@ class Row(Gtk.ListBoxRow):
GLib.idle_add(lambda: window.set_focus(self.character_input))
self.highlight()
character = self.get_character()
# the character is empty and therefore the mapping is not complete
if character is None:
return
# else, the keycode has changed, the character is set, all good
custom_mapping.change(
new=(ev_type, new_keycode),
new_key=new_key,
character=character,
previous=(previous_type, previous_keycode)
previous_key=previous_key
)
def highlight(self):
@ -151,14 +168,16 @@ class Row(Gtk.ListBoxRow):
key = self.get_keycode()
character = self.get_character()
if character is None:
return
self.highlight()
if key is not None:
ev_type, keycode = key
custom_mapping.change(
new=(ev_type, keycode),
new_key=key,
character=character,
previous=(None, None)
previous_key=None
)
def match(self, completion, key, iter):
@ -182,8 +201,8 @@ class Row(Gtk.ListBoxRow):
keycode_input = Gtk.ToggleButton()
keycode_input.set_size_request(130, -1)
if self.keycode is not None:
keycode_input.set_label(to_string(self.ev_type, self.keycode))
if self.key is not None:
keycode_input.set_label(to_string(*self.key))
# make the togglebutton go back to its normal state when doing
# something else in the UI
@ -208,6 +227,7 @@ class Row(Gtk.ListBoxRow):
if character is not None:
character_input.set_text(character)
character_input.connect(
'changed',
self.on_character_input_change
@ -232,8 +252,8 @@ class Row(Gtk.ListBoxRow):
"""Destroy the row and remove it from the config."""
key = self.get_keycode()
if key is not None:
ev_type, keycode = key
custom_mapping.clear(ev_type, keycode)
custom_mapping.clear(key)
self.character_input.set_text('')
self.keycode_input.set_label('')
self.delete_callback(self)

@ -250,10 +250,11 @@ class Window:
def consume_newest_keycode(self):
"""To capture events from keyboards, mice and gamepads."""
# the "event" event of Gtk.Window wouldn't trigger on gamepad
# events, so it became a GLib timeout
ev_type, keycode = keycode_reader.read()
# events, so it became a GLib timeout to periodically check kernel
# events.
key = keycode_reader.read()
if keycode is None or ev_type is None:
if key is None:
return True
click_events = [
@ -261,18 +262,18 @@ class Window:
evdev.ecodes.BTN_TOOL_DOUBLETAP
]
if ev_type == EV_KEY and keycode in click_events:
if key[0] == EV_KEY and key[1] in click_events:
# disable mapping the left mouse button because it would break
# the mouse. Also it is emitted right when focusing the row
# which breaks the current workflow.
return True
self.get('keycode').set_text(to_string(ev_type, keycode))
self.get('keycode').set_text(to_string(*key))
# inform the currently selected row about the new keycode
row, focused = self.get_focused_row()
if isinstance(focused, Gtk.ToggleButton):
row.set_new_keycode(ev_type, keycode)
row.set_new_keycode(key)
return True
@ -293,7 +294,7 @@ class Window:
def check_macro_syntax(self):
"""Check if the programmed macros are allright."""
for (ev_type, keycode), output in custom_mapping:
for key, output in custom_mapping:
if not is_this_a_macro(output):
continue
@ -301,7 +302,7 @@ class Window:
if error is None:
continue
position = to_string(ev_type, keycode)
position = to_string(*key)
msg = f'Syntax error at {position}, hover for info'
self.show_status(CTX_ERROR, msg, error)
@ -431,12 +432,11 @@ class Window:
custom_mapping.load(self.selected_device, self.selected_preset)
key_list = self.get('key_list')
for (ev_type, keycode), output in custom_mapping:
for key, output in custom_mapping:
single_key_mapping = Row(
window=self,
delete_callback=self.on_row_removed,
ev_type=ev_type,
keycode=keycode,
key=key,
character=output
)
key_list.insert(single_key_mapping, -1)

@ -116,6 +116,13 @@ def log_info():
logger.info('Could not figure out the version')
logger.debug(error)
if is_debug():
logger.warning(
'Debug level will log all your keystrokes! Do not post this '
'output in the internet if you typed in sensitive or private '
'information with your device!'
)
logger.debug('pid %s', os.getpid())
@ -141,11 +148,7 @@ def update_verbosity(debug):
def add_filehandler(path=LOG_PATH):
"""Clear the existing logfile and start logging to it."""
if is_debug():
logger.warning(
'Debug level will log all your keystrokes to "%s"',
LOG_PATH
)
logger.info('This output is also stored in "%s"', LOG_PATH)
log_path = os.path.expanduser(path)
log_file = os.path.join(log_path, 'log')

@ -30,6 +30,15 @@ from keymapper.logger import logger
from keymapper.paths import get_config_path, touch
def verify_key(key):
"""Check if the key describes a tuple of (type, code, value) ints."""
if len(key) != 3:
raise ValueError(f'Expected keys to be a 3-tuple, but got {key}')
if sum([not isinstance(value, int) for value in key]) != 0:
raise ValueError(f'Can only use numbers in the tuples, but got {key}')
class Mapping:
"""Contains and manages mappings.
@ -50,83 +59,75 @@ class Mapping:
def __len__(self):
return len(self._mapping)
def change(self, new, character, previous=(None, None)):
def change(self, new_key, character, previous_key=None):
"""Replace the mapping of a keycode with a different one.
Return True on success.
Parameters
----------
new : int, int
new_key : int, int, int
the new key. (type, code, value). key as in hashmap-key
0: type, one of evdev.events, taken from the original source
event. Everything will be mapped to EV_KEY.
1: The source keycode, what the mouse would report without any
modification.
character : string or string[]
2. The value. 1 (down), 2 (up) or any
other value that the device reports. Gamepads use a continuous
space of values for joysticks and triggers.
character : string
A single character known to xkb or linux.
Examples: KP_1, Shift_L, a, B, BTN_LEFT.
previous : int, int
If None, will not remove any previous mapping. If you recently
previous_key : int, int, int
the previous key, same format as new_key
If not set, will not remove any previous mapping. If you recently
used 10 for new_keycode and want to overwrite that with 11,
provide 5 here.
"""
new_type, new_keycode = new
prev_type, prev_keycode = previous
# either both of them are None, or both integers
if prev_keycode is None and prev_keycode != prev_type:
logger.error('Got (%s, %s) for previous', prev_type, prev_keycode)
if new_keycode is None and prev_keycode != new_type:
logger.error('Got (%s, %s) for new', new_type, new_keycode)
try:
new_keycode = int(new_keycode)
new_type = int(new_type)
if prev_keycode is not None:
prev_keycode = int(prev_keycode)
if prev_type is not None:
prev_type = int(prev_type)
except (TypeError, ValueError):
logger.error('Can only use numbers in the tuples')
return False
if new_keycode and character:
logger.debug(
'type:%s, code:%s will map to %s, replacing type:%s, code:%s',
new_type, new_keycode, character, prev_type, prev_keycode
)
self._mapping[(new_type, new_keycode)] = character
code_changed = new_keycode != prev_keycode
if code_changed and prev_keycode is not None:
if character is None:
raise ValueError('Expected `character` not to be None')
verify_key(new_key)
if previous_key:
verify_key(previous_key)
logger.debug(
'%s will map to %s, replacing %s',
new_key, character, previous_key
)
self._mapping[new_key] = character
if previous_key is not None:
code_changed = new_key != previous_key
if code_changed:
# clear previous mapping of that code, because the line
# representing that one will now represent a different one.
self.clear(prev_type, prev_keycode)
self.changed = True
return True
# representing that one will now represent a different one
self.clear(previous_key)
return False
self.changed = True
def clear(self, ev_type, keycode):
def clear(self, key):
"""Remove a keycode from the mapping.
Parameters
----------
keycode : int
ev_type : int
one of evdev.events
key : int, int, int
keycode : int
ev_type : int
one of evdev.events. codes may be the same for various
event types.
value : int
event value. Usually you want 1 (down)
"""
assert keycode is not None
assert ev_type is not None
assert isinstance(ev_type, int)
assert isinstance(keycode, int)
if self._mapping.get((ev_type, keycode)) is not None:
logger.debug(
'type:%s, code:%s will be cleared',
ev_type, keycode
)
del self._mapping[(ev_type, keycode)]
verify_key(key)
if self._mapping.get(key) is not None:
logger.debug('%s will be cleared', key)
del self._mapping[key]
self.changed = True
return
logger.error('Unknown key %s', key)
def empty(self):
"""Remove all mappings."""
@ -144,7 +145,7 @@ class Mapping:
with open(path, 'r') as file:
preset_dict = json.load(file)
if preset_dict.get('mapping') is None:
if not isinstance(preset_dict.get('mapping'), dict):
logger.error('Invalid preset config at "%s"', path)
return
@ -153,18 +154,23 @@ class Mapping:
logger.error('Found invalid key: "%s"', key)
continue
ev_type, keycode = key.split(',')
try:
keycode = int(keycode)
except ValueError:
logger.error('Found non-int keycode: "%s"', keycode)
continue
if key.count(',') == 1:
# support for legacy mapping objects that didn't include
# the value in the key
ev_type, code = key.split(',')
value = 1
if key.count(',') == 2:
ev_type, code, value = key.split(',')
try:
ev_type = int(ev_type)
key = (int(ev_type), int(code), int(value))
except ValueError:
logger.error('Found non-int ev_type: "%s"', ev_type)
logger.error('Found non-int in: "%s"', key)
continue
self._mapping[(ev_type, keycode)] = character
logger.spam('%s maps to %s', key, character)
self._mapping[key] = character
# add any metadata of the mapping
for key in preset_dict:
@ -192,9 +198,9 @@ class Mapping:
# make sure to keep the option to add metadata if ever needed,
# so put the mapping into a special key
json_ready_mapping = {}
# tuple keys are not possible in json
# tuple keys are not possible in json, encode them as string
for key, value in self._mapping.items():
new_key = f'{key[0]},{key[1]}'
new_key = ','.join([str(value) for value in key])
json_ready_mapping[new_key] = value
preset_dict = {'mapping': json_ready_mapping}
@ -204,14 +210,17 @@ class Mapping:
self.changed = False
def get_character(self, ev_type, keycode):
def get_character(self, key):
"""Read the character that is mapped to this keycode.
Parameters
----------
keycode : int
ev_type : int
one of evdev.events. codes may be the same for various
event types.
key : int, int, int
keycode : int
ev_type : int
one of evdev.events. codes may be the same for various
event types.
value : int
event value. Usually you want 1 (down)
"""
return self._mapping.get((ev_type, keycode))
return self._mapping.get(key)

@ -0,0 +1,33 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2020 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/>.
"""Utility functions."""
def sign(value):
"""Get the sign of the value, or 0 if 0."""
if value > 0:
return 1
if value < 0:
return -1
return 0

@ -15,7 +15,7 @@
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
<text x="31.5" y="14">coverage</text>
<text x="80" y="15" fill="#010101" fill-opacity=".3">90%</text>
<text x="80" y="14">90%</text>
<text x="80" y="15" fill="#010101" fill-opacity=".3">91%</text>
<text x="80" y="14">91%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 904 B

After

Width:  |  Height:  |  Size: 904 B

@ -17,7 +17,7 @@
<text x="22.0" y="14">pylint</text>
</g>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="63.0" y="15" fill="#010101" fill-opacity=".3">9.67</text>
<text x="62.0" y="14">9.67</text>
<text x="63.0" y="15" fill="#010101" fill-opacity=".3">9.76</text>
<text x="62.0" y="14">9.76</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -36,9 +36,6 @@ import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GLib', '2.0')
from keymapper.logger import update_verbosity
from keymapper.dev.injector import KeycodeInjector
assert not os.getcwd().endswith('tests')
@ -111,6 +108,8 @@ fixtures = {
evdev.ecodes.EV_ABS: [
evdev.ecodes.ABS_X,
evdev.ecodes.ABS_Y,
evdev.ecodes.ABS_RX,
evdev.ecodes.ABS_RY,
evdev.ecodes.ABS_HAT0X
]
},
@ -174,6 +173,9 @@ class InputEvent:
self.code = code
self.value = value
# tuple shorthand
self.t = (type, code, value)
if timestamp is None:
timestamp = time.time()
@ -210,95 +212,97 @@ def patch_select():
select.select = new_select
def patch_evdev():
def list_devices():
return fixtures.keys()
class InputDevice:
# expose as existing attribute, otherwise the patch for
# evdev < 1.0.0 will crash the test
path = None
class InputDevice:
# expose as existing attribute, otherwise the patch for
# evdev < 1.0.0 will crash the test
path = None
def __init__(self, path):
if path not in fixtures:
raise FileNotFoundError()
def __init__(self, path):
if path not in fixtures:
raise FileNotFoundError()
self.path = path
self.phys = fixtures[path]['phys']
self.name = fixtures[path]['name']
self.fd = self.name
self.capa = copy.deepcopy(fixtures[self.path]['capabilities'])
self.path = path
self.phys = fixtures[path]['phys']
self.name = fixtures[path]['name']
self.fd = self.name
self.capa = copy.deepcopy(fixtures[self.path]['capabilities'])
def absinfo(axis):
return {
evdev.ecodes.EV_ABS: evdev.AbsInfo(
value=None, min=None, fuzz=None, flat=None,
resolution=None, max=MAX_ABS
)
}[axis]
def absinfo(axis):
return {
evdev.ecodes.EV_ABS: evdev.AbsInfo(
value=None, min=None, fuzz=None, flat=None,
resolution=None, max=MAX_ABS
)
}[axis]
self.absinfo = absinfo
self.absinfo = absinfo
def grab(self):
pass
def grab(self):
pass
def read(self):
ret = pending_events.get(self.name, [])
if ret is not None:
# consume all of them
pending_events[self.name] = []
def read(self):
ret = pending_events.get(self.name, [])
if ret is not None:
# consume all of them
pending_events[self.name] = []
return ret
return ret
def read_one(self):
if pending_events.get(self.name) is None:
return None
def read_one(self):
if pending_events.get(self.name) is None:
return None
if len(pending_events[self.name]) == 0:
return None
if len(pending_events[self.name]) == 0:
return None
event = pending_events[self.name].pop(0)
return event
event = pending_events[self.name].pop(0)
return event
def read_loop(self):
"""Read all prepared events at once."""
if pending_events.get(self.name) is None:
return
def read_loop(self):
"""Read all prepared events at once."""
if pending_events.get(self.name) is None:
return
while len(pending_events[self.name]) > 0:
yield pending_events[self.name].pop(0)
time.sleep(EVENT_READ_TIMEOUT)
while len(pending_events[self.name]) > 0:
yield pending_events[self.name].pop(0)
time.sleep(EVENT_READ_TIMEOUT)
async def async_read_loop(self):
"""Read all prepared events at once."""
if pending_events.get(self.name) is None:
return
async def async_read_loop(self):
"""Read all prepared events at once."""
if pending_events.get(self.name) is None:
return
while len(pending_events[self.name]) > 0:
yield pending_events[self.name].pop(0)
await asyncio.sleep(0.01)
while len(pending_events[self.name]) > 0:
yield pending_events[self.name].pop(0)
await asyncio.sleep(0.01)
def capabilities(self, absinfo=True):
return self.capa
def capabilities(self, absinfo=True):
return self.capa
class UInput:
def __init__(self, *args, **kwargs):
self.fd = 0
self.write_count = 0
self.device = InputDevice('/dev/input/event40')
pass
class UInput:
def __init__(self, *args, **kwargs):
self.fd = 0
self.write_count = 0
self.device = InputDevice('/dev/input/event40')
pass
def capabilities(self, *args, **kwargs):
return []
def capabilities(self, *args, **kwargs):
return []
def write(self, type, code, value):
self.write_count += 1
event = InputEvent(type, code, value)
uinput_write_history.append(event)
uinput_write_history_pipe[1].send(event)
def write(self, type, code, value):
self.write_count += 1
event = InputEvent(type, code, value)
uinput_write_history.append(event)
uinput_write_history_pipe[1].send(event)
def syn(self):
pass
def syn(self):
pass
def patch_evdev():
def list_devices():
return fixtures.keys()
evdev.list_devices = list_devices
evdev.InputDevice = InputDevice
@ -326,6 +330,8 @@ patch_evdev()
patch_unsaved()
patch_select()
from keymapper.logger import update_verbosity
from keymapper.dev.injector import KeycodeInjector
# no need for a high number in tests
KeycodeInjector.regrab_timeout = 0.15

@ -28,7 +28,6 @@ class TestConfig(unittest.TestCase):
def tearDown(self):
config.clear_config()
self.assertEqual(len(config.iterate_autoload_presets()), 0)
config.save_config()
def test_get_default(self):
config._config = {}

@ -25,7 +25,7 @@ import unittest
import time
import evdev
from evdev.ecodes import EV_KEY
from evdev.ecodes import EV_KEY, EV_ABS
from gi.repository import Gtk
from keymapper.state import custom_mapping, system_mapping
@ -78,13 +78,14 @@ class TestDaemon(unittest.TestCase):
system_mapping.populate()
def test_daemon(self):
keycode_from_1 = 9
ev_1 = (EV_KEY, 9)
ev_2 = (EV_ABS, 12)
keycode_to_1 = 100
keycode_from_2 = 12
keycode_to_2 = 100
keycode_to_2 = 101
custom_mapping.change((*ev_1, 1), 'a')
custom_mapping.change((*ev_2, -1), 'b')
custom_mapping.change((EV_KEY, keycode_from_1), 'a')
custom_mapping.change((EV_KEY, keycode_from_2), 'b')
system_mapping.clear()
system_mapping._set('a', keycode_to_1)
system_mapping._set('b', keycode_to_2)
@ -94,8 +95,11 @@ class TestDaemon(unittest.TestCase):
custom_mapping.save('device 2', preset)
config.set_autoload_preset('device 2', preset)
"""injection 1"""
# should forward the event unchanged
pending_events['device 2'] = [
InputEvent(evdev.events.EV_KEY, keycode_from_1, 0),
InputEvent(EV_KEY, 13, 1)
]
self.daemon = Daemon()
@ -105,16 +109,18 @@ class TestDaemon(unittest.TestCase):
self.assertFalse(self.daemon.is_injecting('device 1'))
event = uinput_write_history_pipe[0].recv()
self.assertEqual(event.type, evdev.events.EV_KEY)
self.assertEqual(event.code, keycode_to_1)
self.assertEqual(event.value, 0)
self.assertEqual(event.type, EV_KEY)
self.assertEqual(event.code, 13)
self.assertEqual(event.value, 1)
self.daemon.stop_injecting('device 2')
self.assertFalse(self.daemon.is_injecting('device 2'))
"""injection 2"""
# -1234 will be normalized to -1 by the injector
pending_events['device 2'] = [
InputEvent(evdev.events.EV_KEY, keycode_from_2, 1),
InputEvent(evdev.events.EV_KEY, keycode_from_2, 0),
InputEvent(*ev_2, -1234)
]
time.sleep(0.2)
@ -122,16 +128,13 @@ class TestDaemon(unittest.TestCase):
self.daemon.start_injecting('device 2', preset)
# the written key is a key-down event, not the original
# event value of -5678
event = uinput_write_history_pipe[0].recv()
self.assertEqual(event.type, evdev.events.EV_KEY)
self.assertEqual(event.type, EV_KEY)
self.assertEqual(event.code, keycode_to_2)
self.assertEqual(event.value, 1)
event = uinput_write_history_pipe[0].recv()
self.assertEqual(event.type, evdev.events.EV_KEY)
self.assertEqual(event.code, keycode_to_2)
self.assertEqual(event.value, 0)
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,143 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2020 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
import asyncio
from evdev.ecodes import EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL
from keymapper.dev.ev_abs_mapper import ev_abs_mapper
from keymapper.config import config
from keymapper.dev.ev_abs_mapper import MOUSE, WHEEL
from tests.test import InputDevice, UInput, MAX_ABS, clear_write_history, \
uinput_write_history
abs_state = [0, 0, 0, 0]
SPEED = 20
class TestEvAbsMapper(unittest.TestCase):
# there is also `test_abs_to_rel` in test_injector.py
def setUp(self):
config.set('gamepad.joystick.non_linearity', 1)
config.set('gamepad.joystick.pointer_speed', SPEED)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
device = InputDevice('/dev/input/event30')
uinput = UInput()
asyncio.ensure_future(ev_abs_mapper(abs_state, device, uinput))
def tearDown(self):
config.clear_config()
loop = asyncio.get_event_loop()
for task in asyncio.Task.all_tasks():
task.cancel()
loop.stop()
loop.close()
clear_write_history()
def do(self, a, b, c, d, expectation):
"""Present fake values to the loop and observe the outcome."""
clear_write_history()
abs_state[0] = a
abs_state[1] = b
abs_state[2] = c
abs_state[3] = d
# 3 frames
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.sleep(3 / 60))
history = [h.t for h in uinput_write_history]
# sleep long enough to test if multiple events are written
self.assertGreater(len(history), 1)
self.assertIn(expectation, history)
self.assertEqual(history.count(expectation), len(history))
def test_joystick_purpose_1(self):
config.set('gamepad.joystick.left_purpose', MOUSE)
config.set('gamepad.joystick.right_purpose', WHEEL)
self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_X, SPEED))
self.do(-MAX_ABS, 0, 0, 0, (EV_REL, REL_X, -SPEED))
self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_Y, SPEED))
self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_Y, -SPEED))
# wheel event values are negative
self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_HWHEEL, -1))
self.do(0, 0, -MAX_ABS, 0, (EV_REL, REL_HWHEEL, 1))
self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_WHEEL, -1))
self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_WHEEL, 1))
def test_joystick_purpose_2(self):
config.set('gamepad.joystick.left_purpose', WHEEL)
config.set('gamepad.joystick.right_purpose', MOUSE)
self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, -1))
self.do(-MAX_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, 1))
self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_WHEEL, -1))
self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_WHEEL, 1))
# wheel event values are negative
self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_X, SPEED))
self.do(0, 0, -MAX_ABS, 0, (EV_REL, REL_X, -SPEED))
self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_Y, SPEED))
self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_Y, -SPEED))
def test_joystick_purpose_3(self):
config.set('gamepad.joystick.left_purpose', MOUSE)
config.set('gamepad.joystick.right_purpose', MOUSE)
self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_X, SPEED))
self.do(-MAX_ABS, 0, 0, 0, (EV_REL, REL_X, -SPEED))
self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_Y, SPEED))
self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_Y, -SPEED))
# wheel event values are negative
self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_X, SPEED))
self.do(0, 0, -MAX_ABS, 0, (EV_REL, REL_X, -SPEED))
self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_Y, SPEED))
self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_Y, -SPEED))
def test_joystick_purpose_4(self):
config.set('gamepad.joystick.left_purpose', WHEEL)
config.set('gamepad.joystick.right_purpose', WHEEL)
self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, -1))
self.do(-MAX_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, 1))
self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_WHEEL, -1))
self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_WHEEL, 1))
# wheel event values are negative
self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_HWHEEL, -1))
self.do(0, 0, -MAX_ABS, 0, (EV_REL, REL_HWHEEL, 1))
self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_WHEEL, -1))
self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_WHEEL, 1))
if __name__ == "__main__":
unittest.main()

@ -82,16 +82,16 @@ class TestInjector(unittest.TestCase):
}
mapping = Mapping()
mapping.change((EV_KEY, 80), 'a')
mapping.change((EV_KEY, 80, 1), 'a')
macro_code = 'r(2, m(sHiFt_l, r(2, k(1).k(2))))'
macro = parse(macro_code)
mapping.change((EV_KEY, 60), macro_code)
mapping.change((EV_KEY, 60, 111), macro_code)
# going to be ignored, because EV_REL cannot be mapped, that's
# mouse movements.
mapping.change((EV_REL, 1234), 'b')
mapping.change((EV_REL, 1234, 3), 'b')
a = system_mapping.get('a')
shift_l = system_mapping.get('ShIfT_L')
@ -119,7 +119,7 @@ class TestInjector(unittest.TestCase):
def test_grab(self):
# path is from the fixtures
custom_mapping.change((EV_KEY, 10), 'a')
custom_mapping.change((EV_KEY, 10, 1), 'a')
self.injector = KeycodeInjector('device 1', custom_mapping)
path = '/dev/input/event10'
@ -133,7 +133,7 @@ class TestInjector(unittest.TestCase):
def test_fail_grab(self):
self.make_it_fail = 10
custom_mapping.change((EV_KEY, 10), 'a')
custom_mapping.change((EV_KEY, 10, 1), 'a')
self.injector = KeycodeInjector('device 1', custom_mapping)
path = '/dev/input/event10'
@ -150,7 +150,7 @@ class TestInjector(unittest.TestCase):
def test_prepare_device_1(self):
# according to the fixtures, /dev/input/event30 can do ABS_HAT0X
custom_mapping.change((EV_ABS, ABS_HAT0X), 'a')
custom_mapping.change((EV_ABS, ABS_HAT0X, 1), 'a')
self.injector = KeycodeInjector('foobar', custom_mapping)
_prepare_device = self.injector._prepare_device
@ -158,7 +158,7 @@ class TestInjector(unittest.TestCase):
self.assertIsNotNone(_prepare_device('/dev/input/event30')[0])
def test_prepare_device_non_existing(self):
custom_mapping.change((EV_ABS, ABS_HAT0X), 'a')
custom_mapping.change((EV_ABS, ABS_HAT0X, 1), 'a')
self.injector = KeycodeInjector('foobar', custom_mapping)
_prepare_device = self.injector._prepare_device
@ -186,7 +186,7 @@ class TestInjector(unittest.TestCase):
def test_skip_unused_device(self):
# skips a device because its capabilities are not used in the mapping
custom_mapping.change((EV_KEY, 10), 'a')
custom_mapping.change((EV_KEY, 10, 1), 'a')
self.injector = KeycodeInjector('device 1', custom_mapping)
path = '/dev/input/event11'
device, abs_to_rel = self.injector._prepare_device(path)
@ -292,11 +292,11 @@ class TestInjector(unittest.TestCase):
def test_injector(self):
numlock_before = is_numlock_on()
custom_mapping.change((EV_KEY, 8), 'k(KEY_Q).k(w)')
custom_mapping.change((EV_ABS, ABS_HAT0X), 'a')
custom_mapping.change((EV_KEY, 8, 1), 'k(KEY_Q).k(w)')
custom_mapping.change((EV_ABS, ABS_HAT0X, -1), 'a')
# one mapping that is unknown in the system_mapping on purpose
input_b = 10
custom_mapping.change((EV_KEY, input_b), 'b')
custom_mapping.change((EV_KEY, input_b, 1), 'b')
system_mapping.clear()
code_a = 100
@ -312,8 +312,8 @@ class TestInjector(unittest.TestCase):
# should execute a macro
InputEvent(EV_KEY, 8, 1),
InputEvent(EV_KEY, 8, 0),
# normal keystrokes
InputEvent(EV_ABS, ABS_HAT0X, 1),
# gamepad stuff
InputEvent(EV_ABS, ABS_HAT0X, -1),
InputEvent(EV_ABS, ABS_HAT0X, 0),
# just pass those over without modifying
InputEvent(EV_KEY, 10, 1),
@ -366,6 +366,7 @@ class TestInjector(unittest.TestCase):
del history[index_w_0]
# the rest should be in order.
# this should be 1. injected keycodes should always be either 0 or 1
self.assertEqual(history[0], (ev_key, code_a, 1))
self.assertEqual(history[1], (ev_key, code_a, 0))
self.assertEqual(history[2], (ev_key, input_b, 1))

@ -25,7 +25,7 @@ import grp
import os
import unittest
import evdev
from evdev.events import EV_KEY
from evdev.events import EV_KEY, EV_ABS
import json
from unittest.mock import patch
from importlib.util import spec_from_loader, module_from_spec
@ -160,9 +160,9 @@ class TestIntegration(unittest.TestCase):
def test_select_device(self):
# creates a new empty preset when no preset exists for the device
self.window.on_select_device(FakeDropdown('device 1'))
custom_mapping.change((EV_KEY, 50), 'q')
custom_mapping.change((EV_KEY, 51), 'u')
custom_mapping.change((EV_KEY, 52), 'x')
custom_mapping.change((EV_KEY, 50, 1), 'q')
custom_mapping.change((EV_KEY, 51, 1), 'u')
custom_mapping.change((EV_KEY, 52, 1), 'x')
self.assertEqual(len(custom_mapping), 3)
self.window.on_select_device(FakeDropdown('device 2'))
self.assertEqual(len(custom_mapping), 0)
@ -181,8 +181,11 @@ class TestIntegration(unittest.TestCase):
def test_row_keycode_to_string(self):
# not an integration test, but I have all the row tests here already
self.assertEqual(to_string(EV_KEY, 10), '9')
self.assertEqual(to_string(EV_KEY, 39), 'SEMICOLON')
self.assertEqual(to_string(EV_KEY, evdev.ecodes.KEY_9, 1), '9')
self.assertEqual(to_string(EV_KEY, evdev.ecodes.KEY_SEMICOLON, 1), 'SEMICOLON')
self.assertEqual(to_string(EV_ABS, evdev.ecodes.ABS_HAT0X, -1), 'ABS_HAT0X L')
self.assertEqual(to_string(EV_ABS, evdev.ecodes.ABS_HAT0X, 1), 'ABS_HAT0X R')
self.assertEqual(to_string(EV_KEY, evdev.ecodes.BTN_A, 1), 'BTN_A')
def test_row_simple(self):
rows = self.window.get('key_list').get_children()
@ -190,21 +193,21 @@ class TestIntegration(unittest.TestCase):
row = rows[0]
row.set_new_keycode(None, None)
row.set_new_keycode(None)
self.assertIsNone(row.get_keycode())
self.assertEqual(len(custom_mapping), 0)
self.assertEqual(row.keycode_input.get_label(), None)
row.set_new_keycode(EV_KEY, 30)
row.set_new_keycode((EV_KEY, 30, 1))
self.assertEqual(len(custom_mapping), 0)
self.assertEqual(row.get_keycode(), (EV_KEY, 30))
self.assertEqual(row.get_keycode(), (EV_KEY, 30, 1))
# this is KEY_A in linux/input-event-codes.h,
# but KEY_ is removed from the text
self.assertEqual(row.keycode_input.get_label(), 'A')
row.set_new_keycode(EV_KEY, 30)
row.set_new_keycode((EV_KEY, 30, 1))
self.assertEqual(len(custom_mapping), 0)
self.assertEqual(row.get_keycode(), (EV_KEY, 30))
self.assertEqual(row.get_keycode(), (EV_KEY, 30, 1))
time.sleep(0.1)
gtk_iteration()
@ -217,21 +220,23 @@ class TestIntegration(unittest.TestCase):
gtk_iteration()
self.assertEqual(len(self.window.get('key_list').get_children()), 2)
self.assertEqual(custom_mapping.get_character(EV_KEY, 30), 'Shift_L')
self.assertEqual(custom_mapping.get_character((EV_KEY, 30, 1)), 'Shift_L')
self.assertEqual(row.get_character(), 'Shift_L')
self.assertEqual(row.get_keycode(), (EV_KEY, 30))
self.assertEqual(row.get_keycode(), (EV_KEY, 30, 1))
def change_empty_row(self, code, char, code_first=True, success=True):
def change_empty_row(self, key, char, code_first=True, expect_success=True):
"""Modify the one empty row that always exists.
Utility function for other tests.
Parameters
----------
key : int, int, int
type, code, value
code_first : boolean
If True, the code is entered and then the character.
If False, the character is entered first.
success : boolean
expect_success : boolean
If this change on the empty row is going to result in a change
in the mapping eventually. False if this change is going to
cause a duplicate.
@ -243,9 +248,9 @@ class TestIntegration(unittest.TestCase):
# find the empty row
rows = self.get_rows()
row = rows[-1]
self.assertNotIn('changed', row.get_style_context().list_classes())
self.assertIsNone(row.keycode_input.get_label())
self.assertEqual(row.character_input.get_text(), '')
self.assertNotIn('changed', row.get_style_context().list_classes())
if char and not code_first:
# set the character to make the new row complete
@ -255,23 +260,22 @@ class TestIntegration(unittest.TestCase):
self.window.window.set_focus(row.keycode_input)
if code:
if key:
# modifies the keycode in the row not by writing into the input,
# but by sending an event
keycode_reader._pipe[1].send(InputEvent(EV_KEY, code, 1))
keycode_reader._pipe[1].send(InputEvent(*key))
time.sleep(0.1)
gtk_iteration()
if success:
self.assertEqual(row.get_keycode(), (EV_KEY, code))
self.assertIn(
'changed',
row.get_style_context().list_classes()
)
if not success:
if expect_success:
self.assertEqual(row.get_keycode(), key)
css_classes = row.get_style_context().list_classes()
self.assertIn('changed', css_classes)
if not expect_success:
self.assertIsNone(row.get_keycode())
self.assertIsNone(row.get_character())
self.assertNotIn('changed', row.get_style_context().list_classes())
return row
if char and code_first:
# set the character to make the new row complete
@ -286,57 +290,93 @@ class TestIntegration(unittest.TestCase):
# how many rows there should be in the end
num_rows_target = 3
ev_1 = (EV_KEY, 10, 1)
ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1)
"""edit"""
# add two rows by modifiying the one empty row that exists
self.change_empty_row(10, 'a', code_first=False)
self.change_empty_row(11, 'k(b).k(c)')
self.change_empty_row(ev_1, 'a', code_first=False)
self.change_empty_row(ev_2, 'k(b).k(c)')
# one empty row added automatically again
time.sleep(0.1)
gtk_iteration()
# sleep one more time because it's funny to watch the ui
# during the test, how rows turn blue and stuff
time.sleep(0.1)
self.assertEqual(len(self.get_rows()), num_rows_target)
self.assertEqual(custom_mapping.get_character(EV_KEY, 10), 'a')
self.assertEqual(custom_mapping.get_character(EV_KEY, 11), 'k(b).k(c)')
self.assertEqual(custom_mapping.get_character(ev_1), 'a')
self.assertEqual(custom_mapping.get_character(ev_2), 'k(b).k(c)')
self.assertTrue(custom_mapping.changed)
"""save"""
self.window.on_save_preset_clicked(None)
for row in self.get_rows():
self.assertNotIn(
'changed',
row.get_style_context().list_classes()
)
css_classes = row.get_style_context().list_classes()
self.assertNotIn('changed', css_classes)
self.assertFalse(custom_mapping.changed)
"""edit first row"""
# now change the first row and it should turn blue,
# but the other should remain unhighlighted
row = self.get_rows()[0]
row.character_input.set_text('c')
self.assertIn('changed', row.get_style_context().list_classes())
for row in self.get_rows()[1:]:
self.assertNotIn(
'changed',
row.get_style_context().list_classes()
)
css_classes = row.get_style_context().list_classes()
self.assertNotIn('changed', css_classes)
self.assertEqual(custom_mapping.get_character(EV_KEY, 10), 'c')
self.assertEqual(custom_mapping.get_character(EV_KEY, 11), 'k(b).k(c)')
self.assertEqual(custom_mapping.get_character(ev_1), 'c')
self.assertEqual(custom_mapping.get_character(ev_2), 'k(b).k(c)')
self.assertTrue(custom_mapping.changed)
"""add duplicate"""
# try to add a duplicate keycode, it should be ignored
self.change_empty_row(11, 'd', success=False)
self.assertEqual(custom_mapping.get_character(EV_KEY, 11), 'k(b).k(c)')
self.change_empty_row(ev_2, 'd', expect_success=False)
self.assertEqual(custom_mapping.get_character(ev_2), 'k(b).k(c)')
# and the number of rows shouldn't change
self.assertEqual(len(self.get_rows()), num_rows_target)
def test_hat0x(self):
# it should be possible to add all of them
ev_1 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1)
ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, 1)
ev_3 = (EV_ABS, evdev.ecodes.ABS_HAT0Y, -1)
ev_4 = (EV_ABS, evdev.ecodes.ABS_HAT0Y, 1)
self.change_empty_row(ev_1, 'a')
self.change_empty_row(ev_2, 'b')
self.change_empty_row(ev_3, 'c')
self.change_empty_row(ev_4, 'd')
self.assertEqual(custom_mapping.get_character(ev_1), 'a')
self.assertEqual(custom_mapping.get_character(ev_2), 'b')
self.assertEqual(custom_mapping.get_character(ev_3), 'c')
self.assertEqual(custom_mapping.get_character(ev_4), 'd')
self.assertTrue(custom_mapping.changed)
# and trying to add them as duplicate rows will be ignored for each
# of them
self.change_empty_row(ev_1, 'e', expect_success=False)
self.change_empty_row(ev_2, 'f', expect_success=False)
self.change_empty_row(ev_3, 'g', expect_success=False)
self.change_empty_row(ev_4, 'h', expect_success=False)
self.assertEqual(custom_mapping.get_character(ev_1), 'a')
self.assertEqual(custom_mapping.get_character(ev_2), 'b')
self.assertEqual(custom_mapping.get_character(ev_3), 'c')
self.assertEqual(custom_mapping.get_character(ev_4), 'd')
self.assertTrue(custom_mapping.changed)
def test_remove_row(self):
"""Comprehensive test for rows 2."""
# sleeps are added to be able to visually follow and debug the test
# add two rows by modifiying the one empty row that exists
row_1 = self.change_empty_row(10, 'a')
row_2 = self.change_empty_row(11, 'b')
row_1 = self.change_empty_row((EV_KEY, 10, 1), 'a')
row_2 = self.change_empty_row((EV_KEY, 11, 1), 'b')
row_3 = self.change_empty_row(None, 'c')
# no empty row added because one is unfinished
@ -344,23 +384,23 @@ class TestIntegration(unittest.TestCase):
gtk_iteration()
self.assertEqual(len(self.get_rows()), 3)
self.assertEqual(custom_mapping.get_character(EV_KEY, 11), 'b')
self.assertEqual(custom_mapping.get_character((EV_KEY, 11, 1)), 'b')
def remove(row, code, char, num_rows_after):
if code is not None and char is not None:
self.assertEqual(custom_mapping.get_character(EV_KEY, code), char)
self.assertEqual(custom_mapping.get_character((EV_KEY, code, 1)), char)
self.assertEqual(row.get_character(), char)
if code is None:
self.assertIsNone(row.get_keycode())
else:
self.assertEqual(row.get_keycode(), (EV_KEY, code))
self.assertEqual(row.get_keycode(), (EV_KEY, code, 1))
row.on_delete_button_clicked()
time.sleep(0.2)
gtk_iteration()
self.assertIsNone(row.get_keycode())
self.assertIsNone(row.get_character())
self.assertIsNone(custom_mapping.get_character(EV_KEY, code))
self.assertIsNone(custom_mapping.get_character((EV_KEY, code, 1)))
self.assertEqual(len(self.get_rows()), num_rows_after)
remove(row_1, 10, 'a', 2)
@ -371,33 +411,33 @@ class TestIntegration(unittest.TestCase):
remove(row_3, None, 'c', 1)
def test_rename_and_save(self):
custom_mapping.change((EV_KEY, 14), 'a', (None, None))
custom_mapping.change((EV_KEY, 14, 1), 'a', None)
self.assertEqual(self.window.selected_preset, 'new preset')
self.window.on_save_preset_clicked(None)
self.assertEqual(custom_mapping.get_character(EV_KEY, 14), 'a')
self.assertEqual(custom_mapping.get_character((EV_KEY, 14, 1)), 'a')
custom_mapping.change((EV_KEY, 14), 'b', (None, None))
custom_mapping.change((EV_KEY, 14, 1), 'b', None)
self.window.get('preset_name_input').set_text('asdf')
self.window.on_save_preset_clicked(None)
self.assertEqual(self.window.selected_preset, 'asdf')
self.assertTrue(os.path.exists(f'{CONFIG}/device 1/asdf.json'))
self.assertEqual(custom_mapping.get_character(EV_KEY, 14), 'b')
self.assertEqual(custom_mapping.get_character((EV_KEY, 14, 1)), 'b')
def test_check_macro_syntax(self):
status = self.window.get('status_bar')
custom_mapping.change((EV_KEY, 9), 'k(1))', (None, None))
custom_mapping.change((EV_KEY, 9, 1), 'k(1))', None)
self.window.on_save_preset_clicked(None)
tooltip = status.get_tooltip_text().lower()
self.assertIn('brackets', tooltip)
custom_mapping.change((EV_KEY, 9), 'k(1)', (None, None))
custom_mapping.change((EV_KEY, 9, 1), 'k(1)', None)
self.window.on_save_preset_clicked(None)
tooltip = status.get_tooltip_text().lower()
self.assertNotIn('brackets', tooltip)
self.assertIn('saved', tooltip)
self.assertEqual(custom_mapping.get_character(EV_KEY, 9), 'k(1)')
self.assertEqual(custom_mapping.get_character((EV_KEY, 9, 1)), 'k(1)')
def test_select_device_and_preset(self):
# created on start because the first device is selected and some empty
@ -427,7 +467,7 @@ class TestIntegration(unittest.TestCase):
gtk_iteration()
self.assertEqual(self.window.selected_preset, 'new preset')
self.assertFalse(os.path.exists(f'{CONFIG}/device 1/abc 123.json'))
custom_mapping.change((EV_KEY, 10), '1', (None, None))
custom_mapping.change((EV_KEY, 10, 1), '1', None)
self.window.on_save_preset_clicked(None)
gtk_iteration()
self.assertEqual(self.window.selected_preset, 'abc 123')
@ -445,7 +485,7 @@ class TestIntegration(unittest.TestCase):
keycode_from = 9
keycode_to = 200
self.change_empty_row(keycode_from, 'a')
self.change_empty_row((EV_KEY, keycode_from, 1), 'a')
system_mapping.clear()
system_mapping._set('a', keycode_to)
@ -482,7 +522,7 @@ class TestIntegration(unittest.TestCase):
keycode_from = 16
keycode_to = 90
self.change_empty_row(keycode_from, 't')
self.change_empty_row((EV_KEY, keycode_from, 1), 't')
system_mapping.clear()
system_mapping._set('t', keycode_to)

@ -21,8 +21,10 @@
import unittest
import asyncio
import time
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, KEY_A, ABS_X, EV_REL, REL_X
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, ABS_HAT0Y, KEY_A, ABS_X, \
EV_REL, REL_X, BTN_TL
from keymapper.dev.keycode_mapper import should_map_event_as_btn, \
active_macros, handle_keycode
@ -30,14 +32,50 @@ from keymapper.state import system_mapping
from keymapper.dev.macros import parse
from keymapper.config import config
from tests.test import InputEvent
from tests.test import InputEvent, UInput, uinput_write_history, \
clear_write_history
def wait(func, timeout=1.0):
"""Wait for func to return True."""
iterations = 0
sleepytime = 0.1
while not func():
time.sleep(sleepytime)
iterations += 1
if iterations * sleepytime > timeout:
raise Exception('Timeout')
def calculate_event_number(holdtime, before, after):
"""
Parameters
----------
holdtime : int
in ms, how long was the key held down
before : int
how many extra k() calls are executed before h()
after : int
how many extra k() calls are executed after h()
"""
keystroke_sleep = config.get('macros.keystroke_sleep_ms', 10)
# down and up: two sleeps per k
# one initial k(a):
events = before * 2
holdtime -= keystroke_sleep * 2
# hold events
events += (holdtime / (keystroke_sleep * 2)) * 2
# one trailing k(c)
events += after * 2
return events
class TestKeycodeMapper(unittest.TestCase):
def tearDown(self):
system_mapping.populate()
# make sure all macros are stopped by tests
for code in active_macros:
macro = active_macros[code]
for macro in active_macros.values():
if macro.holding:
macro.release_key()
self.assertFalse(macro.holding)
@ -47,7 +85,54 @@ class TestKeycodeMapper(unittest.TestCase):
for key in keys:
del active_macros[key]
system_mapping.populate()
clear_write_history()
def test_d_pad(self):
ev_1 = (EV_ABS, ABS_HAT0X, 1)
ev_2 = (EV_ABS, ABS_HAT0X, -1)
ev_3 = (EV_ABS, ABS_HAT0X, 0)
ev_4 = (EV_ABS, ABS_HAT0Y, 1)
ev_5 = (EV_ABS, ABS_HAT0Y, -1)
ev_6 = (EV_ABS, ABS_HAT0Y, 0)
_key_to_code = {
ev_1: 51,
ev_2: 52,
ev_4: 54,
ev_5: 55,
}
uinput = UInput()
# a bunch of d-pad key down events at once
handle_keycode(_key_to_code, {}, InputEvent(*ev_1), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*ev_4), uinput)
# release all of them
handle_keycode(_key_to_code, {}, InputEvent(*ev_3), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*ev_6), uinput)
# repeat with other values
handle_keycode(_key_to_code, {}, InputEvent(*ev_2), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*ev_5), uinput)
# release all of them again
handle_keycode(_key_to_code, {}, InputEvent(*ev_3), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*ev_6), uinput)
self.assertEqual(len(uinput_write_history), 8)
self.assertEqual(uinput_write_history[0].t, (EV_KEY, 51, 1))
self.assertEqual(uinput_write_history[1].t, (EV_KEY, 54, 1))
self.assertEqual(uinput_write_history[2].t, (EV_KEY, 51, 0))
self.assertEqual(uinput_write_history[3].t, (EV_KEY, 54, 0))
self.assertEqual(uinput_write_history[4].t, (EV_KEY, 52, 1))
self.assertEqual(uinput_write_history[5].t, (EV_KEY, 55, 1))
self.assertEqual(uinput_write_history[6].t, (EV_KEY, 52, 0))
self.assertEqual(uinput_write_history[7].t, (EV_KEY, 55, 0))
def test_should_map_event_as_btn(self):
self.assertTrue(should_map_event_as_btn(EV_ABS, ABS_HAT0X))
@ -56,33 +141,20 @@ class TestKeycodeMapper(unittest.TestCase):
self.assertFalse(should_map_event_as_btn(EV_REL, REL_X))
def test_handle_keycode(self):
_code_to_code = {
1: 101,
2: 102
_key_to_code = {
(EV_KEY, 1, 1): 101,
(EV_KEY, 2, 1): 102
}
history = []
class UInput:
def write(self, type, code, value):
history.append((type, code, value))
def capabilities(self, *args, **kwargs):
return []
def syn(self):
pass
uinput = UInput()
handle_keycode(_key_to_code, {}, InputEvent(EV_KEY, 1, 1), uinput)
handle_keycode(_key_to_code, {}, InputEvent(EV_KEY, 3, 1), uinput)
handle_keycode(_key_to_code, {}, InputEvent(EV_KEY, 2, 1), uinput)
handle_keycode(_code_to_code, {}, InputEvent(EV_KEY, 1, 1), uinput)
handle_keycode(_code_to_code, {}, InputEvent(EV_KEY, 3, 1), uinput)
handle_keycode(_code_to_code, {}, InputEvent(EV_KEY, 2, 1), uinput)
self.assertEqual(len(history), 3)
self.assertEqual(history[0], (EV_KEY, 101, 1))
self.assertEqual(history[1], (EV_KEY, 3, 1))
self.assertEqual(history[2], (EV_KEY, 102, 1))
self.assertEqual(len(uinput_write_history), 3)
self.assertEqual(uinput_write_history[0].t, (EV_KEY, 101, 1))
self.assertEqual(uinput_write_history[1].t, (EV_KEY, 3, 1))
self.assertEqual(uinput_write_history[2].t, (EV_KEY, 102, 1))
def test_handle_keycode_macro(self):
history = []
@ -94,12 +166,12 @@ class TestKeycodeMapper(unittest.TestCase):
system_mapping._set('b', code_b)
macro_mapping = {
1: parse('k(a)'),
2: parse('r(5, k(b))')
(EV_KEY, 1, 1): parse('k(a)'),
(EV_KEY, 2, 1): parse('r(5, k(b))')
}
macro_mapping[1].set_handler(lambda *args: history.append(args))
macro_mapping[2].set_handler(lambda *args: history.append(args))
macro_mapping[(EV_KEY, 1, 1)].set_handler(lambda *args: history.append(args))
macro_mapping[(EV_KEY, 2, 1)].set_handler(lambda *args: history.append(args))
handle_keycode({}, macro_mapping, InputEvent(EV_KEY, 1, 1), None)
handle_keycode({}, macro_mapping, InputEvent(EV_KEY, 2, 1), None)
@ -118,28 +190,6 @@ class TestKeycodeMapper(unittest.TestCase):
self.assertIn((code_b, 1), history)
self.assertIn((code_b, 0), history)
def calculate_event_number(self, holdtime, before, after):
"""
Parameters
----------
holdtime : int
in ms, how long was the key held down
before : int
how many extra k() calls are executed before h()
after : int
how many extra k() calls are executed after h()
"""
keystroke_sleep = config.get('macros.keystroke_sleep_ms', 10)
# down and up: two sleeps per k
# one initial k(a):
events = before * 2
holdtime -= keystroke_sleep * 2
# hold events
events += (holdtime / (keystroke_sleep * 2)) * 2
# one trailing k(c)
events += after * 2
return events
def test_hold(self):
history = []
@ -152,13 +202,13 @@ class TestKeycodeMapper(unittest.TestCase):
system_mapping._set('c', code_c)
macro_mapping = {
1: parse('k(a).h(k(b)).k(c)')
(EV_KEY, 1, 1): parse('k(a).h(k(b)).k(c)')
}
def handler(*args):
history.append(args)
macro_mapping[1].set_handler(handler)
macro_mapping[(EV_KEY, 1, 1)].set_handler(handler)
"""start macro"""
@ -171,8 +221,8 @@ class TestKeycodeMapper(unittest.TestCase):
keystroke_sleep = config.get('macros.keystroke_sleep_ms', 10)
loop.run_until_complete(asyncio.sleep(sleeptime / 1000))
self.assertTrue(active_macros[1].holding)
self.assertTrue(active_macros[1].running)
self.assertTrue(active_macros[(EV_KEY, 1)].holding)
self.assertTrue(active_macros[(EV_KEY, 1)].running)
"""stop macro"""
@ -180,7 +230,7 @@ class TestKeycodeMapper(unittest.TestCase):
loop.run_until_complete(asyncio.sleep(keystroke_sleep * 10 / 1000))
events = self.calculate_event_number(sleeptime, 1, 1)
events = calculate_event_number(sleeptime, 1, 1)
self.assertGreater(len(history), events * 0.9)
self.assertLess(len(history), events * 1.1)
@ -200,8 +250,8 @@ class TestKeycodeMapper(unittest.TestCase):
count_after = len(history)
self.assertEqual(count_before, count_after)
self.assertFalse(active_macros[1].holding)
self.assertFalse(active_macros[1].running)
self.assertFalse(active_macros[(EV_KEY, 1)].holding)
self.assertFalse(active_macros[(EV_KEY, 1)].running)
def test_hold_2(self):
# test irregular input patterns
@ -216,9 +266,9 @@ class TestKeycodeMapper(unittest.TestCase):
system_mapping._set('d', code_d)
macro_mapping = {
1: parse('h(k(b))'),
2: parse('k(c).r(1, r(1, r(1, h(k(a))))).k(d)'),
3: parse('h(k(b))')
(EV_KEY, 1, 1): parse('h(k(b))'),
(EV_KEY, 2, 1): parse('k(c).r(1, r(1, r(1, h(k(a))))).k(d)'),
(EV_KEY, 3, 1): parse('h(k(b))')
}
history = []
@ -226,9 +276,9 @@ class TestKeycodeMapper(unittest.TestCase):
def handler(*args):
history.append(args)
macro_mapping[1].set_handler(handler)
macro_mapping[2].set_handler(handler)
macro_mapping[3].set_handler(handler)
macro_mapping[(EV_KEY, 1, 1)].set_handler(handler)
macro_mapping[(EV_KEY, 2, 1)].set_handler(handler)
macro_mapping[(EV_KEY, 3, 1)].set_handler(handler)
"""start macro 2"""
@ -245,12 +295,12 @@ class TestKeycodeMapper(unittest.TestCase):
handle_keycode({}, macro_mapping, InputEvent(EV_KEY, 1, 1), None)
handle_keycode({}, macro_mapping, InputEvent(EV_KEY, 3, 1), None)
loop.run_until_complete(asyncio.sleep(0.05))
self.assertTrue(active_macros[1].holding)
self.assertTrue(active_macros[1].running)
self.assertTrue(active_macros[2].holding)
self.assertTrue(active_macros[2].running)
self.assertTrue(active_macros[3].holding)
self.assertTrue(active_macros[3].running)
self.assertTrue(active_macros[(EV_KEY, 1)].holding)
self.assertTrue(active_macros[(EV_KEY, 1)].running)
self.assertTrue(active_macros[(EV_KEY, 2)].holding)
self.assertTrue(active_macros[(EV_KEY, 2)].running)
self.assertTrue(active_macros[(EV_KEY, 3)].holding)
self.assertTrue(active_macros[(EV_KEY, 3)].running)
# there should only be one code_c in the events, because no key
# up event was ever done so the hold just continued
@ -293,12 +343,12 @@ class TestKeycodeMapper(unittest.TestCase):
handle_keycode({}, macro_mapping, InputEvent(EV_KEY, 1, 0), None)
handle_keycode({}, macro_mapping, InputEvent(EV_KEY, 3, 0), None)
loop.run_until_complete(asyncio.sleep(0.05))
self.assertFalse(active_macros[1].holding)
self.assertFalse(active_macros[1].running)
self.assertTrue(active_macros[2].holding)
self.assertTrue(active_macros[2].running)
self.assertFalse(active_macros[3].holding)
self.assertFalse(active_macros[3].running)
self.assertFalse(active_macros[(EV_KEY, 1)].holding)
self.assertFalse(active_macros[(EV_KEY, 1)].running)
self.assertTrue(active_macros[(EV_KEY, 2)].holding)
self.assertTrue(active_macros[(EV_KEY, 2)].running)
self.assertFalse(active_macros[(EV_KEY, 3)].holding)
self.assertFalse(active_macros[(EV_KEY, 3)].running)
# stop macro 2
handle_keycode({}, macro_mapping, InputEvent(EV_KEY, 2, 0), None)
@ -321,12 +371,12 @@ class TestKeycodeMapper(unittest.TestCase):
count_after = len(history)
self.assertEqual(count_before, count_after)
self.assertFalse(active_macros[1].holding)
self.assertFalse(active_macros[1].running)
self.assertFalse(active_macros[2].holding)
self.assertFalse(active_macros[2].running)
self.assertFalse(active_macros[3].holding)
self.assertFalse(active_macros[3].running)
self.assertFalse(active_macros[(EV_KEY, 1)].holding)
self.assertFalse(active_macros[(EV_KEY, 1)].running)
self.assertFalse(active_macros[(EV_KEY, 2)].holding)
self.assertFalse(active_macros[(EV_KEY, 2)].running)
self.assertFalse(active_macros[(EV_KEY, 3)].holding)
self.assertFalse(active_macros[(EV_KEY, 3)].running)
def test_hold_3(self):
# test irregular input patterns
@ -339,7 +389,7 @@ class TestKeycodeMapper(unittest.TestCase):
system_mapping._set('c', code_c)
macro_mapping = {
1: parse('k(a).h(k(b)).k(c)'),
(EV_KEY, 1, 1): parse('k(a).h(k(b)).k(c)'),
}
history = []
@ -347,15 +397,15 @@ class TestKeycodeMapper(unittest.TestCase):
def handler(*args):
history.append(args)
macro_mapping[1].set_handler(handler)
macro_mapping[(EV_KEY, 1, 1)].set_handler(handler)
handle_keycode({}, macro_mapping, InputEvent(EV_KEY, 1, 1), None)
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.sleep(0.1))
for _ in range(5):
self.assertTrue(active_macros[1].holding)
self.assertTrue(active_macros[1].running)
self.assertTrue(active_macros[(EV_KEY, 1)].holding)
self.assertTrue(active_macros[(EV_KEY, 1)].running)
handle_keycode({}, macro_mapping, InputEvent(EV_KEY, 1, 1), None)
loop.run_until_complete(asyncio.sleep(0.05))
@ -372,8 +422,8 @@ class TestKeycodeMapper(unittest.TestCase):
self.assertEqual(history.count((code_a, 0)), 1)
self.assertEqual(history.count((code_c, 1)), 1)
self.assertEqual(history.count((code_c, 0)), 1)
self.assertFalse(active_macros[1].holding)
self.assertFalse(active_macros[1].running)
self.assertFalse(active_macros[(EV_KEY, 1)].holding)
self.assertFalse(active_macros[(EV_KEY, 1)].running)
# it's stopped and won't write stuff anymore
count_before = len(history)
@ -382,6 +432,7 @@ class TestKeycodeMapper(unittest.TestCase):
self.assertEqual(count_before, count_after)
def test_hold_two(self):
# holding two macros at the same time
history = []
code_1 = 100
@ -398,29 +449,37 @@ class TestKeycodeMapper(unittest.TestCase):
system_mapping._set('b', code_b)
system_mapping._set('c', code_c)
key_1 = (EV_KEY, 1)
key_2 = (EV_ABS, ABS_HAT0X)
down_1 = (*key_1, 1)
down_2 = (*key_2, -1)
up_1 = (*key_1, 0)
up_2 = (*key_2, 0)
macro_mapping = {
1: parse('k(1).h(k(2)).k(3)'),
2: parse('k(a).h(k(b)).k(c)')
down_1: parse('k(1).h(k(2)).k(3)'),
down_2: parse('k(a).h(k(b)).k(c)')
}
def handler(*args):
history.append(args)
macro_mapping[1].set_handler(handler)
macro_mapping[2].set_handler(handler)
macro_mapping[down_1].set_handler(handler)
macro_mapping[down_2].set_handler(handler)
loop = asyncio.get_event_loop()
# key up won't do anything
handle_keycode({}, macro_mapping, InputEvent(EV_KEY, 1, 0), None)
handle_keycode({}, macro_mapping, InputEvent(EV_KEY, 2, 0), None)
uinput = UInput()
handle_keycode({}, macro_mapping, InputEvent(*up_1), uinput)
handle_keycode({}, macro_mapping, InputEvent(*up_2), uinput)
loop.run_until_complete(asyncio.sleep(0.1))
self.assertEqual(len(active_macros), 0)
"""start macros"""
handle_keycode({}, macro_mapping, InputEvent(EV_KEY, 1, 1), None)
handle_keycode({}, macro_mapping, InputEvent(EV_KEY, 2, 1), None)
handle_keycode({}, macro_mapping, InputEvent(*down_1), None)
handle_keycode({}, macro_mapping, InputEvent(*down_2), None)
# let the mainloop run for some time so that the macro does its stuff
sleeptime = 500
@ -428,24 +487,24 @@ class TestKeycodeMapper(unittest.TestCase):
loop.run_until_complete(asyncio.sleep(sleeptime / 1000))
self.assertEqual(len(active_macros), 2)
self.assertTrue(active_macros[1].holding)
self.assertTrue(active_macros[1].running)
self.assertTrue(active_macros[2].holding)
self.assertTrue(active_macros[2].running)
self.assertTrue(active_macros[key_1].holding)
self.assertTrue(active_macros[key_1].running)
self.assertTrue(active_macros[key_2].holding)
self.assertTrue(active_macros[key_2].running)
"""stop macros"""
handle_keycode({}, macro_mapping, InputEvent(EV_KEY, 1, 0), None)
handle_keycode({}, macro_mapping, InputEvent(EV_KEY, 2, 0), None)
handle_keycode({}, macro_mapping, InputEvent(*up_1), None)
handle_keycode({}, macro_mapping, InputEvent(*up_2), None)
loop.run_until_complete(asyncio.sleep(keystroke_sleep * 10 / 1000))
self.assertFalse(active_macros[1].holding)
self.assertFalse(active_macros[1].running)
self.assertFalse(active_macros[2].holding)
self.assertFalse(active_macros[2].running)
self.assertFalse(active_macros[key_1].holding)
self.assertFalse(active_macros[key_1].running)
self.assertFalse(active_macros[key_2].holding)
self.assertFalse(active_macros[key_2].running)
events = self.calculate_event_number(sleeptime, 1, 1) * 2
events = calculate_event_number(sleeptime, 1, 1) * 2
self.assertGreater(len(history), events * 0.9)
self.assertLess(len(history), events * 1.1)
@ -473,6 +532,132 @@ class TestKeycodeMapper(unittest.TestCase):
count_after = len(history)
self.assertEqual(count_before, count_after)
def test_two_d_pad_macros(self):
# executing two macros that stop automatically at the same time
code_1 = 61
code_2 = 62
system_mapping.clear()
system_mapping._set('1', code_1)
system_mapping._set('2', code_2)
# try two concurrent macros with D-Pad events because they are
# more difficult to manage, since their only difference is their
# value, and one of them is negative.
down_1 = (EV_ABS, ABS_HAT0X, 1)
down_2 = (EV_ABS, ABS_HAT0X, -1)
repeats = 10
macro_mapping = {
down_1: parse(f'r({repeats}, k(1))'),
down_2: parse(f'r({repeats}, k(2))')
}
history = []
def handler(*args):
history.append(args)
macro_mapping[down_1].set_handler(handler)
macro_mapping[down_2].set_handler(handler)
handle_keycode({}, macro_mapping, InputEvent(*down_1), None)
handle_keycode({}, macro_mapping, InputEvent(*down_2), None)
loop = asyncio.get_event_loop()
sleeptime = config.get('macros.keystroke_sleep_ms') / 1000
loop.run_until_complete(asyncio.sleep(1.1 * repeats * 2 * sleeptime))
self.assertEqual(len(history), repeats * 4)
self.assertEqual(history.count((code_1, 1)), 10)
self.assertEqual(history.count((code_1, 0)), 10)
self.assertEqual(history.count((code_2, 1)), 10)
self.assertEqual(history.count((code_2, 0)), 10)
def test_normalize(self):
# -1234 to -1, 5678 to 1, 0 to 0
key_1 = (EV_KEY, BTN_TL)
ev_1 = (*key_1, 5678)
ev_2 = (*key_1, 0)
# doesn't really matter if it makes sense, the A key reports
# negative values now.
key_2 = (EV_KEY, KEY_A)
ev_3 = (*key_2, -1234)
ev_4 = (*key_2, 0)
_key_to_code = {
(*key_1, 1): 41,
(*key_2, -1): 42
}
uinput = UInput()
handle_keycode(_key_to_code, {}, InputEvent(*ev_1), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*ev_2), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*ev_3), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*ev_4), uinput)
self.assertEqual(len(uinput_write_history), 4)
self.assertEqual(uinput_write_history[0].t, (EV_KEY, 41, 1))
self.assertEqual(uinput_write_history[1].t, (EV_KEY, 41, 0))
self.assertEqual(uinput_write_history[2].t, (EV_KEY, 42, 1))
self.assertEqual(uinput_write_history[3].t, (EV_KEY, 42, 0))
def test_filter_trigger_spam(self):
trigger = (EV_KEY, BTN_TL)
_key_to_code = {
(*trigger, 1): 51,
(*trigger, -1): 52
}
uinput = UInput()
"""positive"""
for i in range(1, 20):
handle_keycode(_key_to_code, {}, InputEvent(*trigger, i), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*trigger, 0), uinput)
self.assertEqual(len(uinput_write_history), 2)
"""negative"""
for i in range(1, 20):
handle_keycode(_key_to_code, {}, InputEvent(*trigger, -i), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*trigger, 0), uinput)
self.assertEqual(len(uinput_write_history), 4)
self.assertEqual(uinput_write_history[0].t, (EV_KEY, 51, 1))
self.assertEqual(uinput_write_history[1].t, (EV_KEY, 51, 0))
self.assertEqual(uinput_write_history[2].t, (EV_KEY, 52, 1))
self.assertEqual(uinput_write_history[3].t, (EV_KEY, 52, 0))
def test_ignore_hold(self):
key = (EV_KEY, KEY_A)
ev_1 = (*key, 1)
ev_2 = (*key, 2)
ev_3 = (*key, 0)
_key_to_code = {
(*key, 1): 21,
}
uinput = UInput()
handle_keycode(_key_to_code, {}, InputEvent(*ev_1), uinput)
for _ in range(10):
handle_keycode(_key_to_code, {}, InputEvent(*ev_2), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*ev_3), uinput)
self.assertEqual(len(uinput_write_history), 2)
self.assertEqual(uinput_write_history[0].t, (EV_KEY, 21, 1))
self.assertEqual(uinput_write_history[1].t, (EV_KEY, 21, 0))
# linux will generate events with value 2 after key-mapper injected
# the key-press
if __name__ == "__main__":
unittest.main()

@ -73,7 +73,7 @@ class TestLogger(unittest.TestCase):
def test_debug(self):
path = add_filehandler(os.path.join(tmp, 'logger-test'))
logger.error('abc')
logger.warn('foo')
logger.warning('foo')
logger.info('123')
logger.debug('456')
logger.spam('789')
@ -101,7 +101,7 @@ class TestLogger(unittest.TestCase):
path = add_filehandler(os.path.join(tmp, 'logger-test'))
update_verbosity(debug=False)
logger.error('abc')
logger.warn('foo')
logger.warning('foo')
logger.info('123')
logger.debug('456')
logger.spam('789')

@ -19,18 +19,27 @@
# along with key-mapper. If not, see <https://www.gnu.org/licenses/>.
import os
import shutil
import json
import unittest
from evdev.events import EV_KEY, EV_ABS
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X
from keymapper.mapping import Mapping
from keymapper.state import SystemMapping
from tests.test import tmp
class TestMapping(unittest.TestCase):
def setUp(self):
self.mapping = Mapping()
self.assertFalse(self.mapping.changed)
def tearDown(self):
if os.path.exists(tmp):
shutil.rmtree(tmp)
def test_system_mapping(self):
system_mapping = SystemMapping()
self.assertGreater(len(system_mapping._mapping), 100)
@ -70,114 +79,128 @@ class TestMapping(unittest.TestCase):
self.assertIn('btn_right', names)
def test_clone(self):
ev_1 = (EV_KEY, 1, 1)
ev_2 = (EV_KEY, 2, 0)
mapping1 = Mapping()
mapping1.change((EV_KEY, 1), 'a')
mapping1.change(ev_1, 'a')
mapping2 = mapping1.clone()
mapping1.change((EV_KEY, 2), 'b')
mapping1.change(ev_2, 'b')
self.assertEqual(mapping1.get_character(EV_KEY, 1), 'a')
self.assertEqual(mapping1.get_character(EV_KEY, 2), 'b')
self.assertEqual(mapping1.get_character(ev_1), 'a')
self.assertEqual(mapping1.get_character(ev_2), 'b')
self.assertEqual(mapping2.get_character(EV_KEY, 1), 'a')
self.assertIsNone(mapping2.get_character(EV_KEY, 2))
self.assertEqual(mapping2.get_character(ev_1), 'a')
self.assertIsNone(mapping2.get_character(ev_2))
self.assertIsNone(mapping2.get_character((EV_KEY, 2, 3)))
self.assertIsNone(mapping2.get_character((EV_KEY, 1, 3)))
def test_save_load(self):
self.mapping.change((EV_KEY, 10), '1')
self.mapping.change((EV_KEY, 11), '2')
self.mapping.change((EV_KEY, 12), '3')
one = (EV_KEY, 10, 1)
two = (EV_KEY, 11, 1)
three = (EV_KEY, 12, 1)
self.mapping.change(one, '1')
self.mapping.change(two, '2')
self.mapping.change(three, '3')
self.mapping.config['foo'] = 'bar'
self.mapping.save('device 1', 'test')
path = os.path.join(tmp, 'device 1', 'test.json')
self.assertTrue(os.path.exists(path))
loaded = Mapping()
self.assertEqual(len(loaded), 0)
loaded.load('device 1', 'test')
self.assertEqual(len(loaded), 3)
self.assertEqual(loaded.get_character(EV_KEY, 10), '1')
self.assertEqual(loaded.get_character(EV_KEY, 11), '2')
self.assertEqual(loaded.get_character(EV_KEY, 12), '3')
self.assertEqual(loaded.get_character(one), '1')
self.assertEqual(loaded.get_character(two), '2')
self.assertEqual(loaded.get_character(three), '3')
self.assertEqual(loaded.config['foo'], 'bar')
def test_save_load_2(self):
# loads mappings with only (type, code) as the key
path = os.path.join(tmp, 'device 1', 'test.json')
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w') as file:
json.dump({
'mapping': {
f'{EV_KEY},3': 'a',
f'{EV_ABS},{ABS_HAT0X},-1': 'b',
f'{EV_ABS},{ABS_HAT0X},1': 'c',
}
}, file)
loaded = Mapping()
loaded.load('device 1', 'test')
self.assertEqual(loaded.get_character((EV_KEY, 3, 1)), 'a')
self.assertEqual(loaded.get_character((EV_ABS, ABS_HAT0X, -1)), 'b')
self.assertEqual(loaded.get_character((EV_ABS, ABS_HAT0X, 1)), 'c')
def test_change(self):
ev_1 = (EV_KEY, 1, 111)
ev_2 = (EV_KEY, 1, 222)
ev_3 = (EV_KEY, 2, 111)
ev_4 = (EV_ABS, 1, 111)
# 1 is not assigned yet, ignore it
self.mapping.change((EV_KEY, 2), 'a', (EV_KEY, 1))
self.mapping.change(ev_1, 'a', ev_2)
self.assertTrue(self.mapping.changed)
self.assertIsNone(self.mapping.get_character(EV_KEY, 1))
self.assertEqual(self.mapping.get_character(EV_KEY, 2), 'a')
self.assertIsNone(self.mapping.get_character(ev_2))
self.assertEqual(self.mapping.get_character(ev_1), 'a')
self.assertEqual(len(self.mapping), 1)
# change KEY 2 to ABS 16 and change a to b
self.mapping.change((EV_ABS, 16), 'b', (EV_KEY, 2))
self.assertIsNone(self.mapping.get_character(EV_KEY, 2))
self.assertEqual(self.mapping.get_character(EV_ABS, 16), 'b')
# change ev_1 to ev_3 and change a to b
self.mapping.change(ev_3, 'b', ev_1)
self.assertIsNone(self.mapping.get_character(ev_1))
self.assertEqual(self.mapping.get_character(ev_3), 'b')
self.assertEqual(len(self.mapping), 1)
# add 4
self.mapping.change((EV_KEY, 4), 'c', (None, None))
self.assertEqual(self.mapping.get_character(EV_ABS, 16), 'b')
self.assertEqual(self.mapping.get_character(EV_KEY, 4), 'c')
self.mapping.change(ev_4, 'c', None)
self.assertEqual(self.mapping.get_character(ev_3), 'b')
self.assertEqual(self.mapping.get_character(ev_4), 'c')
self.assertEqual(len(self.mapping), 2)
# change the mapping of 4 to d
self.mapping.change((EV_KEY, 4), 'd', (None, None))
self.assertEqual(self.mapping.get_character(EV_KEY, 4), 'd')
self.mapping.change(ev_4, 'd', None)
self.assertEqual(self.mapping.get_character(ev_4), 'd')
self.assertEqual(len(self.mapping), 2)
# this also works in the same way
self.mapping.change((EV_KEY, 4), 'e', (EV_KEY, 4))
self.assertEqual(self.mapping.get_character(EV_KEY, 4), 'e')
self.assertEqual(len(self.mapping), 2)
# and this
self.mapping.change((EV_KEY, '4'), 'f', (str(EV_KEY), '4'))
self.assertEqual(self.mapping.get_character(EV_KEY, 4), 'f')
self.assertEqual(len(self.mapping), 2)
# non-int keycodes are ignored
self.mapping.change((EV_KEY, 'b'), 'c', (EV_KEY, 'a'))
self.mapping.change((EV_KEY, 'b'), 'c')
self.mapping.change(('foo', 1), 'c', ('foo', 2))
self.mapping.change(('foo', 1), 'c')
self.mapping.change(ev_4, 'e', ev_4)
self.assertEqual(self.mapping.get_character(ev_4), 'e')
self.assertEqual(len(self.mapping), 2)
def test_change_2(self):
self.mapping.change((EV_KEY, 2), 'a')
self.mapping.change((None, 2), 'b', (EV_KEY, 2))
self.assertEqual(self.mapping.get_character(EV_KEY, 2), 'a')
self.mapping.change((EV_KEY, None), 'c', (EV_KEY, 2))
self.assertEqual(self.mapping.get_character(EV_KEY, 2), 'a')
self.assertEqual(len(self.mapping), 1)
def test_clear(self):
# does nothing
self.mapping.clear(EV_KEY, 40)
self.mapping.clear((EV_KEY, 40, 1))
self.assertFalse(self.mapping.changed)
self.assertEqual(len(self.mapping), 0)
self.mapping._mapping[(EV_KEY, 40)] = 'b'
self.mapping._mapping[(EV_KEY, 40, 1)] = 'b'
self.assertEqual(len(self.mapping), 1)
self.mapping.clear(EV_KEY, 40)
self.mapping.clear((EV_KEY, 40, 1))
self.assertEqual(len(self.mapping), 0)
self.assertTrue(self.mapping.changed)
self.mapping.change((EV_KEY, 10), 'KP_1', (None, None))
self.mapping.change((EV_KEY, 10, 1), 'KP_1', None)
self.assertTrue(self.mapping.changed)
self.mapping.change((EV_KEY, 20), 'KP_2', (None, None))
self.mapping.change((EV_KEY, 30), 'KP_3', (None, None))
self.mapping.change((EV_KEY, 20, 1), 'KP_2', None)
self.mapping.change((EV_KEY, 30, 1), 'KP_3', None)
self.assertEqual(len(self.mapping), 3)
self.mapping.clear(EV_KEY, 20)
self.mapping.clear((EV_KEY, 20, 1))
self.assertEqual(len(self.mapping), 2)
self.assertEqual(self.mapping.get_character(EV_KEY, 10), 'KP_1')
self.assertIsNone(self.mapping.get_character(EV_KEY, 20))
self.assertEqual(self.mapping.get_character(EV_KEY, 30), 'KP_3')
self.assertEqual(self.mapping.get_character((EV_KEY, 10, 1)), 'KP_1')
self.assertIsNone(self.mapping.get_character((EV_KEY, 20, 1)))
self.assertEqual(self.mapping.get_character((EV_KEY, 30, 1)), 'KP_3')
def test_empty(self):
self.mapping.change((EV_KEY, 10), '1')
self.mapping.change((EV_KEY, 11), '2')
self.mapping.change((EV_KEY, 12), '3')
self.mapping.change((EV_KEY, 10, 1), '1')
self.mapping.change((EV_KEY, 11, 1), '2')
self.mapping.change((EV_KEY, 12, 1), '3')
self.assertEqual(len(self.mapping), 3)
self.mapping.empty()
self.assertEqual(len(self.mapping), 0)

@ -48,19 +48,20 @@ def wait(func, timeout=1.0):
class TestReader(unittest.TestCase):
def setUp(self):
# verify that tearDown properly cleared the reader
self.assertEqual(keycode_reader.read(), (None, None))
self.assertEqual(keycode_reader.read(), None)
def tearDown(self):
keycode_reader.stop_reading()
keys = list(pending_events.keys())
for key in keys:
del pending_events[key]
keycode_reader.newest_event = None
def test_reading_1(self):
pending_events['device 1'] = [
InputEvent(EV_KEY, CODE_1, 1),
InputEvent(EV_ABS, ABS_HAT0X, 1),
InputEvent(EV_KEY, CODE_3, 1)
InputEvent(EV_KEY, CODE_1, 1, 10000.1234),
InputEvent(EV_KEY, CODE_3, 1, 10001.1234),
InputEvent(EV_ABS, ABS_HAT0X, -1, 10002.1234)
]
keycode_reader.start_reading('device 1')
@ -69,15 +70,25 @@ class TestReader(unittest.TestCase):
wait(keycode_reader._pipe[0].poll, 0.5)
self.assertEqual(keycode_reader.read(), (EV_KEY, CODE_3))
self.assertEqual(keycode_reader.read(), (None, None))
self.assertEqual(keycode_reader.read(), (EV_ABS, ABS_HAT0X, -1))
self.assertEqual(keycode_reader.read(), None)
def test_reading_2(self):
pending_events['device 1'] = [InputEvent(EV_ABS, ABS_HAT0X, 1)]
keycode_reader.start_reading('device 1')
wait(keycode_reader._pipe[0].poll, 0.5)
self.assertEqual(keycode_reader.read(), (EV_ABS, ABS_HAT0X))
self.assertEqual(keycode_reader.read(), (None, None))
self.assertEqual(keycode_reader.read(), (EV_ABS, ABS_HAT0X, 1))
self.assertEqual(keycode_reader.read(), None)
def test_reading_ignore_up(self):
pending_events['device 1'] = [
InputEvent(EV_KEY, CODE_1, 0, 10),
InputEvent(EV_KEY, CODE_2, 0, 11),
InputEvent(EV_KEY, CODE_3, 0, 12),
]
keycode_reader.start_reading('device 1')
time.sleep(0.1)
self.assertEqual(keycode_reader.read(), None)
def test_wrong_device(self):
pending_events['device 1'] = [
@ -87,7 +98,7 @@ class TestReader(unittest.TestCase):
]
keycode_reader.start_reading('device 2')
time.sleep(EVENT_READ_TIMEOUT * 5)
self.assertEqual(keycode_reader.read(), (None, None))
self.assertEqual(keycode_reader.read(), None)
def test_keymapper_devices(self):
# Don't read from keymapper devices, their keycodes are not
@ -101,7 +112,7 @@ class TestReader(unittest.TestCase):
]
keycode_reader.start_reading('device 2')
time.sleep(EVENT_READ_TIMEOUT * 5)
self.assertEqual(keycode_reader.read(), (None, None))
self.assertEqual(keycode_reader.read(), None)
def test_clear(self):
pending_events['device 1'] = [
@ -112,7 +123,7 @@ class TestReader(unittest.TestCase):
keycode_reader.start_reading('device 1')
time.sleep(EVENT_READ_TIMEOUT * 5)
keycode_reader.clear()
self.assertEqual(keycode_reader.read(), (None, None))
self.assertEqual(keycode_reader.read(), None)
def test_switch_device(self):
pending_events['device 2'] = [InputEvent(EV_KEY, CODE_1, 1)]
@ -124,8 +135,8 @@ class TestReader(unittest.TestCase):
keycode_reader.start_reading('device 1')
time.sleep(EVENT_READ_TIMEOUT * 5)
self.assertEqual(keycode_reader.read(), (EV_KEY, CODE_3))
self.assertEqual(keycode_reader.read(), (None, None))
self.assertEqual(keycode_reader.read(), (EV_KEY, CODE_3, 1))
self.assertEqual(keycode_reader.read(), None)
def test_prioritizing_1(self):
# filter the ABS_MISC events of the wacom intuos 5 out that come
@ -140,21 +151,34 @@ class TestReader(unittest.TestCase):
]
keycode_reader.start_reading('device 1')
wait(keycode_reader._pipe[0].poll, 0.5)
self.assertEqual(keycode_reader.read(), (EV_ABS, ABS_HAT0X))
self.assertEqual(keycode_reader.read(), (None, None))
self.assertEqual(keycode_reader.read(), (EV_ABS, ABS_HAT0X, 1))
self.assertEqual(keycode_reader.read(), None)
def test_prioritizing_2(self):
def test_prioritizing_2_normalize(self):
# furthermore, 1234 is 1 in the reader, because it probably is some
# sort of continuous trigger or joystick value
pending_events['device 1'] = [
InputEvent(EV_ABS, ABS_HAT0X, 1, 1234.0000),
InputEvent(EV_ABS, ABS_HAT0X, 1, 1235.0000), # ignored
InputEvent(EV_KEY, KEY_COMMA, 1, 1235.0010),
InputEvent(EV_KEY, KEY_COMMA, 1234, 1235.0010),
InputEvent(EV_ABS, ABS_HAT0X, 1, 1235.0020), # ignored
InputEvent(EV_ABS, ABS_HAT0X, 1, 1235.0030) # ignored
]
keycode_reader.start_reading('device 1')
wait(keycode_reader._pipe[0].poll, 0.5)
self.assertEqual(keycode_reader.read(), (EV_KEY, KEY_COMMA))
self.assertEqual(keycode_reader.read(), (None, None))
self.assertEqual(keycode_reader.read(), (EV_KEY, KEY_COMMA, 1))
self.assertEqual(keycode_reader.read(), None)
def test_prioritizing_3_normalize(self):
# take the sign of -1234, just like in test_prioritizing_2_normalize
pending_events['device 1'] = [
InputEvent(EV_ABS, ABS_HAT0X, -1234, 1234.0000),
InputEvent(EV_ABS, ABS_HAT0X, 0, 1234.0030) # ignored
]
keycode_reader.start_reading('device 1')
wait(keycode_reader._pipe[0].poll, 0.5)
self.assertEqual(keycode_reader.read(), (EV_ABS, ABS_HAT0X, -1))
self.assertEqual(keycode_reader.read(), None)
if __name__ == "__main__":

Loading…
Cancel
Save