key combinations

pull/14/head
sezanzeb 4 years ago
parent bff0c464dc
commit d09f02d573

@ -21,9 +21,6 @@ pacaur -S key-mapper-git
##### Ubuntu/Debian
Download a release .deb file from [/releases](https://github.com/sezanzeb/key-mapper/releases)
or install from source and dpkg using the following commands:
```bash
sudo apt install git python3-setuptools
git clone https://github.com/sezanzeb/key-mapper.git

@ -62,7 +62,8 @@ def get_dbus_interface(fallback=True):
"""
msg = (
'The daemon "key-mapper-service" is not running, mapping keys '
'only works as long as the window is open.'
'only works as long as the window is open. '
'Try `sudo systemctl start key-mapper`'
)
if not is_service_running():

@ -27,6 +27,7 @@ import asyncio
import time
import subprocess
import multiprocessing
import itertools
import evdev
from evdev.ecodes import EV_KEY, EV_ABS, EV_REL
@ -97,6 +98,45 @@ def ensure_numlock(func):
return wrapped
def store_permutations(target, combination, value):
"""Store permutations for key combinations.
Store every permutation of combination, while the last
element needs to remain the last one. It is the finishing
key. E.g. a + b is something different than b + a, but
a + b + c is the same as b + a + c
a, b and c are tuples of (type, code, value)
If combination is not a tuple of 3-tuples, it just uses it as key without
permutating anything.
"""
if not isinstance(combination, tuple):
logger.error('Expected a tuple, but got "%s"', combination)
return
if isinstance(combination[0], tuple):
for permutation in itertools.permutations(combination[:-1]):
target[(*permutation, combination[-1])] = value
else:
target[combination] = value
def is_in_capabilities(key, capabilities):
"""Are this key or all of its sub keys in the capabilities?"""
if isinstance(key[0], tuple):
# it's a key combination
for sub_key in key:
if is_in_capabilities(sub_key, capabilities):
return True
else:
ev_type, code, _ = key
if code in capabilities.get(ev_type, []):
return True
return False
class KeycodeInjector:
"""Keeps injecting keycodes in the background based on the mapping.
@ -139,7 +179,7 @@ class KeycodeInjector:
logger.error('Don\'t know what %s is', output)
continue
key_to_code[key] = target_code
store_permutations(key_to_code, key, target_code)
return key_to_code
@ -173,8 +213,8 @@ class KeycodeInjector:
capabilities = device.capabilities(absinfo=False)
needed = False
for (ev_type, code, _), _ in self.mapping:
if code in capabilities.get(ev_type, []):
for key, _ in self.mapping:
if is_in_capabilities(key, capabilities):
needed = True
break
@ -319,7 +359,7 @@ class KeycodeInjector:
if source is None:
continue
# each device parses the macros with a different handler
# each device needs own macro instances to add a custom handler
logger.debug('Parsing macros for %s', path)
macros = {}
for key, output in self.mapping:
@ -328,7 +368,7 @@ class KeycodeInjector:
if macro is None:
continue
macros[key] = macro
store_permutations(macros, key, macro)
if len(macros) == 0:
logger.debug('No macros configured')
@ -432,7 +472,12 @@ class KeycodeInjector:
continue
if should_map_event_as_btn(event.type, event.code):
handle_keycode(self._key_to_code, macros, event, uinput)
handle_keycode(
self._key_to_code,
macros,
event,
uinput
)
continue
# forward the rest

@ -39,11 +39,17 @@ from keymapper.dev.ev_abs_mapper import JOYSTICK
# 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.
# mapping of future up event (type, code) to (output code, input event)
# This is needed in order to release the correct event mapped on a
# D-Pad. Each direction on one D-Pad axis reports the same type and
# code, but different values. There cannot be both at the same time,
# as pressing one side of a D-Pad forces the other side to go up.
# "I have got this release event, what was this for?"
# It maps to (output_code, input_event) with input_event being the
# same as the key, but with the value of e.g. -1 or 1. The complete
# 3-tuple output event is used to track if a combined button press was done.
# A combination might be desired for D-Pad left, but not D-Pad right.
# (what_will_be_released, what_caused_the_key_down)
unreleased = {}
@ -81,6 +87,10 @@ def is_key_up(event):
return event.value == 0
COMBINATION_INCOMPLETE = 1 # not all keys of the combination are pressed
NOT_COMBINED = 2 # this key is not part of a combination
def handle_keycode(key_to_code, macros, event, uinput):
"""Write mapped keycodes, forward unmapped ones and manage macros.
@ -88,8 +98,12 @@ def handle_keycode(key_to_code, macros, event, uinput):
----------
key_to_code : dict
mapping of (type, code, value) to linux-keycode
or multiple of those like ((...), (...), ...) for combinations
combinations need to be present in every possible valid ordering.
e.g. shift + alt + a and alt + shift + a
macros : dict
mapping of (type, code, value) to _Macro objects
mapping of (type, code, value) to _Macro objects.
Combinations work similar as in key_to_code
event : evdev.InputEvent
"""
if event.type == EV_KEY and event.value == 2:
@ -101,10 +115,22 @@ def handle_keycode(key_to_code, macros, event, uinput):
# 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.
# The key used to index the mappings
key = (event.type, event.code, sign(event.value))
short = (event.type, event.code)
existing_macro = active_macros.get(short)
# the tuple of the actual input event. Used to forward the event if it is
# not mapped, and to index unreleased and active_macros
event_tuple = (event.type, event.code, sign(event.value))
type_code = (event.type, event.code)
# the finishing key has to be the last element in combination, all
# others can have any arbitrary order. By checking all unreleased keys,
# a + b + c takes priority over b + c, if both mappings exist.
combination = tuple([value[1] for value in unreleased.values()] + [key])
if combination in macros or combination in key_to_code:
key = combination
existing_macro = active_macros.get(type_code)
if existing_macro is not None:
if is_key_up(event) and not existing_macro.running:
# key was released, but macro already stopped
@ -125,36 +151,41 @@ def handle_keycode(key_to_code, macros, event, uinput):
if key in macros:
macro = macros[key]
active_macros[short] = macro
active_macros[type_code] = macro
macro.press_key()
logger.spam('got %s, maps to macro %s', key, macro.code)
asyncio.ensure_future(macro.run())
return
if is_key_down(event) and short in unreleased:
if is_key_down(event) and type_code 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
logger.spam('%s, duplicate key down', key)
return
if is_key_up(event) and short in unreleased:
target_type = EV_KEY
if is_key_up(event) and type_code in unreleased:
target_type, target_code = unreleased[type_code][0]
target_value = 0
target_code = unreleased[short]
del unreleased[short]
logger.spam('%s, releasing %s', key, target_code)
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
target_value = 1
logger.spam('%s, maps to %s', key, target_code)
else:
target_type = key[0]
target_code = key[1]
target_value = key[2]
target_type = event_tuple[0]
target_code = event_tuple[1]
target_value = event_tuple[2]
logger.spam('%s, unmapped', key)
if is_key_down(event):
# for a combination, the last key that was pressed is also the
# key that releases it, so type_code is used to index this.
unreleased[type_code] = ((target_type, target_code), event_tuple)
if is_key_up(event) and type_code in unreleased:
del unreleased[type_code]
uinput.write(target_type, target_code, target_value)
uinput.syn()

@ -69,6 +69,11 @@ class _KeycodeReader:
self._process = None
self.fail_counter = 0
self.newest_event = None
# to keep track of combinations.
# "I have got this release event, what was this for?"
# A release event for a D-Pad axis might be any direction, hence
# this maps from release to input in order to remember it.
self._unreleased = {}
def __del__(self):
self.stop_reading()
@ -84,6 +89,7 @@ class _KeycodeReader:
"""Next time when reading don't return the previous keycode."""
# just call read to clear the pipe
self.read()
self._unreleased = {}
def start_reading(self, device_name):
"""Tell the evdev lib to start looking for keycodes.
@ -136,6 +142,10 @@ class _KeycodeReader:
evdev.ecodes.BTN_TOOL_DOUBLETAP
]
if event.type == EV_KEY and event.value == 2:
# ignore hold-down events
return
if event.type == EV_KEY and event.code in click_events:
# disable mapping the left mouse button because it would break
# the mouse. Also it is emitted right when focusing the row
@ -182,6 +192,10 @@ class _KeycodeReader:
)
del rlist[fd]
def are_keys_pressed(self):
"""Check if any keys currently pressed down."""
return len(self._unreleased) > 0
def read(self):
"""Get the newest tuple of event type, keycode or None.
@ -203,10 +217,20 @@ class _KeycodeReader:
while self._pipe[0].poll():
event = self._pipe[0].recv()
without_value = (event.type, event.code)
if event.value == 0:
if without_value in self._unreleased:
del self._unreleased[without_value]
continue
self._unreleased[without_value] = (
event.type,
event.code,
sign(event.value)
)
time = event.sec + event.usec / 1000000
delta = time - newest_time
@ -229,11 +253,15 @@ class _KeycodeReader:
self.newest_event = newest_event
return (None if newest_event is None else (
newest_event.type,
newest_event.code,
sign(newest_event.value)
))
if len(self._unreleased) > 1:
# a combination
return tuple(self._unreleased.values())
elif len(self._unreleased) == 1:
# a single key
return list(self._unreleased.values())[0]
else:
# nothing
return None
keycode_reader = _KeycodeReader()

@ -23,7 +23,6 @@
import evdev
from gi.repository import Gtk, GLib, Gdk
from keymapper.state import custom_mapping, system_mapping
@ -38,8 +37,13 @@ for name in system_mapping.list_names():
store.append([name])
def to_string(ev_type, code, value):
def to_string(key):
"""A nice to show description of the pressed key."""
if isinstance(key[0], tuple):
return ' + '.join([to_string(sub_key) for sub_key in key])
ev_type, code, value = key
try:
key_name = evdev.ecodes.bytype[ev_type][code]
if isinstance(key_name, list):
@ -68,6 +72,10 @@ def to_string(ev_type, code, value):
return 'unknown'
IDLE = 0
HOLDING = 1
class Row(Gtk.ListBoxRow):
"""A single, configurable key mapping."""
__gtype_name__ = 'ListBoxRow'
@ -92,6 +100,17 @@ class Row(Gtk.ListBoxRow):
self.put_together(character)
self.state = IDLE
def release(self):
"""Tell the row that no keys are currently pressed down."""
if self.state == HOLDING:
# A key was pressed and then released.
# Switch to the character. idle_add this so that the
# keycode event won't write into the character input as well.
window = self.window.window
GLib.idle_add(lambda: window.set_focus(self.character_input))
def get_keycode(self):
"""Get a tuple of type, code and value from the left column.
@ -114,6 +133,9 @@ class Row(Gtk.ListBoxRow):
if new_key is None:
return
# it might end up being a key combination
self.state = HOLDING
# keycode didn't change, do nothing
if new_key == previous_key:
return
@ -121,21 +143,20 @@ class Row(Gtk.ListBoxRow):
# keycode is already set by some other row
existing = custom_mapping.get_character(new_key)
if existing is not None:
msg = f'"{to_string(*new_key)}" 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)
self.window.show_status(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(*new_key))
# always ask for get_child to set the label, otherwise line breaking
# has to be configured again.
self.set_keycode_input_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
# keycode event won't write into the character input as well.
window = self.window.window
GLib.idle_add(lambda: window.set_focus(self.character_input))
self.highlight()
character = self.get_character()
@ -186,7 +207,7 @@ class Row(Gtk.ListBoxRow):
if self.get_keycode() is not None:
return
self.keycode_input.set_label('click here')
self.set_keycode_input_label('click here')
self.keycode_input.set_opacity(0.3)
def show_press_key(self):
@ -194,18 +215,29 @@ class Row(Gtk.ListBoxRow):
if self.get_keycode() is not None:
return
self.keycode_input.set_label('press key')
self.set_keycode_input_label('press key')
self.keycode_input.set_opacity(1)
def keycode_input_focus(self, *args):
def on_keycode_input_focus(self, *args):
"""Refresh useful usage information."""
self.show_press_key()
self.window.can_modify_mapping()
def keycode_input_unfocus(self, *args):
"""Refresh useful usage information."""
def on_keycode_input_unfocus(self, *args):
"""Refresh useful usage information and set some state stuff."""
self.show_click_here()
self.keycode_input.set_active(False)
self.state = IDLE
def set_keycode_input_label(self, label):
"""Set the label of the keycode input."""
self.keycode_input.set_label(label)
# make the child label widget break lines, important for
# long combinations
self.keycode_input.get_child().set_line_wrap(True)
self.keycode_input.get_child().set_line_wrap_mode(2)
self.keycode_input.get_child().set_max_width_chars(15)
self.keycode_input.get_child().set_justify(Gtk.Justification.CENTER)
def put_together(self, character):
"""Create all child GTK widgets and connect their signals."""
@ -225,7 +257,7 @@ class Row(Gtk.ListBoxRow):
keycode_input.set_size_request(140, -1)
if self.key is not None:
keycode_input.set_label(to_string(*self.key))
self.set_keycode_input_label(to_string(self.key))
else:
self.show_click_here()
@ -233,11 +265,11 @@ class Row(Gtk.ListBoxRow):
# something else in the UI
keycode_input.connect(
'focus-in-event',
self.keycode_input_focus
self.on_keycode_input_focus
)
keycode_input.connect(
'focus-out-event',
self.keycode_input_unfocus
self.on_keycode_input_unfocus
)
# don't leave the input when using arrow keys or tab. wait for the
# window to consume the keycode from the reader
@ -284,6 +316,6 @@ class Row(Gtk.ListBoxRow):
custom_mapping.clear(key)
self.character_input.set_text('')
self.keycode_input.set_label('')
self.set_keycode_input_label('')
self.key = None
self.delete_callback(self)

@ -343,15 +343,23 @@ class Window:
@with_selected_device
def consume_newest_keycode(self):
"""To capture events from keyboards, mice and gamepads."""
row, focused = self.get_focused_row()
# the "event" event of Gtk.Window wouldn't trigger on gamepad
# events, so it became a GLib timeout to periodically check kernel
# events.
key = keycode_reader.read()
if isinstance(focused, Gtk.ToggleButton):
if not keycode_reader.are_keys_pressed():
row.release()
return True
if key is None:
return True
self.get('keycode').set_text(to_string(*key))
# only show the latest key, becomes too long otherwise
self.get('keycode').set_text(to_string(key).split('+')[-1].strip())
# inform the currently selected row about the new keycode
row, focused = self.get_focused_row()
@ -362,7 +370,7 @@ class Window:
@with_selected_device
def on_apply_system_layout_clicked(self, _):
"""Load the mapping."""
"""Stop injecting the mapping."""
self.dbus.stop_injecting(self.selected_device)
self.show_status(CTX_APPLY, 'Applied the system default')
logger.info('Applied system default for "%s"', self.selected_preset)
@ -382,6 +390,9 @@ class Window:
if context_id == CTX_WARNING:
self.get('warning_status_icon').show()
if len(message) > 40:
message = message[:37] + '...'
status_bar = self.get('status_bar')
status_bar.push(context_id, message)
status_bar.set_tooltip_text(tooltip)
@ -396,7 +407,7 @@ class Window:
if error is None:
continue
position = to_string(*key)
position = to_string(key)
msg = f'Syntax error at {position}, hover for info'
self.show_status(CTX_ERROR, msg, error)

@ -24,6 +24,7 @@
import os
import json
import itertools
import copy
from keymapper.logger import logger
@ -32,12 +33,47 @@ from keymapper.config import ConfigBase, config
def verify_key(key):
"""Check if the key describes a tuple of (type, code, value) ints."""
if len(key) != 3:
"""Check if the key describes a tuple or tuples of (type, code, value).
For combinations it could be e.g. ((1, 2, 1), (1, 3, 1)).
"""
if not isinstance(key, tuple):
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}')
if isinstance(key[0], tuple):
for sub_key in key:
verify_key(sub_key)
else:
if len(key) != 3:
raise ValueError(f'Expected key 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, but got {key}')
def split_key(key):
"""Take a key like "1,2,3" and return a 3-tuple of ints."""
if ',' not in key:
logger.error('Found invalid key: "%s"', key)
return None
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
elif key.count(',') == 2:
ev_type, code, value = key.split(',')
else:
logger.error('Found more than two commas in the key: "%s"', key)
return None
try:
key = (int(ev_type), int(code), int(value))
except ValueError:
logger.error('Found non-int in: "%s"', key)
return None
return key
class Mapping(ConfigBase):
@ -49,7 +85,7 @@ class Mapping(ConfigBase):
def __iter__(self):
"""Iterate over tuples of unique keycodes and their character."""
return iter(sorted(self._mapping.items()))
return iter(self._mapping.items())
def __len__(self):
return len(self._mapping)
@ -100,6 +136,7 @@ class Mapping(ConfigBase):
'%s will map to %s, replacing %s',
new_key, character, previous_key
)
self.clear(new_key) # this also clears all equivalent keys
self._mapping[new_key] = character
if previous_key is not None:
@ -126,6 +163,14 @@ class Mapping(ConfigBase):
"""
verify_key(key)
if isinstance(key[0], tuple):
for permutation in itertools.permutations(key[:-1]):
permutation += (key[-1],)
if permutation in self._mapping:
logger.debug('%s will be cleared', permutation)
del self._mapping[permutation]
return
if self._mapping.get(key) is not None:
logger.debug('%s will be cleared', key)
del self._mapping[key]
@ -163,24 +208,15 @@ class Mapping(ConfigBase):
return
for key, character in preset_dict['mapping'].items():
if ',' not in key:
logger.error('Found invalid key: "%s"', key)
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:
key = (int(ev_type), int(code), int(value))
except ValueError:
logger.error('Found non-int in: "%s"', key)
continue
if '+' in key:
chunks = key.split('+')
key = tuple([split_key(chunk) for chunk in chunks])
if None in key:
continue
else:
key = split_key(key)
if key is None:
continue
logger.spam('%s maps to %s', key, character)
self._mapping[key] = character
@ -218,7 +254,17 @@ class Mapping(ConfigBase):
json_ready_mapping = {}
# tuple keys are not possible in json, encode them as string
for key, value in self._mapping.items():
new_key = ','.join([str(value) for value in key])
if isinstance(key[0], tuple):
# combinations to "1,2,1+1,3,1"
new_key = '+'.join([
','.join([
str(value)
for value in sub_key
])
for sub_key in key
])
else:
new_key = ','.join([str(value) for value in key])
json_ready_mapping[new_key] = value
preset_dict['mapping'] = json_ready_mapping
@ -239,5 +285,16 @@ class Mapping(ConfigBase):
event types.
value : int
event value. Usually you want 1 (down)
Or a tuple of multiple of those. Checks any possible permutation
with the last key being always at the end, to work well with
combinations.
"""
if isinstance(key[0], tuple):
for permutation in itertools.permutations(key[:-1]):
permutation += (key[-1],)
existing = self._mapping.get(permutation)
if existing is not None:
return existing
return self._mapping.get(key)

@ -31,7 +31,7 @@ from keymapper.logger import logger
def get_user():
"""Try to find the user who called sudo."""
"""Try to find the user who called sudo/pkexec."""
try:
return os.getlogin()
except OSError:

@ -25,11 +25,12 @@ requests.
- [x] configure joystick purpose and speed via the GUI
- [x] support for non-GUI TTY environments with a command to stop and start
- [x] start the daemon in such a way to not require usermod
- [x] mapping a combined button press to a key
- [ ] mapping joystick directions as buttons, making it act like a D-Pad
- [ ] automatically load presets when devices get plugged in after login (udev)
- [ ] mapping a combined button press to a key
- [ ] configure locale for preset to provide a different set of possible keys
- [ ] user-friendly way to map btn_left
- [ ] add "disable" as mapping option
## Tests

@ -114,19 +114,21 @@ Here is an example configuration for preset "a" for the "gamepad" device:
"keystroke_sleep_ms": 100
},
"mapping": {
"1,315,1": "1",
"1,315,1+1,16,-1": "1",
"1,307,1": "k(2).k(3)"
}
}
```
Both need to be valid json files, otherwise the parser refuses to work. This
preset maps the EV_KEY down event with code 315 to '1', code 307 to a macro
and sets the time between injected events of macros to 100 ms. Note that
a complete keystroke consists of two events: down and up. Other than that,
it inherits all configurations from `~/.config/key-mapper/config.json`.
If config.json is missing some stuff, it will query the hardcoded default
values.
preset maps the EV_KEY down event with code 307 to a macro and sets the time
between injected events of macros to 100 ms. The other mapping is a key
combination, chained using `+`.
Note that a complete keystroke consists of two events: down and up. Other
than that, it inherits all configurations from
`~/.config/key-mapper/config.json`. If config.json is missing some stuff,
it will query the hardcoded default values.
The event codes can be read using `evtest`. Available names in the mapping
can be listed with `key-mapper-control --key-names`.

@ -352,9 +352,10 @@ patch_select()
from keymapper.logger import update_verbosity
from keymapper.dev.injector import KeycodeInjector
from keymapper.config import config
from keymapper.dev.reader import keycode_reader
from keymapper.getdevices import refresh_devices
from keymapper.state import system_mapping, custom_mapping
from keymapper.dev.keycode_mapper import active_macros
from keymapper.dev.keycode_mapper import active_macros, unreleased
# no need for a high number in tests
KeycodeInjector.regrab_timeout = 0.15
@ -365,6 +366,11 @@ _fixture_copy = copy.deepcopy(fixtures)
def cleanup():
"""Reset the applications state."""
keycode_reader.stop_reading()
keycode_reader.clear()
keycode_reader.newest_event = None
keycode_reader._unreleased = {}
for task in asyncio.Task.all_tasks():
task.cancel()
@ -383,6 +389,9 @@ def cleanup():
for key in list(active_macros.keys()):
del active_macros[key]
for key in list(unreleased.keys()):
del unreleased[key]
for key in list(pending_events.keys()):
del pending_events[key]

@ -24,10 +24,10 @@ import time
import copy
import evdev
from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X
from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X, BTN_A
from keymapper.dev.injector import is_numlock_on, set_numlock, \
ensure_numlock, KeycodeInjector
ensure_numlock, KeycodeInjector, store_permutations, is_in_capabilities
from keymapper.state import custom_mapping, system_mapping
from keymapper.mapping import Mapping
from keymapper.config import config
@ -328,14 +328,18 @@ class TestInjector(unittest.TestCase):
self.assertAlmostEqual(history[-2][2], -1)
def test_injector(self):
# the tests in test_keycode_mapper.py test this stuff in detail
numlock_before = is_numlock_on()
custom_mapping.change((EV_KEY, 8, 1), 'k(KEY_Q).k(w)')
combination = ((EV_KEY, 8, 1), (EV_KEY, 9, 1))
custom_mapping.change(combination, '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, 1), 'b')
# stuff the custom_mapping outputs (except for the unknown b)
system_mapping.clear()
code_a = 100
code_q = 101
@ -344,13 +348,13 @@ class TestInjector(unittest.TestCase):
system_mapping._set('key_q', code_q)
system_mapping._set('w', code_w)
# the second arg of those event objects is 8 lower than the
# keycode used in X and in the mappings
pending_events['device 2'] = [
# should execute a macro
# should execute a macro...
InputEvent(EV_KEY, 8, 1),
InputEvent(EV_KEY, 9, 1), # ...now
InputEvent(EV_KEY, 8, 0),
# gamepad stuff
InputEvent(EV_KEY, 9, 0),
# gamepad stuff. trigger a combination
InputEvent(EV_ABS, ABS_HAT0X, -1),
InputEvent(EV_ABS, ABS_HAT0X, 0),
# just pass those over without modifying
@ -375,41 +379,49 @@ class TestInjector(unittest.TestCase):
event = uinput_write_history_pipe[0].recv()
history.append((event.type, event.code, event.value))
# 1 event before the combination was triggered (+1 for release)
# 4 events for the macro
# 2 for mapped keys
# 3 for forwarded events
self.assertEqual(len(history), 9)
self.assertEqual(len(history), 11)
# since the macro takes a little bit of time to execute, its
# keystrokes are all over the place.
# just check if they are there and if so, remove them from the list.
ev_key = EV_KEY
self.assertIn((ev_key, code_q, 1), history)
self.assertIn((ev_key, code_q, 0), history)
self.assertIn((ev_key, code_w, 1), history)
self.assertIn((ev_key, code_w, 0), history)
index_q_1 = history.index((ev_key, code_q, 1))
index_q_0 = history.index((ev_key, code_q, 0))
index_w_1 = history.index((ev_key, code_w, 1))
index_w_0 = history.index((ev_key, code_w, 0))
self.assertIn((EV_KEY, 8, 1), history)
self.assertIn((EV_KEY, code_q, 1), history)
self.assertIn((EV_KEY, code_q, 1), history)
self.assertIn((EV_KEY, code_q, 0), history)
self.assertIn((EV_KEY, code_w, 1), history)
self.assertIn((EV_KEY, code_w, 0), history)
index_q_1 = history.index((EV_KEY, code_q, 1))
index_q_0 = history.index((EV_KEY, code_q, 0))
index_w_1 = history.index((EV_KEY, code_w, 1))
index_w_0 = history.index((EV_KEY, code_w, 0))
self.assertGreater(index_q_0, index_q_1)
self.assertGreater(index_w_1, index_q_0)
self.assertGreater(index_w_0, index_w_1)
del history[index_q_1]
index_q_0 = history.index((ev_key, code_q, 0))
index_q_0 = history.index((EV_KEY, code_q, 0))
del history[index_q_0]
index_w_1 = history.index((ev_key, code_w, 1))
index_w_1 = history.index((EV_KEY, code_w, 1))
del history[index_w_1]
index_w_0 = history.index((ev_key, code_w, 0))
index_w_0 = history.index((EV_KEY, code_w, 0))
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))
self.assertEqual(history[3], (ev_key, input_b, 0))
self.assertEqual(history[4], (3124, 3564, 6542))
# first the incomplete combination key that wasn't mapped to anything
# and just forwarded. The input event that triggered the macro
# won't appear here.
self.assertEqual(history[0], (EV_KEY, 8, 1))
self.assertEqual(history[1], (EV_KEY, 8, 0))
# value should be 1, even if the input event was -1.
# Injected keycodes should always be either 0 or 1
self.assertEqual(history[2], (EV_KEY, code_a, 1))
self.assertEqual(history[3], (EV_KEY, code_a, 0))
self.assertEqual(history[4], (EV_KEY, input_b, 1))
self.assertEqual(history[5], (EV_KEY, input_b, 0))
self.assertEqual(history[6], (3124, 3564, 6542))
time.sleep(0.1)
self.assertTrue(self.injector._process.is_alive())
@ -417,6 +429,111 @@ class TestInjector(unittest.TestCase):
numlock_after = is_numlock_on()
self.assertEqual(numlock_before, numlock_after)
def test_store_permutations(self):
target = {}
store_permutations(target, ((1,), (2,), (3,), (4,)), 1234)
self.assertEqual(len(target), 6)
self.assertEqual(target[((1,), (2,), (3,), (4,))], 1234)
self.assertEqual(target[((1,), (3,), (2,), (4,))], 1234)
self.assertEqual(target[((2,), (1,), (3,), (4,))], 1234)
self.assertEqual(target[((2,), (3,), (1,), (4,))], 1234)
self.assertEqual(target[((3,), (1,), (2,), (4,))], 1234)
self.assertEqual(target[((3,), (2,), (1,), (4,))], 1234)
store_permutations(target, ((1,), (2,)), 5678)
self.assertEqual(len(target), 7)
self.assertEqual(target[((1,), (2,))], 5678)
store_permutations(target, ((1,),), 3456)
self.assertEqual(len(target), 8)
self.assertEqual(target[((1,),)], 3456)
store_permutations(target, (1,), 7890)
self.assertEqual(len(target), 9)
self.assertEqual(target[(1,)], 7890)
# only accepts tuples, because key-mapper always uses tuples
# for this stuff
store_permutations(target, 1, 1357)
self.assertEqual(len(target), 9)
def test_store_permutations_for_macros(self):
mapping = Mapping()
ev_1 = (EV_KEY, 41, 1)
ev_2 = (EV_KEY, 42, 1)
ev_3 = (EV_KEY, 43, 1)
# a combination
mapping.change((ev_1, ev_2, ev_3), 'k(a)')
self.injector = KeycodeInjector('device 1', mapping)
history = []
class Stop(Exception):
pass
def _modify_capabilities(*args):
history.append(args)
# avoid going into any mainloop
raise Stop()
self.injector._modify_capabilities = _modify_capabilities
try:
self.injector._start_injecting()
except Stop:
pass
# one call
self.assertEqual(len(history), 1)
# first argument of the first call
self.assertEqual(len(history[0][0]), 2)
self.assertEqual(history[0][0][(ev_1, ev_2, ev_3)].code, 'k(a)')
self.assertEqual(history[0][0][(ev_2, ev_1, ev_3)].code, 'k(a)')
def test_key_to_code(self):
mapping = Mapping()
ev_1 = (EV_KEY, 41, 1)
ev_2 = (EV_KEY, 42, 1)
ev_3 = (EV_KEY, 43, 1)
ev_4 = (EV_KEY, 44, 1)
mapping.change(ev_1, 'a')
# a combination
mapping.change((ev_2, ev_3, ev_4), 'b')
self.assertEqual(mapping.get_character((ev_2, ev_3, ev_4)), 'b')
system_mapping.clear()
system_mapping._set('a', 51)
system_mapping._set('b', 52)
injector = KeycodeInjector('device 1', mapping)
self.assertEqual(injector._key_to_code.get(ev_1), 51)
# permutations to make matching combinations easier
self.assertEqual(injector._key_to_code.get((ev_2, ev_3, ev_4)), 52)
self.assertEqual(injector._key_to_code.get((ev_3, ev_2, ev_4)), 52)
self.assertEqual(len(injector._key_to_code), 3)
def test_is_in_capabilities(self):
key = (1, 2, 1)
capabilities = {
1: [9, 2, 5]
}
self.assertTrue(is_in_capabilities(key, capabilities))
key = ((1, 2, 1), (1, 3, 1))
capabilities = {
1: [9, 2, 5]
}
# only one of the codes of the combination is required.
# The goal is to make combinations across those sub-devices possible,
# that make up one hardware device
self.assertTrue(is_in_capabilities(key, capabilities))
key = ((1, 2, 1), (1, 5, 1))
capabilities = {
1: [9, 2, 5]
}
self.assertTrue(is_in_capabilities(key, capabilities))
if __name__ == "__main__":
unittest.main()

@ -40,7 +40,7 @@ from keymapper.state import custom_mapping, system_mapping, XMODMAP_FILENAME
from keymapper.paths import CONFIG_PATH, get_preset_path
from keymapper.config import config, WHEEL, MOUSE
from keymapper.dev.reader import keycode_reader
from keymapper.gtk.row import to_string
from keymapper.gtk.row import to_string, HOLDING, IDLE
from keymapper.dev import permissions
from tests.test import tmp, pending_events, InputEvent, \
@ -227,11 +227,18 @@ 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, 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')
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')
# combinations
self.assertEqual(to_string((
(EV_KEY, evdev.ecodes.BTN_A, 1),
(EV_KEY, evdev.ecodes.BTN_B, 1),
(EV_KEY, evdev.ecodes.BTN_C, 1)
)), 'BTN_A + BTN_B + BTN_C')
def test_row_simple(self):
rows = self.window.get('key_list').get_children()
@ -278,7 +285,8 @@ class TestIntegration(unittest.TestCase):
Parameters
----------
key : int, int, int
type, code, value
type, code, value,
or a tuple of multiple of those
code_first : boolean
If True, the code is entered and then the character.
If False, the character is entered first.
@ -287,6 +295,8 @@ class TestIntegration(unittest.TestCase):
in the mapping eventually. False if this change is going to
cause a duplicate.
"""
self.assertFalse(keycode_reader.are_keys_pressed())
# wait for the window to create a new empty row if needed
time.sleep(0.1)
gtk_iteration()
@ -297,6 +307,7 @@ class TestIntegration(unittest.TestCase):
self.assertIsNone(row.get_keycode())
self.assertEqual(row.character_input.get_text(), '')
self.assertNotIn('changed', row.get_style_context().list_classes())
self.assertEqual(row.state, IDLE)
if char and not code_first:
# set the character to make the new row complete
@ -316,21 +327,52 @@ class TestIntegration(unittest.TestCase):
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(*key))
time.sleep(0.1)
# but by sending an event. Events should be consumed 30 times
# per second, so sleep a bit more than 0.033ms each time
# press down all the keys of a combination
if isinstance(key[0], tuple):
for sub_key in key:
keycode_reader._pipe[1].send(InputEvent(*sub_key))
else:
keycode_reader._pipe[1].send(InputEvent(*key))
# make the window consume the keycode
time.sleep(0.05)
gtk_iteration()
# holding down
self.assertTrue(keycode_reader.are_keys_pressed())
self.assertEqual(row.state, HOLDING)
self.assertTrue(row.keycode_input.is_focus())
# release all the keys
if isinstance(key[0], tuple):
for sub_key in key:
keycode_reader._pipe[1].send(InputEvent(*sub_key[:2], 0))
else:
keycode_reader._pipe[1].send(InputEvent(*key[:2], 0))
# make the window consume the keycode
time.sleep(0.05)
gtk_iteration()
# released
self.assertFalse(keycode_reader.are_keys_pressed())
self.assertEqual(row.state, IDLE)
if expect_success:
self.assertEqual(row.get_keycode(), key)
css_classes = row.get_style_context().list_classes()
self.assertIn('changed', css_classes)
self.assertEqual(row.keycode_input.get_label(), to_string(*key))
self.assertEqual(row.keycode_input.get_label(), to_string(key))
self.assertFalse(row.keycode_input.is_focus())
if not expect_success:
self.assertIsNone(row.get_keycode())
self.assertIsNone(row.get_character())
self.assertNotIn('changed', row.get_style_context().list_classes())
css_classes = row.get_style_context().list_classes()
self.assertNotIn('changed', css_classes)
self.assertEqual(row.state, IDLE)
return row
if char and code_first:
@ -427,6 +469,68 @@ class TestIntegration(unittest.TestCase):
self.assertEqual(custom_mapping.get_character(ev_4), 'd')
self.assertTrue(custom_mapping.changed)
def test_combination(self):
# it should be possible to write a key combination
ev_1 = (EV_KEY, evdev.ecodes.KEY_A, 1)
ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, 1)
ev_3 = (EV_KEY, evdev.ecodes.KEY_C, 1)
ev_4 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1)
combination_1 = (ev_1, ev_2, ev_3)
combination_2 = (ev_2, ev_1, ev_3)
# same as 1, but different D-Pad direction
combination_3 = (ev_1, ev_4, ev_3)
combination_4 = (ev_4, ev_1, ev_3)
# same as 1, but the last key is different
combination_5 = (ev_1, ev_3, ev_2)
combination_6 = (ev_3, ev_1, ev_2)
self.change_empty_row(combination_1, 'a')
self.assertEqual(custom_mapping.get_character(combination_1), 'a')
self.assertEqual(custom_mapping.get_character(combination_2), 'a')
self.assertIsNone(custom_mapping.get_character(combination_3))
self.assertIsNone(custom_mapping.get_character(combination_4))
self.assertIsNone(custom_mapping.get_character(combination_5))
self.assertIsNone(custom_mapping.get_character(combination_6))
# it won't write the same combination again, even if the
# first two events are in a different order
self.change_empty_row(combination_2, 'b', expect_success=False)
self.assertEqual(custom_mapping.get_character(combination_1), 'a')
self.assertEqual(custom_mapping.get_character(combination_2), 'a')
self.assertIsNone(custom_mapping.get_character(combination_3))
self.assertIsNone(custom_mapping.get_character(combination_4))
self.assertIsNone(custom_mapping.get_character(combination_5))
self.assertIsNone(custom_mapping.get_character(combination_6))
self.change_empty_row(combination_3, 'c')
self.assertEqual(custom_mapping.get_character(combination_1), 'a')
self.assertEqual(custom_mapping.get_character(combination_2), 'a')
self.assertEqual(custom_mapping.get_character(combination_3), 'c')
self.assertEqual(custom_mapping.get_character(combination_4), 'c')
self.assertIsNone(custom_mapping.get_character(combination_5))
self.assertIsNone(custom_mapping.get_character(combination_6))
# same as with combination_2, the existing combination_3 blocks
# combination_4 because they have the same keys and end in the
# same key.
self.change_empty_row(combination_4, 'd', expect_success=False)
self.assertEqual(custom_mapping.get_character(combination_1), 'a')
self.assertEqual(custom_mapping.get_character(combination_2), 'a')
self.assertEqual(custom_mapping.get_character(combination_3), 'c')
self.assertEqual(custom_mapping.get_character(combination_4), 'c')
self.assertIsNone(custom_mapping.get_character(combination_5))
self.assertIsNone(custom_mapping.get_character(combination_6))
self.change_empty_row(combination_5, 'e')
self.assertEqual(custom_mapping.get_character(combination_1), 'a')
self.assertEqual(custom_mapping.get_character(combination_2), 'a')
self.assertEqual(custom_mapping.get_character(combination_3), 'c')
self.assertEqual(custom_mapping.get_character(combination_4), 'c')
self.assertEqual(custom_mapping.get_character(combination_5), 'e')
self.assertEqual(custom_mapping.get_character(combination_6), 'e')
def test_remove_row(self):
"""Comprehensive test for rows 2."""
# sleeps are added to be able to visually follow and debug the test.

@ -28,7 +28,7 @@ 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
active_macros, handle_keycode, unreleased
from keymapper.state import system_mapping
from keymapper.dev.macros import parse
from keymapper.config import config
@ -106,18 +106,26 @@ class TestKeycodeMapper(unittest.TestCase):
# 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)
self.assertEqual(len(unreleased), 2)
self.assertEqual(unreleased.get(ev_1[:2]), ((EV_KEY, _key_to_code[ev_1]), ev_1))
self.assertEqual(unreleased.get(ev_4[:2]), ((EV_KEY, _key_to_code[ev_4]), ev_4))
# release all of them
handle_keycode(_key_to_code, {}, InputEvent(*ev_3), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*ev_6), uinput)
self.assertEqual(len(unreleased), 0)
# repeat with other values
handle_keycode(_key_to_code, {}, InputEvent(*ev_2), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*ev_5), uinput)
self.assertEqual(len(unreleased), 2)
self.assertEqual(unreleased.get(ev_2[:2]), ((EV_KEY, _key_to_code[ev_2]), ev_2))
self.assertEqual(unreleased.get(ev_5[:2]), ((EV_KEY, _key_to_code[ev_5]), ev_5))
# 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(unreleased), 0)
self.assertEqual(len(uinput_write_history), 8)
@ -133,6 +141,41 @@ class TestKeycodeMapper(unittest.TestCase):
self.assertEqual(uinput_write_history[6].t, (EV_KEY, 52, 0))
self.assertEqual(uinput_write_history[7].t, (EV_KEY, 55, 0))
def test_d_pad_combination(self):
ev_1 = (EV_ABS, ABS_HAT0X, 1)
ev_2 = (EV_ABS, ABS_HAT0Y, -1)
ev_3 = (EV_ABS, ABS_HAT0X, 0)
ev_4 = (EV_ABS, ABS_HAT0Y, 0)
_key_to_code = {
(ev_1, ev_2): 51,
ev_2: 52,
}
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_2), uinput)
# (what_will_be_released, what_caused_the_key_down)
self.assertEqual(unreleased.get(ev_1[:2]), ((EV_ABS, ABS_HAT0X), ev_1))
self.assertEqual(unreleased.get(ev_2[:2]), ((EV_KEY, 51), ev_2))
self.assertEqual(len(unreleased), 2)
# ev_1 is unmapped and the other is the triggered combination
self.assertEqual(len(uinput_write_history), 2)
self.assertEqual(uinput_write_history[0].t, ev_1)
self.assertEqual(uinput_write_history[1].t, (EV_KEY, 51, 1))
# release all of them
handle_keycode(_key_to_code, {}, InputEvent(*ev_3), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*ev_4), uinput)
self.assertEqual(len(unreleased), 0)
self.assertEqual(len(uinput_write_history), 4)
self.assertEqual(uinput_write_history[2].t, ev_3)
self.assertEqual(uinput_write_history[3].t, (EV_KEY, 51, 0))
def test_should_map_event_as_btn(self):
self.assertTrue(should_map_event_as_btn(EV_ABS, ABS_HAT0X))
self.assertTrue(should_map_event_as_btn(EV_KEY, KEY_A))
@ -159,6 +202,89 @@ class TestKeycodeMapper(unittest.TestCase):
self.assertEqual(uinput_write_history[1].t, (EV_KEY, 3, 1))
self.assertEqual(uinput_write_history[2].t, (EV_KEY, 102, 1))
def test_combination_keycode(self):
combination = ((EV_KEY, 1, 1), (EV_KEY, 2, 1))
_key_to_code = {
combination: 101
}
uinput = UInput()
handle_keycode(_key_to_code, {}, InputEvent(*combination[0]), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*combination[1]), uinput)
self.assertEqual(len(uinput_write_history), 2)
# the first event is written and then the triggered combination
self.assertEqual(uinput_write_history[0].t, (EV_KEY, 1, 1))
self.assertEqual(uinput_write_history[1].t, (EV_KEY, 101, 1))
# release them
handle_keycode(_key_to_code, {}, InputEvent(*combination[0][:2], 0), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*combination[1][:2], 0), uinput)
# the first key writes its release event. The second key is hidden
# behind the executed combination. The result of the combination is
# also released, because it acts like a key.
self.assertEqual(len(uinput_write_history), 4)
self.assertEqual(uinput_write_history[2].t, (EV_KEY, 1, 0))
self.assertEqual(uinput_write_history[3].t, (EV_KEY, 101, 0))
# press them in the wrong order (the wrong key at the end, the order
# of all other keys won't matter). no combination should be triggered
handle_keycode(_key_to_code, {}, InputEvent(*combination[1]), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*combination[0]), uinput)
self.assertEqual(len(uinput_write_history), 6)
self.assertEqual(uinput_write_history[4].t, (EV_KEY, 2, 1))
self.assertEqual(uinput_write_history[5].t, (EV_KEY, 1, 1))
def test_combination_keycode_2(self):
combination_1 = (
(EV_KEY, 1, 1),
(EV_KEY, 2, 1),
(EV_KEY, 3, 1),
(EV_KEY, 4, 1)
)
combination_2 = (
(EV_KEY, 2, 1),
(EV_KEY, 3, 1),
(EV_KEY, 4, 1)
)
down_5 = (EV_KEY, 5, 1)
up_5 = (EV_KEY, 5, 0)
up_4 = (EV_KEY, 4, 0)
_key_to_code = {
combination_1: 101,
combination_2: 102,
down_5: 103
}
uinput = UInput()
handle_keycode(_key_to_code, {}, InputEvent(*combination_1[0]), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*combination_1[1]), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*combination_1[2]), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*combination_1[3]), uinput)
self.assertEqual(len(uinput_write_history), 4)
# the first event is written and then the triggered combination
self.assertEqual(uinput_write_history[0].t, (EV_KEY, 1, 1))
self.assertEqual(uinput_write_history[1].t, (EV_KEY, 2, 1))
self.assertEqual(uinput_write_history[2].t, (EV_KEY, 3, 1))
self.assertEqual(uinput_write_history[3].t, (EV_KEY, 101, 1))
# while the combination is down, another unrelated key can be used
handle_keycode(_key_to_code, {}, InputEvent(*down_5), uinput)
self.assertEqual(len(uinput_write_history), 5)
self.assertEqual(uinput_write_history[4].t, (EV_KEY, 103, 1))
# release the combination by releasing the last key, and release
# the unrelated key
handle_keycode(_key_to_code, {}, InputEvent(*up_4), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*up_5), uinput)
self.assertEqual(len(uinput_write_history), 7)
self.assertEqual(uinput_write_history[5].t, (EV_KEY, 101, 0))
self.assertEqual(uinput_write_history[6].t, (EV_KEY, 103, 0))
def test_handle_keycode_macro(self):
history = []
@ -435,7 +561,8 @@ class TestKeycodeMapper(unittest.TestCase):
self.assertEqual(count_before, count_after)
def test_hold_two(self):
# holding two macros at the same time
# holding two macros at the same time,
# the first one is triggered by a combination
history = []
code_1 = 100
@ -452,37 +579,46 @@ class TestKeycodeMapper(unittest.TestCase):
system_mapping._set('b', code_b)
system_mapping._set('c', code_c)
key_1 = (EV_KEY, 1)
key_0 = (EV_KEY, 10)
key_1 = (EV_KEY, 11)
key_2 = (EV_ABS, ABS_HAT0X)
down_0 = (*key_0, 1)
down_1 = (*key_1, 1)
down_2 = (*key_2, -1)
up_0 = (*key_0, 0)
up_1 = (*key_1, 0)
up_2 = (*key_2, 0)
macro_mapping = {
down_1: parse('k(1).h(k(2)).k(3)', self.mapping),
(down_0, down_1): parse('k(1).h(k(2)).k(3)', self.mapping),
down_2: parse('k(a).h(k(b)).k(c)', self.mapping)
}
def handler(*args):
history.append(args)
macro_mapping[down_1].set_handler(handler)
macro_mapping[(down_0, down_1)].set_handler(handler)
macro_mapping[down_2].set_handler(handler)
loop = asyncio.get_event_loop()
macros_uinput = UInput()
keys_uinput = UInput()
# key up won't do anything
uinput = UInput()
handle_keycode({}, macro_mapping, InputEvent(*up_1), uinput)
handle_keycode({}, macro_mapping, InputEvent(*up_2), uinput)
handle_keycode({}, macro_mapping, InputEvent(*up_0), macros_uinput)
handle_keycode({}, macro_mapping, InputEvent(*up_1), macros_uinput)
handle_keycode({}, macro_mapping, InputEvent(*up_2), macros_uinput)
loop.run_until_complete(asyncio.sleep(0.1))
self.assertEqual(len(active_macros), 0)
"""start macros"""
handle_keycode({}, macro_mapping, InputEvent(*down_1), None)
handle_keycode({}, macro_mapping, InputEvent(*down_2), None)
handle_keycode({}, macro_mapping, InputEvent(*down_0), keys_uinput)
self.assertEqual(keys_uinput.write_count, 1)
handle_keycode({}, macro_mapping, InputEvent(*down_1), keys_uinput)
handle_keycode({}, macro_mapping, InputEvent(*down_2), keys_uinput)
self.assertEqual(keys_uinput.write_count, 1)
# let the mainloop run for some time so that the macro does its stuff
sleeptime = 500
@ -497,6 +633,7 @@ class TestKeycodeMapper(unittest.TestCase):
"""stop macros"""
# releasing the last key of a combination releases the whole macro
handle_keycode({}, macro_mapping, InputEvent(*up_1), None)
handle_keycode({}, macro_mapping, InputEvent(*up_2), None)
@ -640,6 +777,9 @@ class TestKeycodeMapper(unittest.TestCase):
self.assertEqual(uinput_write_history[3].t, (EV_KEY, 52, 0))
def test_ignore_hold(self):
# hold as in event-value 2, not in macro-hold.
# linux will generate events with value 2 after key-mapper injected
# the key-press, so key-mapper doesn't need to forward them.
key = (EV_KEY, KEY_A)
ev_1 = (*key, 1)
ev_2 = (*key, 2)
@ -658,8 +798,6 @@ class TestKeycodeMapper(unittest.TestCase):
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__":

@ -24,7 +24,7 @@ import unittest
import json
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, KEY_A
from keymapper.mapping import Mapping
from keymapper.mapping import Mapping, verify_key
from keymapper.state import SystemMapping, XMODMAP_FILENAME
from keymapper.config import config
from keymapper.paths import get_preset_path
@ -180,7 +180,7 @@ class TestMapping(unittest.TestCase):
self.mapping.change(one, '1')
self.mapping.change(two, '2')
self.mapping.change(three, '3')
self.mapping.change((two, three), '3')
self.mapping._config['foo'] = 'bar'
self.mapping.save(get_preset_path('device 1', 'test'))
@ -194,11 +194,12 @@ class TestMapping(unittest.TestCase):
self.assertEqual(len(loaded), 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.get_character((two, three)), '3')
self.assertEqual(loaded._config['foo'], 'bar')
def test_save_load_2(self):
# loads mappings with only (type, code) as the key
# loads mappings with only (type, code) as the key by using 1 as value,
# loads combinations chained with +
path = os.path.join(tmp, 'presets', 'device 1', 'test.json')
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w') as file:
@ -206,17 +207,29 @@ class TestMapping(unittest.TestCase):
'mapping': {
f'{EV_KEY},3': 'a',
f'{EV_ABS},{ABS_HAT0X},-1': 'b',
f'{EV_ABS},{ABS_HAT0X},1': 'c',
f'{EV_ABS},1,1+{EV_ABS},2,-1+{EV_ABS},3,1': 'c',
# ignored because broken
f'3,1,1+': 'd',
f'3,1,1,2': 'e',
f'3': 'e',
f'+3,1,2': 'f',
f',,+3,1,2': 'g',
f'': 'h',
}
}, file)
loaded = Mapping()
loaded.load(get_preset_path('device 1', 'test'))
self.assertEqual(len(loaded), 3)
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')
self.assertEqual(loaded.get_character(
((EV_ABS, 1, 1), (EV_ABS, 2, -1), (EV_ABS, 3, 1))
), 'c')
def test_change(self):
# the reader would not report values like 111 or 222, only 1 or -1.
# the mapping just does what it is told, so it accepts them.
ev_1 = (EV_KEY, 1, 111)
ev_2 = (EV_KEY, 1, 222)
ev_3 = (EV_KEY, 2, 111)
@ -251,6 +264,33 @@ class TestMapping(unittest.TestCase):
self.assertEqual(self.mapping.get_character(ev_4), 'e')
self.assertEqual(len(self.mapping), 2)
def test_combinations(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)
combi_1 = (ev_1, ev_2, ev_3)
combi_2 = (ev_2, ev_1, ev_3)
combi_3 = (ev_1, ev_2, ev_4)
self.mapping.change(combi_1, 'a')
self.assertEqual(self.mapping.get_character(combi_1), 'a')
self.assertEqual(self.mapping.get_character(combi_2), 'a')
# since combi_1 and combi_2 are equivalent, a changes to b
self.mapping.change(combi_2, 'b')
self.assertEqual(self.mapping.get_character(combi_1), 'b')
self.assertEqual(self.mapping.get_character(combi_2), 'b')
self.mapping.change(combi_3, 'c')
self.assertEqual(self.mapping.get_character(combi_1), 'b')
self.assertEqual(self.mapping.get_character(combi_2), 'b')
self.assertEqual(self.mapping.get_character(combi_3), 'c')
self.mapping.change(combi_3, 'c', combi_1)
self.assertIsNone(self.mapping.get_character(combi_1))
self.assertIsNone(self.mapping.get_character(combi_2))
self.assertEqual(self.mapping.get_character(combi_3), 'c')
def test_clear(self):
# does nothing
self.mapping.clear((EV_KEY, 40, 1))
@ -282,6 +322,22 @@ class TestMapping(unittest.TestCase):
self.mapping.empty()
self.assertEqual(len(self.mapping), 0)
def test_verify_key(self):
self.assertRaises(ValueError, lambda: verify_key(1))
self.assertRaises(ValueError, lambda: verify_key(None))
self.assertRaises(ValueError, lambda: verify_key([1]))
self.assertRaises(ValueError, lambda: verify_key((1,)))
self.assertRaises(ValueError, lambda: verify_key((1, 2)))
self.assertRaises(ValueError, lambda: verify_key(('1', '2', '3')))
self.assertRaises(ValueError, lambda: verify_key('1'))
self.assertRaises(ValueError, lambda: verify_key('(1,2,3)'))
self.assertRaises(ValueError, lambda: verify_key(((1, 2, 3), (1, 2, '3'))))
self.assertRaises(ValueError, lambda: verify_key(((1, 2, 3), (1, 2, 3), None)))
# those don't raise errors
verify_key(((1, 2, 3), (1, 2, 3)))
verify_key((1, 2, 3))
if __name__ == "__main__":
unittest.main()

@ -22,12 +22,12 @@
import unittest
import time
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, KEY_COMMA, BTN_LEFT, \
BTN_TOOL_DOUBLETAP
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, ABS_HAT0Y, KEY_COMMA, \
BTN_LEFT, BTN_TOOL_DOUBLETAP
from keymapper.dev.reader import keycode_reader
from tests.test import InputEvent, pending_events, EVENT_READ_TIMEOUT
from tests.test import InputEvent, pending_events, EVENT_READ_TIMEOUT, cleanup
CODE_1 = 100
@ -52,13 +52,21 @@ class TestReader(unittest.TestCase):
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
cleanup()
def test_reading_1(self):
# a single event
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, 1))
self.assertEqual(keycode_reader.read(), None)
self.assertEqual(len(keycode_reader._unreleased), 1)
def test_reading_2(self):
# a combination of events
pending_events['device 1'] = [
InputEvent(EV_KEY, CODE_1, 1, 10000.1234),
InputEvent(EV_KEY, CODE_3, 1, 10001.1234),
@ -71,15 +79,13 @@ class TestReader(unittest.TestCase):
wait(keycode_reader._pipe[0].poll, 0.5)
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, 1))
self.assertEqual(keycode_reader.read(), (
(EV_KEY, CODE_1, 1),
(EV_KEY, CODE_3, 1),
(EV_ABS, ABS_HAT0X, -1)
))
self.assertEqual(keycode_reader.read(), None)
self.assertEqual(len(keycode_reader._unreleased), 3)
def test_ignore_btn_left(self):
# click events are ignored because overwriting them would render the
@ -95,6 +101,19 @@ class TestReader(unittest.TestCase):
time.sleep(0.1)
self.assertEqual(keycode_reader.read(), (EV_KEY, CODE_2, 1))
self.assertEqual(keycode_reader.read(), None)
self.assertEqual(len(keycode_reader._unreleased), 1)
def test_ignore_value_2(self):
# this is not a combination, because (EV_KEY CODE_3, 2) is ignored
pending_events['device 1'] = [
InputEvent(EV_ABS, ABS_HAT0X, 1),
InputEvent(EV_KEY, CODE_3, 2)
]
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)
self.assertEqual(len(keycode_reader._unreleased), 1)
def test_reading_ignore_up(self):
pending_events['device 1'] = [
@ -106,6 +125,7 @@ class TestReader(unittest.TestCase):
time.sleep(0.1)
self.assertEqual(keycode_reader.read(), (EV_KEY, CODE_2, 1))
self.assertEqual(keycode_reader.read(), None)
self.assertEqual(len(keycode_reader._unreleased), 1)
def test_wrong_device(self):
pending_events['device 1'] = [
@ -116,6 +136,7 @@ class TestReader(unittest.TestCase):
keycode_reader.start_reading('device 2')
time.sleep(EVENT_READ_TIMEOUT * 5)
self.assertEqual(keycode_reader.read(), None)
self.assertEqual(len(keycode_reader._unreleased), 0)
def test_keymapper_devices(self):
# Don't read from keymapper devices, their keycodes are not
@ -130,6 +151,7 @@ class TestReader(unittest.TestCase):
keycode_reader.start_reading('device 2')
time.sleep(EVENT_READ_TIMEOUT * 5)
self.assertEqual(keycode_reader.read(), None)
self.assertEqual(len(keycode_reader._unreleased), 0)
def test_clear(self):
pending_events['device 1'] = [
@ -141,6 +163,7 @@ class TestReader(unittest.TestCase):
time.sleep(EVENT_READ_TIMEOUT * 5)
keycode_reader.clear()
self.assertEqual(keycode_reader.read(), None)
self.assertEqual(len(keycode_reader._unreleased), 0)
def test_switch_device(self):
pending_events['device 2'] = [InputEvent(EV_KEY, CODE_1, 1)]
@ -161,15 +184,24 @@ class TestReader(unittest.TestCase):
# based on the event type
pending_events['device 1'] = [
InputEvent(EV_ABS, ABS_HAT0X, 1, 1234.0000),
InputEvent(EV_ABS, ABS_HAT0X, 0, 1234.0001),
InputEvent(EV_ABS, ABS_HAT0X, 1, 1235.0000), # ignored
InputEvent(EV_ABS, ABS_HAT0X, 0, 1235.0001),
InputEvent(EV_KEY, KEY_COMMA, 1, 1235.0010),
InputEvent(EV_KEY, KEY_COMMA, 0, 1235.0011),
InputEvent(EV_ABS, ABS_HAT0X, 1, 1235.0020), # ignored
InputEvent(EV_ABS, ABS_HAT0X, 0, 1235.0021), # ignored
InputEvent(EV_ABS, ABS_HAT0X, 1, 1236.0000)
]
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)
self.assertEqual(len(keycode_reader._unreleased), 1)
def test_prioritizing_2_normalize(self):
# furthermore, 1234 is 1 in the reader, because it probably is some
@ -180,22 +212,31 @@ class TestReader(unittest.TestCase):
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
# this time, don't release anything. the combination should
# ignore stuff as well.
]
keycode_reader.start_reading('device 1')
wait(keycode_reader._pipe[0].poll, 0.5)
self.assertEqual(keycode_reader.read(), (EV_KEY, KEY_COMMA, 1))
self.assertEqual(keycode_reader.read(), (
(EV_ABS, ABS_HAT0X, 1),
(EV_KEY, KEY_COMMA, 1)
))
self.assertEqual(keycode_reader.read(), None)
self.assertEqual(len(keycode_reader._unreleased), 2)
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
InputEvent(EV_ABS, ABS_HAT0Y, 0, 1234.0030) # ignored
# this time don't release anything as well, but it's not
# a combination because only one event is accepted
]
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)
self.assertEqual(len(keycode_reader._unreleased), 1)
if __name__ == "__main__":

Loading…
Cancel
Save