diff --git a/README.md b/README.md index 0fe0b094..8b2d9957 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/keymapper/config.py b/keymapper/config.py index 656a4823..fb9a9c17 100644 --- a/keymapper/config.py +++ b/keymapper/config.py @@ -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, }, } } diff --git a/keymapper/dev/ev_abs_mapper.py b/keymapper/dev/ev_abs_mapper.py index 3654b1df..bb634c9f 100644 --- a/keymapper/dev/ev_abs_mapper.py +++ b/keymapper/dev/ev_abs_mapper.py @@ -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 diff --git a/keymapper/dev/injector.py b/keymapper/dev/injector.py index 0e6be736..b105424b 100644 --- a/keymapper/dev/injector.py +++ b/keymapper/dev/injector.py @@ -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 diff --git a/keymapper/dev/keycode_mapper.py b/keymapper/dev/keycode_mapper.py index ffb0e5c5..5aec38d5 100644 --- a/keymapper/dev/keycode_mapper.py +++ b/keymapper/dev/keycode_mapper.py @@ -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() diff --git a/keymapper/dev/macros.py b/keymapper/dev/macros.py index eb3fdf8c..444d8441 100644 --- a/keymapper/dev/macros.py +++ b/keymapper/dev/macros.py @@ -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 diff --git a/keymapper/dev/reader.py b/keymapper/dev/reader.py index 986cccbf..9581a2a9 100644 --- a/keymapper/dev/reader.py +++ b/keymapper/dev/reader.py @@ -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() diff --git a/keymapper/gtk/row.py b/keymapper/gtk/row.py index 7fee30c9..5284d197 100644 --- a/keymapper/gtk/row.py +++ b/keymapper/gtk/row.py @@ -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) diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py index 3ef1a6a3..b24ee7d7 100755 --- a/keymapper/gtk/window.py +++ b/keymapper/gtk/window.py @@ -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) diff --git a/keymapper/logger.py b/keymapper/logger.py index 47bdf47b..893ee4df 100644 --- a/keymapper/logger.py +++ b/keymapper/logger.py @@ -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') diff --git a/keymapper/mapping.py b/keymapper/mapping.py index 16f211b2..c9b40d0e 100644 --- a/keymapper/mapping.py +++ b/keymapper/mapping.py @@ -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) diff --git a/keymapper/util.py b/keymapper/util.py new file mode 100644 index 00000000..8fd5665f --- /dev/null +++ b/keymapper/util.py @@ -0,0 +1,33 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# key-mapper - GUI for device specific keyboard mappings +# Copyright (C) 2020 sezanzeb +# +# This file is part of key-mapper. +# +# key-mapper is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# key-mapper is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with key-mapper. If not, see . + + +"""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 diff --git a/readme/coverage.svg b/readme/coverage.svg index c1490035..607d3de4 100644 --- a/readme/coverage.svg +++ b/readme/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 90% - 90% + 91% + 91% diff --git a/readme/pylint.svg b/readme/pylint.svg index a31dd1ad..bde3a939 100644 --- a/readme/pylint.svg +++ b/readme/pylint.svg @@ -17,7 +17,7 @@ pylint - 9.67 - 9.67 + 9.76 + 9.76 \ No newline at end of file diff --git a/tests/test.py b/tests/test.py index dbc1fce0..cb761e98 100644 --- a/tests/test.py +++ b/tests/test.py @@ -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 diff --git a/tests/testcases/test_config.py b/tests/testcases/test_config.py index 18f419af..fb763976 100644 --- a/tests/testcases/test_config.py +++ b/tests/testcases/test_config.py @@ -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 = {} diff --git a/tests/testcases/test_daemon.py b/tests/testcases/test_daemon.py index 8327bc9a..f4d7a6e7 100644 --- a/tests/testcases/test_daemon.py +++ b/tests/testcases/test_daemon.py @@ -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() diff --git a/tests/testcases/test_ev_abs_mapper.py b/tests/testcases/test_ev_abs_mapper.py new file mode 100644 index 00000000..7449bd0c --- /dev/null +++ b/tests/testcases/test_ev_abs_mapper.py @@ -0,0 +1,143 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# key-mapper - GUI for device specific keyboard mappings +# Copyright (C) 2020 sezanzeb +# +# This file is part of key-mapper. +# +# key-mapper is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# key-mapper is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with key-mapper. If not, see . + + +import unittest +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() diff --git a/tests/testcases/test_injector.py b/tests/testcases/test_injector.py index 31b5721f..3b11c3b7 100644 --- a/tests/testcases/test_injector.py +++ b/tests/testcases/test_injector.py @@ -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)) diff --git a/tests/testcases/test_integration.py b/tests/testcases/test_integration.py index cf65bae9..6bf6a014 100644 --- a/tests/testcases/test_integration.py +++ b/tests/testcases/test_integration.py @@ -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) diff --git a/tests/testcases/test_keycode_mapper.py b/tests/testcases/test_keycode_mapper.py index fbd6a352..ce4a73a1 100644 --- a/tests/testcases/test_keycode_mapper.py +++ b/tests/testcases/test_keycode_mapper.py @@ -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() diff --git a/tests/testcases/test_logger.py b/tests/testcases/test_logger.py index 40b38682..320fd607 100644 --- a/tests/testcases/test_logger.py +++ b/tests/testcases/test_logger.py @@ -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') diff --git a/tests/testcases/test_mapping.py b/tests/testcases/test_mapping.py index 70b070d6..53dcd5f4 100644 --- a/tests/testcases/test_mapping.py +++ b/tests/testcases/test_mapping.py @@ -19,18 +19,27 @@ # along with key-mapper. If not, see . +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) diff --git a/tests/testcases/test_reader.py b/tests/testcases/test_reader.py index fd7c9ca5..1cc48cb4 100644 --- a/tests/testcases/test_reader.py +++ b/tests/testcases/test_reader.py @@ -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__":