diff --git a/bin/input-remapper-control b/bin/input-remapper-control index 499e1262d..438fb2d74 100755 --- a/bin/input-remapper-control +++ b/bin/input-remapper-control @@ -29,8 +29,9 @@ import sys from enum import Enum from typing import Union, Optional -from inputremapper.configs.global_config import global_config +from inputremapper.configs.global_config import GlobalConfig from inputremapper.configs.migrations import Migrations +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput, FrontendUInput from inputremapper.logging.logger import logger @@ -66,6 +67,14 @@ class Options: class InputRemapperControl: + def __init__( + self, + global_config: GlobalConfig, + migrations: Migrations, + ): + self.global_config = global_config + self.migrations = migrations + def run(self, cmd) -> None: """Run and log a command.""" logger.info("Running `%s`...", cmd) @@ -81,9 +90,9 @@ class InputRemapperControl: print(group.key) def list_key_names(self): - from inputremapper.configs.system_mapping import system_mapping + from inputremapper.configs.keyboard_layout import keyboard_layout - print("\n".join(system_mapping.list_names())) + print("\n".join(keyboard_layout.list_names())) def communicate( self, @@ -131,7 +140,7 @@ class InputRemapperControl: sys.exit(6) logger.info('Using config from "%s" instead', path) - global_config.load_config(path) + self.global_config.load_config(path) def ensure_migrated(self) -> None: # import stuff late to make sure the correct log level is applied @@ -144,9 +153,9 @@ class InputRemapperControl: # This will also refresh the config of the daemon if the user changed # it in the meantime. # config_dir is either the cli arg or the default path in home - config_dir = os.path.dirname(global_config.path) + config_dir = os.path.dirname(self.global_config.path) self.daemon.set_config_dir(config_dir) - Migrations.migrate() + self.migrations.migrate() def _stop(self, device: str) -> None: group = self._require_group(device) @@ -267,7 +276,14 @@ class InputRemapperControl: def main(options: Options) -> None: - input_remapper_control = InputRemapperControl() + global_config = GlobalConfig() + global_uinputs = GlobalUInputs(FrontendUInput) + migrations = Migrations(global_uinputs) + input_remapper_control = InputRemapperControl( + global_config, + migrations, + ) + if options.debug: logger.update_verbosity(True) diff --git a/bin/input-remapper-gtk b/bin/input-remapper-gtk index 40cc25630..68cd9cf48 100755 --- a/bin/input-remapper-gtk +++ b/bin/input-remapper-gtk @@ -74,21 +74,26 @@ if __name__ == "__main__": # log-level is set. # import input-remapper stuff after setting the log verbosity from inputremapper.gui.messages.message_broker import MessageBroker, MessageType - from inputremapper.configs.system_mapping import system_mapping + from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.gui.data_manager import DataManager from inputremapper.gui.user_interface import UserInterface from inputremapper.gui.controller import Controller - from inputremapper.injection.global_uinputs import GlobalUInputs + from inputremapper.injection.global_uinputs import GlobalUInputs, FrontendUInput from inputremapper.groups import _Groups from inputremapper.gui.reader_client import ReaderClient from inputremapper.daemon import Daemon from inputremapper.configs.global_config import GlobalConfig from inputremapper.configs.migrations import Migrations - Migrations.migrate() + global_uinputs = GlobalUInputs(FrontendUInput) + migrations = Migrations(global_uinputs) + + Migrations(global_uinputs).migrate() message_broker = MessageBroker() + global_config = GlobalConfig() + # create the reader before we start the reader-service (start_processes) otherwise # it can come to race conditions with the creation of pipes reader_client = ReaderClient(message_broker, _Groups()) @@ -96,11 +101,11 @@ if __name__ == "__main__": data_manager = DataManager( message_broker, - GlobalConfig(), + global_config, reader_client, daemon, - GlobalUInputs(), - system_mapping, + global_uinputs, + keyboard_layout, ) controller = Controller(message_broker, data_manager) user_interface = UserInterface(message_broker, controller) diff --git a/bin/input-remapper-reader-service b/bin/input-remapper-reader-service index e564b0426..dde437b4c 100755 --- a/bin/input-remapper-reader-service +++ b/bin/input-remapper-reader-service @@ -28,6 +28,7 @@ import sys from argparse import ArgumentParser from inputremapper.groups import _Groups +from inputremapper.injection.global_uinputs import GlobalUInputs, FrontendUInput from inputremapper.logging.logger import logger if __name__ == "__main__": @@ -57,5 +58,6 @@ if __name__ == "__main__": atexit.register(on_exit) groups = _Groups() - reader_service = ReaderService(groups) + global_uinputs = GlobalUInputs(FrontendUInput) + reader_service = ReaderService(groups, global_uinputs) asyncio.run(reader_service.run()) diff --git a/bin/input-remapper-service b/bin/input-remapper-service index 7e3cc2d9e..ad7ccff71 100755 --- a/bin/input-remapper-service +++ b/bin/input-remapper-service @@ -25,6 +25,9 @@ import sys from argparse import ArgumentParser +from inputremapper.configs.global_config import GlobalConfig +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from inputremapper.logging.logger import logger if __name__ == "__main__": @@ -55,6 +58,10 @@ if __name__ == "__main__": if not options.hide_info: logger.log_info("input-remapper-service") - daemon = Daemon() + global_config = GlobalConfig() + global_uinputs = GlobalUInputs(UInput) + mapping_parser = MappingParser(global_uinputs) + + daemon = Daemon(global_config, global_uinputs, mapping_parser) daemon.publish() daemon.run() diff --git a/inputremapper/configs/global_config.py b/inputremapper/configs/global_config.py index 3c08a5cd0..ee6487f6b 100644 --- a/inputremapper/configs/global_config.py +++ b/inputremapper/configs/global_config.py @@ -25,8 +25,8 @@ from inputremapper.configs.base_config import ConfigBase, INITIAL_CONFIG from inputremapper.configs.paths import PathUtils -from inputremapper.user import UserUtils from inputremapper.logging.logger import logger +from inputremapper.user import UserUtils MOUSE = "mouse" WHEEL = "wheel" @@ -35,7 +35,7 @@ class GlobalConfig(ConfigBase): - """Global default configuration. + """Global default configuration, from which all presets inherit. It can also contain some extra stuff not relevant for presets, like the autoload stuff. If presets have a config key set, it will ignore the default global configuration for that one. If none of the configs @@ -129,7 +129,3 @@ def _save_config(self): json.dump(self._config, file, indent=4) logger.info("Saved config to %s", self.path) file.write("\n") - - -# TODO DI -global_config = GlobalConfig() diff --git a/inputremapper/configs/input_config.py b/inputremapper/configs/input_config.py index 5648859a7..eaf020a3c 100644 --- a/inputremapper/configs/input_config.py +++ b/inputremapper/configs/input_config.py @@ -31,7 +31,7 @@ except ImportError: from pydantic import BaseModel, root_validator, validator -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.gui.messages.message_types import MessageType from inputremapper.logging.logger import logger from inputremapper.utils import get_evdev_constant_name @@ -142,7 +142,7 @@ def _get_name(self) -> Optional[str]: # first try to find the name in xmodmap to not display wrong # names due to the keyboard layout if self.type == ecodes.EV_KEY: - key_name = system_mapping.get_name(self.code) + key_name = keyboard_layout.get_name(self.code) if key_name is None: # if no result, look in the linux combination constants. On a german diff --git a/inputremapper/configs/system_mapping.py b/inputremapper/configs/keyboard_layout.py similarity index 97% rename from inputremapper/configs/system_mapping.py rename to inputremapper/configs/keyboard_layout.py index f88493b3a..0cc91220d 100644 --- a/inputremapper/configs/system_mapping.py +++ b/inputremapper/configs/keyboard_layout.py @@ -41,7 +41,7 @@ LAZY_LOAD = None -class SystemMapping: +class KeyboardLayout: """Stores information about all available keycodes.""" _mapping: Optional[dict] = LAZY_LOAD @@ -49,7 +49,7 @@ class SystemMapping: _case_insensitive_mapping: Optional[dict] = LAZY_LOAD def __getattribute__(self, wanted: str): - """To lazy load system_mapping info only when needed. + """To lazy load keyboard_layout info only when needed. For example, this helps to keep logs of input-remapper-control clear when it doesn't need it the information. @@ -160,7 +160,7 @@ def _set(self, name: str, code: int): def get(self, name: str) -> int: """Return the code mapped to the key.""" - # the correct casing should be shown when asking the system_mapping + # the correct casing should be shown when asking the keyboard_layout # for stuff. indexing case insensitive to support old presets. if name not in self._mapping: # only if not e.g. both "a" and "A" are in the mapping @@ -216,4 +216,4 @@ def _find_legit_mappings(self) -> dict: # TODO DI # this mapping represents the xmodmap output, which stays constant -system_mapping = SystemMapping() +keyboard_layout = KeyboardLayout() diff --git a/inputremapper/configs/mapping.py b/inputremapper/configs/mapping.py index 819e8238f..25f760e5c 100644 --- a/inputremapper/configs/mapping.py +++ b/inputremapper/configs/mapping.py @@ -62,7 +62,7 @@ ) from inputremapper.configs.input_config import InputCombination -from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME +from inputremapper.configs.keyboard_layout import keyboard_layout, DISABLE_NAME from inputremapper.configs.validation_errors import ( OutputSymbolUnknownError, SymbolNotAvailableInTargetError, @@ -286,13 +286,13 @@ def remove_combination_changed_callback(self): def get_output_type_code(self) -> Optional[Tuple[int, int]]: """Returns the output_type and output_code if set, - otherwise looks the output_symbol up in the system_mapping + otherwise looks the output_symbol up in the keyboard_layout return None for unknown symbols and macros """ if self.output_code and self.output_type: return self.output_type, self.output_code if self.output_symbol and not is_this_a_macro(self.output_symbol): - return EV_KEY, system_mapping.get(self.output_symbol) + return EV_KEY, keyboard_layout.get(self.output_symbol) return None def get_output_name_constant(self) -> str: @@ -385,7 +385,7 @@ def validate_symbol(cls, values): parse(symbol, mapping=mapping_mock, verbose=False) return values - code = system_mapping.get(symbol) + code = keyboard_layout.get(symbol) if code is None: raise OutputSymbolUnknownError(symbol) @@ -450,7 +450,7 @@ def validate_output_integrity(cls, values): if type_ is not None or code is not None: raise MacroButTypeOrCodeSetError() - if code is not None and code != system_mapping.get(symbol) or type_ != EV_KEY: + if code is not None and code != keyboard_layout.get(symbol) or type_ != EV_KEY: raise SymbolAndCodeMismatchError(symbol, code) return values diff --git a/inputremapper/configs/migrations.py b/inputremapper/configs/migrations.py index 84d762959..26e143426 100644 --- a/inputremapper/configs/migrations.py +++ b/inputremapper/configs/migrations.py @@ -51,8 +51,8 @@ from inputremapper.configs.mapping import Mapping, UIMapping from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import system_mapping -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.configs.keyboard_layout import keyboard_layout +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.macros.parse import is_this_a_macro from inputremapper.logging.logger import logger, VERSION from inputremapper.user import UserUtils @@ -66,43 +66,46 @@ class Config(TypedDict): class Migrations: - @staticmethod - def migrate(): + def __init__(self, global_uinputs: GlobalUInputs): + self.global_uinputs = global_uinputs + + def migrate(self): """Migrate config files to the current release.""" - Migrations._rename_to_input_remapper() + self._rename_to_input_remapper() - Migrations._copy_to_v2() + self._copy_to_v2() - v = Migrations.config_version() + v = self.config_version() if v < version.parse("0.4.0"): - Migrations._config_suffix() - Migrations._preset_path() + self._config_suffix() + self._preset_path() if v < version.parse("1.2.2"): - Migrations._mapping_keys() + self._mapping_keys() if v < version.parse("1.4.0"): - global_uinputs.prepare_all() - Migrations._add_target() + self.global_uinputs.prepare_all() + self._add_target() if v < version.parse("1.4.1"): - Migrations._otherwise_to_else() + self._otherwise_to_else() if v < version.parse("1.5.0"): - Migrations._remove_logs() + self._remove_logs() if v < version.parse("1.6.0-beta"): - Migrations._convert_to_individual_mappings() + self._convert_to_individual_mappings() # add new migrations here if v < version.parse(VERSION): - Migrations._update_version() + self._update_version() - @staticmethod - def all_presets() -> Iterator[Tuple[os.PathLike, Dict | List]]: + def all_presets( + self, + ) -> Iterator[Tuple[os.PathLike, Dict | List]]: """Get all presets for all groups as list.""" if not os.path.exists(PathUtils.get_preset_path()): return @@ -124,8 +127,7 @@ def all_presets() -> Iterator[Tuple[os.PathLike, Dict | List]]: logger.warning('Invalid json format in preset "%s"', preset) continue - @staticmethod - def config_version(): + def config_version(self): """Get the version string in config.json as packaging.Version object.""" config_path = os.path.join(PathUtils.config_path(), "config.json") @@ -140,8 +142,7 @@ def config_version(): return version.parse("0.0.0") - @staticmethod - def _config_suffix(): + def _config_suffix(self): """Append the .json suffix to the config file.""" deprecated_path = os.path.join(PathUtils.config_path(), "config") config_path = os.path.join(PathUtils.config_path(), "config.json") @@ -149,8 +150,7 @@ def _config_suffix(): logger.info('Moving "%s" to "%s"', deprecated_path, config_path) os.rename(deprecated_path, config_path) - @staticmethod - def _preset_path(): + def _preset_path(self): """Migrate the folder structure from < 0.4.0. Move existing presets into the new subfolder 'presets' @@ -173,13 +173,12 @@ def _preset_path(): logger.info("done") - @staticmethod - def _mapping_keys(): + def _mapping_keys(self): """Update all preset mappings. Update all keys in preset to include value e.g.: '1,5'->'1,5,1' """ - for preset, preset_structure in Migrations.all_presets(): + for preset, preset_structure in self.all_presets(): if isinstance(preset_structure, list): continue # the preset must be at least 1.6-beta version @@ -199,8 +198,7 @@ def _mapping_keys(): json.dump(preset_structure, file, indent=4) file.write("\n") - @staticmethod - def _update_version(): + def _update_version(self): """Write the current version to the config file.""" config_file = os.path.join(PathUtils.config_path(), "config.json") if not os.path.exists(config_file): @@ -214,8 +212,7 @@ def _update_version(): logger.info('Updating version in config to "%s"', VERSION) json.dump(config, file, indent=4) - @staticmethod - def _rename_to_input_remapper(): + def _rename_to_input_remapper(self): """Rename .config/key-mapper to .config/input-remapper.""" old_config_path = os.path.join(UserUtils.home, ".config/key-mapper") if not os.path.exists(PathUtils.config_path()) and os.path.exists( @@ -224,8 +221,7 @@ def _rename_to_input_remapper(): logger.info("Moving %s to %s", old_config_path, PathUtils.config_path()) shutil.move(old_config_path, PathUtils.config_path()) - @staticmethod - def _find_target(symbol): + def _find_target(self, symbol): """Try to find a uinput with the required capabilities for the symbol.""" capabilities = {EV_KEY: set(), EV_REL: set()} @@ -234,22 +230,21 @@ def _find_target(symbol): # capabilities = parse(symbol).get_capabilities() return None - capabilities[EV_KEY] = {system_mapping.get(symbol)} + capabilities[EV_KEY] = {keyboard_layout.get(symbol)} if len(capabilities[EV_REL]) > 0: return "mouse" - for name, uinput in global_uinputs.devices.items(): + for name, uinput in self.global_uinputs.devices.items(): if capabilities[EV_KEY].issubset(uinput.capabilities()[EV_KEY]): return name logger.info('could not find a suitable target UInput for "%s"', symbol) return None - @staticmethod - def _add_target(): + def _add_target(self): """Add the target field to each preset mapping.""" - for preset, preset_structure in Migrations.all_presets(): + for preset, preset_structure in self.all_presets(): if isinstance(preset_structure, list): continue @@ -261,7 +256,7 @@ def _add_target(): if isinstance(symbol, list): continue - target = Migrations._find_target(symbol) + target = self._find_target(symbol) if target is None: target = "keyboard" symbol = ( @@ -288,10 +283,9 @@ def _add_target(): json.dump(preset_structure, file, indent=4) file.write("\n") - @staticmethod - def _otherwise_to_else(): + def _otherwise_to_else(self): """Conditional macros should use an "else" parameter instead of "otherwise".""" - for preset, preset_structure in Migrations.all_presets(): + for preset, preset_structure in self.all_presets(): if isinstance(preset_structure, list): continue @@ -328,8 +322,9 @@ def _otherwise_to_else(): json.dump(preset_structure, file, indent=4) file.write("\n") - @staticmethod - def _input_combination_from_string(combination_string: str) -> InputCombination: + def _input_combination_from_string( + self, combination_string: str + ) -> InputCombination: configs = [] for event_str in combination_string.split("+"): type_, code, analog_threshold = event_str.split(",") @@ -343,14 +338,15 @@ def _input_combination_from_string(combination_string: str) -> InputCombination: return InputCombination(configs) - @staticmethod - def _convert_to_individual_mappings() -> None: + def _convert_to_individual_mappings( + self, + ) -> None: """Convert preset.json from {key: [symbol, target]} to [{input_combination: ..., output_symbol: symbol, ...}] """ - for old_preset_path, old_preset in Migrations.all_presets(): + for old_preset_path, old_preset in self.all_presets(): if isinstance(old_preset, list): continue @@ -363,9 +359,7 @@ def _convert_to_individual_mappings() -> None: symbol_target, ) try: - combination = Migrations._input_combination_from_string( - combination - ) + combination = self._input_combination_from_string(combination) except ValueError: logger.error( "unable to migrate mapping with invalid combination %s", @@ -482,8 +476,7 @@ def _convert_to_individual_mappings() -> None: migrated_preset.save() - @staticmethod - def _copy_to_v2(): + def _copy_to_v2(self): """Move the beta config to the v2 path, or copy the v1 config to the v2 path.""" # TODO test if os.path.exists(PathUtils.config_path()): @@ -495,7 +488,7 @@ def _copy_to_v2(): old_path = os.path.join(UserUtils.home, ".config/input-remapper") if os.path.exists(os.path.join(old_path, "config.json")): # no beta path, only old presets exist. COPY to v2 path, which will then be - # migrated by the various migrations. + # migrated by the various self. logger.debug("copying all from %s to %s", old_path, PathUtils.config_path()) shutil.copytree(old_path, PathUtils.config_path()) return @@ -511,8 +504,7 @@ def _copy_to_v2(): logger.debug("moving %s to %s", beta_path, PathUtils.config_path()) shutil.move(beta_path, PathUtils.config_path()) - @staticmethod - def _remove_logs(): + def _remove_logs(self): """We will try to rely on journalctl for this in the future.""" try: PathUtils.remove(f"{UserUtils.home}/.log/input-remapper") diff --git a/inputremapper/configs/validation_errors.py b/inputremapper/configs/validation_errors.py index 156a7c980..e1c163959 100644 --- a/inputremapper/configs/validation_errors.py +++ b/inputremapper/configs/validation_errors.py @@ -31,7 +31,7 @@ from evdev.ecodes import EV_KEY -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.injection.global_uinputs import GlobalUInputs @@ -62,7 +62,7 @@ def __init__(self, analog_events): class SymbolNotAvailableInTargetError(ValueError): def __init__(self, symbol, target): - code = system_mapping.get(symbol) + code = keyboard_layout.get(symbol) fitting_targets = GlobalUInputs.find_fitting_default_uinputs(EV_KEY, code) fitting_targets_string = '", "'.join(fitting_targets) @@ -92,8 +92,8 @@ class SymbolAndCodeMismatchError(ValueError): def __init__(self, symbol, code): super().__init__( "output_symbol and output_code mismatch: " - f"output macro is {symbol} -> {system_mapping.get(symbol)} " - f"but output_code is {code} -> {system_mapping.get_name(code)} " + f"output macro is {symbol} -> {keyboard_layout.get(symbol)} " + f"but output_code is {code} -> {keyboard_layout.get_name(code)} " ) diff --git a/inputremapper/daemon.py b/inputremapper/daemon.py index cf949e9cd..eb2d0d6b8 100644 --- a/inputremapper/daemon.py +++ b/inputremapper/daemon.py @@ -35,19 +35,21 @@ import gi from pydbus import SystemBus +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser + gi.require_version("GLib", "2.0") from gi.repository import GLib from inputremapper.logging.logger import logger from inputremapper.injection.injector import Injector, InjectorState from inputremapper.configs.preset import Preset -from inputremapper.configs.global_config import global_config -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.global_config import GlobalConfig +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.groups import groups from inputremapper.configs.paths import PathUtils from inputremapper.user import UserUtils from inputremapper.injection.macros.macro import macro_variables -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs BUS_NAME = "inputremapper.Control" @@ -184,9 +186,19 @@ class Daemon: """ - def __init__(self): + def __init__( + self, + global_config: GlobalConfig, + global_uinputs: GlobalUInputs, + mapping_parser: MappingParser, + ): """Constructs the daemon.""" logger.debug("Creating daemon") + + self.global_config = global_config + self.global_uinputs = global_uinputs + self.mapping_parser = mapping_parser + self.injectors: Dict[str, Injector] = {} self.config_dir = None @@ -333,7 +345,7 @@ def set_config_dir(self, config_dir: str): return self.config_dir = config_dir - global_config.load_config(config_path) + self.global_config.load_config(str(config_path)) def _autoload(self, group_key: str): """Check if autoloading is a good idea, and if so do it. @@ -351,7 +363,7 @@ def _autoload(self, group_key: str): # either not relevant for input-remapper, or not connected yet return - preset = global_config.get(["autoload", group.key], log_unknown=False) + preset = self.global_config.get(["autoload", group.key], log_unknown=False) if preset is None: # no autoloading is configured for this device @@ -415,7 +427,7 @@ def autoload(self): ) return - autoload_presets = list(global_config.iterate_autoload_presets()) + autoload_presets = list(self.global_config.iterate_autoload_presets()) logger.info("Autoloading for all devices") @@ -475,13 +487,13 @@ def start_injecting(self, group_key: str, preset_name: str) -> bool: xmodmap = json.load(file) logger.debug('Using keycodes from "%s"', xmodmap_path) - # this creates the system_mapping._xmodmap, which we need to do now + # this creates the keyboard_layout._xmodmap, which we need to do now # otherwise it might be created later which will override the changes # we do here. - # Do we really need to lazyload in the system_mapping? + # Do we really need to lazyload in the keyboard_layout? # this kind of bug is stupid to track down - system_mapping.get_name(0) - system_mapping.update(xmodmap) + keyboard_layout.get_name(0) + keyboard_layout.update(xmodmap) # the service now has process wide knowledge of xmodmap # keys of the users session except FileNotFoundError: @@ -500,13 +512,13 @@ def start_injecting(self, group_key: str, preset_name: str) -> bool: # confusing the system. Seems to be especially important with # gamepads, because some apps treat the first gamepad they found # as the only gamepad they'll ever care about. - global_uinputs.prepare_single(mapping.target_uinput) + self.global_uinputs.prepare_single(mapping.target_uinput) if self.injectors.get(group_key) is not None: self.stop_injecting(group_key) try: - injector = Injector(group, preset) + injector = Injector(group, preset, self.mapping_parser) injector.start() self.injectors[group.key] = injector except OSError: diff --git a/inputremapper/gui/autocompletion.py b/inputremapper/gui/autocompletion.py index 455be58cb..28bb6f4fd 100644 --- a/inputremapper/gui/autocompletion.py +++ b/inputremapper/gui/autocompletion.py @@ -28,7 +28,7 @@ from gi.repository import Gdk, Gtk, GLib, GObject from inputremapper.configs.mapping import MappingData -from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME +from inputremapper.configs.keyboard_layout import keyboard_layout, DISABLE_NAME from inputremapper.gui.components.editor import CodeEditor from inputremapper.gui.controller import Controller from inputremapper.gui.messages.message_broker import MessageBroker, MessageType @@ -111,7 +111,7 @@ def propose_symbols(text_iter: Gtk.TextIter, codes: List[int]) -> List[Tuple[str incomplete_name = incomplete_name.lower() - names = list(system_mapping.list_names(codes=codes)) + [DISABLE_NAME] + names = list(keyboard_layout.list_names(codes=codes)) + [DISABLE_NAME] return [ (name, name) diff --git a/inputremapper/gui/components/editor.py b/inputremapper/gui/components/editor.py index f250a42f4..80ec4818d 100644 --- a/inputremapper/gui/components/editor.py +++ b/inputremapper/gui/components/editor.py @@ -57,7 +57,7 @@ from inputremapper.gui.utils import HandlerDisabled, Colors from inputremapper.injection.mapping_handlers.axis_transform import Transformation from inputremapper.input_event import InputEvent -from inputremapper.configs.system_mapping import system_mapping, XKB_KEYCODE_OFFSET +from inputremapper.configs.keyboard_layout import keyboard_layout, XKB_KEYCODE_OFFSET from inputremapper.utils import get_evdev_constant_name Capabilities = Dict[int, List] @@ -384,9 +384,9 @@ def _display(self, event): if is_press and len(self._combination) > 0: names = [ - system_mapping.get_name(code) + keyboard_layout.get_name(code) for code in self._combination - if code is not None and system_mapping.get_name(code) is not None + if code is not None and keyboard_layout.get_name(code) is not None ] self._gui.set_text(" + ".join(names)) diff --git a/inputremapper/gui/data_manager.py b/inputremapper/gui/data_manager.py index f4d89feae..69577f12b 100644 --- a/inputremapper/gui/data_manager.py +++ b/inputremapper/gui/data_manager.py @@ -30,7 +30,7 @@ from inputremapper.configs.mapping import UIMapping, MappingData from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import SystemMapping +from inputremapper.configs.keyboard_layout import KeyboardLayout from inputremapper.daemon import DaemonProxy from inputremapper.exceptions import DataManagementError from inputremapper.groups import _Group @@ -73,13 +73,13 @@ def __init__( reader_client: ReaderClient, daemon: DaemonProxy, uinputs: GlobalUInputs, - system_mapping: SystemMapping, + keyboard_layout: KeyboardLayout, ): self.message_broker = message_broker self._reader_client = reader_client self._daemon = daemon self._uinputs = uinputs - self._system_mapping = system_mapping + self._keyboard_layout = keyboard_layout uinputs.prepare_all() self._config = config @@ -454,7 +454,7 @@ def update_mapping(self, **kwargs): raise DataManagementError("Cannot modify Mapping: Mapping is not set") if symbol := kwargs.get("output_symbol"): - kwargs["output_symbol"] = self._system_mapping.correct_case(symbol) + kwargs["output_symbol"] = self._keyboard_layout.correct_case(symbol) combination = self.active_mapping.input_combination for key, value in kwargs.items(): diff --git a/inputremapper/gui/reader_service.py b/inputremapper/gui/reader_service.py index b0180e38d..3f1f1d9a4 100644 --- a/inputremapper/gui/reader_service.py +++ b/inputremapper/gui/reader_service.py @@ -55,6 +55,7 @@ from inputremapper.configs.mapping import Mapping from inputremapper.groups import _Groups, _Group from inputremapper.injection.event_reader import EventReader +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.abs_to_btn_handler import AbsToBtnHandler from inputremapper.injection.mapping_handlers.mapping_handler import ( NotifyCallback, @@ -113,10 +114,11 @@ class ReaderService: _maximum_lifetime: int = 60 * 15 _timeout_tolerance: int = 60 - def __init__(self, groups: _Groups): + def __init__(self, groups: _Groups, global_uinputs: GlobalUInputs): """Construct the reader-service and initialize its communication pipes.""" self._start_time = time.time() self.groups = groups + self.global_uinputs = global_uinputs self._results_pipe = Pipe(get_pipe_paths()[0]) self._commands_pipe = Pipe(get_pipe_paths()[1]) self._pipe = multiprocessing.Pipe() @@ -291,7 +293,9 @@ def _create_event_pipeline(self, sources: List[evdev.InputDevice]) -> ContextDum output_symbol="KEY_A", ) handler: MappingHandler = AbsToBtnHandler( - InputCombination([input_config]), mapping + InputCombination([input_config]), + mapping, + self.global_uinputs, ) handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) context_dummy.add_handler(input_config, handler) @@ -303,7 +307,11 @@ def _create_event_pipeline(self, sources: List[evdev.InputDevice]) -> ContextDum target_uinput="keyboard", output_symbol="KEY_A", ) - handler = AbsToBtnHandler(InputCombination([input_config]), mapping) + handler = AbsToBtnHandler( + InputCombination([input_config]), + mapping, + self.global_uinputs, + ) handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) context_dummy.add_handler(input_config, handler) @@ -322,7 +330,11 @@ def _create_event_pipeline(self, sources: List[evdev.InputDevice]) -> ContextDum release_timeout=0.3, force_release_timeout=True, ) - handler = RelToBtnHandler(InputCombination([input_config]), mapping) + handler = RelToBtnHandler( + InputCombination([input_config]), + mapping, + self.global_uinputs, + ) handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) context_dummy.add_handler(input_config, handler) @@ -337,7 +349,11 @@ def _create_event_pipeline(self, sources: List[evdev.InputDevice]) -> ContextDum release_timeout=0.3, force_release_timeout=True, ) - handler = RelToBtnHandler(InputCombination([input_config]), mapping) + handler = RelToBtnHandler( + InputCombination([input_config]), + mapping, + self.global_uinputs, + ) handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) context_dummy.add_handler(input_config, handler) diff --git a/inputremapper/gui/utils.py b/inputremapper/gui/utils.py index 15122514d..20b55c511 100644 --- a/inputremapper/gui/utils.py +++ b/inputremapper/gui/utils.py @@ -145,9 +145,6 @@ def run_all_now(self): logger.error(exception) -debounce_manager = DebounceManager() - - def debounce(timeout): """Debounce a method call to improve performance. @@ -177,6 +174,9 @@ def wrapped(self, *args, **kwargs): return decorator +debounce_manager = DebounceManager() + + class HandlerDisabled: """Safely modify a widget without causing handlers to be called. diff --git a/inputremapper/injection/context.py b/inputremapper/injection/context.py index cf56608b1..ce73b3f4f 100644 --- a/inputremapper/injection/context.py +++ b/inputremapper/injection/context.py @@ -34,7 +34,7 @@ NotifyCallback, ) from inputremapper.injection.mapping_handlers.mapping_parser import ( - parse_mappings, + MappingParser, EventPipelines, ) from inputremapper.input_event import InputEvent @@ -82,6 +82,7 @@ def __init__( preset: Preset, source_devices: Dict[DeviceHash, evdev.InputDevice], forward_devices: Dict[DeviceHash, evdev.UInput], + mapping_parser: MappingParser, ): if len(forward_devices) == 0: logger.warning("Not forward_devices set") @@ -93,7 +94,7 @@ def __init__( self._source_devices = source_devices self._forward_devices = forward_devices self._notify_callbacks = defaultdict(list) - self._handlers = parse_mappings(preset, self) + self._handlers = mapping_parser.parse_mappings(preset, self) self._create_callbacks() diff --git a/inputremapper/injection/global_uinputs.py b/inputremapper/injection/global_uinputs.py index 3fba120c7..6802db5aa 100644 --- a/inputremapper/injection/global_uinputs.py +++ b/inputremapper/injection/global_uinputs.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -from typing import Dict, Union, Tuple, Optional, List +from typing import Dict, Union, Tuple, Optional, List, Type import evdev @@ -83,7 +83,7 @@ def can_emit(self, event: Tuple[int, int, int]): class FrontendUInput: """Uinput which can not actually send events, for use in the frontend.""" - def __init__(self, *args, events=None, name="py-evdev-uinput", **kwargs): + def __init__(self, *_, events=None, name="py-evdev-uinput", **__): # see https://python-evdev.readthedocs.io/en/latest/apidoc.html#module-evdev.uinput # noqa pylint: disable=line-too-long self.events = events self.name = name @@ -97,10 +97,12 @@ def capabilities(self): class GlobalUInputs: """Manages all UInputs that are shared between all injection processes.""" - def __init__(self): + def __init__( + self, + uinput_factory: Union[Type[UInput], Type[FrontendUInput]], + ): self.devices: Dict[str, Union[UInput, FrontendUInput]] = {} - self._uinput_factory = None - self.is_service = inputremapper.utils.is_service() + self._uinput_factory = uinput_factory def __iter__(self): return iter(uinput for _, uinput in self.devices.items()) @@ -121,28 +123,11 @@ def find_fitting_default_uinputs(type_: int, code: int) -> List[str]: ] def reset(self): - self.is_service = inputremapper.utils.is_service() - self._uinput_factory = None self.devices = {} self.prepare_all() - def ensure_uinput_factory_set(self): - if self._uinput_factory is not None: - return - - # TODO this should be solved via DI - # overwrite global_uinputs.is_service in tests to control this - if self.is_service: - logger.debug("Creating regular UInputs") - self._uinput_factory = UInput - else: - logger.debug("Creating FrontendUInputs") - self._uinput_factory = FrontendUInput - def prepare_all(self): """Generate UInputs.""" - self.ensure_uinput_factory_set() - for name, events in DEFAULT_UINPUTS.items(): if name in self.devices.keys(): continue @@ -158,8 +143,6 @@ def prepare_single(self, name: str): This has to be done in the main process before injections that use it start. """ - self.ensure_uinput_factory_set() - if name not in DEFAULT_UINPUTS: raise KeyError("Could not find a matching uinput to generate.") @@ -204,7 +187,3 @@ def get_uinput(self, name: str) -> Optional[evdev.UInput]: return None return self.devices.get(name) - - -# TODO DI -global_uinputs = GlobalUInputs() diff --git a/inputremapper/injection/injector.py b/inputremapper/injection/injector.py index 35db52f49..2636765d9 100644 --- a/inputremapper/injection/injector.py +++ b/inputremapper/injection/injector.py @@ -43,6 +43,7 @@ from inputremapper.gui.messages.message_broker import MessageType from inputremapper.injection.context import Context from inputremapper.injection.event_reader import EventReader +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from inputremapper.injection.numlock import set_numlock, is_numlock_on, ensure_numlock from inputremapper.logging.logger import logger from inputremapper.utils import get_device_hash @@ -119,7 +120,12 @@ class Injector(multiprocessing.Process): regrab_timeout = 0.2 - def __init__(self, group: _Group, preset: Preset) -> None: + def __init__( + self, + group: _Group, + preset: Preset, + mapping_parser: MappingParser, + ) -> None: """ Parameters @@ -128,6 +134,7 @@ def __init__(self, group: _Group, preset: Preset) -> None: the device group """ self.group = group + self.mapping_parser = mapping_parser self._state = InjectorState.UNKNOWN # used to interact with the parts of this class that are running within @@ -415,7 +422,12 @@ def run(self) -> None: # create this within the process after the event loop creation, # so that the macros use the correct loop - self.context = Context(self.preset, sources, forward_devices) + self.context = Context( + self.preset, + sources, + forward_devices, + self.mapping_parser, + ) self._stop_event = asyncio.Event() if len(sources) == 0: diff --git a/inputremapper/injection/macros/macro.py b/inputremapper/injection/macros/macro.py index b0d3fd0e9..90eb4f57f 100644 --- a/inputremapper/injection/macros/macro.py +++ b/inputremapper/injection/macros/macro.py @@ -54,7 +54,7 @@ REL_HWHEEL, ) -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.configs.validation_errors import ( SymbolNotAvailableInTargetError, MacroParsingError, @@ -89,78 +89,6 @@ def __repr__(self): return f'' -def _type_check(value: Any, allowed_types, display_name=None, position=None) -> Any: - """Validate a parameter used in a macro. - - If the value is a Variable, it will be returned and should be resolved - during runtime with _resolve. - """ - if isinstance(value, Variable): - # it is a variable and will be read at runtime - return value - - for allowed_type in allowed_types: - if allowed_type is None: - if value is None: - return value - - continue - - # try to parse "1" as 1 if possible - if allowed_type != Macro: - # the macro constructor with a single argument always succeeds, - # but will definitely not result in the correct macro - try: - return allowed_type(value) - except (TypeError, ValueError): - pass - - if isinstance(value, allowed_type): - return value - - if display_name is not None and position is not None: - raise MacroParsingError( - msg=f"Expected parameter {position} for {display_name} to be " - f"one of {allowed_types}, but got {value}" - ) - - raise MacroParsingError( - msg=f"Expected parameter to be one of {allowed_types}, but got {value}" - ) - - -def _type_check_variablename(name: str): - """Check if this is a legit variable name. - - Because they could clash with language features. If the macro is able to be - parsed at all due to a problematic choice of a variable name. - - Allowed examples: "foo", "Foo1234_", "_foo_1234" - Not allowed: "1_foo", "foo=blub", "$foo", "foo,1234", "foo()" - """ - if not isinstance(name, str) or not re.match(r"^[A-Za-z_][A-Za-z_0-9]*$", name): - raise MacroParsingError(msg=f'"{name}" is not a legit variable name') - - -def _resolve(argument, allowed_types=None): - """If the argument is a variable, figure out its value and cast it. - - Variables are prefixed with `$` in the syntax. - - Use this just-in-time when you need the actual value of the variable - during runtime. - """ - if isinstance(argument, Variable): - value = argument.resolve() - logger.debug('"%s" is "%s"', argument, value) - if allowed_types: - return _type_check(value, allowed_types) - else: - return value - - return argument - - class Macro: """Supports chaining and preparing actions. @@ -312,10 +240,10 @@ def add_key(self, symbol: str): async def task(handler: Callable): # if the code is $foo, figure out the correct code now. - resolved_symbol = _resolve(symbol, [str]) + resolved_symbol = self._resolve(symbol, [str]) code = self._type_check_symbol(resolved_symbol) - resolved_code = _resolve(code, [int]) + resolved_code = self._resolve(code, [int]) handler(EV_KEY, resolved_code, 1) await self._keycode_pause() handler(EV_KEY, resolved_code, 0) @@ -328,10 +256,10 @@ def add_key_down(self, symbol: str): self._type_check_symbol(symbol) async def task(handler: Callable): - resolved_symbol = _resolve(symbol, [str]) + resolved_symbol = self._resolve(symbol, [str]) code = self._type_check_symbol(resolved_symbol) - resolved_code = _resolve(code, [int]) + resolved_code = self._resolve(code, [int]) handler(EV_KEY, resolved_code, 1) self.tasks.append(task) @@ -341,17 +269,17 @@ def add_key_up(self, symbol: str): self._type_check_symbol(symbol) async def task(handler: Callable): - resolved_symbol = _resolve(symbol, [str]) + resolved_symbol = self._resolve(symbol, [str]) code = self._type_check_symbol(resolved_symbol) - resolved_code = _resolve(code, [int]) + resolved_code = self._resolve(code, [int]) handler(EV_KEY, resolved_code, 0) self.tasks.append(task) def add_hold(self, macro=None): """Loops the execution until key release.""" - _type_check(macro, [Macro, str, None], "hold", 1) + self._type_check(macro, [Macro, str, None], "hold", 1) if macro is None: self.tasks.append(lambda _: self._trigger_release_event.wait()) @@ -364,10 +292,10 @@ def add_hold(self, macro=None): self._type_check_symbol(symbol) async def task(handler: Callable): - resolved_symbol = _resolve(symbol, [str]) + resolved_symbol = self._resolve(symbol, [str]) code = self._type_check_symbol(resolved_symbol) - resolved_code = _resolve(code, [int]) + resolved_code = self._resolve(code, [int]) handler(EV_KEY, resolved_code, 1) await self._trigger_release_event.wait() handler(EV_KEY, resolved_code, 0) @@ -395,14 +323,14 @@ def add_modify(self, modifier: str, macro: Macro): modifier macro """ - _type_check(macro, [Macro], "modify", 2) + self._type_check(macro, [Macro], "modify", 2) self._type_check_symbol(modifier) self.child_macros.append(macro) async def task(handler: Callable): # TODO test var - resolved_modifier = _resolve(modifier, [str]) + resolved_modifier = self._resolve(modifier, [str]) code = self._type_check_symbol(resolved_modifier) handler(EV_KEY, code, 1) @@ -419,7 +347,7 @@ def add_hold_keys(self, *symbols): self._type_check_symbol(symbol) async def task(handler: Callable): - resolved_symbols = [_resolve(symbol, [str]) for symbol in symbols] + resolved_symbols = [self._resolve(symbol, [str]) for symbol in symbols] codes = [self._type_check_symbol(symbol) for symbol in resolved_symbols] for code in codes: @@ -436,11 +364,11 @@ async def task(handler: Callable): def add_repeat(self, repeats: Union[str, int], macro: Macro): """Repeat actions.""" - repeats = _type_check(repeats, [int], "repeat", 1) - _type_check(macro, [Macro], "repeat", 2) + repeats = self._type_check(repeats, [int], "repeat", 1) + self._type_check(macro, [Macro], "repeat", 2) async def task(handler: Callable): - for _ in range(_resolve(repeats, [int])): + for _ in range(self._resolve(repeats, [int])): await macro.run(handler) self.tasks.append(task) @@ -457,9 +385,9 @@ def add_event(self, type_: Union[str, int], code: Union[str, int], value: int): examples: 52, 'KEY_A' value """ - type_ = _type_check(type_, [int, str], "event", 1) - code = _type_check(code, [int, str], "event", 2) - value = _type_check(value, [int, str], "event", 3) + type_ = self._type_check(type_, [int, str], "event", 1) + code = self._type_check(code, [int, str], "event", 2) + value = self._type_check(value, [int, str], "event", 3) if isinstance(type_, str): type_ = ecodes[type_.upper()] @@ -476,9 +404,9 @@ def add_mouse( acceleration: Optional[float] = None, ): """Move the mouse cursor.""" - _type_check(direction, [str], "mouse", 1) - speed = _type_check(speed, [int], "mouse", 2) - acceleration = _type_check(acceleration, [float, None], "mouse", 3) + self._type_check(direction, [str], "mouse", 1) + speed = self._type_check(speed, [int], "mouse", 2) + acceleration = self._type_check(acceleration, [float, None], "mouse", 3) code, value = { "up": (REL_Y, -1), @@ -488,8 +416,8 @@ def add_mouse( }[direction.lower()] async def task(handler: Callable): - resolved_speed = _resolve(speed, [int]) - resolved_accel = _resolve(acceleration, [float, None]) + resolved_speed = self._resolve(speed, [int]) + resolved_accel = self._resolve(acceleration, [float, None]) if resolved_accel: current_speed = 0.0 @@ -517,8 +445,8 @@ async def task(handler: Callable): def add_wheel(self, direction: str, speed: int): """Move the scroll wheel.""" - _type_check(direction, [str], "wheel", 1) - speed = _type_check(speed, [int], "wheel", 2) + self._type_check(direction, [str], "wheel", 1) + speed = self._type_check(speed, [int], "wheel", 2) code, value = { "up": ([REL_WHEEL, REL_WHEEL_HI_RES], [1 / 120, 1]), @@ -528,7 +456,7 @@ def add_wheel(self, direction: str, speed: int): }[direction.lower()] async def task(handler: Callable): - resolved_speed = _resolve(speed, [int]) + resolved_speed = self._resolve(speed, [int]) remainder = [0.0, 0.0] while self.is_holding(): for i in range(0, 2): @@ -542,20 +470,20 @@ async def task(handler: Callable): def add_wait(self, time: Union[int, float]): """Wait time in milliseconds.""" - time = _type_check(time, [int, float], "wait", 1) + time = self._type_check(time, [int, float], "wait", 1) async def task(_): - await asyncio.sleep(_resolve(time, [int, float]) / 1000) + await asyncio.sleep(self._resolve(time, [int, float]) / 1000) self.tasks.append(task) def add_set(self, variable: str, value): """Set a variable to a certain value.""" - _type_check_variablename(variable) + self._type_check_variablename(variable) async def task(_): # can also copy with set(a, $b) - resolved_value = _resolve(value) + resolved_value = self._resolve(value) logger.debug('"%s" set to "%s"', variable, resolved_value) macro_variables[variable] = value @@ -563,8 +491,8 @@ async def task(_): def add_add(self, variable: str, value: Union[int, float]): """Add a number to a variable.""" - _type_check_variablename(variable) - _type_check(value, [int, float], "value", 1) + self._type_check_variablename(variable) + self._type_check(value, [int, float], "value", 1) async def task(_): current = macro_variables[variable] @@ -573,7 +501,7 @@ async def task(_): macro_variables[variable] = 0 current = 0 - resolved_value = _resolve(value) + resolved_value = self._resolve(value) if not isinstance(resolved_value, (int, float)): logger.error('Expected delta "%s" to be a number', resolved_value) return @@ -598,8 +526,8 @@ def add_ifeq(self, variable, value, then=None, else_=None): "foo" without breaking old functionality, because "foo" is treated as a variable name. """ - _type_check(then, [Macro, None], "ifeq", 3) - _type_check(else_, [Macro, None], "ifeq", 4) + self._type_check(then, [Macro, None], "ifeq", 3) + self._type_check(else_, [Macro, None], "ifeq", 4) async def task(handler: Callable): set_value = macro_variables.get(variable) @@ -619,12 +547,12 @@ async def task(handler: Callable): def add_if_eq(self, value_1, value_2, then=None, else_=None): """Compare two values.""" - _type_check(then, [Macro, None], "if_eq", 3) - _type_check(else_, [Macro, None], "if_eq", 4) + self._type_check(then, [Macro, None], "if_eq", 3) + self._type_check(else_, [Macro, None], "if_eq", 4) async def task(handler: Callable): - resolved_value_1 = _resolve(value_1) - resolved_value_2 = _resolve(value_2) + resolved_value_1 = self._resolve(value_1) + resolved_value_2 = self._resolve(value_2) if resolved_value_1 == resolved_value_2: if then is not None: await then.run(handler) @@ -646,9 +574,9 @@ def add_if_tap(self, then=None, else_=None, timeout=300): macro key pressed -> released (does other stuff in the meantime) -> if_tap starts -> pressed -> released -> then """ - _type_check(then, [Macro, None], "if_tap", 1) - _type_check(else_, [Macro, None], "if_tap", 2) - timeout = _type_check(timeout, [int, float], "if_tap", 3) + self._type_check(then, [Macro, None], "if_tap", 1) + self._type_check(else_, [Macro, None], "if_tap", 2) + timeout = self._type_check(timeout, [int, float], "if_tap", 3) if isinstance(then, Macro): self.child_macros.append(then) @@ -664,7 +592,7 @@ async def wait(): await self._trigger_release_event.wait() async def task(handler: Callable): - resolved_timeout = _resolve(timeout, [int, float]) / 1000 + resolved_timeout = self._resolve(timeout, [int, float]) / 1000 try: await asyncio.wait_for(wait(), resolved_timeout) if then: @@ -677,8 +605,8 @@ async def task(handler: Callable): def add_if_single(self, then, else_, timeout=None): """If a key was pressed without combining it.""" - _type_check(then, [Macro, None], "if_single", 1) - _type_check(else_, [Macro, None], "if_single", 2) + self._type_check(then, [Macro, None], "if_single", 1) + self._type_check(else_, [Macro, None], "if_single", 2) if isinstance(then, Macro): self.child_macros.append(then) @@ -700,7 +628,7 @@ async def listener(event): self.context.listeners.add(listener) - resolved_timeout = _resolve(timeout, allowed_types=[int, float, None]) + resolved_timeout = Macro._resolve(timeout, allowed_types=[int, float, None]) await asyncio.wait( [ asyncio.Task(listener_done.wait()), @@ -728,7 +656,7 @@ def _type_check_symbol(self, keyname: Union[str, Variable]) -> Union[Variable, i return keyname symbol = str(keyname) - code = system_mapping.get(symbol) + code = keyboard_layout.get(symbol) if code is None: raise MacroParsingError(msg=f'Unknown key "{symbol}"') @@ -741,3 +669,75 @@ def _type_check_symbol(self, keyname: Union[str, Variable]) -> Union[Variable, i raise SymbolNotAvailableInTargetError(symbol, target) return code + + @staticmethod + def _type_check(value: Any, allowed_types, display_name=None, position=None) -> Any: + """Validate a parameter used in a macro. + + If the value is a Variable, it will be returned and should be resolved + during runtime with _resolve. + """ + if isinstance(value, Variable): + # it is a variable and will be read at runtime + return value + + for allowed_type in allowed_types: + if allowed_type is None: + if value is None: + return value + + continue + + # try to parse "1" as 1 if possible + if allowed_type != Macro: + # the macro constructor with a single argument always succeeds, + # but will definitely not result in the correct macro + try: + return allowed_type(value) + except (TypeError, ValueError): + pass + + if isinstance(value, allowed_type): + return value + + if display_name is not None and position is not None: + raise MacroParsingError( + msg=f"Expected parameter {position} for {display_name} to be " + f"one of {allowed_types}, but got {value}" + ) + + raise MacroParsingError( + msg=f"Expected parameter to be one of {allowed_types}, but got {value}" + ) + + @staticmethod + def _type_check_variablename(name: str): + """Check if this is a legit variable name. + + Because they could clash with language features. If the macro is able to be + parsed at all due to a problematic choice of a variable name. + + Allowed examples: "foo", "Foo1234_", "_foo_1234" + Not allowed: "1_foo", "foo=blub", "$foo", "foo,1234", "foo()" + """ + if not isinstance(name, str) or not re.match(r"^[A-Za-z_][A-Za-z_0-9]*$", name): + raise MacroParsingError(msg=f'"{name}" is not a legit variable name') + + @staticmethod + def _resolve(argument, allowed_types=None): + """If the argument is a variable, figure out its value and cast it. + + Variables are prefixed with `$` in the syntax. + + Use this just-in-time when you need the actual value of the variable + during runtime. + """ + if isinstance(argument, Variable): + value = argument.resolve() + logger.debug('"%s" is "%s"', argument, value) + if allowed_types: + return Macro._type_check(value, allowed_types) + else: + return value + + return argument diff --git a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py index 2159896fe..0f06a1e18 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_abs_handler.py @@ -25,7 +25,7 @@ from inputremapper import exceptions from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.axis_transform import Transformation from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, @@ -49,9 +49,10 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ) -> None: - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) # find the input event we are supposed to map. If the input combination is # BTN_A + ABS_X + BTN_B, then use the value of ABS_X for the transformation @@ -133,7 +134,7 @@ def _scale_to_target(self, x: float) -> int: def _write(self, value: int): """Inject.""" try: - global_uinputs.write( + self.global_uinputs.write( (*self._output_axis, value), self.mapping.target_uinput ) except OverflowError: diff --git a/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py b/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py index 544786fde..e173d7e91 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_btn_handler.py @@ -24,6 +24,7 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, InputEventHandler, @@ -43,9 +44,10 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ): - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) self._active = False self._input_config = combination[0] diff --git a/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py b/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py index fe323b1a0..ebde56417 100644 --- a/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py +++ b/inputremapper/injection/mapping_handlers/abs_to_rel_handler.py @@ -41,7 +41,7 @@ WHEEL_HI_RES_SCALING, DEFAULT_REL_RATE, ) -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.axis_transform import Transformation from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, @@ -134,9 +134,10 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ) -> None: - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) # find the input event we are supposed to map assert (map_axis := combination.find_analog_input_config(type_=EV_ABS)) @@ -228,7 +229,9 @@ def _write(self, type_, keycode, value): return # rel 0 does not make sense try: - global_uinputs.write((type_, keycode, value), self.mapping.target_uinput) + self.global_uinputs.write( + (type_, keycode, value), self.mapping.target_uinput + ) except OverflowError: # screwed up the calculation of mouse movements logger.error("OverflowError (%s, %s, %s)", type_, keycode, value) diff --git a/inputremapper/injection/mapping_handlers/axis_switch_handler.py b/inputremapper/injection/mapping_handlers/axis_switch_handler.py index f96b1fc0e..2f86ef178 100644 --- a/inputremapper/injection/mapping_handlers/axis_switch_handler.py +++ b/inputremapper/injection/mapping_handlers/axis_switch_handler.py @@ -24,6 +24,7 @@ from inputremapper.configs.input_config import InputCombination from inputremapper.configs.input_config import InputConfig from inputremapper.configs.mapping import Mapping +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, HandlerEnums, @@ -59,9 +60,10 @@ def __init__( combination: InputCombination, mapping: Mapping, context: ContextProtocol, + global_uinputs: GlobalUInputs, **_, ): - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) trigger_keys = tuple( event.input_match_hash for event in combination diff --git a/inputremapper/injection/mapping_handlers/combination_handler.py b/inputremapper/injection/mapping_handlers/combination_handler.py index 0ec784867..52429e251 100644 --- a/inputremapper/injection/mapping_handlers/combination_handler.py +++ b/inputremapper/injection/mapping_handlers/combination_handler.py @@ -26,6 +26,7 @@ from inputremapper.configs.input_config import InputCombination from inputremapper.configs.mapping import Mapping +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, InputEventHandler, @@ -52,10 +53,11 @@ def __init__( combination: InputCombination, mapping: Mapping, context: Context, + global_uinputs: GlobalUInputs, **_, ) -> None: logger.debug(str(mapping)) - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) self._pressed_keys = {} self._output_state = False self._context = context diff --git a/inputremapper/injection/mapping_handlers/hierarchy_handler.py b/inputremapper/injection/mapping_handlers/hierarchy_handler.py index 4f71581eb..2cc80f5ee 100644 --- a/inputremapper/injection/mapping_handlers/hierarchy_handler.py +++ b/inputremapper/injection/mapping_handlers/hierarchy_handler.py @@ -23,6 +23,7 @@ from evdev.ecodes import EV_ABS, EV_REL from inputremapper.configs.input_config import InputCombination, InputConfig +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, InputEventHandler, @@ -41,14 +42,17 @@ class HierarchyHandler(MappingHandler): _input_config: InputConfig def __init__( - self, handlers: List[MappingHandler], input_config: InputConfig + self, + handlers: List[MappingHandler], + input_config: InputConfig, + global_uinputs: GlobalUInputs, ) -> None: self.handlers = handlers self._input_config = input_config combination = InputCombination([input_config]) # use the mapping from the first child TODO: find a better solution mapping = handlers[0].mapping - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) def __str__(self): return f"HierarchyHandler for {self._input_config}" diff --git a/inputremapper/injection/mapping_handlers/key_handler.py b/inputremapper/injection/mapping_handlers/key_handler.py index 7ce023fb4..871128506 100644 --- a/inputremapper/injection/mapping_handlers/key_handler.py +++ b/inputremapper/injection/mapping_handlers/key_handler.py @@ -23,7 +23,7 @@ from inputremapper.configs.input_config import InputCombination from inputremapper.configs.mapping import Mapping from inputremapper.exceptions import MappingParsingError -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, HandlerEnums, @@ -43,9 +43,10 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ): - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) maps_to = mapping.get_output_type_code() if not maps_to: raise MappingParsingError( @@ -71,7 +72,7 @@ def notify(self, event: InputEvent, *_, **__) -> bool: event_tuple = (*self._maps_to, event.value) try: - global_uinputs.write(event_tuple, self.mapping.target_uinput) + self.global_uinputs.write(event_tuple, self.mapping.target_uinput) self._active = bool(event.value) return True except exceptions.Error: @@ -81,7 +82,7 @@ def reset(self) -> None: logger.debug("resetting key_handler") if self._active: event_tuple = (*self._maps_to, 0) - global_uinputs.write(event_tuple, self.mapping.target_uinput) + self.global_uinputs.write(event_tuple, self.mapping.target_uinput) self._active = False def needs_wrapping(self) -> bool: diff --git a/inputremapper/injection/mapping_handlers/macro_handler.py b/inputremapper/injection/mapping_handlers/macro_handler.py index ed6af7103..e61a6bb0f 100644 --- a/inputremapper/injection/mapping_handlers/macro_handler.py +++ b/inputremapper/injection/mapping_handlers/macro_handler.py @@ -22,7 +22,7 @@ from inputremapper.configs.input_config import InputCombination from inputremapper.configs.mapping import Mapping -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.macros.macro import Macro from inputremapper.injection.macros.parse import parse from inputremapper.injection.mapping_handlers.mapping_handler import ( @@ -45,10 +45,11 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, *, context: ContextProtocol, ): - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) self._active = False assert self.mapping.output_symbol is not None self._macro = parse(self.mapping.output_symbol, context, mapping) @@ -79,7 +80,9 @@ def notify(self, event: InputEvent, *_, **__) -> bool: def handler(type_, code, value) -> None: """Handler for macros.""" - global_uinputs.write((type_, code, value), self.mapping.target_uinput) + self.global_uinputs.write( + (type_, code, value), self.mapping.target_uinput + ) asyncio.ensure_future(self.run_macro(handler)) return True diff --git a/inputremapper/injection/mapping_handlers/mapping_handler.py b/inputremapper/injection/mapping_handlers/mapping_handler.py index 87608a043..0eb6f7ecb 100644 --- a/inputremapper/injection/mapping_handlers/mapping_handler.py +++ b/inputremapper/injection/mapping_handlers/mapping_handler.py @@ -68,6 +68,7 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping from inputremapper.exceptions import MappingParsingError +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.input_event import InputEvent from inputremapper.logging.logger import logger @@ -154,6 +155,7 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ) -> None: """Initialize the handler @@ -167,6 +169,7 @@ def __init__( self.mapping = mapping self.input_configs = list(combination) self._sub_handler = None + self.global_uinputs = global_uinputs def notify( self, diff --git a/inputremapper/injection/mapping_handlers/mapping_parser.py b/inputremapper/injection/mapping_handlers/mapping_parser.py index 65012e833..31477ab33 100644 --- a/inputremapper/injection/mapping_handlers/mapping_parser.py +++ b/inputremapper/injection/mapping_handlers/mapping_parser.py @@ -27,8 +27,9 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import DISABLE_CODE, DISABLE_NAME +from inputremapper.configs.keyboard_layout import DISABLE_CODE, DISABLE_NAME from inputremapper.exceptions import MappingParsingError +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.macros.parse import is_this_a_macro from inputremapper.injection.mapping_handlers.abs_to_abs_handler import AbsToAbsHandler from inputremapper.injection.mapping_handlers.abs_to_btn_handler import AbsToBtnHandler @@ -76,250 +77,285 @@ } -def parse_mappings(preset: Preset, context: ContextProtocol) -> EventPipelines: - """Create a dict with a list of MappingHandler for each InputEvent.""" - handlers = [] - for mapping in preset: - # start with the last handler in the chain, each mapping only has one output, - # but may have multiple inputs, therefore the last handler is a good starting - # point to assemble the pipeline - handler_enum = _get_output_handler(mapping) - constructor = mapping_handler_classes[handler_enum] - if not constructor: - logger.warning( - "a mapping handler '%s' for %s is not implemented", - handler_enum, - mapping.format_name(), - ) - continue +class MappingParser: + def __init__( + self, + global_uinputs: GlobalUInputs, + ): + self.global_uinputs = global_uinputs + + def parse_mappings( + self, + preset: Preset, + context: ContextProtocol, + ) -> EventPipelines: + """Create a dict with a list of MappingHandler for each InputEvent.""" + handlers = [] + for mapping in preset: + # start with the last handler in the chain, each mapping only has one output, + # but may have multiple inputs, therefore the last handler is a good starting + # point to assemble the pipeline + handler_enum = self._get_output_handler(mapping) + constructor = mapping_handler_classes[handler_enum] + if not constructor: + logger.warning( + "a mapping handler '%s' for %s is not implemented", + handler_enum, + mapping.format_name(), + ) + continue - output_handler = constructor( - mapping.input_combination, - mapping, - context=context, - ) + output_handler = constructor( + mapping.input_combination, + mapping, + context=context, + global_uinputs=self.global_uinputs, + ) - # layer other handlers on top until the outer handler needs ranking or can - # directly handle a input event - handlers.extend(_create_event_pipeline(output_handler, context)) + # layer other handlers on top until the outer handler needs ranking or can + # directly handle a input event + handlers.extend(self._create_event_pipeline(output_handler, context)) + + # figure out which handlers need ranking and wrap them with hierarchy_handlers + need_ranking = defaultdict(set) + for handler in handlers.copy(): + if handler.needs_ranking(): + combination = handler.rank_by() + if not combination: + raise MappingParsingError( + f"{type(handler).__name__} claims to need ranking but does not " + f"return a combination to rank by", + mapping_handler=handler, + ) + + need_ranking[combination].add(handler) + handlers.remove(handler) + + # the HierarchyHandler's might not be the starting point of the event pipeline, + # layer other handlers on top again. + ranked_handlers = self._create_hierarchy_handlers(need_ranking) + for handler in ranked_handlers: + handlers.extend( + self._create_event_pipeline(handler, context, ignore_ranking=True) + ) - # figure out which handlers need ranking and wrap them with hierarchy_handlers - need_ranking = defaultdict(set) - for handler in handlers.copy(): - if handler.needs_ranking(): - combination = handler.rank_by() - if not combination: - raise MappingParsingError( - f"{type(handler).__name__} claims to need ranking but does not " - f"return a combination to rank by", - mapping_handler=handler, + # group all handlers by the input events they take care of. One handler might end + # up in multiple groups if it takes care of multiple InputEvents + event_pipelines: EventPipelines = defaultdict(set) + for handler in handlers: + assert handler.input_configs + for input_config in handler.input_configs: + logger.debug( + "event-pipeline with entry point: %s %s", + get_evdev_constant_name(*input_config.type_and_code), + input_config.input_match_hash, + ) + logger.debug_mapping_handler(handler) + event_pipelines[input_config].add(handler) + + return event_pipelines + + def _create_event_pipeline( + self, + handler: MappingHandler, + context: ContextProtocol, + ignore_ranking=False, + ) -> List[MappingHandler]: + """Recursively wrap a handler with other handlers until the + outer handler needs ranking or is finished wrapping. + """ + if not handler.needs_wrapping() or ( + handler.needs_ranking() and not ignore_ranking + ): + return [handler] + + handlers = [] + for combination, handler_enum in handler.wrap_with().items(): + constructor = mapping_handler_classes[handler_enum] + if not constructor: + raise NotImplementedError( + f"mapping handler {handler_enum} is not implemented" ) - need_ranking[combination].add(handler) - handlers.remove(handler) - - # the HierarchyHandler's might not be the starting point of the event pipeline, - # layer other handlers on top again. - ranked_handlers = _create_hierarchy_handlers(need_ranking) - for handler in ranked_handlers: - handlers.extend(_create_event_pipeline(handler, context, ignore_ranking=True)) - - # group all handlers by the input events they take care of. One handler might end - # up in multiple groups if it takes care of multiple InputEvents - event_pipelines: EventPipelines = defaultdict(set) - for handler in handlers: - assert handler.input_configs - for input_config in handler.input_configs: - logger.debug( - "event-pipeline with entry point: %s %s", - get_evdev_constant_name(*input_config.type_and_code), - input_config.input_match_hash, - ) - logger.debug_mapping_handler(handler) - event_pipelines[input_config].add(handler) - - return event_pipelines - - -def _create_event_pipeline( - handler: MappingHandler, context: ContextProtocol, ignore_ranking=False -) -> List[MappingHandler]: - """Recursively wrap a handler with other handlers until the - outer handler needs ranking or is finished wrapping. - """ - if not handler.needs_wrapping() or (handler.needs_ranking() and not ignore_ranking): - return [handler] - - handlers = [] - for combination, handler_enum in handler.wrap_with().items(): - constructor = mapping_handler_classes[handler_enum] - if not constructor: - raise NotImplementedError( - f"mapping handler {handler_enum} is not implemented" + super_handler = constructor( + combination, + handler.mapping, + context=context, + global_uinputs=self.global_uinputs, ) + super_handler.set_sub_handler(handler) + for event in combination: + # the handler now has a super_handler which takes care about the events. + # so we need to hide them on the handler + handler.occlude_input_event(event) - super_handler = constructor(combination, handler.mapping, context=context) - super_handler.set_sub_handler(handler) - for event in combination: - # the handler now has a super_handler which takes care about the events. - # so we need to hide them on the handler - handler.occlude_input_event(event) + handlers.extend(self._create_event_pipeline(super_handler, context)) - handlers.extend(_create_event_pipeline(super_handler, context)) + if handler.input_configs: + # the handler was only partially wrapped, + # we need to return it as a toplevel handler + handlers.append(handler) - if handler.input_configs: - # the handler was only partially wrapped, - # we need to return it as a toplevel handler - handlers.append(handler) + return handlers - return handlers + def _get_output_handler(self, mapping: Mapping) -> HandlerEnums: + """Determine the correct output handler. + this is used as a starting point for the mapping parser + """ + if mapping.output_code == DISABLE_CODE or mapping.output_symbol == DISABLE_NAME: + return HandlerEnums.disable -def _get_output_handler(mapping: Mapping) -> HandlerEnums: - """Determine the correct output handler. + if mapping.output_symbol: + if is_this_a_macro(mapping.output_symbol): + return HandlerEnums.macro - this is used as a starting point for the mapping parser - """ - if mapping.output_code == DISABLE_CODE or mapping.output_symbol == DISABLE_NAME: - return HandlerEnums.disable + return HandlerEnums.key - if mapping.output_symbol: - if is_this_a_macro(mapping.output_symbol): - return HandlerEnums.macro + if mapping.output_type == EV_KEY: + return HandlerEnums.key - return HandlerEnums.key + input_event = self._maps_axis(mapping.input_combination) + if not input_event: + raise MappingParsingError( + f"This {mapping = } does not map to an axis, key or macro", + mapping=Mapping, + ) - if mapping.output_type == EV_KEY: - return HandlerEnums.key + if mapping.output_type == EV_REL: + if input_event.type == EV_KEY: + return HandlerEnums.btn2rel + if input_event.type == EV_REL: + return HandlerEnums.rel2rel + if input_event.type == EV_ABS: + return HandlerEnums.abs2rel + + if mapping.output_type == EV_ABS: + if input_event.type == EV_KEY: + return HandlerEnums.btn2abs + if input_event.type == EV_REL: + return HandlerEnums.rel2abs + if input_event.type == EV_ABS: + return HandlerEnums.abs2abs - input_event = _maps_axis(mapping.input_combination) - if not input_event: raise MappingParsingError( - f"This {mapping = } does not map to an axis, key or macro", - mapping=Mapping, + f"the output of {mapping = } is unknown", mapping=Mapping ) - if mapping.output_type == EV_REL: - if input_event.type == EV_KEY: - return HandlerEnums.btn2rel - if input_event.type == EV_REL: - return HandlerEnums.rel2rel - if input_event.type == EV_ABS: - return HandlerEnums.abs2rel - - if mapping.output_type == EV_ABS: - if input_event.type == EV_KEY: - return HandlerEnums.btn2abs - if input_event.type == EV_REL: - return HandlerEnums.rel2abs - if input_event.type == EV_ABS: - return HandlerEnums.abs2abs - - raise MappingParsingError(f"the output of {mapping = } is unknown", mapping=Mapping) - - -def _maps_axis(combination: InputCombination) -> Optional[InputConfig]: - """Whether this InputCombination contains an InputEvent that is treated as - an axis and not a binary (key or button) event. - """ - for event in combination: - if event.defines_analog_input: - return event - return None - - -def _create_hierarchy_handlers( - handlers: Dict[InputCombination, Set[MappingHandler]] -) -> Set[MappingHandler]: - """Sort handlers by input events and create Hierarchy handlers.""" - sorted_handlers = set() - all_combinations = handlers.keys() - events = set() - - # gather all InputEvents from all handlers - for combination in all_combinations: + def _maps_axis(self, combination: InputCombination) -> Optional[InputConfig]: + """Whether this InputCombination contains an InputEvent that is treated as + an axis and not a binary (key or button) event. + """ for event in combination: - events.add(event) - - # create a ranking for each event - for event in events: - # find all combinations (from handlers) which contain the event - combinations_with_event = [ - combination for combination in all_combinations if event in combination - ] - - if len(combinations_with_event) == 1: - # there was only one handler containing that event return it as is - sorted_handlers.update(handlers[combinations_with_event[0]]) - continue - - # there are multiple handler with the same event. - # rank them and create the HierarchyHandler - sorted_combinations = _order_combinations(combinations_with_event, event) - sub_handlers: List[MappingHandler] = [] - for combination in sorted_combinations: - sub_handlers.append(*handlers[combination]) - - sorted_handlers.add(HierarchyHandler(sub_handlers, event)) - for handler in sub_handlers: - # the handler now has a HierarchyHandler which takes care about this event. - # so we hide need to hide it on the handler - handler.occlude_input_event(event) - - return sorted_handlers - - -def _order_combinations( - combinations: List[InputCombination], common_config: InputConfig -) -> List[InputCombination]: - """Reorder the keys according to some rules. - - such that a combination a+b+c is in front of a+b which is in front of b - for a+b+c vs. b+d+e: a+b+c would be in front of b+d+e, because the common key b - has the higher index in the a+b+c (1), than in the b+c+d (0) list - in this example b would be the common key - as for combinations like a+b+c and e+d+c with the common key c: ¯\\_(ツ)_/¯ - - Parameters - ---------- - combinations - the list which needs ordering - common_config - the InputConfig all InputCombination's in combinations have in common - """ - combinations.sort(key=len) - - for start, end in _ranges_with_constant_length(combinations.copy()): - sub_list = combinations[start:end] - sub_list.sort(key=lambda x: x.index(common_config)) - combinations[start:end] = sub_list - - combinations.reverse() - return combinations - - -def _ranges_with_constant_length(x: Sequence[Sized]) -> Iterable[Tuple[int, int]]: - """Get all ranges of x for which the elements have constant length - - Parameters - ---------- - x: Sequence[Sized] - l must be ordered by increasing length of elements - """ - start_idx = 0 - last_len = 0 - for idx, y in enumerate(x): - if len(y) > last_len and idx - start_idx > 1: - yield start_idx, idx - - if len(y) == last_len and idx + 1 == len(x): - yield start_idx, idx + 1 - - if len(y) > last_len: - start_idx = idx - - if len(y) < last_len: - raise MappingParsingError( - "ranges_with_constant_length was called with an unordered list" + if event.defines_analog_input: + return event + return None + + def _create_hierarchy_handlers( + self, + handlers: Dict[InputCombination, Set[MappingHandler]], + ) -> Set[MappingHandler]: + """Sort handlers by input events and create Hierarchy handlers.""" + sorted_handlers = set() + all_combinations = handlers.keys() + events = set() + + # gather all InputEvents from all handlers + for combination in all_combinations: + for event in combination: + events.add(event) + + # create a ranking for each event + for event in events: + # find all combinations (from handlers) which contain the event + combinations_with_event = [ + combination for combination in all_combinations if event in combination + ] + + if len(combinations_with_event) == 1: + # there was only one handler containing that event return it as is + sorted_handlers.update(handlers[combinations_with_event[0]]) + continue + + # there are multiple handler with the same event. + # rank them and create the HierarchyHandler + sorted_combinations = self._order_combinations( + combinations_with_event, + event, ) - last_len = len(y) + sub_handlers: List[MappingHandler] = [] + for combination in sorted_combinations: + sub_handlers.append(*handlers[combination]) + + sorted_handlers.add( + HierarchyHandler( + sub_handlers, + event, + self.global_uinputs, + ) + ) + for handler in sub_handlers: + # the handler now has a HierarchyHandler which takes care about this event. + # so we hide need to hide it on the handler + handler.occlude_input_event(event) + + return sorted_handlers + + def _order_combinations( + self, + combinations: List[InputCombination], + common_config: InputConfig, + ) -> List[InputCombination]: + """Reorder the keys according to some rules. + + such that a combination a+b+c is in front of a+b which is in front of b + for a+b+c vs. b+d+e: a+b+c would be in front of b+d+e, because the common key b + has the higher index in the a+b+c (1), than in the b+c+d (0) list + in this example b would be the common key + as for combinations like a+b+c and e+d+c with the common key c: ¯\\_(ツ)_/¯ + + Parameters + ---------- + combinations + the list which needs ordering + common_config + the InputConfig all InputCombination's in combinations have in common + """ + combinations.sort(key=len) + + for start, end in self._ranges_with_constant_length(combinations.copy()): + sub_list = combinations[start:end] + sub_list.sort(key=lambda x: x.index(common_config)) + combinations[start:end] = sub_list + + combinations.reverse() + return combinations + + def _ranges_with_constant_length( + self, + x: Sequence[Sized], + ) -> Iterable[Tuple[int, int]]: + """Get all ranges of x for which the elements have constant length + + Parameters + ---------- + x: Sequence[Sized] + l must be ordered by increasing length of elements + """ + start_idx = 0 + last_len = 0 + for idx, y in enumerate(x): + if len(y) > last_len and idx - start_idx > 1: + yield start_idx, idx + + if len(y) == last_len and idx + 1 == len(x): + yield start_idx, idx + 1 + + if len(y) > last_len: + start_idx = idx + + if len(y) < last_len: + raise MappingParsingError( + "ranges_with_constant_length was called with an unordered list" + ) + last_len = len(y) diff --git a/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py b/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py index e8b5ec6a4..ec6de883e 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_abs_handler.py @@ -39,7 +39,7 @@ REL_XY_SCALING, DEFAULT_REL_RATE, ) -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.axis_transform import Transformation from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, @@ -74,9 +74,10 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ) -> None: - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) # find the input event we are supposed to map. If the input combination is # BTN_A + REL_X + BTN_B, then use the value of REL_X for the transformation @@ -227,7 +228,7 @@ def _scale_to_target(self, x: float) -> int: def _write(self, value: int) -> None: """Inject.""" try: - global_uinputs.write( + self.global_uinputs.write( (*self._output_axis, value), self.mapping.target_uinput ) except OverflowError: diff --git a/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py b/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py index bbc39d6c8..387de3823 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_btn_handler.py @@ -25,6 +25,7 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, InputEventHandler, @@ -49,9 +50,10 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ) -> None: - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) self._active = False self._input_config = combination[0] diff --git a/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py b/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py index d41a2c465..227c89e06 100644 --- a/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py +++ b/inputremapper/injection/mapping_handlers/rel_to_rel_handler.py @@ -37,7 +37,7 @@ WHEEL_SCALING, WHEEL_HI_RES_SCALING, ) -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.mapping_handlers.axis_transform import Transformation from inputremapper.injection.mapping_handlers.mapping_handler import ( MappingHandler, @@ -91,9 +91,10 @@ def __init__( self, combination: InputCombination, mapping: Mapping, + global_uinputs: GlobalUInputs, **_, ) -> None: - super().__init__(combination, mapping) + super().__init__(combination, mapping, global_uinputs) assert self.mapping.output_code is not None @@ -256,7 +257,7 @@ def _write(self, code: int, value: int): if value == 0: return - global_uinputs.write( + self.global_uinputs.write( (EV_REL, code, value), self.mapping.target_uinput, ) diff --git a/tests/integration/test_components.py b/tests/integration/test_components.py index 6d1f23b70..37c1a67ad 100644 --- a/tests/integration/test_components.py +++ b/tests/integration/test_components.py @@ -37,7 +37,7 @@ from tests.lib.logger import logger from inputremapper.gui.controller import Controller -from inputremapper.configs.system_mapping import XKB_KEYCODE_OFFSET +from inputremapper.configs.keyboard_layout import XKB_KEYCODE_OFFSET from inputremapper.gui.utils import CTX_ERROR, CTX_WARNING, gtk_iteration from inputremapper.gui.messages.message_broker import ( MessageBroker, diff --git a/tests/integration/test_daemon.py b/tests/integration/test_daemon.py index 1ed96839f..27593a24f 100644 --- a/tests/integration/test_daemon.py +++ b/tests/integration/test_daemon.py @@ -43,6 +43,8 @@ def gtk_iteration(): @test_setup class TestDBusDaemon(unittest.TestCase): def setUp(self): + # You need to install input-remapper into your system in order for this test + # to work. self.process = multiprocessing.Process( target=os.system, args=("input-remapper-service -d",) ) diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index be79a778f..9aa395283 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -46,13 +46,14 @@ get_incomplete_parameter, get_incomplete_function_name, ) +from inputremapper.injection.global_uinputs import GlobalUInputs, FrontendUInput, UInput +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from inputremapper.input_event import InputEvent from tests.integration.test_components import FlowBoxTestUtils from tests.lib.cleanup import cleanup from tests.lib.constants import EVENT_READ_TIMEOUT from tests.lib.fixtures import fixtures from tests.lib.fixtures import prepare_presets -from tests.lib.global_uinputs import reset_global_uinputs_for_service from tests.lib.logger import logger from tests.lib.pipes import push_event, push_events, uinput_write_history_pipe from tests.lib.project_root import get_project_root @@ -64,10 +65,10 @@ gi.require_version("GLib", "2.0") from gi.repository import Gtk, GLib, Gdk, GtkSource -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.configs.mapping import Mapping from inputremapper.configs.paths import PathUtils -from inputremapper.configs.global_config import global_config +from inputremapper.configs.global_config import GlobalConfig from inputremapper.groups import _Groups from inputremapper.gui.data_manager import DataManager from inputremapper.gui.messages.message_broker import ( @@ -102,7 +103,14 @@ def launch( argv=None, -) -> Tuple[UserInterface, Controller, DataManager, MessageBroker, DaemonProxy]: +) -> Tuple[ + UserInterface, + Controller, + DataManager, + MessageBroker, + DaemonProxy, + GlobalConfig, +]: """Start input-remapper-gtk with the command line argument array argv.""" bin_path = os.path.join(get_project_root(), "bin", "input-remapper-gtk") @@ -127,12 +135,14 @@ def launch( module.data_manager, module.message_broker, module.daemon, + module.global_config, ) def start_reader_service(): def process(): - reader_service = ReaderService(_Groups()) + global_uinputs = GlobalUInputs(FrontendUInput) + reader_service = ReaderService(_Groups(), global_uinputs) loop = asyncio.new_event_loop() loop.run_until_complete(reader_service.run()) @@ -154,15 +164,25 @@ def os_system_patch(cmd, original_os_system=os.system): def patch_launch(): """patch the launch function such that we don't connect to the dbus and don't use pkexec to start the reader-service""" + + def bootstrap_daemon(): + # The daemon gets fresh instances of everything, because as far as I remember + # it runs in a separate process. + global_config = GlobalConfig() + global_uinputs = GlobalUInputs(UInput) + mapping_parser = MappingParser(global_uinputs) + + return Daemon( + global_config, + global_uinputs, + mapping_parser, + ) + with patch.object( os, "system", os_system_patch, - ), patch.object( - Daemon, - "connect", - Daemon, - ): + ), patch.object(Daemon, "connect", bootstrap_daemon): yield @@ -213,12 +233,25 @@ def os_system(cmd, original_os_system=os.system): # time for the application self.os_system_patch.start() + def bootstrap_daemon(self): + # The daemon gets fresh instances of everything, because as far as I remember + # it runs in a separate process. + global_config = GlobalConfig() + global_uinputs = GlobalUInputs(UInput) + mapping_parser = MappingParser(global_uinputs) + + return Daemon( + global_config, + global_uinputs, + mapping_parser, + ) + def patch_daemon(self): # don't try to connect, return an object instance of it instead self.daemon_connect_patch = patch.object( Daemon, "connect", - Daemon, + lambda: self.bootstrap_daemon(), ) self.daemon_connect_patch.start() @@ -233,6 +266,7 @@ def setUp(self): self.data_manager, self.message_broker, self.daemon, + self.global_config, ) = launch() def tearDown(self): @@ -306,6 +340,7 @@ def setUp(self): self.data_manager, self.message_broker, self.daemon, + self.global_config, ) = launch() get = self.user_interface.get @@ -340,7 +375,7 @@ def grab(_): evdev.InputDevice.grab = grab - global_config._save_config() + self.global_config._save_config() self.throttle(20) @@ -1781,11 +1816,6 @@ def test_cannot_record_keys(self): self.assertIn("Stop", text) def test_start_injecting(self): - # It's 2023 everyone! That means this test randomly stopped working because it - # used FrontendUInputs instead of regular UInputs. I guess a fucking ghost - # was fixing this for us during 2022, but it seems to have disappeared. - reset_global_uinputs_for_service() - self.controller.load_group("Foo Device 2") with spy(self.daemon, "set_config_dir") as spy1: @@ -1840,8 +1870,6 @@ def test_start_injecting(self): self.assertNotIn("input-remapper", device_group_entry.name) def test_stop_injecting(self): - reset_global_uinputs_for_service() - self.controller.load_group("Foo Device 2") self.start_injector_btn.clicked() gtk_iteration() @@ -2128,9 +2156,9 @@ def test_autocomplete_key(self): complete_key_name = "Test_Foo_Bar" - system_mapping.clear() - system_mapping._set(complete_key_name, 1) - system_mapping._set("KEY_A", 30) # we need this for the UIMapping to work + keyboard_layout.clear() + keyboard_layout._set(complete_key_name, 1) + keyboard_layout._set("KEY_A", 30) # we need this for the UIMapping to work # it can autocomplete a combination inbetween other things incomplete = "qux_1\n + + qux_2" diff --git a/tests/lib/cleanup.py b/tests/lib/cleanup.py index 14b688a70..daee127ee 100644 --- a/tests/lib/cleanup.py +++ b/tests/lib/cleanup.py @@ -20,30 +20,31 @@ from __future__ import annotations +import asyncio import copy import os import shutil import time -import asyncio -import psutil from pickle import UnpicklingError -# TODO on it. You don't need a framework for this by the way: -# don't import anything from input_remapper gloablly here, because some files execute -# code when imported, which can screw up patches. I wish we had a dependency injection -# framework that patches together the dependencies during runtime... +import psutil +from tests.lib.constants import EVENT_READ_TIMEOUT +from tests.lib.fixtures import fixtures from tests.lib.logger import logger +from tests.lib.patches import uinputs from tests.lib.pipes import ( uinput_write_history_pipe, uinput_write_history, pending_events, setup_pipe, ) -from tests.lib.constants import EVENT_READ_TIMEOUT from tests.lib.tmp import tmp -from tests.lib.fixtures import fixtures -from tests.lib.patches import uinputs + +# TODO on it. You don't need a framework for this by the way: +# don't import anything from input_remapper gloablly here, because some files execute +# code when imported, which can screw up patches. I wish we had a dependency injection +# framework that patches together the dependencies during runtime... environ_copy = copy.deepcopy(os.environ) @@ -81,12 +82,9 @@ def quick_cleanup(log=True): # Reminder: before patches are applied in test.py, no inputremapper module # may be imported. So tests.lib imports them just-in-time in functions instead. from inputremapper.injection.macros.macro import macro_variables - from inputremapper.configs.global_config import global_config - from inputremapper.configs.system_mapping import system_mapping + from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.gui.utils import debounce_manager - from inputremapper.configs.paths import PathUtils - from inputremapper.injection.global_uinputs import global_uinputs - from tests.lib.global_uinputs import reset_global_uinputs_for_service + from inputremapper.injection.global_uinputs import GlobalUInputs if log: logger.info("Quick cleanup...") @@ -130,11 +128,7 @@ def quick_cleanup(log=True): if os.path.exists(tmp): shutil.rmtree(tmp) - global_config.path = os.path.join(PathUtils.get_config_path(), "config.json") - global_config.clear_config() - global_config._save_config() - - system_mapping.populate() + keyboard_layout.populate() clear_write_history() @@ -155,11 +149,6 @@ def quick_cleanup(log=True): assert not pipe.poll() assert macro_variables.is_alive(1) - for uinput in global_uinputs.devices.values(): - uinput.write_count = 0 - uinput.write_history = [] - - reset_global_uinputs_for_service() if log: logger.info("Quick cleanup done") diff --git a/tests/lib/fixtures.py b/tests/lib/fixtures.py index 2cbe9b6c2..4d3614f24 100644 --- a/tests/lib/fixtures.py +++ b/tests/lib/fixtures.py @@ -22,12 +22,16 @@ import dataclasses import json +import time from hashlib import md5 from typing import Dict, Optional -import time import evdev +from inputremapper.configs.input_config import InputCombination +from inputremapper.configs.mapping import Mapping +from inputremapper.configs.paths import PathUtils +from inputremapper.configs.preset import Preset from tests.lib.logger import logger # input-remapper is only interested in devices that have EV_KEY, add some @@ -347,12 +351,6 @@ def prepare_presets(): """prepare a few presets for use in tests "Foo Device 2/preset3" is the newest and "Foo Device 2/preset2" is set to autoload """ - from inputremapper.configs.preset import Preset - from inputremapper.configs.mapping import Mapping - from inputremapper.configs.paths import PathUtils - from inputremapper.configs.global_config import global_config - from inputremapper.configs.input_config import InputCombination - preset1 = Preset(PathUtils.get_preset_path("Foo Device", "preset1")) preset1.add( Mapping.from_combination( @@ -379,6 +377,4 @@ def prepare_presets(): with open(PathUtils.get_config_path("config.json"), "w") as file: json.dump({"autoload": {"Foo Device 2": "preset2"}}, file, indent=4) - global_config.load_config() - return preset1, preset2, preset3 diff --git a/tests/lib/global_uinputs.py b/tests/lib/global_uinputs.py deleted file mode 100644 index cf6608c3c..000000000 --- a/tests/lib/global_uinputs.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -# input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2024 sezanzeb -# -# This file is part of input-remapper. -# -# input-remapper 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. -# -# input-remapper 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 input-remapper. If not, see . - -import sys -from unittest.mock import patch - -from inputremapper.injection.global_uinputs import global_uinputs - - -def reset_global_uinputs_for_service(): - with patch.object(sys, "argv", ["input-remapper-service"]): - # patch argv for global_uinputs to think it is a service - global_uinputs.reset() - - -def reset_global_uinputs_for_gui(): - with patch.object(sys, "argv", ["input-remapper-gtk"]): - global_uinputs.reset() diff --git a/tests/lib/test_setup.py b/tests/lib/test_setup.py index 4961128e1..19b71724b 100644 --- a/tests/lib/test_setup.py +++ b/tests/lib/test_setup.py @@ -23,8 +23,6 @@ import os import tracemalloc -from inputremapper.configs.global_config import global_config -from inputremapper.configs.paths import PathUtils from tests.lib.cleanup import cleanup, quick_cleanup from tests.lib.fixture_pipes import create_fixture_pipes, remove_fixture_pipes from tests.lib.is_service_running import is_service_running @@ -64,14 +62,6 @@ def setUpClass(): for patch in patches: patch.start() - # TODO if global_config is injected instead, it could work without doing this - # load_config call here. Because right now the constructor uses variables - # that are unpatched once global_config.py is imported. - global_config.path = os.path.join( - PathUtils.config_path(), - "config.json", - ) - original_setUpClass() def tearDownClass(): @@ -95,11 +85,6 @@ def tearDown(self): quick_cleanup() - # This assertion was important to me somehow in other tests after the cleanup, - # so I added it to the test setup. I don't remember if this is important to - # check. - assert len(global_config.iterate_autoload_presets()) == 0 - for patch in patches: patch.stop() diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index f80720eeb..317cd3803 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -22,16 +22,16 @@ import os import unittest -from inputremapper.configs.global_config import global_config +from inputremapper.configs.global_config import GlobalConfig from inputremapper.configs.paths import PathUtils - -from tests.lib.tmp import tmp from tests.lib.test_setup import test_setup +from tests.lib.tmp import tmp @test_setup class TestConfig(unittest.TestCase): def test_basic(self): + global_config = GlobalConfig() self.assertEqual(global_config.get("a"), None) global_config.set("a", 1) @@ -48,6 +48,7 @@ def test_basic(self): self.assertEqual(global_config._config["a"]["b"]["c"], 3) def test_autoload(self): + global_config = GlobalConfig() self.assertEqual(len(global_config.iterate_autoload_presets()), 0) self.assertFalse(global_config.is_autoloaded("d1", "a")) self.assertFalse(global_config.is_autoloaded("d2.foo", "b")) @@ -92,9 +93,9 @@ def test_autoload(self): self.assertRaises(ValueError, global_config.is_autoloaded, None, "a") def test_initial(self): + global_config = GlobalConfig() # when loading for the first time, create a config file with # the default values - os.remove(global_config.path) self.assertFalse(os.path.exists(global_config.path)) global_config.load_config() self.assertTrue(os.path.exists(global_config.path)) @@ -104,6 +105,7 @@ def test_initial(self): self.assertIn('"autoload": {}', contents) def test_save_load(self): + global_config = GlobalConfig() self.assertEqual(len(global_config.iterate_autoload_presets()), 0) global_config.load_config() diff --git a/tests/unit/test_context.py b/tests/unit/test_context.py index 87b6dacd6..650606177 100644 --- a/tests/unit/test_context.py +++ b/tests/unit/test_context.py @@ -33,6 +33,8 @@ from inputremapper.configs.mapping import Mapping from inputremapper.configs.preset import Preset from inputremapper.injection.context import Context +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from inputremapper.input_event import InputEvent from tests.lib.test_setup import test_setup @@ -40,6 +42,9 @@ @test_setup class TestContext(unittest.TestCase): def test_callbacks(self): + global_uinputs = GlobalUInputs(UInput) + mapping_parser = MappingParser(global_uinputs) + preset = Preset() cfg = { "input_combination": InputCombination.from_tuples((EV_ABS, ABS_X)), @@ -81,7 +86,7 @@ def test_callbacks(self): ), ) - context = Context(preset, {}, {}) + context = Context(preset, {}, {}, mapping_parser) expected_num_callbacks = { # ABS_X -> "d" and ABS_X -> wheel have the same type and code diff --git a/tests/unit/test_control.py b/tests/unit/test_control.py index f2d800385..a7e4fe31a 100644 --- a/tests/unit/test_control.py +++ b/tests/unit/test_control.py @@ -27,12 +27,16 @@ from importlib.machinery import SourceFileLoader from importlib.util import spec_from_loader, module_from_spec from unittest.mock import patch +import re -from inputremapper.configs.global_config import global_config +from inputremapper.configs.global_config import GlobalConfig +from inputremapper.configs.migrations import Migrations from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset from inputremapper.daemon import Daemon from inputremapper.groups import groups +from inputremapper.injection.global_uinputs import GlobalUInputs, FrontendUInput +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from tests.lib.test_setup import test_setup from tests.lib.tmp import tmp @@ -40,7 +44,7 @@ def import_control(): """Import the core function of the input-remapper-control command.""" bin_path = os.path.join( - os.getcwd().replace("/tests", ""), + re.sub("/tests.*", "", os.getcwd()), "bin", "input-remapper-control", ) @@ -65,7 +69,13 @@ def import_control(): @test_setup class TestControl(unittest.TestCase): def setUp(self): - self.input_remapper_control = InputRemapperControl() + self.global_config = GlobalConfig() + self.global_uinputs = GlobalUInputs(FrontendUInput) + self.migrations = Migrations(self.global_uinputs) + self.mapping_parser = MappingParser(self.global_uinputs) + self.input_remapper_control = InputRemapperControl( + self.global_config, self.migrations + ) def test_autoload(self): device_keys = ["Foo Device 2", "Bar Device"] @@ -81,7 +91,7 @@ def test_autoload(self): Preset(paths[1]).save() Preset(paths[2]).save() - daemon = Daemon() + daemon = Daemon(self.global_config, self.global_uinputs, self.mapping_parser) self.input_remapper_control.set_daemon(daemon) @@ -101,8 +111,8 @@ def start_injecting(device: str, preset: str): patch.object(daemon, "start_injecting", start_injecting).start() - global_config.set_autoload_preset(groups_[0].key, presets[0]) - global_config.set_autoload_preset(groups_[1].key, presets[1]) + self.global_config.set_autoload_preset(groups_[0].key, presets[0]) + self.global_config.set_autoload_preset(groups_[1].key, presets[1]) self.input_remapper_control.communicate( command="autoload", @@ -181,7 +191,7 @@ def start_injecting(device: str, preset: str): daemon.autoload_history.may_autoload(groups_[1].key, presets[1]) ) self.assertEqual(stop_counter, 3) - global_config.set_autoload_preset(groups_[1].key, presets[2]) + self.global_config.set_autoload_preset(groups_[1].key, presets[2]) self.input_remapper_control.communicate( command="autoload", config_dir=None, @@ -236,16 +246,16 @@ def test_autoload_other_path(self): Preset(paths[0]).save() Preset(paths[1]).save() - daemon = Daemon() + daemon = Daemon(self.global_config, self.global_uinputs, self.mapping_parser) self.input_remapper_control.set_daemon(daemon) start_history = [] daemon.start_injecting = lambda *args: start_history.append(args) - global_config.path = os.path.join(config_dir, "config.json") - global_config.load_config() - global_config.set_autoload_preset(device_names[0], presets[0]) - global_config.set_autoload_preset(device_names[1], presets[1]) + self.global_config.path = os.path.join(config_dir, "config.json") + self.global_config.load_config() + self.global_config.set_autoload_preset(device_names[0], presets[0]) + self.global_config.set_autoload_preset(device_names[1], presets[1]) self.input_remapper_control.communicate( command="autoload", @@ -262,7 +272,7 @@ def test_start_stop(self): group = groups.find(key="Foo Device 2") preset = "preset9" - daemon = Daemon() + daemon = Daemon(self.global_config, self.global_uinputs, self.mapping_parser) self.input_remapper_control.set_daemon(daemon) start_history = [] @@ -306,7 +316,7 @@ def test_config_not_found(self): path = "~/a/preset.json" config_dir = "/foo/bar" - daemon = Daemon() + daemon = Daemon(self.global_config, self.global_uinputs, self.mapping_parser) self.input_remapper_control.set_daemon(daemon) start_history = [] @@ -335,26 +345,26 @@ def test_config_not_found(self): ) def test_autoload_config_dir(self): - daemon = Daemon() + daemon = Daemon(self.global_config, self.global_uinputs, self.mapping_parser) path = os.path.join(tmp, "foo") os.makedirs(path) with open(os.path.join(path, "config.json"), "w") as file: file.write('{"foo":"bar"}') - self.assertIsNone(global_config.get("foo")) + self.assertIsNone(self.global_config.get("foo")) daemon.set_config_dir(path) # since daemon and this test share the same memory, the global_config # object that this test can access will be modified - self.assertEqual(global_config.get("foo"), "bar") + self.assertEqual(self.global_config.get("foo"), "bar") # passing a path that doesn't exist or a path that doesn't contain # a config.json file won't do anything os.makedirs(os.path.join(tmp, "bar")) daemon.set_config_dir(os.path.join(tmp, "bar")) - self.assertEqual(global_config.get("foo"), "bar") + self.assertEqual(self.global_config.get("foo"), "bar") daemon.set_config_dir(os.path.join(tmp, "qux")) - self.assertEqual(global_config.get("foo"), "bar") + self.assertEqual(self.global_config.get("foo"), "bar") def test_internals_reader(self): with patch.object(os, "system") as os_system_patch: diff --git a/tests/unit/test_controller.py b/tests/unit/test_controller.py index 39015835d..f28aab23c 100644 --- a/tests/unit/test_controller.py +++ b/tests/unit/test_controller.py @@ -26,7 +26,7 @@ import gi from evdev.ecodes import EV_ABS, ABS_X, ABS_Y, ABS_RX -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.injection.injector import InjectorState gi.require_version("Gtk", "3.0") @@ -50,7 +50,7 @@ from inputremapper.gui.reader_client import ReaderClient from inputremapper.gui.utils import CTX_ERROR, CTX_APPLY, gtk_iteration from inputremapper.gui.gettext import _ -from inputremapper.injection.global_uinputs import GlobalUInputs +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput, FrontendUInput from inputremapper.configs.mapping import UIMapping, MappingData, Mapping from tests.lib.spy import spy from tests.lib.patches import FakeDaemonProxy @@ -68,7 +68,7 @@ class TestController(unittest.TestCase): def setUp(self) -> None: super().setUp() self.message_broker = MessageBroker() - uinputs = GlobalUInputs() + uinputs = GlobalUInputs(FrontendUInput) uinputs.prepare_all() self.data_manager = DataManager( self.message_broker, @@ -76,7 +76,7 @@ def setUp(self) -> None: ReaderClient(self.message_broker, _Groups()), FakeDaemonProxy(), uinputs, - system_mapping, + keyboard_layout, ) self.user_interface = MagicMock() self.controller = Controller(self.message_broker, self.data_manager) diff --git a/tests/unit/test_daemon.py b/tests/unit/test_daemon.py index e6f01314a..6e0c0529b 100644 --- a/tests/unit/test_daemon.py +++ b/tests/unit/test_daemon.py @@ -18,38 +18,36 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . -from unittest.mock import patch, MagicMock - -from evdev._ecodes import EV_ABS - -from inputremapper.input_event import InputEvent -from tests.lib.logger import logger -from tests.lib.cleanup import cleanup -from tests.lib.fixtures import Fixture -from tests.lib.pipes import push_events, uinput_write_history_pipe -from tests.lib.tmp import tmp -from tests.lib.fixtures import fixtures - +import json import os -import unittest import time -import json +import unittest +from unittest.mock import patch, MagicMock import evdev +from evdev._ecodes import EV_ABS from evdev.ecodes import EV_KEY, KEY_B, KEY_A, ABS_X, BTN_A, BTN_B from pydbus import SystemBus -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.global_config import GlobalConfig +from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping -from inputremapper.configs.global_config import global_config -from inputremapper.groups import groups from inputremapper.configs.paths import PathUtils -from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.preset import Preset -from inputremapper.injection.injector import InjectorState +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.daemon import Daemon -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.groups import groups +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput +from inputremapper.injection.injector import InjectorState +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser +from inputremapper.input_event import InputEvent +from tests.lib.cleanup import cleanup +from tests.lib.fixtures import Fixture +from tests.lib.fixtures import fixtures +from tests.lib.logger import logger +from tests.lib.pipes import push_events, uinput_write_history_pipe from tests.lib.test_setup import test_setup, is_service_running +from tests.lib.tmp import tmp @test_setup @@ -58,12 +56,15 @@ class TestDaemon(unittest.TestCase): def setUp(self): self.daemon = None + self.global_config = GlobalConfig() + self.global_uinputs = GlobalUInputs(UInput) PathUtils.mkdir(PathUtils.get_config_path()) - global_config._save_config() + self.global_config._save_config() + self.mapping_parser = MappingParser(self.global_uinputs) # the daemon should be able to create them on demand: - global_uinputs.devices = {} - global_uinputs.is_service = True + self.global_uinputs.devices = {} + self.global_uinputs.is_service = True def tearDown(self): # avoid race conditions with other tests, daemon may run processes @@ -134,7 +135,7 @@ def test_daemon(self): ) ) preset.save() - global_config.set_autoload_preset(group.key, preset_name) + self.global_config.set_autoload_preset(group.key, preset_name) """Injection 1""" @@ -144,19 +145,23 @@ def test_daemon(self): [InputEvent.key(BTN_B, 1, fixtures.gamepad.get_device_hash())], ) - self.daemon = Daemon() + self.daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) self.assertFalse(uinput_write_history_pipe[0].poll()) # has been cleanedUp in setUp - self.assertNotIn("keyboard", global_uinputs.devices) + self.assertNotIn("keyboard", self.global_uinputs.devices) logger.info(f"start injector for {group.key}") self.daemon.start_injecting(group.key, preset_name) # created on demand - self.assertIn("keyboard", global_uinputs.devices) - self.assertNotIn("gamepad", global_uinputs.devices) + self.assertIn("keyboard", self.global_uinputs.devices) + self.assertNotIn("gamepad", self.global_uinputs.devices) self.assertEqual(self.daemon.get_state(group.key), InjectorState.STARTING) self.assertEqual(self.daemon.get_state(group2.key), InjectorState.UNKNOWN) @@ -202,15 +207,19 @@ def test_daemon(self): self.assertEqual(event.value, 1) def test_config_dir(self): - global_config.set("foo", "bar") - self.assertEqual(global_config.get("foo"), "bar") + self.global_config.set("foo", "bar") + self.assertEqual(self.global_config.get("foo"), "bar") # freshly loads the config and therefore removes the previosly added key. # This is important so that if the service is started via sudo or pkexec # it knows where to look for configuration files. - self.daemon = Daemon() + self.daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) self.assertEqual(self.daemon.config_dir, PathUtils.get_config_path()) - self.assertIsNone(global_config.get("foo")) + self.assertIsNone(self.global_config.get("foo")) def test_refresh_on_start(self): if os.path.exists(PathUtils.get_config_path("xmodmap.json")): @@ -227,8 +236,8 @@ def test_refresh_on_start(self): # this test only makes sense if this device is unknown yet self.assertIsNone(group) - system_mapping.clear() - system_mapping._set("a", KEY_A) + keyboard_layout.clear() + keyboard_layout._set("a", KEY_A) preset = Preset(PathUtils.get_preset_path(group_name, preset_name)) preset.add( @@ -241,12 +250,16 @@ def test_refresh_on_start(self): # make the daemon load the file instead with open(PathUtils.get_config_path("xmodmap.json"), "w") as file: - json.dump(system_mapping._mapping, file, indent=4) - system_mapping.clear() + json.dump(keyboard_layout._mapping, file, indent=4) + keyboard_layout.clear() preset.save() - global_config.set_autoload_preset(group_key, preset_name) - self.daemon = Daemon() + self.global_config.set_autoload_preset(group_key, preset_name) + self.daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) # make sure the devices are populated groups.refresh() @@ -283,7 +296,11 @@ def test_refresh_for_unknown_key(self): # this test only makes sense if this device is unknown yet self.assertIsNone(groups.find(name=device_9876)) - self.daemon = Daemon() + self.daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) # make sure the devices are populated groups.refresh() @@ -329,7 +346,7 @@ def test_xmodmap_file(self): ) preset.save() - system_mapping.clear() + keyboard_layout.clear() push_events( fixtures.bar_device, @@ -345,8 +362,8 @@ def test_xmodmap_file(self): # an existing config file is needed otherwise set_config_dir refuses # to use the directory config_path = os.path.join(config_dir, "config.json") - global_config.path = config_path - global_config._save_config() + self.global_config.path = config_path + self.global_config._save_config() # finally, create the xmodmap file xmodmap_path = os.path.join(config_dir, "xmodmap.json") @@ -355,7 +372,11 @@ def test_xmodmap_file(self): # test setup complete - self.daemon = Daemon() + self.daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) self.daemon.set_config_dir(config_dir) self.daemon.start_injecting(group.key, preset_name) @@ -373,7 +394,11 @@ def test_start_stop(self): group = groups.find(key=group_key) preset_name = "preset8" - daemon = Daemon() + daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) self.daemon = daemon pereset = Preset(group.get_preset_path(preset_name)) @@ -441,7 +466,7 @@ def test_autoload(self): group_key = "Qux/Device?" group = groups.find(key=group_key) - daemon = Daemon() + daemon = Daemon(self.global_config, self.global_uinputs, self.mapping_parser) self.daemon = daemon preset = Preset(group.get_preset_path(preset_name)) @@ -459,7 +484,7 @@ def test_autoload(self): self.assertNotIn(group_key, daemon.autoload_history._autoload_history) self.assertTrue(daemon.autoload_history.may_autoload(group_key, preset_name)) - global_config.set_autoload_preset(group_key, preset_name) + self.global_config.set_autoload_preset(group_key, preset_name) len_before = len(self.daemon.autoload_history._autoload_history) # now autoloading is configured, so it will autoload self.daemon._autoload(group_key) @@ -498,7 +523,11 @@ def test_autoload(self): self.assertEqual(len_before, len_after) def test_autoload_2(self): - self.daemon = Daemon() + self.daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) history = self.daemon.autoload_history._autoload_history # existing device @@ -513,10 +542,10 @@ def test_autoload_2(self): ) ) preset.save() - global_config.set_autoload_preset(group.key, preset_name) + self.global_config.set_autoload_preset(group.key, preset_name) # ignored, won't cause problems: - global_config.set_autoload_preset("non-existant-key", "foo") + self.global_config.set_autoload_preset("non-existant-key", "foo") self.daemon.autoload() self.assertEqual(len(history), 1) @@ -537,9 +566,13 @@ def test_autoload_3(self): ) preset.save() - global_config.set_autoload_preset(group.key, preset_name) + self.global_config.set_autoload_preset(group.key, preset_name) - self.daemon = Daemon() + self.daemon = Daemon( + self.global_config, + self.global_uinputs, + self.mapping_parser, + ) groups.set_groups([]) # caused the bug self.assertIsNone(groups.find(key="Foo Device 2")) self.daemon.autoload() diff --git a/tests/unit/test_data_manager.py b/tests/unit/test_data_manager.py index 18c5873b3..770c7caad 100644 --- a/tests/unit/test_data_manager.py +++ b/tests/unit/test_data_manager.py @@ -25,12 +25,12 @@ from typing import List from unittest.mock import MagicMock, call -from inputremapper.configs.global_config import global_config +from inputremapper.configs.global_config import GlobalConfig from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import UIMapping, MappingData from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.exceptions import DataManagementError from inputremapper.groups import _Groups from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME @@ -43,7 +43,7 @@ CombinationUpdate, ) from inputremapper.gui.reader_client import ReaderClient -from inputremapper.injection.global_uinputs import GlobalUInputs +from inputremapper.injection.global_uinputs import GlobalUInputs, FrontendUInput from tests.lib.fixtures import prepare_presets from tests.lib.patches import FakeDaemonProxy from tests.lib.test_setup import test_setup @@ -62,15 +62,16 @@ class TestDataManager(unittest.TestCase): def setUp(self) -> None: self.message_broker = MessageBroker() self.reader = ReaderClient(self.message_broker, _Groups()) - self.uinputs = GlobalUInputs() + self.uinputs = GlobalUInputs(FrontendUInput) self.uinputs.prepare_all() + self.global_config = GlobalConfig() self.data_manager = DataManager( self.message_broker, - global_config, + self.global_config, self.reader, FakeDaemonProxy(), self.uinputs, - system_mapping, + keyboard_layout, ) def test_load_group_provides_presets(self): diff --git a/tests/unit/test_event_pipeline/test_event_pipeline.py b/tests/unit/test_event_pipeline/test_event_pipeline.py index 97271993c..bd542d9e5 100644 --- a/tests/unit/test_event_pipeline/test_event_pipeline.py +++ b/tests/unit/test_event_pipeline/test_event_pipeline.py @@ -54,11 +54,12 @@ DEFAULT_REL_RATE, ) from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.injection.context import Context from inputremapper.injection.event_reader import EventReader -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from inputremapper.input_event import InputEvent from tests.lib.cleanup import cleanup from tests.lib.logger import logger @@ -71,8 +72,11 @@ class EventPipelineTestBase(unittest.IsolatedAsyncioTestCase): """Test the event pipeline form event_reader to UInput.""" def setUp(self): - global_uinputs.is_service = True - global_uinputs.prepare_all() + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() + self.mapping_parser = MappingParser(self.global_uinputs) + self.global_uinputs.is_service = True + self.global_uinputs.prepare_all() self.forward_uinput = evdev.UInput() self.stop_event = asyncio.Event() @@ -100,6 +104,7 @@ def create_event_reader( preset, source_devices={}, forward_devices={source.get_device_hash(): self.forward_uinput}, + mapping_parser=self.mapping_parser, ) reader = EventReader( context, @@ -136,19 +141,19 @@ async def test_any_event_as_button(self): c_up = (EV_ABS, ABS_HAT0X, 0) # first change the system mapping because Mapping will validate against it - system_mapping.clear() + keyboard_layout.clear() code_w = 71 code_b = 72 code_c = 73 code_d = 74 code_a = 75 code_s = 76 - system_mapping._set("w", code_w) - system_mapping._set("d", code_d) - system_mapping._set("a", code_a) - system_mapping._set("s", code_s) - system_mapping._set("b", code_b) - system_mapping._set("c", code_c) + keyboard_layout._set("w", code_w) + keyboard_layout._set("d", code_d) + keyboard_layout._set("a", code_a) + keyboard_layout._set("s", code_s) + keyboard_layout._set("b", code_b) + keyboard_layout._set("c", code_c) preset = Preset() preset.add( @@ -213,7 +218,7 @@ async def test_any_event_as_button(self): # wait a bit for the rel_to_btn handler to send the key up await asyncio.sleep(0.1) - history = global_uinputs.get_uinput("keyboard").write_history + history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(history.count((EV_KEY, code_b, 1)), 1) self.assertEqual(history.count((EV_KEY, code_c, 1)), 1) @@ -251,10 +256,10 @@ async def test_reset_releases_keys(self): ) event_reader = self.create_event_reader(preset, fixtures.foo_device_2_keyboard) - a = system_mapping.get("a") - b = system_mapping.get("b") - c = system_mapping.get("c") - d = system_mapping.get("d") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") + c = keyboard_layout.get("c") + d = keyboard_layout.get("d") await self.send_events( [ @@ -267,7 +272,7 @@ async def test_reset_releases_keys(self): await asyncio.sleep(0.1) forwarded_history = self.forward_uinput.write_history - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(len(forwarded_history), 0) # a down, b down, c down, d down @@ -277,7 +282,7 @@ async def test_reset_releases_keys(self): await asyncio.sleep(0.1) forwarded_history = self.forward_uinput.write_history - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(len(forwarded_history), 0) # all a, b, c, d down+up @@ -292,7 +297,7 @@ async def test_forward_abs(self): """Test if EV_ABS events are forwarded when other events of the same input are not.""" preset = Preset() # BTN_A -> 77 - system_mapping._set("b", 77) + keyboard_layout._set("b", 77) preset.add( Mapping.from_combination( InputCombination([InputConfig(type=EV_KEY, code=BTN_A)]), @@ -317,7 +322,7 @@ async def test_forward_abs(self): ) history = self.forward_uinput.write_history - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(history.count((EV_ABS, ABS_X, 10)), 1) self.assertEqual(history.count((EV_ABS, ABS_Y, 20)), 1) @@ -330,7 +335,7 @@ async def test_forward_rel(self): """Test if EV_REL events are forwarded when other events of the same input are not.""" preset = Preset() # BTN_A -> 77 - system_mapping._set("b", 77) + keyboard_layout._set("b", 77) preset.add( Mapping.from_combination( InputCombination([InputConfig(type=EV_KEY, code=BTN_LEFT)]), @@ -356,7 +361,7 @@ async def test_forward_rel(self): await asyncio.sleep(0.1) history = self.forward_uinput.write_history - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(history.count((EV_REL, REL_X, 10)), 1) self.assertEqual(history.count((EV_REL, REL_Y, 20)), 1) @@ -367,9 +372,9 @@ async def test_forward_rel(self): async def test_combination(self): """Test if combinations map to keys properly.""" - a = system_mapping.get("a") - b = system_mapping.get("b") - c = system_mapping.get("c") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") + c = keyboard_layout.get("c") origin = fixtures.gamepad origin_hash = origin.get_device_hash() @@ -439,7 +444,7 @@ async def test_combination(self): event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history @@ -461,7 +466,7 @@ async def test_combination(self): event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertNotIn((EV_KEY, a, 1), keyboard_history) self.assertNotIn((EV_KEY, a, 0), keyboard_history) @@ -486,7 +491,7 @@ async def test_ignore_hold(self): output_symbol="a", ) ) - a = system_mapping.get("a") + a = keyboard_layout.get("a") event_reader = self.create_event_reader(preset, fixtures.gamepad) await self.send_events( @@ -494,7 +499,7 @@ async def test_ignore_hold(self): event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertEqual(len(keyboard_history), 2) self.assertEqual(len(forwarded_history), 0) @@ -560,9 +565,9 @@ async def test_ignore_disabled(self): ) ) - a = system_mapping.get("a") - b = system_mapping.get("b") - c = system_mapping.get("c") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") + c = keyboard_layout.get("c") event_reader = self.create_event_reader(preset, origin) @@ -576,7 +581,7 @@ async def test_ignore_disabled(self): ], event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertIn((EV_KEY, a, 1), keyboard_history) self.assertIn((EV_KEY, a, 0), keyboard_history) @@ -586,7 +591,7 @@ async def test_ignore_disabled(self): """A combination that ends in a disabled key""" # ev_5 should be forwarded and the combination triggered await self.send_events(combi_1, event_reader) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertIn((EV_KEY, b, 1), keyboard_history) self.assertEqual(len(keyboard_history), 3) @@ -596,7 +601,7 @@ async def test_ignore_disabled(self): # release what the combination maps to await self.send_events([ev_4, ev_6], event_reader) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertIn((EV_KEY, b, 0), keyboard_history) self.assertEqual(len(keyboard_history), 4) @@ -606,7 +611,7 @@ async def test_ignore_disabled(self): """A combination that starts with a disabled key""" # only the combination should get triggered await self.send_events(combi_2, event_reader) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertIn((EV_KEY, c, 1), keyboard_history) self.assertEqual(len(keyboard_history), 5) @@ -616,7 +621,7 @@ async def test_ignore_disabled(self): # release what the combination maps to await self.send_events([ev_4, ev_6], event_reader) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history for event in keyboard_history: print(event.event_tuple) @@ -634,8 +639,8 @@ async def test_combination_keycode_macro_mix(self): up_1 = (EV_ABS, ABS_HAT0X, 0) up_2 = (EV_ABS, ABS_HAT0Y, 0) - a = system_mapping.get("a") - b = system_mapping.get("b") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") preset = Preset() preset.add( @@ -655,7 +660,7 @@ async def test_combination_keycode_macro_mix(self): # macro starts await self.send_events([InputEvent.from_tuple(down_1)], event_reader) await asyncio.sleep(0.05) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertEqual(len(forwarded_history), 0) self.assertGreater(len(keyboard_history), 1) @@ -666,29 +671,29 @@ async def test_combination_keycode_macro_mix(self): # combination triggered await self.send_events([InputEvent.from_tuple(down_2)], event_reader) await asyncio.sleep(0) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertIn((EV_KEY, b, 1), keyboard_history) - len_a = len(global_uinputs.get_uinput("keyboard").write_history) + len_a = len(self.global_uinputs.get_uinput("keyboard").write_history) await asyncio.sleep(0.05) - len_b = len(global_uinputs.get_uinput("keyboard").write_history) + len_b = len(self.global_uinputs.get_uinput("keyboard").write_history) # still running self.assertGreater(len_b, len_a) # release await self.send_events([InputEvent.from_tuple(up_1)], event_reader) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history[-1], (EV_KEY, b, 0)) await asyncio.sleep(0.05) - len_c = len(global_uinputs.get_uinput("keyboard").write_history) + len_c = len(self.global_uinputs.get_uinput("keyboard").write_history) await asyncio.sleep(0.05) - len_d = len(global_uinputs.get_uinput("keyboard").write_history) + len_d = len(self.global_uinputs.get_uinput("keyboard").write_history) # not running anymore self.assertEqual(len_c, len_d) await self.send_events([InputEvent.from_tuple(up_2)], event_reader) await asyncio.sleep(0.05) - len_e = len(global_uinputs.get_uinput("keyboard").write_history) + len_e = len(self.global_uinputs.get_uinput("keyboard").write_history) self.assertEqual(len_e, len_d) async def test_wheel_combination_release_failure(self): @@ -717,8 +722,8 @@ async def test_wheel_combination_release_failure(self): InputCombination.from_tuples((1, 276, 1), (2, 8, -1)) ) - system_mapping.clear() - system_mapping._set("a", 30) + keyboard_layout.clear() + keyboard_layout._set("a", 30) a = 30 m = Mapping.from_combination(combination, output_symbol="a") @@ -735,18 +740,18 @@ async def test_wheel_combination_release_failure(self): await self.send_events([scroll], event_reader) # "maps to 30" - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history[0], (EV_KEY, a, 1)) await self.send_events([scroll] * 5, event_reader) # nothing new since all of them were duplicate key downs - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(len(keyboard_history), 1) await self.send_events([btn_up], event_reader) # releasing the combination - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history[1], (EV_KEY, a, 0)) # more scroll events @@ -800,7 +805,7 @@ async def test_can_not_map(self): event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertEqual(len(forwarded_history), 4) @@ -821,7 +826,7 @@ async def test_axis_switch(self): rel_rate = 60 # rate [Hz] at which events are produced gain = 0.5 # halve the speed of the rel axis preset = Preset() - mouse = global_uinputs.get_uinput("mouse") + mouse = self.global_uinputs.get_uinput("mouse") forward_history = self.forward_uinput.write_history mouse_history = mouse.write_history @@ -976,7 +981,7 @@ async def test_abs_to_abs(self): await asyncio.sleep(0.2) - history = global_uinputs.get_uinput("gamepad").write_history + history = self.global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [ @@ -1026,7 +1031,7 @@ async def test_abs_to_abs_with_input_switch(self): await asyncio.sleep(0.2) - history = global_uinputs.get_uinput("gamepad").write_history + history = self.global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [ @@ -1083,7 +1088,7 @@ def next_usec_time(): await asyncio.sleep(0.1) - history = global_uinputs.get_uinput("gamepad").write_history + history = self.global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [ @@ -1102,7 +1107,7 @@ def next_usec_time(): event_reader, ) await asyncio.sleep(0.7) - history = global_uinputs.get_uinput("gamepad").write_history + history = self.global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [ @@ -1163,7 +1168,7 @@ async def test_rel_to_abs_with_input_switch(self): await asyncio.sleep(0.2) - history = global_uinputs.get_uinput("gamepad").write_history + history = self.global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [ @@ -1229,7 +1234,7 @@ async def test_abs_to_rel(self): event_reader, ) - mouse_history = global_uinputs.get_uinput("mouse").write_history + mouse_history = self.global_uinputs.get_uinput("mouse").write_history if mouse_history[0].type == EV_ABS: raise AssertionError( @@ -1303,7 +1308,7 @@ async def test_abs_to_wheel_hi_res_quirk(self): ], event_reader, ) - m_history = global_uinputs.get_uinput("mouse").write_history + m_history = self.global_uinputs.get_uinput("mouse").write_history rel_wheel = sum([event.value for event in m_history if event.code == REL_WHEEL]) rel_wheel_hi_res = sum( @@ -1332,11 +1337,11 @@ async def test_rel_to_btn(self): # should be forwarded and present in the capabilities hw_left = (EV_REL, REL_HWHEEL, -1) - system_mapping.clear() + keyboard_layout.clear() code_b = 91 code_c = 92 - system_mapping._set("b", code_b) - system_mapping._set("c", code_c) + keyboard_layout._set("b", code_b) + keyboard_layout._set("c", code_c) # set a high release timeout to make sure the tests pass release_timeout = 0.2 @@ -1370,7 +1375,7 @@ async def test_rel_to_btn(self): # wait more than the release_timeout to make sure all handlers finish await asyncio.sleep(release_timeout * 1.2) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertEqual(keyboard_history.count((EV_KEY, code_b, 1)), 1) self.assertEqual(keyboard_history.count((EV_KEY, code_c, 1)), 1) @@ -1407,8 +1412,8 @@ async def test_rel_trigger_threshold(self): preset.add(mapping_1) preset.add(mapping_2) - a = system_mapping.get("a") - b = system_mapping.get("b") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") event_reader = self.create_event_reader(preset, fixtures.foo_device_2_mouse) @@ -1422,7 +1427,7 @@ async def test_rel_trigger_threshold(self): event_reader, ) await asyncio.sleep(release_timeout * 1.5) # release a - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history, [(EV_KEY, a, 1), (EV_KEY, a, 0)]) self.assertEqual(keyboard_history.count((EV_KEY, a, 1)), 1) @@ -1437,14 +1442,14 @@ async def test_rel_trigger_threshold(self): ], event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history.count((EV_KEY, a, 1)), 2) self.assertEqual(keyboard_history.count((EV_KEY, b, 1)), 1) self.assertEqual(keyboard_history.count((EV_KEY, b, 0)), 1) self.assertEqual(keyboard_history.count((EV_KEY, a, 0)), 1) await asyncio.sleep(release_timeout * 1.5) # release a - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertEqual(keyboard_history.count((EV_KEY, a, 0)), 2) self.assertEqual( @@ -1476,8 +1481,8 @@ async def test_abs_trigger_threshold(self): preset.add(mapping_1) preset.add(mapping_2) - a = system_mapping.get("a") - b = system_mapping.get("b") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") event_reader = self.create_event_reader(preset, fixtures.gamepad) @@ -1494,7 +1499,7 @@ async def test_abs_trigger_threshold(self): ], event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history.count((EV_KEY, a, 1)), 1) self.assertNotIn((EV_KEY, a, 0), keyboard_history) @@ -1508,7 +1513,7 @@ async def test_abs_trigger_threshold(self): ], event_reader, ) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history self.assertEqual(keyboard_history.count((EV_KEY, a, 1)), 1) self.assertEqual(keyboard_history.count((EV_KEY, b, 1)), 1) self.assertEqual(keyboard_history.count((EV_KEY, b, 0)), 1) @@ -1516,7 +1521,7 @@ async def test_abs_trigger_threshold(self): # 0% release a await event_reader.handle(InputEvent.abs(ABS_X, 0)) - keyboard_history = global_uinputs.get_uinput("keyboard").write_history + keyboard_history = self.global_uinputs.get_uinput("keyboard").write_history forwarded_history = self.forward_uinput.write_history self.assertEqual(keyboard_history.count((EV_KEY, a, 0)), 1) self.assertEqual(len(forwarded_history), 0) @@ -1545,7 +1550,7 @@ async def _test(self, input_code, input_value, output_code, output_value, gain=1 event_reader, ) - history = global_uinputs.get_uinput("mouse").write_history + history = self.global_uinputs.get_uinput("mouse").write_history self.assertEqual(len(history), 1) self.assertEqual( @@ -1602,7 +1607,7 @@ async def test_x_to_hwheel(self): event_reader, ) - history = global_uinputs.get_uinput("mouse").write_history + history = self.global_uinputs.get_uinput("mouse").write_history # injects both REL_WHEEL and REL_WHEEL_HI_RES events self.assertEqual(len(history), 2) self.assertEqual( @@ -1629,7 +1634,7 @@ async def test_x_to_hwheel(self): async def test_remainder(self): preset = Preset() - history = global_uinputs.get_uinput("mouse").write_history + history = self.global_uinputs.get_uinput("mouse").write_history # REL_WHEEL_HI_RES to REL_Y input_config = InputConfig(type=EV_REL, code=REL_WHEEL_HI_RES) diff --git a/tests/unit/test_event_pipeline/test_mapping_handlers.py b/tests/unit/test_event_pipeline/test_mapping_handlers.py index 1b3262740..361d5d335 100644 --- a/tests/unit/test_event_pipeline/test_mapping_handlers.py +++ b/tests/unit/test_event_pipeline/test_mapping_handlers.py @@ -48,7 +48,7 @@ from inputremapper.configs.mapping import Mapping, DEFAULT_REL_RATE from inputremapper.configs.input_config import InputCombination, InputConfig -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput from inputremapper.injection.mapping_handlers.abs_to_abs_handler import AbsToAbsHandler from inputremapper.injection.mapping_handlers.abs_to_btn_handler import AbsToBtnHandler from inputremapper.injection.mapping_handlers.abs_to_rel_handler import AbsToRelHandler @@ -99,6 +99,8 @@ def setUp(self): InputConfig(type=1, code=3), ) ) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = AxisSwitchHandler( input_combination, Mapping( @@ -108,6 +110,7 @@ def setUp(self): output_code=1, ), MagicMock(), + self.global_uinputs, ) @@ -117,6 +120,8 @@ def setUp(self): input_combination = InputCombination( [InputConfig(type=3, code=5, analog_threshold=10)] ) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = AbsToBtnHandler( input_combination, Mapping( @@ -124,6 +129,7 @@ def setUp(self): target_uinput="mouse", output_symbol="BTN_LEFT", ), + global_uinputs=self.global_uinputs, ) @@ -131,6 +137,8 @@ def setUp(self): class TestAbsToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination([InputConfig(type=EV_ABS, code=ABS_X)]) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = AbsToAbsHandler( input_combination, Mapping( @@ -139,6 +147,7 @@ def setUp(self): output_type=EV_ABS, output_code=ABS_X, ), + global_uinputs=self.global_uinputs, ) async def test_reset(self): @@ -147,7 +156,7 @@ async def test_reset(self): source=InputDevice("/dev/input/event15"), ) self.handler.reset() - history = global_uinputs.get_uinput("gamepad").write_history + history = self.global_uinputs.get_uinput("gamepad").write_history self.assertEqual( history, [InputEvent.from_tuple((3, 0, MAX_ABS)), InputEvent.from_tuple((3, 0, 0))], @@ -158,6 +167,8 @@ async def test_reset(self): class TestRelToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination([InputConfig(type=EV_REL, code=REL_X)]) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = RelToAbsHandler( input_combination, Mapping( @@ -166,6 +177,7 @@ def setUp(self): output_type=EV_ABS, output_code=ABS_X, ), + self.global_uinputs, ) async def test_reset(self): @@ -174,7 +186,7 @@ async def test_reset(self): source=InputDevice("/dev/input/event15"), ) self.handler.reset() - history = global_uinputs.get_uinput("gamepad").write_history + history = self.global_uinputs.get_uinput("gamepad").write_history self.assertEqual(len(history), 2) # something large, doesn't matter @@ -222,6 +234,8 @@ async def test_rate_stays(self): class TestAbsToRelHandler(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination([InputConfig(type=EV_ABS, code=ABS_X)]) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = AbsToRelHandler( input_combination, Mapping( @@ -230,6 +244,7 @@ def setUp(self): output_type=EV_REL, output_code=REL_X, ), + self.global_uinputs, ) async def test_reset(self): @@ -241,10 +256,10 @@ async def test_reset(self): self.handler.reset() await asyncio.sleep(0.05) - count = global_uinputs.get_uinput("mouse").write_count + count = self.global_uinputs.get_uinput("mouse").write_count self.assertGreater(count, 6) # count should be 60*0.2 = 12 await asyncio.sleep(0.2) - self.assertEqual(count, global_uinputs.get_uinput("mouse").write_count) + self.assertEqual(count, self.global_uinputs.get_uinput("mouse").write_count) @test_setup @@ -286,6 +301,8 @@ def setUp(self): self.context_mock = MagicMock() + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = CombinationHandler( input_combination, Mapping( @@ -294,6 +311,7 @@ def setUp(self): output_symbol="BTN_LEFT", ), self.context_mock, + global_uinputs=self.global_uinputs, ) def test_forward_correctly(self): @@ -394,9 +412,12 @@ def setUp(self): self.mock1 = MagicMock() self.mock2 = MagicMock() self.mock3 = MagicMock() + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = HierarchyHandler( [self.mock1, self.mock2, self.mock3], InputConfig(type=EV_KEY, code=KEY_A), + self.global_uinputs, ) def test_reset(self): @@ -415,6 +436,8 @@ def setUp(self): InputConfig(type=1, code=3), ) ) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = KeyHandler( input_combination, Mapping( @@ -422,6 +445,7 @@ def setUp(self): target_uinput="mouse", output_symbol="BTN_LEFT", ), + self.global_uinputs, ) def test_reset(self): @@ -429,12 +453,12 @@ def test_reset(self): InputEvent(0, 0, EV_REL, REL_X, 1, actions=(EventActions.as_key,)), source=InputDevice("/dev/input/event11"), ) - history = global_uinputs.get_uinput("mouse").write_history + history = self.global_uinputs.get_uinput("mouse").write_history self.assertEqual(history[0], InputEvent.key(BTN_LEFT, 1)) self.assertEqual(len(history), 1) self.handler.reset() - history = global_uinputs.get_uinput("mouse").write_history + history = self.global_uinputs.get_uinput("mouse").write_history self.assertEqual(history[1], InputEvent.key(BTN_LEFT, 0)) self.assertEqual(len(history), 2) @@ -449,6 +473,8 @@ def setUp(self): ) ) self.context_mock = MagicMock() + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = MacroHandler( input_combination, Mapping( @@ -457,6 +483,7 @@ def setUp(self): output_symbol="hold_keys(BTN_LEFT, BTN_RIGHT)", ), context=self.context_mock, + global_uinputs=self.global_uinputs, ) async def test_reset(self): @@ -466,14 +493,14 @@ async def test_reset(self): ) await asyncio.sleep(0.1) - history = global_uinputs.get_uinput("mouse").write_history + history = self.global_uinputs.get_uinput("mouse").write_history self.assertIn(InputEvent.key(BTN_LEFT, 1), history) self.assertIn(InputEvent.key(BTN_RIGHT, 1), history) self.assertEqual(len(history), 2) self.handler.reset() await asyncio.sleep(0.1) - history = global_uinputs.get_uinput("mouse").write_history + history = self.global_uinputs.get_uinput("mouse").write_history self.assertIn(InputEvent.key(BTN_LEFT, 0), history[-2:]) self.assertIn(InputEvent.key(BTN_RIGHT, 0), history[-2:]) self.assertEqual(len(history), 4) @@ -485,6 +512,8 @@ def setUp(self): input_combination = InputCombination( [InputConfig(type=2, code=0, analog_threshold=10)] ) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = RelToBtnHandler( input_combination, Mapping( @@ -492,6 +521,7 @@ def setUp(self): target_uinput="mouse", output_symbol="BTN_LEFT", ), + self.global_uinputs, ) @@ -501,6 +531,8 @@ class TestRelToRelHanlder(BaseTests, unittest.IsolatedAsyncioTestCase): def setUp(self): input_combination = InputCombination([InputConfig(type=EV_REL, code=REL_X)]) + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() self.handler = RelToRelHandler( input_combination, Mapping( @@ -510,6 +542,7 @@ def setUp(self): output_value=20, target_uinput="mouse", ), + self.global_uinputs, ) def test_should_map(self): diff --git a/tests/unit/test_event_reader.py b/tests/unit/test_event_reader.py index c3e39a3ff..cbbae18d9 100644 --- a/tests/unit/test_event_reader.py +++ b/tests/unit/test_event_reader.py @@ -39,10 +39,11 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.injection.context import Context from inputremapper.injection.event_reader import EventReader -from inputremapper.injection.global_uinputs import global_uinputs +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from inputremapper.input_event import InputEvent from inputremapper.utils import get_device_hash from tests.lib.fixtures import fixtures @@ -53,15 +54,17 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase): def setUp(self): self.gamepad_source = evdev.InputDevice(fixtures.gamepad.path) + self.global_uinputs = GlobalUInputs(UInput) + self.mapping_parser = MappingParser(self.global_uinputs) self.stop_event = asyncio.Event() self.preset = Preset() - global_uinputs.is_service = True - global_uinputs.prepare_all() + self.global_uinputs.is_service = True + self.global_uinputs.prepare_all() async def setup(self, source, mapping): """Set a EventReader up for the test and run it in the background.""" - context = Context(mapping, {}, {}) + context = Context(mapping, {}, {}, self.mapping_parser) context.uinput = evdev.UInput() event_reader = EventReader(context, source, self.stop_event) asyncio.ensure_future(event_reader.run()) @@ -72,8 +75,8 @@ async def test_if_single_joystick_then(self): # TODO: Move this somewhere more sensible # Integration test style for if_single. # won't care about the event, because the purpose is not set to BUTTON - code_a = system_mapping.get("a") - code_shift = system_mapping.get("KEY_LEFTSHIFT") + code_a = keyboard_layout.get("a") + code_shift = keyboard_layout.get("KEY_LEFTSHIFT") trigger = evdev.ecodes.BTN_A self.preset.add( @@ -174,7 +177,7 @@ async def test_if_single_joystick_then(self): await asyncio.sleep(0.1) self.stop_event.set() # stop the reader - history = global_uinputs.get_uinput("keyboard").write_history + history = self.global_uinputs.get_uinput("keyboard").write_history self.assertIn((EV_KEY, code_a, 1), history) self.assertIn((EV_KEY, code_a, 0), history) self.assertNotIn((EV_KEY, code_shift, 1), history) @@ -186,7 +189,7 @@ async def test_if_single_joystick_then(self): async def test_if_single_joystick_under_threshold(self): """Triggers then because the joystick events value is too low.""" # TODO: Move this somewhere more sensible - code_a = system_mapping.get("a") + code_a = keyboard_layout.get("a") trigger = evdev.ecodes.BTN_A self.preset.add( Mapping.from_combination( @@ -233,7 +236,7 @@ async def test_if_single_joystick_under_threshold(self): ) await asyncio.sleep(0.1) self.assertEqual(len(context.listeners), 0) - history = global_uinputs.get_uinput("keyboard").write_history + history = self.global_uinputs.get_uinput("keyboard").write_history # the key that triggered if_single should be injected after # if_single had a chance to inject keys (if the macro is fast enough), diff --git a/tests/unit/test_global_uinputs.py b/tests/unit/test_global_uinputs.py index 458b8c1a7..6e5f94655 100644 --- a/tests/unit/test_global_uinputs.py +++ b/tests/unit/test_global_uinputs.py @@ -30,9 +30,9 @@ from inputremapper.exceptions import EventNotHandled, UinputNotAvailable from inputremapper.injection.global_uinputs import ( - global_uinputs, FrontendUInput, GlobalUInputs, + UInput, ) from inputremapper.input_event import InputEvent from tests.lib.cleanup import cleanup @@ -58,11 +58,12 @@ def test_init(self): @test_setup -class TestGlobalUinputs(unittest.TestCase): +class TestGlobalUInputs(unittest.TestCase): def setUp(self) -> None: cleanup() def test_iter(self): + global_uinputs = GlobalUInputs(FrontendUInput) for uinput in global_uinputs: self.assertIsInstance(uinput, evdev.UInput) @@ -71,6 +72,9 @@ def test_write(self): implicitly tests get_uinput and UInput.can_emit """ + global_uinputs = GlobalUInputs(UInput) + global_uinputs.prepare_all() + ev_1 = InputEvent.key(KEY_A, 1) ev_2 = InputEvent.abs(ABS_X, 10) @@ -86,9 +90,13 @@ def test_write(self): global_uinputs.write(ev_1.event_tuple, "foo") def test_creates_frontend_uinputs(self): - frontend_uinputs = GlobalUInputs() - with patch.object(sys, "argv", ["foo"]): - frontend_uinputs.prepare_all() - + frontend_uinputs = GlobalUInputs(FrontendUInput) + frontend_uinputs.prepare_all() uinput = frontend_uinputs.get_uinput("keyboard") self.assertIsInstance(uinput, FrontendUInput) + + def test_creates_backend_service_uinputs(self): + frontend_uinputs = GlobalUInputs(UInput) + frontend_uinputs.prepare_all() + uinput = frontend_uinputs.get_uinput("keyboard") + self.assertIsInstance(uinput, UInput) diff --git a/tests/unit/test_injector.py b/tests/unit/test_injector.py index 9636b375c..7fc2fa921 100644 --- a/tests/unit/test_injector.py +++ b/tests/unit/test_injector.py @@ -17,6 +17,8 @@ # # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser try: from pydantic.v1 import ValidationError @@ -43,8 +45,8 @@ from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.configs.mapping import Mapping from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import ( - system_mapping, +from inputremapper.configs.keyboard_layout import ( + keyboard_layout, DISABLE_CODE, DISABLE_NAME, ) @@ -87,6 +89,9 @@ def setUpClass(cls): def setUp(self): self.failed = 0 self.make_it_fail = 2 + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() + self.mapping_parser = MappingParser(self.global_uinputs) def grab_fail_twice(_): if self.failed < self.make_it_fail: @@ -107,7 +112,7 @@ def tearDown(self): evdev.InputDevice.grab = self.grab def initialize_injector(self, group, preset: Preset): - self.injector = Injector(group, preset) + self.injector = Injector(group, preset, self.mapping_parser) self.injector._devices = self.injector.group.get_devices() self.injector._update_preset() @@ -123,10 +128,14 @@ def test_grab(self): ) ) - self.injector = Injector(groups.find(key="Foo Device 2"), preset) + self.injector = Injector( + groups.find(key="Foo Device 2"), + preset, + self.mapping_parser, + ) # this test needs to pass around all other constraints of # _grab_device - self.injector.context = Context(preset, {}, {}) + self.injector.context = Context(preset, {}, {}, self.mapping_parser) device = self.injector._grab_device(evdev.InputDevice(path)) gamepad = classify(device) == DeviceType.GAMEPAD self.assertFalse(gamepad) @@ -145,9 +154,13 @@ def test_fail_grab(self): ) ) - self.injector = Injector(groups.find(key="Foo Device 2"), preset) + self.injector = Injector( + groups.find(key="Foo Device 2"), + preset, + self.mapping_parser, + ) path = "/dev/input/event10" - self.injector.context = Context(preset, {}, {}) + self.injector.context = Context(preset, {}, {}, self.mapping_parser) device = self.injector._grab_device(evdev.InputDevice(path)) self.assertIsNone(device) self.assertGreaterEqual(self.failed, 1) @@ -182,7 +195,7 @@ def test_grab_device_1(self): ), ) self.initialize_injector(groups.find(name="gamepad"), preset) - self.injector.context = Context(preset, {}, {}) + self.injector.context = Context(preset, {}, {}, self.mapping_parser) self.injector.group.paths = [ "/dev/input/event10", "/dev/input/event30", @@ -209,7 +222,7 @@ def test_forward_gamepad_events(self): ) self.initialize_injector(groups.find(name="gamepad"), preset) - self.injector.context = Context(preset, {}, {}) + self.injector.context = Context(preset, {}, {}, self.mapping_parser) path = "/dev/input/event30" devices = self.injector._grab_devices() @@ -229,7 +242,7 @@ def test_skip_unused_device(self): ) ) self.initialize_injector(groups.find(key="Foo Device 2"), preset) - self.injector.context = Context(preset, {}, {}) + self.injector.context = Context(preset, {}, {}, self.mapping_parser) # grabs only one device even though the group has 4 devices devices = self.injector._grab_devices() @@ -248,7 +261,7 @@ def test_skip_unknown_device(self): # skips a device because its capabilities are not used in the preset self.initialize_injector(groups.find(key="Foo Device 2"), preset) - self.injector.context = Context(preset, {}, {}) + self.injector.context = Context(preset, {}, {}, self.mapping_parser) devices = self.injector._grab_devices() # skips the device alltogether, so no grab attempts fail @@ -256,7 +269,11 @@ def test_skip_unknown_device(self): self.assertEqual(devices, {}) def test_get_udev_name(self): - self.injector = Injector(groups.find(key="Foo Device 2"), Preset()) + self.injector = Injector( + groups.find(key="Foo Device 2"), + Preset(), + self.mapping_parser, + ) suffix = "mapped" prefix = "input-remapper" expected = f'{prefix} {"a" * (80 - len(suffix) - len(prefix) - 2)} {suffix}' @@ -301,7 +318,11 @@ def test_capabilities_and_uinput_presence(self, ungrab_patch): ) preset.add(m1) preset.add(m2) - self.injector = Injector(groups.find(key="Foo Device 2"), preset) + self.injector = Injector( + groups.find(key="Foo Device 2"), + preset, + self.mapping_parser, + ) self.injector.stop_injecting() self.injector.run() @@ -353,13 +374,13 @@ def test_injector(self): numlock_before = is_numlock_on() # stuff the preset outputs - system_mapping.clear() + keyboard_layout.clear() code_a = 100 code_q = 101 code_w = 102 - system_mapping._set("a", code_a) - system_mapping._set("key_q", code_q) - system_mapping._set("w", code_w) + keyboard_layout._set("a", code_a) + keyboard_layout._set("key_q", code_q) + keyboard_layout._set("w", code_w) preset = Preset() preset.add( @@ -398,7 +419,7 @@ def test_injector(self): "a", ) ) - # one mapping that is unknown in the system_mapping on purpose + # one mapping that is unknown in the keyboard_layout on purpose input_b = 10 with self.assertRaises(ValidationError): preset.add( @@ -417,7 +438,11 @@ def test_injector(self): ) ) - self.injector = Injector(groups.find(key="Foo Device 2"), preset) + self.injector = Injector( + groups.find(key="Foo Device 2"), + preset, + self.mapping_parser, + ) self.assertEqual(self.injector.get_state(), InjectorState.UNKNOWN) self.injector.start() self.assertEqual(self.injector.get_state(), InjectorState.STARTING) @@ -544,6 +569,10 @@ def test_is_in_capabilities(self): @test_setup class TestModifyCapabilities(unittest.TestCase): def setUp(self): + self.global_uinputs = GlobalUInputs(UInput) + self.global_uinputs.prepare_all() + self.mapping_parser = MappingParser(self.global_uinputs) + class FakeDevice: def __init__(self): self._capabilities = { @@ -619,11 +648,11 @@ def capabilities(self, absinfo=False): ), ) - self.a = system_mapping.get("a") - self.shift_l = system_mapping.get("ShIfT_L") - self.one = system_mapping.get(1) - self.two = system_mapping.get("2") - self.left = system_mapping.get("BtN_lEfT") + self.a = keyboard_layout.get("a") + self.shift_l = keyboard_layout.get("ShIfT_L") + self.one = keyboard_layout.get(1) + self.two = keyboard_layout.get("2") + self.left = keyboard_layout.get("BtN_lEfT") self.fake_device = FakeDevice() self.preset = preset self.macro = macro @@ -641,7 +670,7 @@ def check_keys(self, capabilities): def test_copy_capabilities(self): # I don't know what ABS_VOLUME is, for now I would like to just always # remove it until somebody complains, since its presence broke stuff - self.injector = Injector(mock.Mock(), self.preset) + self.injector = Injector(mock.Mock(), self.preset, self.mapping_parser) self.fake_device._capabilities = { EV_ABS: [ABS_VOLUME, (ABS_X, evdev.AbsInfo(0, 0, 500, 0, 0, 0))], EV_KEY: [1, 2, 3], diff --git a/tests/unit/test_macros.py b/tests/unit/test_macros.py index d05b2b78d..6f0b5ce44 100644 --- a/tests/unit/test_macros.py +++ b/tests/unit/test_macros.py @@ -40,18 +40,16 @@ ) from inputremapper.configs.preset import Preset -from inputremapper.configs.system_mapping import system_mapping +from inputremapper.configs.keyboard_layout import keyboard_layout from inputremapper.configs.validation_errors import ( MacroParsingError, SymbolNotAvailableInTargetError, ) from inputremapper.injection.context import Context +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput from inputremapper.injection.macros.macro import ( Macro, - _type_check, macro_variables, - _type_check_variablename, - _resolve, Variable, ) from inputremapper.injection.macros.parse import ( @@ -67,14 +65,21 @@ get_macro_argument_names, get_num_parameters, ) +from inputremapper.injection.mapping_handlers.mapping_parser import MappingParser from inputremapper.input_event import InputEvent from tests.lib.logger import logger from tests.lib.test_setup import test_setup class MacroTestBase(unittest.IsolatedAsyncioTestCase): + @classmethod + def setUpClass(cls): + macro_variables.start() + def setUp(self): self.result = [] + self.global_uinputs = GlobalUInputs(UInput) + self.mapping_parser = MappingParser(self.global_uinputs) try: self.loop = asyncio.get_event_loop() @@ -84,7 +89,12 @@ def setUp(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) - self.context = Context(Preset(), source_devices={}, forward_devices={}) + self.context = Context( + Preset(), + source_devices={}, + forward_devices={}, + mapping_parser=self.mapping_parser, + ) def tearDown(self): self.result = [] @@ -210,80 +220,110 @@ async def test_count_brackets(self): self.assertEqual(_count_brackets("a(b(c))d()"), 7) def test_resolve(self): - self.assertEqual(_resolve("a"), "a") - self.assertEqual(_resolve(1), 1) - self.assertEqual(_resolve(None), None) + self.assertEqual(Macro._resolve("a"), "a") + self.assertEqual(Macro._resolve(1), 1) + self.assertEqual(Macro._resolve(None), None) # $ is part of a custom string here - self.assertEqual(_resolve('"$a"'), '"$a"') - self.assertEqual(_resolve("'$a'"), "'$a'") + self.assertEqual(Macro._resolve('"$a"'), '"$a"') + self.assertEqual(Macro._resolve("'$a'"), "'$a'") # variables are expected to be of the Variable type here, not a $string - self.assertEqual(_resolve("$a"), "$a") + self.assertEqual(Macro._resolve("$a"), "$a") variable = Variable("a") - self.assertEqual(_resolve(variable), None) + self.assertEqual(Macro._resolve(variable), None) macro_variables["a"] = 1 - self.assertEqual(_resolve(variable), 1) + self.assertEqual(Macro._resolve(variable), 1) def test_type_check(self): # allows params that can be cast to the target type - self.assertEqual(_type_check(1, [str, None], "foo", 0), "1") - self.assertEqual(_type_check("1", [int, None], "foo", 1), 1) - self.assertEqual(_type_check(1.2, [str], "foo", 2), "1.2") + self.assertEqual(Macro._type_check(1, [str, None], "foo", 0), "1") + self.assertEqual(Macro._type_check("1", [int, None], "foo", 1), 1) + self.assertEqual(Macro._type_check(1.2, [str], "foo", 2), "1.2") self.assertRaises( MacroParsingError, - lambda: _type_check("1.2", [int], "foo", 3), + lambda: Macro._type_check("1.2", [int], "foo", 3), + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check("a", [None], "foo", 0) + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check("a", [int], "foo", 1) ) - self.assertRaises(MacroParsingError, lambda: _type_check("a", [None], "foo", 0)) - self.assertRaises(MacroParsingError, lambda: _type_check("a", [int], "foo", 1)) self.assertRaises( MacroParsingError, - lambda: _type_check("a", [int, float], "foo", 2), + lambda: Macro._type_check("a", [int, float], "foo", 2), ) self.assertRaises( MacroParsingError, - lambda: _type_check("a", [int, None], "foo", 3), + lambda: Macro._type_check("a", [int, None], "foo", 3), ) - self.assertEqual(_type_check("a", [int, float, None, str], "foo", 4), "a") + self.assertEqual(Macro._type_check("a", [int, float, None, str], "foo", 4), "a") # variables are expected to be of the Variable type here, not a $string - self.assertRaises(MacroParsingError, lambda: _type_check("$a", [int], "foo", 4)) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check("$a", [int], "foo", 4) + ) variable = Variable("a") - self.assertEqual(_type_check(variable, [int], "foo", 4), variable) + self.assertEqual(Macro._type_check(variable, [int], "foo", 4), variable) self.assertRaises( MacroParsingError, - lambda: _type_check("a", [Macro], "foo", 0), + lambda: Macro._type_check("a", [Macro], "foo", 0), ) - self.assertRaises(MacroParsingError, lambda: _type_check(1, [Macro], "foo", 0)) - self.assertEqual(_type_check("1", [Macro, int], "foo", 4), 1) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check(1, [Macro], "foo", 0) + ) + self.assertEqual(Macro._type_check("1", [Macro, int], "foo", 4), 1) def test_type_check_variablename(self): - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("1a")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("$a")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("a()")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("1")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("+")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("-")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("*")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("a,b")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("a,b")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename("#")) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename(1)) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename(None)) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename([])) - self.assertRaises(MacroParsingError, lambda: _type_check_variablename(())) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("1a") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("$a") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("a()") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("1") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("+") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("-") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("*") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("a,b") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("a,b") + ) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename("#") + ) + self.assertRaises(MacroParsingError, lambda: Macro._type_check_variablename(1)) + self.assertRaises( + MacroParsingError, lambda: Macro._type_check_variablename(None) + ) + self.assertRaises(MacroParsingError, lambda: Macro._type_check_variablename([])) + self.assertRaises(MacroParsingError, lambda: Macro._type_check_variablename(())) # doesn't raise - _type_check_variablename("a") - _type_check_variablename("_a") - _type_check_variablename("_A") - _type_check_variablename("A") - _type_check_variablename("Abcd") - _type_check_variablename("Abcd_") - _type_check_variablename("Abcd_1234") - _type_check_variablename("Abcd1234_") + Macro._type_check_variablename("a") + Macro._type_check_variablename("_a") + Macro._type_check_variablename("_A") + Macro._type_check_variablename("A") + Macro._type_check_variablename("Abcd") + Macro._type_check_variablename("Abcd_") + Macro._type_check_variablename("Abcd_1234") + Macro._type_check_variablename("Abcd1234_") def test_split_keyword_arg(self): self.assertTupleEqual(_split_keyword_arg("_A=b"), ("_A", "b")) @@ -346,19 +386,19 @@ async def test_run_plus_syntax(self): self.assertTrue(macro.is_holding()) # starting from the left, presses each one down - self.assertEqual(self.result[0], (EV_KEY, system_mapping.get("a"), 1)) - self.assertEqual(self.result[1], (EV_KEY, system_mapping.get("b"), 1)) - self.assertEqual(self.result[2], (EV_KEY, system_mapping.get("c"), 1)) - self.assertEqual(self.result[3], (EV_KEY, system_mapping.get("d"), 1)) + self.assertEqual(self.result[0], (EV_KEY, keyboard_layout.get("a"), 1)) + self.assertEqual(self.result[1], (EV_KEY, keyboard_layout.get("b"), 1)) + self.assertEqual(self.result[2], (EV_KEY, keyboard_layout.get("c"), 1)) + self.assertEqual(self.result[3], (EV_KEY, keyboard_layout.get("d"), 1)) # and then releases starting with the previously pressed key macro.release_trigger() await asyncio.sleep(0.2) self.assertFalse(macro.is_holding()) - self.assertEqual(self.result[4], (EV_KEY, system_mapping.get("d"), 0)) - self.assertEqual(self.result[5], (EV_KEY, system_mapping.get("c"), 0)) - self.assertEqual(self.result[6], (EV_KEY, system_mapping.get("b"), 0)) - self.assertEqual(self.result[7], (EV_KEY, system_mapping.get("a"), 0)) + self.assertEqual(self.result[4], (EV_KEY, keyboard_layout.get("d"), 0)) + self.assertEqual(self.result[5], (EV_KEY, keyboard_layout.get("c"), 0)) + self.assertEqual(self.result[6], (EV_KEY, keyboard_layout.get("b"), 0)) + self.assertEqual(self.result[7], (EV_KEY, keyboard_layout.get("a"), 0)) async def test_extract_params(self): # splits strings, doesn't try to understand their meaning yet @@ -438,7 +478,7 @@ async def test_parse_params(self): async def test_0(self): macro = parse("key(1)", self.context, DummyMapping, True) - one_code = system_mapping.get("1") + one_code = keyboard_layout.get("1") await macro.run(self.handler) self.assertListEqual( @@ -454,12 +494,12 @@ async def test_1(self): self.assertListEqual( self.result, [ - (EV_KEY, system_mapping.get("1"), 1), - (EV_KEY, system_mapping.get("1"), 0), - (EV_KEY, system_mapping.get("a"), 1), - (EV_KEY, system_mapping.get("a"), 0), - (EV_KEY, system_mapping.get("3"), 1), - (EV_KEY, system_mapping.get("3"), 0), + (EV_KEY, keyboard_layout.get("1"), 1), + (EV_KEY, keyboard_layout.get("1"), 0), + (EV_KEY, keyboard_layout.get("a"), 1), + (EV_KEY, keyboard_layout.get("a"), 0), + (EV_KEY, keyboard_layout.get("3"), 1), + (EV_KEY, keyboard_layout.get("3"), 0), ], ) self.assertEqual(len(macro.child_macros), 0) @@ -560,8 +600,8 @@ async def test_raises_error(self): ) async def test_key(self): - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") macro = parse("set(foo, b).key($foo).key(a)", self.context, DummyMapping) await macro.run(self.handler) self.assertListEqual( @@ -575,8 +615,8 @@ async def test_key(self): ) async def test_key_down_up(self): - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") macro = parse( "set(foo, b).key_down($foo).key_up($foo).key_up(a).key_down(a)", self.context, @@ -594,9 +634,9 @@ async def test_key_down_up(self): ) async def test_modify(self): - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") - code_c = system_mapping.get("c") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") + code_c = keyboard_layout.get("c") macro = parse( "set(foo, b).modify($foo, modify(a, key(c)))", self.context, @@ -616,7 +656,7 @@ async def test_modify(self): ) async def test_hold_variable(self): - code_a = system_mapping.get("a") + code_a = keyboard_layout.get("a") macro = parse("set(foo, a).hold($foo)", self.context, DummyMapping) await macro.run(self.handler) self.assertListEqual( @@ -634,9 +674,9 @@ async def test_hold_keys(self): # then run, just like how it is going to happen during runtime asyncio.ensure_future(macro.run(self.handler)) - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") - code_c = system_mapping.get("c") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") + code_c = keyboard_layout.get("c") await asyncio.sleep(0.2) self.assertListEqual( @@ -685,10 +725,10 @@ async def test_hold(self): await asyncio.sleep(0.05) self.assertFalse(macro.is_holding()) - self.assertEqual(self.result[0], (EV_KEY, system_mapping.get("1"), 1)) - self.assertEqual(self.result[-1], (EV_KEY, system_mapping.get("3"), 0)) + self.assertEqual(self.result[0], (EV_KEY, keyboard_layout.get("1"), 1)) + self.assertEqual(self.result[-1], (EV_KEY, keyboard_layout.get("3"), 0)) - code_a = system_mapping.get("a") + code_a = keyboard_layout.get("a") self.assertGreater(self.result.count((EV_KEY, code_a, 1)), 2) self.assertEqual(len(macro.child_macros), 1) @@ -722,8 +762,8 @@ async def test_dont_hold(self): # and the child macro of hold is never called. self.assertEqual(len(self.result), 4) - self.assertEqual(self.result[0], (EV_KEY, system_mapping.get("1"), 1)) - self.assertEqual(self.result[-1], (EV_KEY, system_mapping.get("3"), 0)) + self.assertEqual(self.result[0], (EV_KEY, keyboard_layout.get("1"), 1)) + self.assertEqual(self.result[-1], (EV_KEY, keyboard_layout.get("3"), 0)) self.assertEqual(len(macro.child_macros), 1) @@ -748,8 +788,8 @@ async def test_just_hold(self): self.assertFalse(macro.is_holding()) self.assertEqual(len(self.result), 4) - self.assertEqual(self.result[0], (EV_KEY, system_mapping.get("1"), 1)) - self.assertEqual(self.result[-1], (EV_KEY, system_mapping.get("3"), 0)) + self.assertEqual(self.result[0], (EV_KEY, keyboard_layout.get("1"), 1)) + self.assertEqual(self.result[-1], (EV_KEY, keyboard_layout.get("3"), 0)) self.assertEqual(len(macro.child_macros), 0) @@ -763,8 +803,8 @@ async def test_dont_just_hold(self): # completely self.assertEqual(len(self.result), 4) - self.assertEqual(self.result[0], (EV_KEY, system_mapping.get("1"), 1)) - self.assertEqual(self.result[-1], (EV_KEY, system_mapping.get("3"), 0)) + self.assertEqual(self.result[0], (EV_KEY, keyboard_layout.get("1"), 1)) + self.assertEqual(self.result[-1], (EV_KEY, keyboard_layout.get("3"), 0)) self.assertEqual(len(macro.child_macros), 0) @@ -784,7 +824,7 @@ async def test_hold_down(self): await asyncio.sleep(0.2) self.assertTrue(macro.is_holding()) self.assertEqual(len(self.result), 1) - self.assertEqual(self.result[0], (EV_KEY, system_mapping.get("a"), 1)) + self.assertEqual(self.result[0], (EV_KEY, keyboard_layout.get("a"), 1)) """up""" @@ -793,8 +833,8 @@ async def test_hold_down(self): self.assertFalse(macro.is_holding()) self.assertEqual(len(self.result), 2) - self.assertEqual(self.result[0], (EV_KEY, system_mapping.get("a"), 1)) - self.assertEqual(self.result[1], (EV_KEY, system_mapping.get("a"), 0)) + self.assertEqual(self.result[0], (EV_KEY, keyboard_layout.get("a"), 1)) + self.assertEqual(self.result[1], (EV_KEY, keyboard_layout.get("a"), 0)) async def test_2(self): start = time.time() @@ -805,7 +845,7 @@ async def test_2(self): self.context, DummyMapping, ) - k_code = system_mapping.get("k") + k_code = keyboard_layout.get("k") await macro.run(self.handler) keystroke_sleep = DummyMapping.macro_key_sleep_ms @@ -824,7 +864,7 @@ async def test_2(self): async def test_3(self): start = time.time() macro = parse("repeat(3, key(m).w(100))", self.context, DummyMapping) - m_code = system_mapping.get("m") + m_code = keyboard_layout.get("m") await macro.run(self.handler) keystroke_time = 6 * DummyMapping.macro_key_sleep_ms @@ -854,9 +894,9 @@ async def test_4(self): DummyMapping, ) - r = system_mapping.get("r") - minus = system_mapping.get("minus") - m = system_mapping.get("m") + r = keyboard_layout.get("r") + minus = keyboard_layout.get("minus") + m = keyboard_layout.get("m") await macro.run(self.handler) self.assertListEqual( @@ -888,9 +928,9 @@ async def test_5(self): self.assertEqual(len(macro.child_macros), 1) self.assertEqual(len(macro.child_macros[0].child_macros), 1) - w = system_mapping.get("w") - left = system_mapping.get("bTn_lEfT") - k = system_mapping.get("k") + w = keyboard_layout.get("w") + left = keyboard_layout.get("bTn_lEfT") + k = keyboard_layout.get("k") await macro.run(self.handler) @@ -919,9 +959,9 @@ async def test_duplicate_run(self): # internal state (in particular the _trigger_release_event). # I actually don't know at all what kind of bugs that might produce, # lets just avoid it. It might cause it to be held down forever. - a = system_mapping.get("a") - b = system_mapping.get("b") - c = system_mapping.get("c") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") + c = keyboard_layout.get("c") macro = parse("key(a).modify(b, hold()).key(c)", self.context, DummyMapping) asyncio.ensure_future(macro.run(self.handler)) @@ -1027,7 +1067,7 @@ async def test_mouse_accel(self): async def test_event_1(self): macro = parse("e(EV_KEY, KEY_A, 1)", self.context, DummyMapping) - a_code = system_mapping.get("a") + a_code = keyboard_layout.get("a") await macro.run(self.handler) self.assertListEqual(self.result, [(EV_KEY, a_code, 1)]) @@ -1160,15 +1200,15 @@ async def test_ifeq_runs(self): self.context, DummyMapping, ) - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") await macro.run(self.handler) self.assertListEqual(self.result, [(EV_KEY, code_a, 1), (EV_KEY, code_a, 0)]) self.assertEqual(len(macro.child_macros), 2) async def test_ifeq_none(self): - code_a = system_mapping.get("a") + code_a = keyboard_layout.get("a") # first param None macro = parse( @@ -1205,8 +1245,8 @@ async def test_ifeq_none(self): async def test_ifeq_unknown_key(self): macro = parse("ifeq(qux, 2, key(a), key(b))", self.context, DummyMapping) - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") await macro.run(self.handler) self.assertListEqual(self.result, [(EV_KEY, code_b, 1), (EV_KEY, code_b, 0)]) @@ -1214,8 +1254,8 @@ async def test_ifeq_unknown_key(self): async def test_if_eq(self): """new version of ifeq""" - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") a_press = [(EV_KEY, code_a, 1), (EV_KEY, code_a, 0)] b_press = [(EV_KEY, code_b, 1), (EV_KEY, code_b, 0)] @@ -1268,8 +1308,8 @@ async def test(macro, expected): async def test_if_eq_runs_multiprocessed(self): """ifeq on variables that have been set in other processes works.""" macro = parse("if_eq($foo, 3, key(a), key(b))", self.context, DummyMapping) - code_a = system_mapping.get("a") - code_b = system_mapping.get("b") + code_a = keyboard_layout.get("a") + code_b = keyboard_layout.get("b") self.assertEqual(len(macro.child_macros), 2) @@ -1310,10 +1350,10 @@ async def test_if_single(self): macro = parse("if_single(key(x), key(y))", self.context, DummyMapping) self.assertEqual(len(macro.child_macros), 2) - a = system_mapping.get("a") + a = keyboard_layout.get("a") - x = system_mapping.get("x") - y = system_mapping.get("y") + x = keyboard_layout.get("x") + y = keyboard_layout.get("y") await self.trigger_sequence(macro, InputEvent.key(a, 1)) await asyncio.sleep(0.1) @@ -1334,11 +1374,11 @@ async def test_if_single_ignores_releases(self): ) self.assertEqual(len(macro.child_macros), 2) - a = system_mapping.get("a") - b = system_mapping.get("b") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") - x = system_mapping.get("x") - y = system_mapping.get("y") + x = keyboard_layout.get("x") + y = keyboard_layout.get("y") # pressing the macro key await self.trigger_sequence(macro, InputEvent.key(a, 1)) @@ -1372,11 +1412,11 @@ async def test_if_not_single(self): self.assertEqual(len(macro.child_macros), 1) self.assertEqual(len(macro.child_macros[0].child_macros), 2) - a = system_mapping.get("a") - b = system_mapping.get("b") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") - x = system_mapping.get("x") - y = system_mapping.get("y") + x = keyboard_layout.get("x") + y = keyboard_layout.get("y") # press the trigger key await self.trigger_sequence(macro, InputEvent.key(a, 1)) @@ -1393,10 +1433,10 @@ async def test_if_not_single_none(self): macro = parse("if_single(key(x),)", self.context, DummyMapping) self.assertEqual(len(macro.child_macros), 1) - a = system_mapping.get("a") - b = system_mapping.get("b") + a = keyboard_layout.get("a") + b = keyboard_layout.get("b") - x = system_mapping.get("x") + x = keyboard_layout.get("x") # press trigger key await self.trigger_sequence(macro, InputEvent.key(a, 1)) @@ -1417,8 +1457,8 @@ async def test_if_single_times_out(self): ) self.assertEqual(len(macro.child_macros), 2) - a = system_mapping.get("a") - y = system_mapping.get("y") + a = keyboard_layout.get("a") + y = keyboard_layout.get("y") await self.trigger_sequence(macro, InputEvent.key(a, 1)) @@ -1437,8 +1477,8 @@ async def test_if_single_ignores_joystick(self): # Integration test style for if_single. # If a joystick that is mapped to a button is moved, if_single stops macro = parse("if_single(k(a), k(KEY_LEFTSHIFT))", self.context, DummyMapping) - code_shift = system_mapping.get("KEY_LEFTSHIFT") - code_a = system_mapping.get("a") + code_shift = keyboard_layout.get("KEY_LEFTSHIFT") + code_a = keyboard_layout.get("a") trigger = 1 await self.trigger_sequence(macro, InputEvent.key(trigger, 1)) @@ -1458,8 +1498,8 @@ async def test_if_tap(self): macro = parse("if_tap(key(x), key(y), 100)", self.context, DummyMapping) self.assertEqual(len(macro.child_macros), 2) - x = system_mapping.get("x") - y = system_mapping.get("y") + x = keyboard_layout.get("x") + y = keyboard_layout.get("y") # this is the regular routine of how a macro is started. the tigger is pressed # already when the macro runs, and released during if_tap within the timeout. @@ -1539,7 +1579,7 @@ async def test_if_tap_none(self): # first param none macro = parse("if_tap(, key(y), 100)", self.context, DummyMapping) self.assertEqual(len(macro.child_macros), 1) - y = system_mapping.get("y") + y = keyboard_layout.get("y") macro.press_trigger() asyncio.ensure_future(macro.run(self.handler)) await asyncio.sleep(0.05) @@ -1550,7 +1590,7 @@ async def test_if_tap_none(self): # second param none macro = parse("if_tap(key(y), , 50)", self.context, DummyMapping) self.assertEqual(len(macro.child_macros), 1) - y = system_mapping.get("y") + y = keyboard_layout.get("y") macro.press_trigger() asyncio.ensure_future(macro.run(self.handler)) await asyncio.sleep(0.1) @@ -1564,8 +1604,8 @@ async def test_if_not_tap(self): macro = parse("if_tap(key(x), key(y), 50)", self.context, DummyMapping) self.assertEqual(len(macro.child_macros), 2) - x = system_mapping.get("x") - y = system_mapping.get("y") + x = keyboard_layout.get("x") + y = keyboard_layout.get("y") macro.press_trigger() asyncio.ensure_future(macro.run(self.handler)) @@ -1580,8 +1620,8 @@ async def test_if_not_tap_named(self): macro = parse("if_tap(key(x), key(y), timeout=50)", self.context, DummyMapping) self.assertEqual(len(macro.child_macros), 2) - x = system_mapping.get("x") - y = system_mapping.get("y") + x = keyboard_layout.get("x") + y = keyboard_layout.get("y") macro.press_trigger() asyncio.ensure_future(macro.run(self.handler)) diff --git a/tests/unit/test_mapping.py b/tests/unit/test_mapping.py index 283747c10..372f4e873 100644 --- a/tests/unit/test_mapping.py +++ b/tests/unit/test_mapping.py @@ -37,7 +37,7 @@ from pydantic import ValidationError from inputremapper.configs.mapping import Mapping, UIMapping -from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME +from inputremapper.configs.keyboard_layout import keyboard_layout, DISABLE_NAME from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.gui.messages.message_broker import MessageType from tests.lib.test_setup import test_setup @@ -105,7 +105,7 @@ def test_get_output_type_code(self): "output_symbol": "a", } m = Mapping(**cfg) - a = system_mapping.get("a") + a = keyboard_layout.get("a") self.assertEqual(m.get_output_type_code(), (EV_KEY, a)) m.output_symbol = "key(a)" @@ -127,7 +127,7 @@ def test_strips_output_symbol(self): "output_symbol": "\t a \n", } m = Mapping(**cfg) - a = system_mapping.get("a") + a = keyboard_layout.get("a") self.assertEqual(m.get_output_type_code(), (EV_KEY, a)) def test_combination_changed_callback(self): @@ -191,7 +191,7 @@ def test_init_fails(self): Mapping(**cfg) # matching type, code and symbol - a = system_mapping.get("a") + a = keyboard_layout.get("a") cfg["output_code"] = a cfg["output_symbol"] = "a" cfg["output_type"] = EV_KEY diff --git a/tests/unit/test_migrations.py b/tests/unit/test_migrations.py index 08c494e74..7a95af480 100644 --- a/tests/unit/test_migrations.py +++ b/tests/unit/test_migrations.py @@ -38,6 +38,7 @@ from inputremapper.configs.migrations import Migrations from inputremapper.configs.paths import PathUtils from inputremapper.configs.preset import Preset +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput from inputremapper.logging.logger import VERSION from inputremapper.user import UserUtils from tests.lib.test_setup import test_setup @@ -60,6 +61,10 @@ def setUp(self): UserUtils.home, ".config", "input-remapper", "beta_1.6.0-beta" ) + global_uinputs = GlobalUInputs(UInput) + global_uinputs.prepare_all() + self.migrations = Migrations(global_uinputs) + def test_migrate_suffix(self): old = os.path.join(PathUtils.config_path(), "config") new = os.path.join(PathUtils.config_path(), "config.json") @@ -73,7 +78,7 @@ def test_migrate_suffix(self): with open(old, "w") as f: f.write("{}") - Migrations.migrate() + self.migrations.migrate() self.assertTrue(os.path.exists(new)) self.assertFalse(os.path.exists(old)) @@ -94,7 +99,7 @@ def test_rename_config(self): with open(old_config_json, "w") as f: f.write('{"foo":"bar"}') - Migrations.migrate() + self.migrations.migrate() self.assertTrue(os.path.exists(new)) self.assertFalse(os.path.exists(old)) @@ -116,7 +121,7 @@ def test_wont_migrate_suffix(self): with open(old, "w") as f: f.write("{}") - Migrations.migrate() + self.migrations.migrate() self.assertTrue(os.path.exists(new)) self.assertTrue(os.path.exists(old)) @@ -135,7 +140,7 @@ def test_migrate_preset(self): with open(p2, "w") as f: f.write("{}") - Migrations.migrate() + self.migrations.migrate() self.assertFalse( os.path.exists(os.path.join(PathUtils.config_path(), "foo1", "bar1.json")) @@ -173,7 +178,7 @@ def test_wont_migrate_preset(self): # already migrated PathUtils.mkdir(os.path.join(PathUtils.config_path(), "presets")) - Migrations.migrate() + self.migrations.migrate() self.assertTrue( os.path.exists(os.path.join(PathUtils.config_path(), "foo1", "bar1.json")) @@ -225,7 +230,7 @@ def test_migrate_mappings(self): }, file, ) - Migrations.migrate() + self.migrations.migrate() # use UIMapping to also load invalid mappings preset = Preset(PathUtils.get_preset_path("Foo Device", "test"), UIMapping) preset.load() @@ -332,7 +337,7 @@ def test_migrate_otherwise(self): file, ) - Migrations.migrate() + self.migrations.migrate() preset = Preset(PathUtils.get_preset_path("Foo Device", "test"), UIMapping) preset.load() @@ -384,8 +389,11 @@ def test_add_version(self): with open(path, "w") as file: file.write("{}") - Migrations.migrate() - self.assertEqual(version.parse(VERSION), Migrations.config_version()) + self.migrations.migrate() + self.assertEqual( + version.parse(VERSION), + self.migrations.config_version(), + ) def test_update_version(self): path = os.path.join(PathUtils.config_path(), "config.json") @@ -393,22 +401,25 @@ def test_update_version(self): with open(path, "w") as file: json.dump({"version": "0.1.0"}, file) - Migrations.migrate() - self.assertEqual(version.parse(VERSION), Migrations.config_version()) + self.migrations.migrate() + self.assertEqual( + version.parse(VERSION), + self.migrations.config_version(), + ) def test_config_version(self): path = os.path.join(PathUtils.config_path(), "config.json") with open(path, "w") as file: file.write("{}") - self.assertEqual("0.0.0", Migrations.config_version().public) + self.assertEqual("0.0.0", self.migrations.config_version().public) try: os.remove(path) except FileNotFoundError: pass - self.assertEqual("0.0.0", Migrations.config_version().public) + self.assertEqual("0.0.0", self.migrations.config_version().public) def test_migrate_left_and_right_purpose(self): path = os.path.join( @@ -430,7 +441,7 @@ def test_migrate_left_and_right_purpose(self): }, file, ) - Migrations.migrate() + self.migrations.migrate() preset = Preset(PathUtils.get_preset_path("Foo Device", "test"), UIMapping) preset.load() @@ -516,7 +527,7 @@ def test_migrate_left_and_right_purpose2(self): }, file, ) - Migrations.migrate() + self.migrations.migrate() preset = Preset(PathUtils.get_preset_path("Foo Device", "test"), UIMapping) preset.load() @@ -632,7 +643,7 @@ def test_prioritize_v1_over_beta_configs(self): self.assertFalse(os.path.exists(PathUtils.get_preset_path(device_name, "foo"))) self.assertFalse(os.path.exists(PathUtils.get_config_path("config.json"))) - Migrations.migrate() + self.migrations.migrate() self.assertTrue(os.path.exists(PathUtils.get_preset_path(device_name, "foo"))) self.assertTrue(os.path.exists(PathUtils.get_config_path("config.json"))) @@ -678,7 +689,7 @@ def test_copy_over_beta_configs(self): self.assertFalse(os.path.exists(PathUtils.get_preset_path(device_name, "bar"))) self.assertFalse(os.path.exists(PathUtils.get_config_path("config.json"))) - Migrations.migrate() + self.migrations.migrate() self.assertTrue(os.path.exists(PathUtils.get_preset_path(device_name, "bar"))) self.assertTrue(os.path.exists(PathUtils.get_config_path("config.json"))) diff --git a/tests/unit/test_reader.py b/tests/unit/test_reader.py index c0e0b5e46..690753b54 100644 --- a/tests/unit/test_reader.py +++ b/tests/unit/test_reader.py @@ -52,6 +52,7 @@ from inputremapper.gui.messages.message_types import MessageType from inputremapper.gui.reader_client import ReaderClient from inputremapper.gui.reader_service import ReaderService, ContextDummy +from inputremapper.injection.global_uinputs import GlobalUInputs, UInput, FrontendUInput from inputremapper.input_event import InputEvent from tests.lib.constants import ( EVENT_READ_TIMEOUT, @@ -109,7 +110,9 @@ async def create_reader_service(self, groups: Optional[_Groups] = None): if not groups: groups = self.groups - self.reader_service = ReaderService(groups) + global_uinputs = GlobalUInputs(UInput) + assert groups is not None + self.reader_service = ReaderService(groups, global_uinputs) asyncio.ensure_future(self.reader_service.run()) async def test_should_forward_to_dummy(self): @@ -164,6 +167,7 @@ def setUp(self): self.reader_service_process = None self.groups = _Groups() self.message_broker = MessageBroker() + self.global_uinputs = GlobalUInputs(UInput) self.reader_client = ReaderClient(self.message_broker, self.groups) def tearDown(self): @@ -182,10 +186,13 @@ def create_reader_service(self, groups: Optional[_Groups] = None): groups = self.groups def start_reader_service(): - reader_service = ReaderService(groups) - # this is a new process, so create a new event loop, or something + # this is a new process, so create a new event loop, and all dependencies + # from scratch, or something loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) + global_uinputs = GlobalUInputs(FrontendUInput) + global_uinputs.reset() + reader_service = ReaderService(groups, global_uinputs) loop.run_until_complete(reader_service.run()) self.reader_service_process = multiprocessing.Process( diff --git a/tests/unit/test_system_mapping.py b/tests/unit/test_system_mapping.py index 3b2a5d462..a7aa2cee9 100644 --- a/tests/unit/test_system_mapping.py +++ b/tests/unit/test_system_mapping.py @@ -27,25 +27,25 @@ from evdev.ecodes import BTN_LEFT, KEY_A from inputremapper.configs.paths import PathUtils -from inputremapper.configs.system_mapping import SystemMapping, XMODMAP_FILENAME +from inputremapper.configs.keyboard_layout import KeyboardLayout, XMODMAP_FILENAME from tests.lib.test_setup import test_setup @test_setup class TestSystemMapping(unittest.TestCase): def test_update(self): - system_mapping = SystemMapping() - system_mapping.update({"foo1": 101, "bar1": 102}) - system_mapping.update({"foo2": 201, "bar2": 202}) - self.assertEqual(system_mapping.get("foo1"), 101) - self.assertEqual(system_mapping.get("bar2"), 202) + keyboard_layout = KeyboardLayout() + keyboard_layout.update({"foo1": 101, "bar1": 102}) + keyboard_layout.update({"foo2": 201, "bar2": 202}) + self.assertEqual(keyboard_layout.get("foo1"), 101) + self.assertEqual(keyboard_layout.get("bar2"), 202) def test_xmodmap_file(self): - system_mapping = SystemMapping() + keyboard_layout = KeyboardLayout() path = os.path.join(PathUtils.config_path(), XMODMAP_FILENAME) os.remove(path) - system_mapping.populate() + keyboard_layout.populate() self.assertTrue(os.path.exists(path)) with open(path, "r") as file: content = json.load(file) @@ -67,11 +67,11 @@ def check_output(*args, **kwargs): return SubprocessMock() with patch.object(subprocess, "check_output", check_output): - system_mapping = SystemMapping() + keyboard_layout = KeyboardLayout() path = os.path.join(PathUtils.config_path(), XMODMAP_FILENAME) os.remove(path) - system_mapping.populate() + keyboard_layout.populate() self.assertFalse(os.path.exists(path)) def test_xmodmap_command_missing(self): @@ -80,61 +80,61 @@ def check_output(*args, **kwargs): raise FileNotFoundError with patch.object(subprocess, "check_output", check_output): - system_mapping = SystemMapping() + keyboard_layout = KeyboardLayout() path = os.path.join(PathUtils.config_path(), XMODMAP_FILENAME) os.remove(path) - system_mapping.populate() + keyboard_layout.populate() self.assertFalse(os.path.exists(path)) def test_correct_case(self): - system_mapping = SystemMapping() - system_mapping.clear() - system_mapping._set("A", 31) - system_mapping._set("a", 32) - system_mapping._set("abcd_B", 33) - - self.assertEqual(system_mapping.correct_case("a"), "a") - self.assertEqual(system_mapping.correct_case("A"), "A") - self.assertEqual(system_mapping.correct_case("ABCD_b"), "abcd_B") + keyboard_layout = KeyboardLayout() + keyboard_layout.clear() + keyboard_layout._set("A", 31) + keyboard_layout._set("a", 32) + keyboard_layout._set("abcd_B", 33) + + self.assertEqual(keyboard_layout.correct_case("a"), "a") + self.assertEqual(keyboard_layout.correct_case("A"), "A") + self.assertEqual(keyboard_layout.correct_case("ABCD_b"), "abcd_B") # unknown stuff is returned as is - self.assertEqual(system_mapping.correct_case("FOo"), "FOo") + self.assertEqual(keyboard_layout.correct_case("FOo"), "FOo") - self.assertEqual(system_mapping.get("A"), 31) - self.assertEqual(system_mapping.get("a"), 32) - self.assertEqual(system_mapping.get("ABCD_b"), 33) - self.assertEqual(system_mapping.get("abcd_B"), 33) + self.assertEqual(keyboard_layout.get("A"), 31) + self.assertEqual(keyboard_layout.get("a"), 32) + self.assertEqual(keyboard_layout.get("ABCD_b"), 33) + self.assertEqual(keyboard_layout.get("abcd_B"), 33) - def test_system_mapping(self): - system_mapping = SystemMapping() - system_mapping.populate() - self.assertGreater(len(system_mapping._mapping), 100) + def test_keyboard_layout(self): + keyboard_layout = KeyboardLayout() + keyboard_layout.populate() + self.assertGreater(len(keyboard_layout._mapping), 100) # this is case-insensitive - self.assertEqual(system_mapping.get("1"), 2) - self.assertEqual(system_mapping.get("KeY_1"), 2) + self.assertEqual(keyboard_layout.get("1"), 2) + self.assertEqual(keyboard_layout.get("KeY_1"), 2) - self.assertEqual(system_mapping.get("AlT_L"), 56) - self.assertEqual(system_mapping.get("KEy_LEFtALT"), 56) + self.assertEqual(keyboard_layout.get("AlT_L"), 56) + self.assertEqual(keyboard_layout.get("KEy_LEFtALT"), 56) - self.assertEqual(system_mapping.get("kEY_LeFTSHIFT"), 42) - self.assertEqual(system_mapping.get("ShiFt_L"), 42) + self.assertEqual(keyboard_layout.get("kEY_LeFTSHIFT"), 42) + self.assertEqual(keyboard_layout.get("ShiFt_L"), 42) - self.assertEqual(system_mapping.get("BTN_left"), 272) + self.assertEqual(keyboard_layout.get("BTN_left"), 272) - self.assertIsNotNone(system_mapping.get("KEY_KP4")) - self.assertEqual(system_mapping.get("KP_Left"), system_mapping.get("KEY_KP4")) + self.assertIsNotNone(keyboard_layout.get("KEY_KP4")) + self.assertEqual(keyboard_layout.get("KP_Left"), keyboard_layout.get("KEY_KP4")) # this only lists the correct casing, # includes linux constants and xmodmap symbols - names = system_mapping.list_names() + names = keyboard_layout.list_names() self.assertIn("2", names) self.assertIn("c", names) self.assertIn("KEY_3", names) self.assertNotIn("key_3", names) self.assertIn("KP_Down", names) self.assertNotIn("kp_down", names) - names = system_mapping._mapping.keys() + names = keyboard_layout._mapping.keys() self.assertIn("F4", names) self.assertNotIn("f4", names) self.assertIn("BTN_RIGHT", names) @@ -143,22 +143,22 @@ def test_system_mapping(self): self.assertIn("KP_Home", names) self.assertNotIn("kp_home", names) - self.assertEqual(system_mapping.get("disable"), -1) + self.assertEqual(keyboard_layout.get("disable"), -1) def test_get_name_no_xmodmap(self): # if xmodmap is not installed, uses the linux constant names - system_mapping = SystemMapping() + keyboard_layout = KeyboardLayout() def check_output(*args, **kwargs): raise FileNotFoundError with patch.object(subprocess, "check_output", check_output): - system_mapping.populate() - self.assertEqual(system_mapping.get_name(KEY_A), "KEY_A") + keyboard_layout.populate() + self.assertEqual(keyboard_layout.get_name(KEY_A), "KEY_A") # testing for BTN_LEFT is especially important, because # `evdev.ecodes.BTN.get(code)` returns an array of ['BTN_LEFT', 'BTN_MOUSE'] - self.assertEqual(system_mapping.get_name(BTN_LEFT), "BTN_LEFT") + self.assertEqual(keyboard_layout.get_name(BTN_LEFT), "BTN_LEFT") if __name__ == "__main__": diff --git a/tests/unit/test_test.py b/tests/unit/test_test.py index 913eb1912..b57f48d69 100644 --- a/tests/unit/test_test.py +++ b/tests/unit/test_test.py @@ -31,6 +31,7 @@ from inputremapper.gui.messages.message_broker import MessageBroker from inputremapper.gui.reader_client import ReaderClient from inputremapper.gui.reader_service import ReaderService +from inputremapper.injection.global_uinputs import UInput, GlobalUInputs from inputremapper.input_event import InputEvent from inputremapper.utils import get_device_hash from tests.lib.cleanup import cleanup @@ -91,9 +92,10 @@ def create_reader_service(): # this will cause pending events to be copied over to the reader-service # process def start_reader_service(): - # there is no point in using the global groups object - # because the reader-service runs in a different process - reader_service = ReaderService(_Groups()) + # Create dependencies from scratch, because the reader-service runs + # in a different process + global_uinputs = GlobalUInputs(UInput) + reader_service = ReaderService(_Groups(), global_uinputs) loop = asyncio.new_event_loop() loop.run_until_complete(reader_service.run())