From e81328d1104390c448276f2207fa9eba61d9e6c2 Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Sat, 13 Feb 2021 22:34:12 +0100 Subject: [PATCH] moved injection-wide infos to a dedicated context class --- keymapper/injection/context.py | 153 ++++++++++++++++++++ keymapper/injection/event_producer.py | 48 +++---- keymapper/injection/injector.py | 128 ++++------------- keymapper/injection/keycode_mapper.py | 42 ++---- keymapper/injection/readme.md | 10 +- tests/testcases/test_daemon.py | 4 +- tests/testcases/test_event_producer.py | 7 +- tests/testcases/test_injector.py | 98 ++++++------- tests/testcases/test_keycode_mapper.py | 187 ++++++++++++------------- 9 files changed, 348 insertions(+), 329 deletions(-) create mode 100644 keymapper/injection/context.py diff --git a/keymapper/injection/context.py b/keymapper/injection/context.py new file mode 100644 index 00000000..952c6894 --- /dev/null +++ b/keymapper/injection/context.py @@ -0,0 +1,153 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# key-mapper - GUI for device specific keyboard mappings +# Copyright (C) 2021 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 . + + +"""Stores injection-process wide information.""" + + +from keymapper.logger import logger +from keymapper.injection.macros import parse, is_this_a_macro +from keymapper.state import system_mapping +from keymapper.config import NONE, MOUSE, WHEEL + + +class Context: + """ + Stores injection-process wide information. + + In some ways this is a wrapper for the mapping that derives some + information that is specifically important to the injection. + + One Context exists for each injection process, which is shared + with all coroutines and used objects. + + Benefits of the context: + - less redundant passing around of parameters + - more clear which parameters are shared between coroutines and which + ones are unique to a certain method + - easier to add new process wide information without having to adjust + all function calls in unittests + - makes the injection class shorter and more specific to a certain task, + which is actually spinning up the injection. + + Members + ------- + mapping : Mapping + the mapping that is the source of key_to_code and macros, + only used to query config values. + key_to_code : dict + mapping of ((type, code, value),) to linux-keycode + or multiple of those like ((...), (...), ...) for combinations + combinations need to be present in every possible valid ordering. + e.g. shift + alt + a and alt + shift + a. + This is needed to query keycodes more efficiently without having + to search mapping each time. + macros : dict + mapping of ((type, code, value),) to _Macro objects. + Combinations work similar as in key_to_code + """ + def __init__(self, mapping): + self.mapping = mapping + + # avoid searching through the mapping at runtime, + # might be a bit expensive + self.key_to_code = self._map_keys_to_codes() + self.macros = self._parse_macros() + self.left_purpose = None + self.right_purpose = None + self.update_purposes() + + def update_purposes(self): + """Read joystick purposes from the configuration.""" + self.left_purpose = self.mapping.get('gamepad.joystick.left_purpose') + self.right_purpose = self.mapping.get('gamepad.joystick.right_purpose') + + def _parse_macros(self): + """To quickly get the target macro during operation.""" + logger.debug('Parsing macros') + macros = {} + for key, output in self.mapping: + if is_this_a_macro(output): + macro = parse(output, self.mapping) + if macro is None: + continue + + for permutation in key.get_permutations(): + macros[permutation.keys] = macro + + if len(macros) == 0: + logger.debug('No macros configured') + + return macros + + def _map_keys_to_codes(self): + """To quickly get target keycodes during operation. + + Returns a mapping of one or more 3-tuples to ints. + Examples: + ((1, 2, 1),): 3 + ((1, 5, 1), (1, 4, 1)): 4 + """ + key_to_code = {} + for key, output in self.mapping: + if is_this_a_macro(output): + continue + + target_code = system_mapping.get(output) + if target_code is None: + logger.error('Don\'t know what %s is', output) + continue + + for permutation in key.get_permutations(): + if permutation.keys[-1][-1] not in [-1, 1]: + logger.error( + 'Expected values to be -1 or 1 at this point: %s', + permutation.keys + ) + key_to_code[permutation.keys] = target_code + + return key_to_code + + def is_mapped(self, key): + """Check if this key is used for macros or mappings. + + Parameters + ---------- + key : ((int, int, int),) + One or more 3-tuples of type, code, value + """ + return key in self.macros or key in self.key_to_code + + def forwards_joystick(self): + """If at least one of the joysticks remains a regular joystick.""" + return NONE in (self.left_purpose, self.right_purpose) + + def maps_joystick(self): + """If at least one of the joysticks will serve a special purpose.""" + return (self.left_purpose, self.right_purpose) != (NONE, NONE) + + def joystick_as_mouse(self): + """If at least one joystick maps to an EV_REL capability.""" + purposes = (self.left_purpose, self.right_purpose) + return MOUSE in purposes or WHEEL in purposes + + def writes_keys(self): + """Check if anything is being mapped to keys.""" + return len(self.macros) == 0 and len(self.key_to_code) == 0 diff --git a/keymapper/injection/event_producer.py b/keymapper/injection/event_producer.py index 25ba53e8..eaa4d150 100644 --- a/keymapper/injection/event_producer.py +++ b/keymapper/injection/event_producer.py @@ -51,15 +51,10 @@ class EventProducer: This class does not handle injecting macro stuff over time, that is done by the keycode_mapper. """ - def __init__(self, mapping): - """Construct the event producer without it doing anything yet. + def __init__(self, context): + """Construct the event producer without it doing anything yet.""" + self.context = context - Parameters - ---------- - mapping : Mapping - the mapping object that configures the current injection - """ - self.mapping = mapping self.mouse_uinput = None self.max_abs = None # events only take ints, so a movement of 0.3 needs to add @@ -69,15 +64,6 @@ class EventProducer: self.abs_state = {ABS_X: 0, ABS_Y: 0, ABS_RX: 0, ABS_RY: 0} self.debounces = {} - self.left_purpose = None - self.right_purpose = None - - self.update_purposes() - - def update_purposes(self): - """Figure out how the joysticks should be used""" - self.left_purpose = self.mapping.get('gamepad.joystick.left_purpose') - self.right_purpose = self.mapping.get('gamepad.joystick.right_purpose') def notify(self, event): """Tell the EventProducer about the newest ABS event. @@ -150,7 +136,7 @@ class EventProducer: self.max_abs = max_abs logger.debug('Max abs of "%s": %s', device.name, max_abs) - def get_abs_values(self, left_purpose, right_purpose): + def get_abs_values(self): """Get the raw values for wheel and mouse movement. If two joysticks have the same purpose, the one that reports higher @@ -158,26 +144,26 @@ class EventProducer: """ mouse_x, mouse_y, wheel_x, wheel_y = 0, 0, 0, 0 - if left_purpose == MOUSE: + if self.context.left_purpose == MOUSE: mouse_x = abs_max(mouse_x, self.abs_state[ABS_X]) mouse_y = abs_max(mouse_y, self.abs_state[ABS_Y]) - if left_purpose == WHEEL: + if self.context.left_purpose == WHEEL: wheel_x = abs_max(wheel_x, self.abs_state[ABS_X]) wheel_y = abs_max(wheel_y, self.abs_state[ABS_Y]) - if right_purpose == MOUSE: + if self.context.right_purpose == MOUSE: mouse_x = abs_max(mouse_x, self.abs_state[ABS_RX]) mouse_y = abs_max(mouse_y, self.abs_state[ABS_RY]) - if right_purpose == WHEEL: + if self.context.right_purpose == WHEEL: wheel_x = abs_max(wheel_x, self.abs_state[ABS_RX]) wheel_y = abs_max(wheel_y, self.abs_state[ABS_RY]) return mouse_x, mouse_y, wheel_x, wheel_y def is_handled(self, event): - """Check if the event is something ev_abs will take care of.""" + """Check if the event is something this will take care of.""" if event.type != EV_ABS or event.code not in utils.JOYSTICK: return False @@ -185,11 +171,13 @@ class EventProducer: return False purposes = [MOUSE, WHEEL] + left_purpose = self.context.left_purpose + right_purpose = self.context.right_purpose - if event.code in (ABS_X, ABS_Y) and self.left_purpose in purposes: + if event.code in (ABS_X, ABS_Y) and left_purpose in purposes: return True - if event.code in (ABS_RX, ABS_RY) and self.right_purpose in purposes: + if event.code in (ABS_RX, ABS_RY) and right_purpose in purposes: return True return False @@ -201,19 +189,17 @@ class EventProducer: its position, this will keep injecting the mouse movement events. """ max_abs = self.max_abs - mapping = self.mapping + mapping = self.context.mapping pointer_speed = mapping.get('gamepad.joystick.pointer_speed') non_linearity = mapping.get('gamepad.joystick.non_linearity') x_scroll_speed = mapping.get('gamepad.joystick.x_scroll_speed') y_scroll_speed = mapping.get('gamepad.joystick.y_scroll_speed') - left_purpose = self.left_purpose - right_purpose = self.right_purpose if max_abs is not None: logger.info( 'Left joystick as %s, right joystick as %s', - left_purpose, - right_purpose + self.context.left_purpose, + self.context.right_purpose ) start = time.time() @@ -243,7 +229,7 @@ class EventProducer: max_speed = ((max_abs ** 2) * 2) ** 0.5 - abs_values = self.get_abs_values(left_purpose, right_purpose) + abs_values = self.get_abs_values() if len([val for val in abs_values if val > max_abs]) > 0: logger.error( diff --git a/keymapper/injection/injector.py b/keymapper/injection/injector.py index c1f44e56..b14381f2 100644 --- a/keymapper/injection/injector.py +++ b/keymapper/injection/injector.py @@ -36,10 +36,8 @@ from keymapper.getdevices import get_devices, is_gamepad from keymapper.injection.keycode_mapper import KeycodeMapper from keymapper import utils from keymapper.injection.event_producer import EventProducer -from keymapper.injection.macros import parse, is_this_a_macro -from keymapper.state import system_mapping from keymapper.mapping import DISABLE_CODE -from keymapper.config import NONE, MOUSE, WHEEL +from keymapper.injection.context import Context DEV_NAME = 'key-mapper' @@ -148,11 +146,12 @@ class Injector(multiprocessing.Process): mapping : Mapping """ self.device = device - self.mapping = mapping - self._key_to_code = self._map_keys_to_codes() self._event_producer = None self._state = UNKNOWN self._msg_pipe = multiprocessing.Pipe() + + self.context = Context(mapping) + super().__init__() # Functions to interact with the running process: @@ -200,54 +199,6 @@ class Injector(multiprocessing.Process): # Process internal stuff: - def _forwards_joystick(self): - """If at least one of the joysticks remains a regular joystick.""" - left_purpose = self.mapping.get('gamepad.joystick.left_purpose') - right_purpose = self.mapping.get('gamepad.joystick.right_purpose') - return NONE in (left_purpose, right_purpose) - - def _maps_joystick(self): - """If at least one of the joysticks will serve a special purpose.""" - left_purpose = self.mapping.get('gamepad.joystick.left_purpose') - right_purpose = self.mapping.get('gamepad.joystick.right_purpose') - return (left_purpose, right_purpose) != (NONE, NONE) - - def _joystick_as_mouse(self): - """If at least one joystick maps to an EV_REL capability.""" - purposes = ( - self.mapping.get('gamepad.joystick.left_purpose'), - self.mapping.get('gamepad.joystick.right_purpose') - ) - return MOUSE in purposes or WHEEL in purposes - - def _map_keys_to_codes(self): - """To quickly get target keycodes during operation. - - Returns a mapping of one or more 3-tuples to ints. - Examples: - ((1, 2, 1),): 3 - ((1, 5, 1), (1, 4, 1)): 4 - """ - key_to_code = {} - for key, output in self.mapping: - if is_this_a_macro(output): - continue - - target_code = system_mapping.get(output) - if target_code is None: - logger.error('Don\'t know what %s is', output) - continue - - for permutation in key.get_permutations(): - if permutation.keys[-1][-1] not in [-1, 1]: - logger.error( - 'Expected values to be -1 or 1 at this point: %s', - permutation.keys - ) - key_to_code[permutation.keys] = target_code - - return key_to_code - def _grab_device(self, path): """Try to grab the device, return None if not needed/possible.""" try: @@ -259,14 +210,14 @@ class Injector(multiprocessing.Process): capabilities = device.capabilities(absinfo=False) needed = False - for key, _ in self.mapping: + for key, _ in self.context.mapping: if is_in_capabilities(key, capabilities): needed = True break gamepad = is_gamepad(device) - if gamepad and self._maps_joystick(): + if gamepad and self.context.maps_joystick(): needed = True if not needed: @@ -301,7 +252,7 @@ class Injector(multiprocessing.Process): return device - def _modify_capabilities(self, macros, input_device, gamepad): + def _modify_capabilities(self, input_device, gamepad): """Adds all used keycodes into a copy of a devices capabilities. Sometimes capabilities are a bit tricky and change how the system @@ -309,8 +260,6 @@ class Injector(multiprocessing.Process): Parameters ---------- - macros : dict - mapping of int to _Macro input_device : evdev.InputDevice gamepad : bool if ABS capabilities should be removed in favor of REL @@ -326,11 +275,11 @@ class Injector(multiprocessing.Process): # to act like the device. capabilities = input_device.capabilities(absinfo=True) - if (self._key_to_code or macros) and capabilities.get(EV_KEY) is None: + if self.context.writes_keys and capabilities.get(EV_KEY) is None: capabilities[EV_KEY] = [] # Furthermore, support all injected keycodes - for code in self._key_to_code.values(): + for code in self.context.key_to_code.values(): if code == DISABLE_CODE: continue @@ -338,10 +287,10 @@ class Injector(multiprocessing.Process): capabilities[EV_KEY].append(code) # and all keycodes that are injected by macros - for macro in macros.values(): + for macro in self.context.macros.values(): capabilities[EV_KEY] += list(macro.get_capabilities()) - if gamepad and self._joystick_as_mouse(): + if gamepad and self.context.joystick_as_mouse(): # REL_WHEEL was also required to recognize the gamepad # as mouse, even if no joystick is used as wheel. capabilities[EV_REL] = [ @@ -364,7 +313,7 @@ class Injector(multiprocessing.Process): del capabilities[ecodes.EV_SYN] if ecodes.EV_FF in capabilities: del capabilities[ecodes.EV_FF] - if gamepad and not self._forwards_joystick(): + if gamepad and not self.context.forwards_joystick(): # Key input to text inputs and such only works without ABS # events in the capabilities, possibly due to some intentional # constraints in wayland/X. So if the joysticks are not used @@ -408,6 +357,8 @@ class Injector(multiprocessing.Process): logger.error('Cannot inject for unknown device "%s"', self.device) return + logger.info('Starting injecting the mapping for "%s"', self.device) + # create a new event loop, because somehow running an infinite loop # that sleeps on iterations (event_producer) in one process causes # another injection process to screw up reading from the grabbed @@ -415,32 +366,13 @@ class Injector(multiprocessing.Process): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - numlock_state = is_numlock_on() + self._event_producer = EventProducer(self.context) + numlock_state = is_numlock_on() coroutines = [] - logger.info('Starting injecting the mapping for "%s"', self.device) - - paths = get_devices()[self.device]['paths'] - - self._event_producer = EventProducer(self.mapping) - - logger.debug('Parsing macros') - macros = {} - for key, output in self.mapping: - if is_this_a_macro(output): - macro = parse(output, self.mapping) - if macro is None: - continue - - for permutation in key.get_permutations(): - macros[permutation.keys] = macro - - if len(macros) == 0: - logger.debug('No macros configured') - # Watch over each one of the potentially multiple devices per hardware - for path in paths: + for path in get_devices()[self.device]['paths']: source = self._grab_device(path) if source is None: # this path doesn't need to be grabbed for injection, because @@ -459,7 +391,7 @@ class Injector(multiprocessing.Process): uinput = evdev.UInput( name=f'{DEV_NAME} {self.device}', phys=DEV_NAME, - events=self._modify_capabilities(macros, source, gamepad) + events=self._modify_capabilities(source, gamepad) ) logger.spam( @@ -468,12 +400,12 @@ class Injector(multiprocessing.Process): ) # actual reading of events - coroutines.append(self._event_consumer(macros, source, uinput)) + coroutines.append(self._event_consumer(source, uinput)) # The event source of the current iteration will deliver events # that are needed for this. It is that one that will be mapped # to a mouse-like devnode. - if gamepad and self._joystick_as_mouse(): + if gamepad and self.context.joystick_as_mouse(): self._event_producer.set_max_abs_from(source) self._event_producer.set_mouse_uinput(uinput) @@ -513,7 +445,7 @@ class Injector(multiprocessing.Process): uinput.write(EV_KEY, code, value) uinput.syn() - async def _event_consumer(self, macros, source, uinput): + async def _event_consumer(self, source, uinput): """Reads input events to inject keycodes or talk to the event_producer. Can be stopped by stopping the asyncio loop. This loop @@ -523,8 +455,6 @@ class Injector(multiprocessing.Process): Parameters ---------- - macros : int: _Macro - macro with a handler that writes to the provided uinput source : evdev.InputDevice where to read keycodes from uinput : evdev.UInput @@ -535,10 +465,7 @@ class Injector(multiprocessing.Process): source.path, source.fd ) - keycode_handler = KeycodeMapper( - source, self.mapping, uinput, - self._key_to_code, macros - ) + keycode_handler = KeycodeMapper(self.context, source, uinput) async for event in source.async_read_loop(): if self._event_producer.is_handled(event): @@ -547,12 +474,10 @@ class Injector(multiprocessing.Process): continue # for mapped stuff - if utils.should_map_event_as_btn(event, self.mapping): + if utils.should_map_event_as_btn(event, self.context.mapping): will_report_key_up = utils.will_report_key_up(event) - keycode_handler.handle_keycode( - event, - ) + keycode_handler.handle_keycode(event) if not will_report_key_up: # simulate a key-up event if no down event arrives anymore. @@ -561,10 +486,7 @@ class Injector(multiprocessing.Process): self._event_producer.debounce( debounce_id=(event.type, event.code, event.value), func=keycode_handler.handle_keycode, - args=( - release, - False - ), + args=(release, False), ticks=3, ) diff --git a/keymapper/injection/keycode_mapper.py b/keymapper/injection/keycode_mapper.py index b077d9e5..58120b8a 100644 --- a/keymapper/injection/keycode_mapper.py +++ b/keymapper/injection/keycode_mapper.py @@ -193,7 +193,7 @@ def print_unreleased(): class KeycodeMapper: """Injects keycodes and starts macros.""" - def __init__(self, source, mapping, uinput, key_to_code, macros): + def __init__(self, context, source, uinput): """Create a keycode mapper for one virtual device. There may be multiple KeycodeMappers for one hardware device. They @@ -201,34 +201,21 @@ class KeycodeMapper: Parameters ---------- + context : Context + the configuration of the Injector process source : InputDevice where events used in handle_keycode come from - mapping : Mapping - the mapping that is the source of key_to_code and macros, - only used to query config values. uinput : UInput: where to inject events to - key_to_code : dict - mapping of ((type, code, value),) to linux-keycode - or multiple of those like ((...), (...), ...) for combinations - combinations need to be present in every possible valid ordering. - e.g. shift + alt + a and alt + shift + a. - This is needed to query keycodes more efficiently without having - to search mapping each time. - macros : dict - mapping of ((type, code, value),) to _Macro objects. - Combinations work similar as in key_to_code """ self.source = source self.max_abs = utils.get_max_abs(source) - self.mapping = mapping + self.context = context self.uinput = uinput # some type checking, prevents me from forgetting what that stuff # is supposed to be when writing tests. - # TODO create that stuff (including macros) from mapping here instead - # of the injector, to not provide redundant parameters - for key in key_to_code: + for key in context.key_to_code: for sub_key in key: if abs(sub_key[2]) > 1: raise ValueError( @@ -236,9 +223,6 @@ class KeycodeMapper: f'but got {key}' ) - self.key_to_code = key_to_code - self.macros = macros - def macro_write(self, code, value): """Handler for macros.""" self.uinput.write(EV_KEY, code, value) @@ -301,7 +285,7 @@ class KeycodeMapper: # the newest input are of interest continue - if subset in self.macros or subset in self.key_to_code: + if self.context.is_mapped(subset): key = subset break else: @@ -347,7 +331,7 @@ class KeycodeMapper: active_macro = active_macros.get(type_code) key = self._get_key(event_tuple) - is_mapped = key in self.macros or key in self.key_to_code + is_mapped = self.context.is_mapped(key) """Releasing keys and macros""" @@ -402,7 +386,9 @@ class KeycodeMapper: return # it would start a macro usually - if key in self.macros and active_macro and active_macro.running: + in_macros = key in self.context.macros + running = active_macro and active_macro.running + if in_macros and 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 @@ -417,8 +403,8 @@ class KeycodeMapper: # triggering a combination, so they should be remembered in # unreleased - if key in self.macros: - macro = self.macros[key] + if key in self.context.macros: + macro = self.context.macros[key] active_macros[type_code] = macro Unreleased((None, None), event_tuple, key) macro.press_key() @@ -426,8 +412,8 @@ class KeycodeMapper: asyncio.ensure_future(macro.run(self.macro_write)) return - if key in self.key_to_code: - target_code = self.key_to_code[key] + if key in self.context.key_to_code: + target_code = self.context.key_to_code[key] # remember the key that triggered this # (this combination or this single key) Unreleased((EV_KEY, target_code), event_tuple, key) diff --git a/keymapper/injection/readme.md b/keymapper/injection/readme.md index cec4892d..5de7b226 100644 --- a/keymapper/injection/readme.md +++ b/keymapper/injection/readme.md @@ -2,11 +2,5 @@ This folder contains all classes that are only relevant for the injection process. There is one process for each device that is being injected for, -and one context object that is being passed around everywhere for all to use. - -The benefit of the context object over regular parameters is that the same -parameters don't have to be passed to classes and stored redundantly all -the time. The context is like the processes global configuration and you -can use whatever is inside. Just don't modify it. If you access a context -member in two classes you definitely know that those two are working with -the same thing without having to rely on scattering your pointers everywhere. +and one context object per process that is being passed around for all +classes to use. diff --git a/tests/testcases/test_daemon.py b/tests/testcases/test_daemon.py index cd2d60fa..e4b474d2 100644 --- a/tests/testcases/test_daemon.py +++ b/tests/testcases/test_daemon.py @@ -353,7 +353,7 @@ class TestDaemon(unittest.TestCase): # after passing device and preset, the correct preset is read # from the path injector = daemon.injectors[device] - self.assertEqual(injector.mapping.get_character(Key(3, 2, 1)), 'a') + self.assertEqual(injector.context.mapping.get_character(Key(3, 2, 1)), 'a') # start again previous_injector = daemon.injectors[device] @@ -367,7 +367,7 @@ class TestDaemon(unittest.TestCase): self.assertNotEqual(previous_injector, daemon.injectors[device]) self.assertNotEqual(daemon.injectors[device].get_state(), STOPPED) injector = daemon.injectors[device] - self.assertEqual(injector.mapping.get_character(Key(3, 2, 1)), 'a') + self.assertEqual(injector.context.mapping.get_character(Key(3, 2, 1)), 'a') # trying to inject a non existing preset keeps the previous inejction # alive diff --git a/tests/testcases/test_event_producer.py b/tests/testcases/test_event_producer.py index a194bcf0..785dead8 100644 --- a/tests/testcases/test_event_producer.py +++ b/tests/testcases/test_event_producer.py @@ -27,6 +27,7 @@ from evdev.ecodes import EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL, \ from keymapper.config import config from keymapper.mapping import Mapping +from keymapper.injection.context import Context from keymapper.injection.event_producer import EventProducer, MOUSE, WHEEL from tests.test import InputDevice, UInput, MAX_ABS, clear_write_history, \ @@ -37,16 +38,16 @@ abs_state = [0, 0, 0, 0] class TestEventProducer(unittest.TestCase): - # there is also `test_abs_to_rel` in test_injector.py def setUp(self): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) self.mapping = Mapping() + self.context = Context(self.mapping) device = InputDevice('/dev/input/event30') uinput = UInput() - self.event_producer = EventProducer(self.mapping) + self.event_producer = EventProducer(self.context) self.event_producer.set_max_abs_from(device) self.event_producer.set_mouse_uinput(uinput) asyncio.ensure_future(self.event_producer.run()) @@ -113,7 +114,7 @@ class TestEventProducer(unittest.TestCase): def do(self, a, b, c, d, expectation): """Present fake values to the loop and observe the outcome.""" clear_write_history() - self.event_producer.update_purposes() + self.event_producer.context.update_purposes() self.event_producer.notify(new_event(EV_ABS, ABS_X, a)) self.event_producer.notify(new_event(EV_ABS, ABS_Y, b)) self.event_producer.notify(new_event(EV_ABS, ABS_RX, c)) diff --git a/tests/testcases/test_injector.py b/tests/testcases/test_injector.py index fc6048f8..a28adf1e 100644 --- a/tests/testcases/test_injector.py +++ b/tests/testcases/test_injector.py @@ -86,8 +86,8 @@ class TestInjector(unittest.TestCase): # this test needs to pass around all other constraints of # _grab_device device = self.injector._grab_device(path) - abs_to_rel = is_gamepad(device) - self.assertFalse(abs_to_rel) + gamepad = is_gamepad(device) + self.assertFalse(gamepad) self.assertEqual(self.failed, 2) # success on the third try device.name = fixtures[path]['name'] @@ -132,15 +132,11 @@ class TestInjector(unittest.TestCase): path = '/dev/input/event30' device = self.injector._grab_device(path) - abs_to_rel = is_gamepad(device) + gamepad = is_gamepad(device) self.assertIsNotNone(device) - self.assertTrue(abs_to_rel) + self.assertTrue(gamepad) - capabilities = self.injector._modify_capabilities( - {}, - device, - abs_to_rel - ) + capabilities = self.injector._modify_capabilities(device, gamepad) self.assertNotIn(EV_ABS, capabilities) self.assertIn(EV_REL, capabilities) @@ -168,11 +164,7 @@ class TestInjector(unittest.TestCase): self.assertIsNotNone(device) gamepad = is_gamepad(device) self.assertTrue(gamepad) - capabilities = self.injector._modify_capabilities( - {}, - device, - gamepad - ) + capabilities = self.injector._modify_capabilities(device, gamepad) self.assertIn(EV_ABS, capabilities) def test_gamepad_purpose_none_2(self): @@ -189,11 +181,7 @@ class TestInjector(unittest.TestCase): self.assertIsNotNone(device) gamepad = is_gamepad(device) self.assertTrue(gamepad) - capabilities = self.injector._modify_capabilities( - {}, - device, - gamepad - ) + capabilities = self.injector._modify_capabilities(device, gamepad) self.assertIn(EV_ABS, capabilities) self.assertIn(EV_REL, capabilities) @@ -202,11 +190,7 @@ class TestInjector(unittest.TestCase): gamepad = is_gamepad(device) self.assertIsNotNone(device) self.assertTrue(gamepad) - capabilities = self.injector._modify_capabilities( - {}, - device, - gamepad - ) + capabilities = self.injector._modify_capabilities(device, gamepad) self.assertIn(EV_ABS, capabilities) self.assertIn(EV_REL, capabilities) self.assertIn(EV_KEY, capabilities) @@ -228,9 +212,7 @@ class TestInjector(unittest.TestCase): device = self.injector._grab_device(path) gamepad = is_gamepad(device) self.assertNotIn(EV_KEY, device.capabilities()) - capabilities = self.injector._modify_capabilities( - {}, device, gamepad - ) + capabilities = self.injector._modify_capabilities(device, gamepad) self.assertIn(EV_KEY, capabilities) self.assertIn(evdev.ecodes.BTN_MOUSE, capabilities[EV_KEY]) @@ -246,9 +228,7 @@ class TestInjector(unittest.TestCase): fixtures[path]['capabilities'][EV_KEY].append(KEY_A) device = self.injector._grab_device(path) gamepad = is_gamepad(device) - capabilities = self.injector._modify_capabilities( - {}, device, gamepad - ) + capabilities = self.injector._modify_capabilities(device, gamepad) self.assertIn(EV_KEY, capabilities) self.assertIn(evdev.ecodes.BTN_MOUSE, capabilities[EV_KEY]) self.assertIn(evdev.ecodes.KEY_A, capabilities[EV_KEY]) @@ -260,9 +240,7 @@ class TestInjector(unittest.TestCase): gamepad = is_gamepad(device) self.assertIn(EV_KEY, device.capabilities()) self.assertNotIn(evdev.ecodes.BTN_MOUSE, device.capabilities()[EV_KEY]) - capabilities = self.injector._modify_capabilities( - {}, device, gamepad - ) + capabilities = self.injector._modify_capabilities(device, gamepad) self.assertIn(EV_KEY, capabilities) self.assertGreater(len(capabilities), 1) self.assertIn(evdev.ecodes.BTN_MOUSE, capabilities[EV_KEY]) @@ -731,9 +709,10 @@ class TestInjector(unittest.TestCase): # one call self.assertEqual(len(history), 1) # first argument of the first call - self.assertEqual(len(history[0][0]), 2) - self.assertEqual(history[0][0][(ev_1, ev_2, ev_3)].code, 'k(a)') - self.assertEqual(history[0][0][(ev_2, ev_1, ev_3)].code, 'k(a)') + macros = self.injector.context.macros + self.assertEqual(len(macros), 2) + self.assertEqual(macros[(ev_1, ev_2, ev_3)].code, 'k(a)') + self.assertEqual(macros[(ev_2, ev_1, ev_3)].code, 'k(a)') def test_key_to_code(self): mapping = Mapping() @@ -751,11 +730,11 @@ class TestInjector(unittest.TestCase): system_mapping._set('b', 52) injector = Injector('device 1', mapping) - self.assertEqual(injector._key_to_code.get((ev_1,)), 51) + self.assertEqual(injector.context.key_to_code.get((ev_1,)), 51) # permutations to make matching combinations easier - self.assertEqual(injector._key_to_code.get((ev_2, ev_3, ev_4)), 52) - self.assertEqual(injector._key_to_code.get((ev_3, ev_2, ev_4)), 52) - self.assertEqual(len(injector._key_to_code), 3) + self.assertEqual(injector.context.key_to_code.get((ev_2, ev_3, ev_4)), 52) + self.assertEqual(injector.context.key_to_code.get((ev_3, ev_2, ev_4)), 52) + self.assertEqual(len(injector.context.key_to_code), 3) def test_is_in_capabilities(self): key = Key(1, 2, 1) @@ -840,10 +819,11 @@ class TestModifyCapabilities(unittest.TestCase): quick_cleanup() def test_modify_capabilities(self): + self.mapping.change(Key(EV_KEY, 60, 1), self.macro.code) + self.injector = Injector('foo', self.mapping) capabilities = self.injector._modify_capabilities( - {60: self.macro}, self.fake_device, gamepad=False ) @@ -851,7 +831,8 @@ class TestModifyCapabilities(unittest.TestCase): self.assertIn(EV_ABS, capabilities) self.check_keys(capabilities) keys = capabilities[EV_KEY] - # mouse capabilities are not needed + # mouse capabilities were not present in the fake_device and are + # still not needed self.assertNotIn(self.left, keys) self.assertNotIn(evdev.ecodes.EV_SYN, capabilities) @@ -870,6 +851,8 @@ class TestModifyCapabilities(unittest.TestCase): self.assertEqual(capabilities[EV_ABS][2], 3) def test_no_abs_volume(self): + self.mapping.change(Key(EV_KEY, 60, 1), self.macro.code) + # I don't know what ABS_VOLUME is, for now I would like to just always # remove it until somebody complains self.injector = Injector('foo', self.mapping) @@ -878,23 +861,23 @@ class TestModifyCapabilities(unittest.TestCase): } capabilities = self.injector._modify_capabilities( - {60: self.macro}, self.fake_device, gamepad=False ) self.assertNotIn(ABS_VOLUME, capabilities[EV_ABS]) def test_modify_capabilities_gamepad(self): + self.mapping.change(Key((EV_KEY, 60, 1)), self.macro.code) + config.set('gamepad.joystick.left_purpose', MOUSE) self.mapping.set('gamepad.joystick.right_purpose', WHEEL) self.injector = Injector('foo', self.mapping) - self.assertFalse(self.injector._forwards_joystick()) - self.assertTrue(self.injector._maps_joystick()) - self.assertTrue(self.injector._joystick_as_mouse()) + self.assertFalse(self.injector.context.forwards_joystick()) + self.assertTrue(self.injector.context.maps_joystick()) + self.assertTrue(self.injector.context.joystick_as_mouse()) capabilities = self.injector._modify_capabilities( - {60: self.macro}, self.fake_device, gamepad=True ) @@ -909,16 +892,17 @@ class TestModifyCapabilities(unittest.TestCase): self.assertIn(self.left, keys) def test_modify_capabilities_gamepad_none_none(self): + self.mapping.change(Key(EV_KEY, 60, 1), self.macro.code) + config.set('gamepad.joystick.left_purpose', NONE) self.mapping.set('gamepad.joystick.right_purpose', NONE) self.injector = Injector('foo', self.mapping) - self.assertTrue(self.injector._forwards_joystick()) - self.assertFalse(self.injector._maps_joystick()) - self.assertFalse(self.injector._joystick_as_mouse()) + self.assertTrue(self.injector.context.forwards_joystick()) + self.assertFalse(self.injector.context.maps_joystick()) + self.assertFalse(self.injector.context.joystick_as_mouse()) capabilities = self.injector._modify_capabilities( - {60: self.macro}, self.fake_device, gamepad=True ) @@ -927,16 +911,17 @@ class TestModifyCapabilities(unittest.TestCase): self.assertIn(EV_ABS, capabilities) def test_modify_capabilities_gamepad_buttons_buttons(self): + self.mapping.change(Key((EV_KEY, 60, 1)), self.macro.code) + config.set('gamepad.joystick.left_purpose', BUTTONS) self.mapping.set('gamepad.joystick.right_purpose', BUTTONS) self.injector = Injector('foo', self.mapping) - self.assertFalse(self.injector._forwards_joystick()) - self.assertTrue(self.injector._maps_joystick()) - self.assertFalse(self.injector._joystick_as_mouse()) + self.assertFalse(self.injector.context.forwards_joystick()) + self.assertTrue(self.injector.context.maps_joystick()) + self.assertFalse(self.injector.context.joystick_as_mouse()) capabilities = self.injector._modify_capabilities( - {60: self.macro}, self.fake_device, gamepad=True ) @@ -946,6 +931,8 @@ class TestModifyCapabilities(unittest.TestCase): self.assertNotIn(EV_REL, capabilities) def test_modify_capabilities_buttons_buttons(self): + self.mapping.change(Key(EV_KEY, 60, 1), self.macro.code) + # those settings shouldn't have an effect with gamepad=False config.set('gamepad.joystick.left_purpose', BUTTONS) self.mapping.set('gamepad.joystick.right_purpose', BUTTONS) @@ -953,7 +940,6 @@ class TestModifyCapabilities(unittest.TestCase): self.injector = Injector('foo', self.mapping) capabilities = self.injector._modify_capabilities( - {60: self.macro}, self.fake_device, gamepad=False ) diff --git a/tests/testcases/test_keycode_mapper.py b/tests/testcases/test_keycode_mapper.py index 5fde94ee..541ffe0a 100644 --- a/tests/testcases/test_keycode_mapper.py +++ b/tests/testcases/test_keycode_mapper.py @@ -30,6 +30,7 @@ from keymapper.injection.keycode_mapper import active_macros, KeycodeMapper, \ unreleased, subsets from keymapper.state import system_mapping from keymapper.injection.macros import parse +from keymapper.injection.context import Context from keymapper.config import config, BUTTONS from keymapper.mapping import Mapping, DISABLE_CODE @@ -110,30 +111,33 @@ class TestKeycodeMapper(unittest.TestCase): ev_5 = (EV_ABS, ABS_HAT0Y, -1) ev_6 = (EV_ABS, ABS_HAT0Y, 0) - _key_to_code = { + uinput = UInput() + context = Context(self.mapping) + context.key_to_code = { (ev_1,): 51, (ev_2,): 52, (ev_4,): 54, (ev_5,): 55, } - uinput = UInput() - - keycode_mapper = KeycodeMapper( - self.source, self.mapping, uinput, - _key_to_code, {} - ) + keycode_mapper = KeycodeMapper(context, self.source, uinput) # a bunch of d-pad key down events at once keycode_mapper.handle_keycode(new_event(*ev_1)) keycode_mapper.handle_keycode(new_event(*ev_4)) self.assertEqual(len(unreleased), 2) - self.assertEqual(unreleased.get(ev_1[:2]).target_type_code, (EV_KEY, _key_to_code[(ev_1,)])) + self.assertEqual( + unreleased.get(ev_1[:2]).target_type_code, + (EV_KEY, context.key_to_code[(ev_1,)]) + ) self.assertEqual(unreleased.get(ev_1[:2]).input_event_tuple, ev_1) - self.assertEqual(unreleased.get(ev_1[:2]).key, (ev_1,)) # as seen in _key_to_code + self.assertEqual(unreleased.get(ev_1[:2]).key, (ev_1,)) # as seen in key_to_code - self.assertEqual(unreleased.get(ev_4[:2]).target_type_code, (EV_KEY, _key_to_code[(ev_4,)]), ev_4) + self.assertEqual( + unreleased.get(ev_4[:2]).target_type_code, + (EV_KEY, context.key_to_code[(ev_4,)]), ev_4 + ) self.assertEqual(unreleased.get(ev_4[:2]).input_event_tuple, ev_4) self.assertEqual(unreleased.get(ev_4[:2]).key, (ev_4,)) @@ -146,10 +150,22 @@ class TestKeycodeMapper(unittest.TestCase): keycode_mapper.handle_keycode(new_event(*ev_2)) keycode_mapper.handle_keycode(new_event(*ev_5)) self.assertEqual(len(unreleased), 2) - self.assertEqual(unreleased.get(ev_2[:2]).target_type_code, (EV_KEY, _key_to_code[(ev_2,)])) - self.assertEqual(unreleased.get(ev_2[:2]).input_event_tuple, ev_2) - self.assertEqual(unreleased.get(ev_5[:2]).target_type_code, (EV_KEY, _key_to_code[(ev_5,)])) - self.assertEqual(unreleased.get(ev_5[:2]).input_event_tuple, ev_5) + self.assertEqual( + unreleased.get(ev_2[:2]).target_type_code, + (EV_KEY, context.key_to_code[(ev_2,)]) + ) + self.assertEqual( + unreleased.get(ev_2[:2]).input_event_tuple, + ev_2 + ) + self.assertEqual( + unreleased.get(ev_5[:2]).target_type_code, + (EV_KEY, context.key_to_code[(ev_5,)]) + ) + self.assertEqual( + unreleased.get(ev_5[:2]).input_event_tuple, + ev_5 + ) # release all of them again keycode_mapper.handle_keycode(new_event(*ev_3)) @@ -175,10 +191,8 @@ class TestKeycodeMapper(unittest.TestCase): up = (EV_KEY, 91, 0) uinput = UInput() - keycode_mapper = KeycodeMapper( - self.source, self.mapping, uinput, - {}, {} - ) + context = Context(self.mapping) + keycode_mapper = KeycodeMapper(context, self.source, uinput) keycode_mapper.handle_keycode(new_event(*down), False) self.assertEqual(unreleased[(EV_KEY, 91)].input_event_tuple, down) @@ -207,10 +221,9 @@ class TestKeycodeMapper(unittest.TestCase): # something with gamepad capabilities source = InputDevice('/dev/input/event30') - keycode_mapper = KeycodeMapper( - source, self.mapping, uinput, - _key_to_code, {} - ) + context = Context(self.mapping) + context.key_to_code = _key_to_code + keycode_mapper = KeycodeMapper(context, source, uinput) keycode_mapper.handle_keycode(new_event(*ev_3)) keycode_mapper.handle_keycode(new_event(*ev_1)) @@ -234,10 +247,8 @@ class TestKeycodeMapper(unittest.TestCase): up = (EV_KEY, 91, 0) uinput = UInput() - keycode_mapper = KeycodeMapper( - self.source, self.mapping, uinput, - {}, {} - ) + context = Context(self.mapping) + keycode_mapper = KeycodeMapper(context, self.source, uinput) for _ in range(10): keycode_mapper.handle_keycode(new_event(*down)) @@ -266,10 +277,9 @@ class TestKeycodeMapper(unittest.TestCase): (down_1, down_2): 71 } - keycode_mapper = KeycodeMapper( - self.source, self.mapping, uinput, - key_to_code, {} - ) + context = Context(self.mapping) + context.key_to_code = key_to_code + keycode_mapper = KeycodeMapper(context, self.source, uinput) keycode_mapper.handle_keycode(new_event(*down_1)) for _ in range(10): @@ -302,10 +312,9 @@ class TestKeycodeMapper(unittest.TestCase): uinput = UInput() - keycode_mapper = KeycodeMapper( - self.source, self.mapping, uinput, - _key_to_code, {} - ) + context = Context(self.mapping) + context.key_to_code = _key_to_code + keycode_mapper = KeycodeMapper(context, self.source, uinput) # a bunch of d-pad key down events at once keycode_mapper.handle_keycode(new_event(*ev_1)) @@ -339,10 +348,9 @@ class TestKeycodeMapper(unittest.TestCase): uinput = UInput() - keycode_mapper = KeycodeMapper( - self.source, self.mapping, uinput, - _key_to_code, {} - ) + context = Context(self.mapping) + context.key_to_code = _key_to_code + keycode_mapper = KeycodeMapper(context, self.source, uinput) keycode_mapper.handle_keycode(new_event(EV_KEY, 1, 1)) keycode_mapper.handle_keycode(new_event(EV_KEY, 3, 1)) @@ -361,10 +369,9 @@ class TestKeycodeMapper(unittest.TestCase): uinput = UInput() - keycode_mapper = KeycodeMapper( - self.source, self.mapping, uinput, - _key_to_code, {} - ) + context = Context(self.mapping) + context.key_to_code = _key_to_code + keycode_mapper = KeycodeMapper(context, self.source, uinput) keycode_mapper.handle_keycode(new_event(*combination[0])) keycode_mapper.handle_keycode(new_event(*combination[1])) @@ -425,10 +432,9 @@ class TestKeycodeMapper(unittest.TestCase): source = InputDevice('/dev/input/event30') - keycode_mapper = KeycodeMapper( - source, self.mapping, uinput, - _key_to_code, {} - ) + context = Context(self.mapping) + context.key_to_code = _key_to_code + keycode_mapper = KeycodeMapper(context, source, uinput) # 10 and 11: insert some more arbitrary key-down events, # they should not break the combinations @@ -478,10 +484,9 @@ class TestKeycodeMapper(unittest.TestCase): ((EV_KEY, 2, 1),): parse('r(5, k(b))', self.mapping) } - keycode_mapper = KeycodeMapper( - self.source, self.mapping, None, - {}, macro_mapping - ) + context = Context(self.mapping) + context.macros = macro_mapping + keycode_mapper = KeycodeMapper(context, self.source, None) keycode_mapper.macro_write = lambda *args: history.append(args) keycode_mapper.macro_write = lambda *args: history.append(args) @@ -531,10 +536,9 @@ class TestKeycodeMapper(unittest.TestCase): def handler(*args): history.append(args) - keycode_mapper = KeycodeMapper( - self.source, self.mapping, None, - {}, macro_mapping - ) + context = Context(self.mapping) + context.macros = macro_mapping + keycode_mapper = KeycodeMapper(context, self.source, None) keycode_mapper.macro_write = handler @@ -604,10 +608,9 @@ class TestKeycodeMapper(unittest.TestCase): def handler(*args): history.append(args) - keycode_mapper = KeycodeMapper( - self.source, self.mapping, None, - {}, macro_mapping - ) + context = Context(self.mapping) + context.macros = macro_mapping + keycode_mapper = KeycodeMapper(context, self.source, None) keycode_mapper.macro_write = handler keycode_mapper.macro_write = handler @@ -730,10 +733,9 @@ class TestKeycodeMapper(unittest.TestCase): def handler(*args): history.append(args) - keycode_mapper = KeycodeMapper( - self.source, self.mapping, None, - {}, macro_mapping - ) + context = Context(self.mapping) + context.macros = macro_mapping + keycode_mapper = KeycodeMapper(context, self.source, None) keycode_mapper.macro_write = handler @@ -808,12 +810,12 @@ class TestKeycodeMapper(unittest.TestCase): loop = asyncio.get_event_loop() + context = Context(self.mapping) + context.macros = macro_mapping + uinput_1 = UInput() - keycode_mapper = KeycodeMapper( - self.source, self.mapping, uinput_1, - {}, macro_mapping - ) + keycode_mapper = KeycodeMapper(context, self.source, uinput_1) keycode_mapper.macro_write = handler @@ -828,10 +830,7 @@ class TestKeycodeMapper(unittest.TestCase): uinput_2 = UInput() - keycode_mapper = KeycodeMapper( - self.source, self.mapping, uinput_2, - {}, macro_mapping - ) + keycode_mapper = KeycodeMapper(context, self.source, uinput_2) keycode_mapper.macro_write = handler @@ -858,10 +857,7 @@ class TestKeycodeMapper(unittest.TestCase): """stop macros""" - keycode_mapper = KeycodeMapper( - self.source, self.mapping, None, - {}, macro_mapping - ) + keycode_mapper = KeycodeMapper(context, self.source, None) # releasing the last key of a combination releases the whole macro keycode_mapper.handle_keycode(new_event(*up_1)) @@ -934,10 +930,9 @@ class TestKeycodeMapper(unittest.TestCase): def handler(*args): history.append(args) - keycode_mapper = KeycodeMapper( - self.source, self.mapping, None, - {}, macro_mapping - ) + context = Context(self.mapping) + context.macros = macro_mapping + keycode_mapper = KeycodeMapper(context, self.source, None) keycode_mapper.macro_write = handler keycode_mapper.macro_write = handler @@ -970,10 +965,9 @@ class TestKeycodeMapper(unittest.TestCase): uinput = UInput() - keycode_mapper = KeycodeMapper( - self.source, self.mapping, uinput, - _key_to_code, {} - ) + context = Context(self.mapping) + context.key_to_code = _key_to_code + keycode_mapper = KeycodeMapper(context, self.source, uinput) """positive""" @@ -1016,10 +1010,9 @@ class TestKeycodeMapper(unittest.TestCase): uinput = UInput() - keycode_mapper = KeycodeMapper( - self.source, self.mapping, uinput, - _key_to_code, {} - ) + context = Context(self.mapping) + context.key_to_code = _key_to_code + keycode_mapper = KeycodeMapper(context, self.source, uinput) keycode_mapper.handle_keycode(new_event(*ev_1)) @@ -1056,10 +1049,9 @@ class TestKeycodeMapper(unittest.TestCase): uinput = UInput() - keycode_mapper = KeycodeMapper( - self.source, self.mapping, uinput, - _key_to_code, {} - ) + context = Context(self.mapping) + context.key_to_code = _key_to_code + keycode_mapper = KeycodeMapper(context, self.source, uinput) """single keys""" @@ -1152,10 +1144,10 @@ class TestKeycodeMapper(unittest.TestCase): loop = asyncio.get_event_loop() - keycode_mapper = KeycodeMapper( - self.source, self.mapping, uinput, - _key_to_code, macro_mapping - ) + context = Context(self.mapping) + context.key_to_code = _key_to_code + context.macros = macro_mapping + keycode_mapper = KeycodeMapper(context, self.source, uinput) keycode_mapper.macro_write = handler @@ -1221,10 +1213,9 @@ class TestKeycodeMapper(unittest.TestCase): uinput = UInput() - keycode_mapper = KeycodeMapper( - self.source, self.mapping, - uinput, k2c, {} - ) + context = Context(self.mapping) + context.key_to_code = k2c + keycode_mapper = KeycodeMapper(context, self.source, uinput) keycode_mapper.handle_keycode(new_event(*btn_down)) # "forwarding"