From b3e1e4ca1976bb420017c737525151ac37099e6e Mon Sep 17 00:00:00 2001 From: jonasBoss Date: Mon, 31 Jan 2022 20:58:37 +0100 Subject: [PATCH] Refactorings (#275) --- bin/input-remapper-control | 17 +- bin/input-remapper-gtk | 8 +- inputremapper/configs/__init__.py | 0 .../{config.py => configs/base_config.py} | 109 ---- inputremapper/{ => configs}/data.py | 0 inputremapper/configs/global_config.py | 129 +++++ inputremapper/{ => configs}/migrations.py | 6 +- inputremapper/{ => configs}/paths.py | 0 inputremapper/configs/preset.py | 414 ++++++++++++++ inputremapper/{ => configs}/system_mapping.py | 2 +- inputremapper/daemon.py | 24 +- inputremapper/event_combination.py | 220 ++++++++ inputremapper/exceptions.py | 5 + inputremapper/groups.py | 13 +- .../{custom_mapping.py => active_preset.py} | 6 +- inputremapper/gui/editor/autocompletion.py | 2 +- inputremapper/gui/editor/editor.py | 125 ++--- inputremapper/gui/reader.py | 46 +- inputremapper/gui/user_interface.py | 93 ++-- inputremapper/injection/consumer_control.py | 11 +- .../injection/consumers/joystick_to_mouse.py | 12 +- .../injection/consumers/keycode_mapper.py | 13 +- inputremapper/injection/context.py | 51 +- inputremapper/injection/injector.py | 74 ++- inputremapper/injection/macros/macro.py | 5 +- inputremapper/input_event.py | 135 +++++ inputremapper/ipc/pipe.py | 2 +- inputremapper/ipc/socket.py | 2 +- inputremapper/key.py | 265 --------- inputremapper/mapping.py | 290 ---------- inputremapper/presets.py | 172 ------ inputremapper/utils.py | 10 +- tests/integration/test_data.py | 2 +- tests/integration/test_gui.py | 512 ++++++++++-------- tests/test.py | 24 +- tests/unit/test_config.py | 132 ++--- tests/unit/test_consumer_control.py | 32 +- tests/unit/test_context.py | 20 +- tests/unit/test_control.py | 44 +- tests/unit/test_daemon.py | 66 +-- tests/unit/test_dev_utils.py | 8 +- ...{test_key.py => test_event_combination.py} | 84 +-- tests/unit/test_event_producer.py | 30 +- tests/unit/test_groups.py | 2 +- tests/unit/test_injector.py | 182 ++++--- tests/unit/test_input_event.py | 118 ++++ tests/unit/test_keycode_mapper.py | 132 ++--- tests/unit/test_logger.py | 2 +- tests/unit/test_macros.py | 24 +- tests/unit/test_mapping.py | 373 ------------- tests/unit/test_migrations.py | 35 +- tests/unit/test_paths.py | 2 +- tests/unit/test_preset.py | 361 ++++++++++++ tests/unit/test_presets.py | 10 +- tests/unit/test_reader.py | 72 +-- 55 files changed, 2412 insertions(+), 2116 deletions(-) create mode 100644 inputremapper/configs/__init__.py rename inputremapper/{config.py => configs/base_config.py} (58%) rename inputremapper/{ => configs}/data.py (100%) create mode 100644 inputremapper/configs/global_config.py rename inputremapper/{ => configs}/migrations.py (97%) rename inputremapper/{ => configs}/paths.py (100%) create mode 100644 inputremapper/configs/preset.py rename inputremapper/{ => configs}/system_mapping.py (99%) create mode 100644 inputremapper/event_combination.py rename inputremapper/gui/{custom_mapping.py => active_preset.py} (87%) create mode 100644 inputremapper/input_event.py delete mode 100644 inputremapper/key.py delete mode 100644 inputremapper/mapping.py delete mode 100644 inputremapper/presets.py rename tests/unit/{test_key.py => test_event_combination.py} (51%) create mode 100644 tests/unit/test_input_event.py delete mode 100644 tests/unit/test_mapping.py create mode 100644 tests/unit/test_preset.py diff --git a/bin/input-remapper-control b/bin/input-remapper-control index 122bcd9e..73b5bbfd 100755 --- a/bin/input-remapper-control +++ b/bin/input-remapper-control @@ -30,9 +30,8 @@ import logging import subprocess from inputremapper.logger import logger, update_verbosity, log_info, add_filehandler -from inputremapper.migrations import migrate -from inputremapper.config import config - +from inputremapper.configs.migrations import migrate +from inputremapper.configs.global_config import global_config # import inputremapper modules as late as possible to make sure the correct # log level is applied before anything is logged @@ -80,7 +79,7 @@ def utils(options): print(group.key) if options.key_names: - from inputremapper.system_mapping import system_mapping + from inputremapper.configs.system_mapping import system_mapping print('\n'.join(system_mapping.list_names())) @@ -89,7 +88,7 @@ def communicate(options, daemon): # import stuff late to make sure the correct log level is applied # before anything is logged from inputremapper.groups import groups - from inputremapper.paths import USER + from inputremapper.configs.paths import USER def require_group(): if options.device is None: @@ -125,14 +124,14 @@ def communicate(options, daemon): sys.exit(1) logger.info('Using config from "%s" instead', path) - config.load_config(path) + global_config.load_config(path) if USER != 'root': # Might be triggered by udev, so skip the root user. # 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(config.path) + config_dir = os.path.dirname(global_config.path) daemon.set_config_dir(config_dir) migrate() @@ -217,7 +216,7 @@ def main(options): logger.debug('Call for "%s"', sys.argv) - from inputremapper.paths import USER + from inputremapper.configs.paths import USER boot_finished = systemd_finished() is_root = USER == "root" is_autoload = options.command == AUTOLOAD @@ -282,7 +281,7 @@ if __name__ == '__main__': ) parser.add_argument( '--symbol-names', action='store_true', dest='key_names', - help='Print all available names for the mapping', + help='Print all available names for the preset', default=False ) parser.add_argument( diff --git a/bin/input-remapper-gtk b/bin/input-remapper-gtk index 52b90ccf..c311bb3d 100755 --- a/bin/input-remapper-gtk +++ b/bin/input-remapper-gtk @@ -26,7 +26,7 @@ import sys import atexit import gettext import locale -from inputremapper.data import get_data_path +from inputremapper.configs.data import get_data_path import os.path from argparse import ArgumentParser @@ -50,7 +50,7 @@ _ = translate.gettext Gtk.init() from inputremapper.logger import logger, update_verbosity, log_info -from inputremapper.migrations import migrate +from inputremapper.configs.migrations import migrate if __name__ == '__main__': @@ -69,10 +69,10 @@ if __name__ == '__main__': # import input-remapper stuff after setting the log verbosity from inputremapper.gui.user_interface import UserInterface from inputremapper.daemon import Daemon - from inputremapper.daemon import config + from inputremapper.configs.global_config import global_config migrate() - config.load_config() + global_config.load_config() user_interface = UserInterface() diff --git a/inputremapper/configs/__init__.py b/inputremapper/configs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/inputremapper/config.py b/inputremapper/configs/base_config.py similarity index 58% rename from inputremapper/config.py rename to inputremapper/configs/base_config.py index 83d66e45..abe1d5c1 100644 --- a/inputremapper/config.py +++ b/inputremapper/configs/base_config.py @@ -18,20 +18,10 @@ # You should have received a copy of the GNU General Public License # along with input-remapper. If not, see . - -"""Store which presets should be enabled for which device on login.""" - - -import os -import json import copy -from inputremapper.paths import CONFIG_PATH, USER, touch from inputremapper.logger import logger, VERSION -MOUSE = "mouse" -WHEEL = "wheel" -BUTTONS = "buttons" NONE = "none" INITIAL_CONFIG = { @@ -173,102 +163,3 @@ class ConfigBase: def clear_config(self): """Remove all configurations in memory.""" self._config = {} - - -class GlobalConfig(ConfigBase): - """Global default configuration. - - 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 - have the key set, a hardcoded default value will be used. - """ - - def __init__(self): - self.path = os.path.join(CONFIG_PATH, "config.json") - super().__init__() - - def set_autoload_preset(self, group_key, preset): - """Set a preset to be automatically applied on start. - - Parameters - ---------- - group_key : string - the unique identifier of the group. This is used instead of the - name to enable autoloading two different presets when two similar - devices are connected. - preset : string or None - if None, don't autoload something for this device. - """ - if preset is not None: - self.set(["autoload", group_key], preset) - else: - logger.info('Not injecting for "%s" automatically anmore', group_key) - self.remove(["autoload", group_key]) - - self._save_config() - - def iterate_autoload_presets(self): - """Get tuples of (device, preset).""" - return self._config.get("autoload", {}).items() - - def is_autoloaded(self, group_key, preset): - """Should this preset be loaded automatically?""" - if group_key is None or preset is None: - raise ValueError("Expected group_key and preset to not be None") - - return self.get(["autoload", group_key], log_unknown=False) == preset - - def load_config(self, path=None): - """Load the config from the file system. - - Parameters - ---------- - path : string or None - If set, will change the path to load from and save to. - """ - if path is not None: - if not os.path.exists(path): - logger.error('Config at "%s" not found', path) - return - - self.path = path - - self.clear_config() - - if not os.path.exists(self.path): - # treated like an empty config - logger.debug('Config "%s" doesn\'t exist yet', self.path) - self.clear_config() - self._config = copy.deepcopy(INITIAL_CONFIG) - self._save_config() - return - - with open(self.path, "r") as file: - try: - self._config.update(json.load(file)) - logger.info('Loaded config from "%s"', self.path) - except json.decoder.JSONDecodeError as error: - logger.error( - 'Failed to parse config "%s": %s. Using defaults', - self.path, - str(error), - ) - # uses the default configuration when the config object - # is empty automatically - - def _save_config(self): - """Save the config to the file system.""" - if USER == "root": - logger.debug("Skipping config file creation for the root user") - return - - touch(self.path) - - with open(self.path, "w") as file: - json.dump(self._config, file, indent=4) - logger.info("Saved config to %s", self.path) - file.write("\n") - - -config = GlobalConfig() diff --git a/inputremapper/data.py b/inputremapper/configs/data.py similarity index 100% rename from inputremapper/data.py rename to inputremapper/configs/data.py diff --git a/inputremapper/configs/global_config.py b/inputremapper/configs/global_config.py new file mode 100644 index 00000000..b589ca4b --- /dev/null +++ b/inputremapper/configs/global_config.py @@ -0,0 +1,129 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# input-remapper - GUI for device specific keyboard mappings +# Copyright (C) 2022 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 . +"""Store which presets should be enabled for which device on login.""" + +import os +import json +import copy + +from inputremapper.configs.paths import CONFIG_PATH, USER, touch +from inputremapper.logger import logger +from inputremapper.configs.base_config import ConfigBase, INITIAL_CONFIG + +MOUSE = "mouse" +WHEEL = "wheel" +BUTTONS = "buttons" +NONE = "none" + + +class GlobalConfig(ConfigBase): + """Global default configuration. + 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 + have the key set, a hardcoded default value will be used. + """ + + def __init__(self): + self.path = os.path.join(CONFIG_PATH, "config.json") + super().__init__() + + def set_autoload_preset(self, group_key, preset): + """Set a preset to be automatically applied on start. + Parameters + ---------- + group_key : string + the unique identifier of the group. This is used instead of the + name to enable autoloading two different presets when two similar + devices are connected. + preset : string or None + if None, don't autoload something for this device. + """ + if preset is not None: + self.set(["autoload", group_key], preset) + else: + logger.info('Not injecting for "%s" automatically anmore', group_key) + self.remove(["autoload", group_key]) + + self._save_config() + + def iterate_autoload_presets(self): + """Get tuples of (device, preset).""" + return self._config.get("autoload", {}).items() + + def is_autoloaded(self, group_key, preset): + """Should this preset be loaded automatically?""" + if group_key is None or preset is None: + raise ValueError("Expected group_key and preset to not be None") + + return self.get(["autoload", group_key], log_unknown=False) == preset + + def load_config(self, path=None): + """Load the config from the file system. + Parameters + ---------- + path : string or None + If set, will change the path to load from and save to. + """ + if path is not None: + if not os.path.exists(path): + logger.error('Config at "%s" not found', path) + return + + self.path = path + + self.clear_config() + + if not os.path.exists(self.path): + # treated like an empty config + logger.debug('Config "%s" doesn\'t exist yet', self.path) + self.clear_config() + self._config = copy.deepcopy(INITIAL_CONFIG) + self._save_config() + return + + with open(self.path, "r") as file: + try: + self._config.update(json.load(file)) + logger.info('Loaded config from "%s"', self.path) + except json.decoder.JSONDecodeError as error: + logger.error( + 'Failed to parse config "%s": %s. Using defaults', + self.path, + str(error), + ) + # uses the default configuration when the config object + # is empty automatically + + def _save_config(self): + """Save the config to the file system.""" + if USER == "root": + logger.debug("Skipping config file creation for the root user") + return + + touch(self.path) + + with open(self.path, "w") as file: + json.dump(self._config, file, indent=4) + logger.info("Saved config to %s", self.path) + file.write("\n") + + +global_config = GlobalConfig() diff --git a/inputremapper/migrations.py b/inputremapper/configs/migrations.py similarity index 97% rename from inputremapper/migrations.py rename to inputremapper/configs/migrations.py index bcd13ecc..1cb033e5 100644 --- a/inputremapper/migrations.py +++ b/inputremapper/configs/migrations.py @@ -32,8 +32,8 @@ from evdev.ecodes import EV_KEY, EV_REL from inputremapper.logger import logger, VERSION from inputremapper.user import HOME -from inputremapper.paths import get_preset_path, mkdir, CONFIG_PATH -from inputremapper.system_mapping import system_mapping +from inputremapper.configs.paths import get_preset_path, mkdir, CONFIG_PATH +from inputremapper.configs.system_mapping import system_mapping from inputremapper.injection.global_uinputs import global_uinputs from inputremapper.injection.macros.parse import parse, is_this_a_macro @@ -102,7 +102,7 @@ def _preset_path(): def _mapping_keys(): """Update all preset mappings. - Update all keys in mapping to include value e.g.: "1,5"->"1,5,1" + Update all keys in preset to include value e.g.: "1,5"->"1,5,1" """ if not os.path.exists(get_preset_path()): return # don't execute if there are no presets diff --git a/inputremapper/paths.py b/inputremapper/configs/paths.py similarity index 100% rename from inputremapper/paths.py rename to inputremapper/configs/paths.py diff --git a/inputremapper/configs/preset.py b/inputremapper/configs/preset.py new file mode 100644 index 00000000..dff9a8ec --- /dev/null +++ b/inputremapper/configs/preset.py @@ -0,0 +1,414 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# input-remapper - GUI for device specific keyboard mappings +# Copyright (C) 2022 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 . +from __future__ import annotations + +"""Contains and manages mappings.""" + +import os +import re +import json +import glob +import time + +from typing import Tuple, Dict, List +from evdev.ecodes import EV_KEY, BTN_LEFT + +from inputremapper.logger import logger +from inputremapper.configs.paths import touch, get_preset_path, mkdir +from inputremapper.configs.global_config import global_config +from inputremapper.configs.base_config import ConfigBase +from inputremapper.event_combination import EventCombination +from inputremapper.injection.macros.parse import clean +from inputremapper.groups import groups + + +class Preset(ConfigBase): + """Contains and manages mappings of a single preset.""" + + _mapping: Dict[EventCombination, Tuple[str, str]] + + def __init__(self): + # a mapping of a EventCombination object to (symbol, target) tuple + self._mapping: Dict[EventCombination, Tuple[str, str]] = {} + self._changed = False + + # are there actually any keys set in the preset file? + self.num_saved_keys = 0 + + super().__init__(fallback=global_config) + + def __iter__(self) -> Preset._mapping.items: + """Iterate over EventCombination objects and their mappings.""" + return iter(self._mapping.items()) + + def __len__(self): + return len(self._mapping) + + def set(self, *args): + """Set a config value. See `ConfigBase.set`.""" + self._changed = True + return super().set(*args) + + def remove(self, *args): + """Remove a config value. See `ConfigBase.remove`.""" + self._changed = True + return super().remove(*args) + + def change(self, new_combination, target, symbol, previous_combination=None): + """Replace the mapping of a keycode with a different one. + + Parameters + ---------- + new_combination : EventCombination + target : string + name of target uinput + symbol : string + A single symbol known to xkb or linux. + Examples: KEY_KP1, Shift_L, a, B, BTN_LEFT. + previous_combination : EventCombination or None + the previous combination + + If not set, will not remove any previous mapping. If you recently + used (1, 10, 1) for new_key and want to overwrite that with + (1, 11, 1), provide (1, 10, 1) here. + """ + if not isinstance(new_combination, EventCombination): + raise TypeError( + f"Expected {new_combination} to be a EventCombination object" + ) + + if symbol is None or symbol.strip() == "": + raise ValueError("Expected `symbol` not to be empty") + + if target is None or target.strip() == "": + raise ValueError("Expected `target` not to be None") + + target = target.strip() + symbol = symbol.strip() + output = (symbol, target) + + if previous_combination is None and self._mapping.get(new_combination): + # the combination didn't change + previous_combination = new_combination + + key_changed = new_combination != previous_combination + if not key_changed and (symbol, target) == self._mapping.get(new_combination): + # nothing was changed, no need to act + return + + self.clear(new_combination) # this also clears all equivalent keys + + logger.debug('changing %s to "%s"', new_combination, clean(symbol)) + + self._mapping[new_combination] = output + + if key_changed and previous_combination is not None: + # clear previous mapping of that code, because the line + # representing that one will now represent a different one + self.clear(previous_combination) + + self._changed = True + + def has_unsaved_changes(self): + """Check if there are unsaved changed.""" + return self._changed + + def set_has_unsaved_changes(self, changed): + """Write down if there are unsaved changes, or if they have been saved.""" + self._changed = changed + + def clear(self, combination): + """Remove a keycode from the preset. + + Parameters + ---------- + combination : EventCombination + """ + if not isinstance(combination, EventCombination): + raise TypeError( + f"Expected combination to be a EventCombination object but got {combination}" + ) + + for permutation in combination.get_permutations(): + if permutation in self._mapping: + logger.debug("%s cleared", permutation) + del self._mapping[permutation] + self._changed = True + # there should be only one variation of the permutations + # in the preset actually + + def empty(self): + """Remove all mappings and custom configs without saving.""" + self._mapping = {} + self._changed = True + self.clear_config() + + def load(self, path): + """Load a dumped JSON from home to overwrite the mappings. + + Parameters + path : string + Path of the preset file + """ + logger.info('Loading preset from "%s"', path) + + if not os.path.exists(path): + raise FileNotFoundError(f'Tried to load non-existing preset "{path}"') + + self.empty() + self._changed = False + + with open(path, "r") as file: + preset_dict = json.load(file) + + if not isinstance(preset_dict.get("mapping"), dict): + logger.error( + "Expected mapping to be a dict, but was %s. " + 'Invalid preset config at "%s"', + preset_dict.get("mapping"), + path, + ) + return + + for combination, symbol in preset_dict["mapping"].items(): + try: + combination = EventCombination.from_string(combination) + except ValueError as error: + logger.error(str(error)) + continue + + if isinstance(symbol, list): + symbol = tuple(symbol) # use a immutable type + + logger.debug("%s maps to %s", combination, symbol) + self._mapping[combination] = symbol + + # add any metadata of the preset + for key in preset_dict: + if key == "mapping": + continue + self._config[key] = preset_dict[key] + + self._changed = False + self.num_saved_keys = len(self) + + def save(self, path): + """Dump as JSON into home.""" + logger.info("Saving preset to %s", path) + + touch(path) + + with open(path, "w") as file: + if self._config.get("mapping") is not None: + logger.error( + '"mapping" is reserved and cannot be used as config ' "key: %s", + self._config.get("mapping"), + ) + + preset_dict = self._config.copy() # shallow copy + + # make sure to keep the option to add metadata if ever needed, + # so put the mapping into a special key + json_ready_mapping = {} + # tuple keys are not possible in json, encode them as string + for combination, value in self._mapping.items(): + new_key = combination.json_str() + json_ready_mapping[new_key] = value + + preset_dict["mapping"] = json_ready_mapping + json.dump(preset_dict, file, indent=4) + file.write("\n") + + self._changed = False + self.num_saved_keys = len(self) + + def get_mapping(self, combination: EventCombination): + """Read the (symbol, target)-tuple that is mapped to this keycode. + + Parameters + ---------- + combination : EventCombination + """ + if not isinstance(combination, EventCombination): + raise TypeError( + f"Expected combination to be a EventCombination object but got {combination}" + ) + + for permutation in combination.get_permutations(): + existing = self._mapping.get(permutation) + if existing is not None: + return existing + + return None + + def dangerously_mapped_btn_left(self): + """Return True if this mapping disables BTN_Left.""" + if self.get_mapping(EventCombination([EV_KEY, BTN_LEFT, 1])) is not None: + values = [value[0].lower() for value in self._mapping.values()] + return "btn_left" not in values + + return False + + +########################################################################### +# Method from previously presets.py +# TODO: See what can be implemented as classmethod or +# member function of Preset +########################################################################### + + +def get_available_preset_name(group_name, preset="new preset", copy=False): + """Increment the preset name until it is available.""" + if group_name is None: + # endless loop otherwise + raise ValueError("group_name may not be None") + + preset = preset.strip() + + if copy and not re.match(r"^.+\scopy( \d+)?$", preset): + preset = f"{preset} copy" + + # find a name that is not already taken + if os.path.exists(get_preset_path(group_name, preset)): + # if there already is a trailing number, increment it instead of + # adding another one + match = re.match(r"^(.+) (\d+)$", preset) + if match: + preset = match[1] + i = int(match[2]) + 1 + else: + i = 2 + + while os.path.exists(get_preset_path(group_name, f"{preset} {i}")): + i += 1 + + return f"{preset} {i}" + + return preset + + +def get_presets(group_name: str) -> List[str]: + """Get all preset filenames for the device and user, starting with the newest. + + Parameters + ---------- + group_name : string + """ + device_folder = get_preset_path(group_name) + mkdir(device_folder) + + paths = glob.glob(os.path.join(device_folder, "*.json")) + presets = [ + os.path.splitext(os.path.basename(path))[0] + for path in sorted(paths, key=os.path.getmtime) + ] + # the highest timestamp to the front + presets.reverse() + return presets + + +def get_any_preset() -> Tuple[str | None, str | None]: + """Return the first found tuple of (device, preset).""" + group_names = groups.list_group_names() + if len(group_names) == 0: + return None, None + any_device = list(group_names)[0] + any_preset = (get_presets(any_device) or [None])[0] + return any_device, any_preset + + +def find_newest_preset(group_name=None): + """Get a tuple of (device, preset) that was most recently modified + in the users home directory. + + If no device has been configured yet, return an arbitrary device. + + Parameters + ---------- + group_name : string + If set, will return the newest preset for the device or None + """ + # sort the oldest files to the front in order to use pop to get the newest + if group_name is None: + paths = sorted( + glob.glob(os.path.join(get_preset_path(), "*/*.json")), key=os.path.getmtime + ) + else: + paths = sorted( + glob.glob(os.path.join(get_preset_path(group_name), "*.json")), + key=os.path.getmtime, + ) + + if len(paths) == 0: + logger.debug("No presets found") + return get_any_preset() + + group_names = groups.list_group_names() + + newest_path = None + while len(paths) > 0: + # take the newest path + path = paths.pop() + preset = os.path.split(path)[1] + group_name = os.path.split(os.path.split(path)[0])[1] + if group_name in group_names: + newest_path = path + break + + if newest_path is None: + return get_any_preset() + + preset = os.path.splitext(preset)[0] + logger.debug('The newest preset is "%s", "%s"', group_name, preset) + + return group_name, preset + + +def delete_preset(group_name, preset): + """Delete one of the users presets.""" + preset_path = get_preset_path(group_name, preset) + if not os.path.exists(preset_path): + logger.debug('Cannot remove non existing path "%s"', preset_path) + return + + logger.info('Removing "%s"', preset_path) + os.remove(preset_path) + + device_path = get_preset_path(group_name) + if os.path.exists(device_path) and len(os.listdir(device_path)) == 0: + logger.debug('Removing empty dir "%s"', device_path) + os.rmdir(device_path) + + +def rename_preset(group_name, old_preset_name, new_preset_name): + """Rename one of the users presets while avoiding name conflicts.""" + if new_preset_name == old_preset_name: + return None + + new_preset_name = get_available_preset_name(group_name, new_preset_name) + logger.info('Moving "%s" to "%s"', old_preset_name, new_preset_name) + os.rename( + get_preset_path(group_name, old_preset_name), + get_preset_path(group_name, new_preset_name), + ) + # set the modification date to now + now = time.time() + os.utime(get_preset_path(group_name, new_preset_name), (now, now)) + return new_preset_name diff --git a/inputremapper/system_mapping.py b/inputremapper/configs/system_mapping.py similarity index 99% rename from inputremapper/system_mapping.py rename to inputremapper/configs/system_mapping.py index 0849a3ea..819f741a 100644 --- a/inputremapper/system_mapping.py +++ b/inputremapper/configs/system_mapping.py @@ -28,7 +28,7 @@ import subprocess import evdev from inputremapper.logger import logger -from inputremapper.paths import get_config_path, touch +from inputremapper.configs.paths import get_config_path, touch from inputremapper.utils import is_service DISABLE_NAME = "disable" diff --git a/inputremapper/daemon.py b/inputremapper/daemon.py index 30ade99a..b0257918 100644 --- a/inputremapper/daemon.py +++ b/inputremapper/daemon.py @@ -39,11 +39,11 @@ from gi.repository import GLib from inputremapper.logger import logger, is_debug from inputremapper.injection.injector import Injector, UNKNOWN -from inputremapper.mapping import Mapping -from inputremapper.config import config -from inputremapper.system_mapping import system_mapping +from inputremapper.configs.preset import Preset +from inputremapper.configs.global_config import global_config +from inputremapper.configs.system_mapping import system_mapping from inputremapper.groups import groups -from inputremapper.paths import get_config_path, USER +from inputremapper.configs.paths import get_config_path, USER from inputremapper.injection.macros.macro import macro_variables from inputremapper.injection.global_uinputs import global_uinputs @@ -59,7 +59,7 @@ class AutoloadHistory: def __init__(self): """Construct this with an empty history.""" - # mapping of device -> (timestamp, preset) + # preset of device -> (timestamp, preset) self._autoload_history = {} def remember(self, group_key, preset): @@ -279,7 +279,7 @@ class Daemon: self.refreshed_devices_at = now def stop_injecting(self, group_key): - """Stop injecting the mapping for a single device.""" + """Stop injecting the preset mappings for a single device.""" if self.injectors.get(group_key) is None: logger.debug( 'Tried to stop injector, but none is running for group "%s"', group_key @@ -313,7 +313,7 @@ class Daemon: return self.config_dir = config_dir - config.load_config(config_path) + global_config.load_config(config_path) def _autoload(self, group_key): """Check if autoloading is a good idea, and if so do it. @@ -331,7 +331,7 @@ class Daemon: # either not relevant for input-remapper, or not connected yet return - preset = config.get(["autoload", group.key], log_unknown=False) + preset = global_config.get(["autoload", group.key], log_unknown=False) if preset is None: # no autoloading is configured for this device @@ -395,7 +395,7 @@ class Daemon: ) return - autoload_presets = list(config.iterate_autoload_presets()) + autoload_presets = list(global_config.iterate_autoload_presets()) logger.info("Autoloading for all devices") @@ -438,9 +438,9 @@ class Daemon: self.config_dir, "presets", group.name, f"{preset}.json" ) - mapping = Mapping() + preset = Preset() try: - mapping.load(preset_path) + preset.load(preset_path) except FileNotFoundError as error: logger.error(str(error)) return False @@ -466,7 +466,7 @@ class Daemon: logger.error('Could not find "%s"', xmodmap_path) try: - injector = Injector(group, mapping) + injector = Injector(group, preset) injector.start() self.injectors[group.key] = injector except OSError: diff --git a/inputremapper/event_combination.py b/inputremapper/event_combination.py new file mode 100644 index 00000000..6cc70d8d --- /dev/null +++ b/inputremapper/event_combination.py @@ -0,0 +1,220 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# input-remapper - GUI for device specific keyboard mappings +# Copyright (C) 2022 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 . + +from __future__ import annotations + +import itertools + +from typing import Tuple, Iterable + +import evdev +from evdev import ecodes + +from inputremapper.logger import logger +from inputremapper.configs.system_mapping import system_mapping +from inputremapper.input_event import InputEvent +from inputremapper.exceptions import InputEventCreationError + +# having shift in combinations modifies the configured output, +# ctrl might not work at all +DIFFICULT_COMBINATIONS = [ + ecodes.KEY_LEFTSHIFT, + ecodes.KEY_RIGHTSHIFT, + ecodes.KEY_LEFTCTRL, + ecodes.KEY_RIGHTCTRL, + ecodes.KEY_LEFTALT, + ecodes.KEY_RIGHTALT, +] + + +class EventCombination(Tuple[InputEvent]): + """one or multiple InputEvent objects for use as an unique identifier for mappings""" + + # tuple is immutable, therefore we need to override __new__() + # https://jfine-python-classes.readthedocs.io/en/latest/subclass-tuple.html + def __new__(cls, *init_args) -> EventCombination: + events = [] + for init_arg in init_args: + event = None + for constructor in InputEvent.__get_validators__(): + try: + event = constructor(init_arg) + break + except InputEventCreationError: + pass + if event: + events.append(event) + else: + raise ValueError(f"failed to create InputEvent with {init_arg = }") + + return super().__new__(cls, events) + + def __str__(self): + # only used in tests and logging + return f"EventCombination({', '.join([str(e.event_tuple) for e in self])})" + + @classmethod + def __get_validators__(cls): + """used by pydantic to create EventCombination objects""" + yield cls.from_string + yield cls.from_events + + @classmethod + def from_string(cls, init_string: str) -> EventCombination: + init_args = init_string.split("+") + return cls(*init_args) + + @classmethod + def from_events( + cls, init_events: Iterable[InputEvent | evdev.InputEvent] + ) -> EventCombination: + return cls(*init_events) + + def contains_type_and_code(self, type, code) -> bool: + """if a InputEvent contains the type and code""" + for event in self: + if event.type_and_code == (type, code): + return True + return False + + def is_problematic(self): + """Is this combination going to work properly on all systems?""" + if len(self) <= 1: + return False + + for event in self: + if event.type != ecodes.EV_KEY: + continue + + if event.code in DIFFICULT_COMBINATIONS: + return True + + return False + + def get_permutations(self): + """Get a list of EventCombination objects representing all possible permutations. + + combining a + b + c should have the same result as b + a + c. + Only the last combination remains the same in the returned result. + """ + if len(self) <= 2: + return [self] + + permutations = [] + for permutation in itertools.permutations(self[:-1]): + permutations.append(EventCombination(*permutation, self[-1])) + + return permutations + + def json_str(self) -> str: + return "+".join([event.json_str() for event in self]) + + def beautify(self) -> str: + """Get a human readable string representation.""" + result = [] + + for event in self: + + if event.type not in ecodes.bytype: + logger.error("Unknown type for %s", event) + result.append(str(event.code)) + continue + + if event.code not in ecodes.bytype[event.type]: + logger.error("Unknown combination code for %s", event) + result.append(str(event.code)) + continue + + key_name = None + + # first try to find the name in xmodmap to not display wrong + # names due to the keyboard layout + if event.type == ecodes.EV_KEY: + key_name = system_mapping.get_name(event.code) + + if key_name is None: + # if no result, look in the linux combination constants. On a german + # keyboard for example z and y are switched, which will therefore + # cause the wrong letter to be displayed. + key_name = ecodes.bytype[event.type][event.code] + if isinstance(key_name, list): + key_name = key_name[0] + + if event.type != ecodes.EV_KEY: + direction = { + # D-Pad + (ecodes.ABS_HAT0X, -1): "Left", + (ecodes.ABS_HAT0X, 1): "Right", + (ecodes.ABS_HAT0Y, -1): "Up", + (ecodes.ABS_HAT0Y, 1): "Down", + (ecodes.ABS_HAT1X, -1): "Left", + (ecodes.ABS_HAT1X, 1): "Right", + (ecodes.ABS_HAT1Y, -1): "Up", + (ecodes.ABS_HAT1Y, 1): "Down", + (ecodes.ABS_HAT2X, -1): "Left", + (ecodes.ABS_HAT2X, 1): "Right", + (ecodes.ABS_HAT2Y, -1): "Up", + (ecodes.ABS_HAT2Y, 1): "Down", + # joystick + (ecodes.ABS_X, 1): "Right", + (ecodes.ABS_X, -1): "Left", + (ecodes.ABS_Y, 1): "Down", + (ecodes.ABS_Y, -1): "Up", + (ecodes.ABS_RX, 1): "Right", + (ecodes.ABS_RX, -1): "Left", + (ecodes.ABS_RY, 1): "Down", + (ecodes.ABS_RY, -1): "Up", + # wheel + (ecodes.REL_WHEEL, -1): "Down", + (ecodes.REL_WHEEL, 1): "Up", + (ecodes.REL_HWHEEL, -1): "Left", + (ecodes.REL_HWHEEL, 1): "Right", + }.get((event.code, event.value)) + if direction is not None: + key_name += f" {direction}" + + key_name = key_name.replace("ABS_Z", "Trigger Left") + key_name = key_name.replace("ABS_RZ", "Trigger Right") + + key_name = key_name.replace("ABS_HAT0X", "DPad") + key_name = key_name.replace("ABS_HAT0Y", "DPad") + key_name = key_name.replace("ABS_HAT1X", "DPad 2") + key_name = key_name.replace("ABS_HAT1Y", "DPad 2") + key_name = key_name.replace("ABS_HAT2X", "DPad 3") + key_name = key_name.replace("ABS_HAT2Y", "DPad 3") + + key_name = key_name.replace("ABS_X", "Joystick") + key_name = key_name.replace("ABS_Y", "Joystick") + key_name = key_name.replace("ABS_RX", "Joystick 2") + key_name = key_name.replace("ABS_RY", "Joystick 2") + + key_name = key_name.replace("BTN_", "Button ") + key_name = key_name.replace("KEY_", "") + + key_name = key_name.replace("REL_", "") + key_name = key_name.replace("HWHEEL", "Wheel") + key_name = key_name.replace("WHEEL", "Wheel") + + key_name = key_name.replace("_", " ") + key_name = key_name.replace(" ", " ") + + result.append(key_name) + + return " + ".join(result) diff --git a/inputremapper/exceptions.py b/inputremapper/exceptions.py index 8f42c263..5f757bba 100644 --- a/inputremapper/exceptions.py +++ b/inputremapper/exceptions.py @@ -39,3 +39,8 @@ class UinputNotAvailable(Error): class EventNotHandled(Error): def __init__(self, event): super().__init__(f"the event {event} can not be handled") + + +class InputEventCreationError(Error): + def __init__(self, msg): + super().__init__(msg) diff --git a/inputremapper/groups.py b/inputremapper/groups.py index 16f10b52..b3b432a5 100644 --- a/inputremapper/groups.py +++ b/inputremapper/groups.py @@ -36,6 +36,7 @@ import multiprocessing import threading import asyncio import json +from typing import List import evdev from evdev.ecodes import ( @@ -53,7 +54,7 @@ from evdev.ecodes import ( ) from inputremapper.logger import logger -from inputremapper.paths import get_preset_path +from inputremapper.configs.paths import get_preset_path TABLET_KEYS = [ @@ -82,7 +83,7 @@ if not hasattr(evdev.InputDevice, "path"): def _is_gamepad(capabilities): - """Check if joystick movements are available for mapping.""" + """Check if joystick movements are available for preset.""" # A few buttons that indicate a gamepad buttons = { evdev.ecodes.BTN_BASE, @@ -252,7 +253,7 @@ class _Group: presets folder structure """ - def __init__(self, paths, names, types, key): + def __init__(self, paths: List[str], names: List[str], types: List[str], key: str): """Specify a group Parameters @@ -276,7 +277,7 @@ class _Group: """ # There might be multiple groups with the same name here when two # similar devices are connected to the computer. - self.name = sorted(names, key=len)[0] + self.name: str = sorted(names, key=len)[0] self.key = key @@ -415,7 +416,7 @@ class _Groups: """Contains and manages all groups.""" def __init__(self): - self._groups = None + self._groups: List[_Group] = None def __getattribute__(self, key): """To lazy load group info only when needed. @@ -463,7 +464,7 @@ class _Groups: """Overwrite all groups.""" self._groups = new_groups - def list_group_names(self): + def list_group_names(self) -> List[str]: """Return a list of all 'name' properties of the groups.""" return [ group.name diff --git a/inputremapper/gui/custom_mapping.py b/inputremapper/gui/active_preset.py similarity index 87% rename from inputremapper/gui/custom_mapping.py rename to inputremapper/gui/active_preset.py index e130687e..d48d0f6e 100644 --- a/inputremapper/gui/custom_mapping.py +++ b/inputremapper/gui/active_preset.py @@ -19,10 +19,10 @@ # along with input-remapper. If not, see . -"""One mapping object for the GUI application.""" +"""One preset object for the GUI application.""" -from inputremapper.mapping import Mapping +from inputremapper.configs.preset import Preset -custom_mapping = Mapping() +active_preset = Preset() diff --git a/inputremapper/gui/editor/autocompletion.py b/inputremapper/gui/editor/autocompletion.py index 091577b9..97d726d9 100644 --- a/inputremapper/gui/editor/autocompletion.py +++ b/inputremapper/gui/editor/autocompletion.py @@ -27,7 +27,7 @@ import re from gi.repository import Gdk, Gtk, GLib, GObject from evdev.ecodes import EV_KEY -from inputremapper.system_mapping import system_mapping +from inputremapper.configs.system_mapping import system_mapping from inputremapper.injection.macros.parse import ( FUNCTIONS, get_macro_argument_names, diff --git a/inputremapper/gui/editor/editor.py b/inputremapper/gui/editor/editor.py index dca74a19..bb06124d 100644 --- a/inputremapper/gui/editor/editor.py +++ b/inputremapper/gui/editor/editor.py @@ -24,12 +24,12 @@ import re -from gi.repository import Gtk, GLib, GtkSource, Gdk +from gi.repository import Gtk, GLib, Gdk from inputremapper.gui.editor.autocompletion import Autocompletion -from inputremapper.system_mapping import system_mapping -from inputremapper.gui.custom_mapping import custom_mapping -from inputremapper.key import Key +from inputremapper.configs.system_mapping import system_mapping +from inputremapper.gui.active_preset import active_preset +from inputremapper.event_combination import EventCombination from inputremapper.logger import logger from inputremapper.gui.reader import reader from inputremapper.gui.utils import CTX_KEYCODE, CTX_WARNING, CTX_ERROR @@ -46,7 +46,7 @@ class SelectionLabel(Gtk.ListBoxRow): def __init__(self): super().__init__() - self.key = None + self.combination = None self.symbol = "" label = Gtk.Label() @@ -62,21 +62,21 @@ class SelectionLabel(Gtk.ListBoxRow): self.show_all() - def set_key(self, key): - """Set the key this button represents + def set_combination(self, combination: EventCombination): + """Set the combination this button represents Parameters ---------- - key : Key + combination : EventCombination """ - self.key = key - if key: - self.label.set_label(key.beautify()) + self.combination = combination + if combination: + self.label.set_label(combination.beautify()) else: self.label.set_label("new entry") - def get_key(self): - return self.key + def get_combination(self) -> EventCombination: + return self.combination def set_label(self, label): return self.label.set_label(label) @@ -85,14 +85,14 @@ class SelectionLabel(Gtk.ListBoxRow): return self.label.get_label() def __str__(self): - return f"SelectionLabel({str(self.key)})" + return f"SelectionLabel({str(self.combination)})" def __repr__(self): return self.__str__() def ensure_everything_saved(func): - """Make sure the editor has written its changes to custom_mapping and save.""" + """Make sure the editor has written its changes to active_preset and save.""" def wrapped(self, *args, **kwargs): if self.user_interface.preset_name: @@ -120,7 +120,7 @@ class Editor: self.window = self.get("window") self.timeout = GLib.timeout_add(100, self.check_add_new_key) - self.active_selection_label = None + self.active_selection_label: SelectionLabel = None selection_label_listbox = self.get("selection_label_listbox") selection_label_listbox.connect("row-selected", self.on_mapping_selected) @@ -185,7 +185,7 @@ class Editor: Otherwise the inputs will be read and then saved into the next preset. """ if self.active_selection_label: - self.set_key(None) + self.set_combination(None) self.disable_symbol_input(clear=True) self.set_target_selection("keyboard") # sane default @@ -294,7 +294,7 @@ class Editor: selection_label_listbox = selection_label_listbox.get_children() for selection_label in selection_label_listbox: - if selection_label.get_key() is None: + if selection_label.get_combination() is None: # unfinished row found break else: @@ -342,7 +342,7 @@ class Editor: @ensure_everything_saved def on_mapping_selected(self, _=None, selection_label=None): - """One of the buttons in the left "key" column was clicked. + """One of the buttons in the left "combination" column was clicked. Load the information from that mapping entry into the editor. """ @@ -351,20 +351,20 @@ class Editor: if selection_label is None: return - key = selection_label.key - self.set_key(key) + combination = selection_label.combination + self.set_combination(combination) - if key is None: + if combination is None: self.disable_symbol_input(clear=True) # default target should fit in most cases self.set_target_selection("keyboard") - # symbol input disabled until a key is configured + # symbol input disabled until a combination is configured self.disable_target_selector() - # symbol input disabled until a key is configured + # symbol input disabled until a combination is configured else: - if custom_mapping.get_mapping(key): - self.set_symbol_input_text(custom_mapping.get_mapping(key)[0]) - self.set_target_selection(custom_mapping.get_mapping(key)[1]) + if active_preset.get_mapping(combination): + self.set_symbol_input_text(active_preset.get_mapping(combination)[0]) + self.set_target_selection(active_preset.get_mapping(combination)[1]) self.enable_symbol_input() self.enable_target_selector() @@ -380,14 +380,14 @@ class Editor: @ensure_everything_saved def load_custom_mapping(self): - """Display the entries in custom_mapping.""" + """Display the entries in active_preset.""" selection_label_listbox = self.get("selection_label_listbox") selection_label_listbox.forall(selection_label_listbox.remove) - for key, _ in custom_mapping: + for key, _ in active_preset: selection_label = SelectionLabel() - selection_label.set_key(key) + selection_label.set_combination(key) selection_label_listbox.insert(selection_label, -1) self.check_add_new_key() @@ -410,19 +410,19 @@ class Editor: def get_target_selector(self): return self.get("target-selector") - def set_key(self, key): + def set_combination(self, combination): """Show what the user is currently pressing in the user interface.""" - self.active_selection_label.set_key(key) + self.active_selection_label.set_combination(combination) - def get_key(self): - """Get the Key object from the left column. + def get_combination(self): + """Get the EventCombination object from the left column. Or None if no code is mapped on this row. """ if self.active_selection_label is None: return None - return self.active_selection_label.key + return self.active_selection_label.combination def set_symbol_input_text(self, symbol): self.get("code_editor").get_buffer().set_text(symbol or "") @@ -437,11 +437,11 @@ class Editor: def get_symbol_input_text(self): """Get the assigned symbol from the text input. - This might not be stored in custom_mapping yet, and might therefore also not + This might not be stored in active_preset yet, and might therefore also not be part of the preset json file yet. If there is no symbol, this returns None. This is important for some other - logic down the road in custom_mapping or something. + logic down the road in active_preset or something. """ buffer = self.get("code_editor").get_buffer() symbol = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True) @@ -469,7 +469,7 @@ class Editor: return self._reset_keycode_consumption() reader.clear() - if not self.user_interface.can_modify_mapping(): + if not self.user_interface.can_modify_preset(): # because the device is in grab mode by the daemon and # therefore the original keycode inaccessible logger.info("Cannot change keycodes while injecting") @@ -487,12 +487,12 @@ class Editor: ): return - key = self.get_key() + key = self.get_combination() if key is not None: - custom_mapping.clear(key) + active_preset.clear(key) # make sure there is no outdated information lying around in memory - self.set_key(None) + self.set_combination(None) self.load_custom_mapping() @@ -521,48 +521,48 @@ class Editor: if symbol != correct_case: self.get_text_input().get_buffer().set_text(correct_case) - # make sure the custom_mapping is up to date - key = self.get_key() + # make sure the active_preset is up to date + key = self.get_combination() if correct_case is not None and key is not None and target is not None: - custom_mapping.change(key, target, correct_case) + active_preset.change(key, target, correct_case) # save to disk if required - if custom_mapping.has_unsaved_changes(): + if active_preset.has_unsaved_changes(): self.user_interface.save_preset() def is_waiting_for_input(self): - """Check if the user is interacting with the ToggleButton for key recording.""" + """Check if the user is interacting with the ToggleButton for combination recording.""" return self.get_recording_toggle().get_active() - def consume_newest_keycode(self, key): + def consume_newest_keycode(self, combination): """To capture events from keyboards, mice and gamepads. Parameters ---------- - key : Key or None + combination : EventCombination or None """ self._switch_focus_if_complete() - if key is None: + if combination is None: return if not self.is_waiting_for_input(): return - if not isinstance(key, Key): - raise TypeError("Expected new_key to be a Key object") + if not isinstance(combination, EventCombination): + raise TypeError("Expected new_key to be a EventCombination object") # keycode is already set by some other row - existing = custom_mapping.get_mapping(key) + existing = active_preset.get_mapping(combination) if existing is not None: existing = list(existing) existing[0] = re.sub(r"\s", "", existing[0]) - msg = f'"{key.beautify()}" already mapped to "{tuple(existing)}"' - logger.info("%s %s", key, msg) + msg = f'"{combination.beautify()}" already mapped to "{tuple(existing)}"' + logger.info("%s %s", combination, msg) self.user_interface.show_status(CTX_KEYCODE, msg) return True - if key.is_problematic(): + if combination.is_problematic(): self.user_interface.show_status( CTX_WARNING, "ctrl, alt and shift may not combine properly", @@ -573,17 +573,17 @@ class Editor: # the newest_keycode is populated since the ui regularly polls it # in order to display it in the status bar. - previous_key = self.get_key() + previous_key = self.get_combination() # it might end up being a key combination, wait for more self._input_has_arrived = True # keycode didn't change, do nothing - if key == previous_key: + if combination == previous_key: logger.debug("%s didn't change", previous_key) return - self.set_key(key) + self.set_combination(combination) symbol = self.get_symbol_input_text() target = self.get_target_selection() @@ -593,8 +593,11 @@ class Editor: return # else, the keycode has changed, the symbol is set, all good - custom_mapping.change( - new_key=key, target=target, symbol=symbol, previous_key=previous_key + active_preset.change( + new_combination=combination, + target=target, + symbol=symbol, + previous_combination=previous_key, ) def _switch_focus_if_complete(self): @@ -612,7 +615,7 @@ class Editor: return all_keys_released = reader.get_unreleased_keys() is None - if all_keys_released and self._input_has_arrived and self.get_key(): + if all_keys_released and self._input_has_arrived and self.get_combination(): # A key was pressed and then released. # Switch to the symbol. idle_add this so that the # keycode event won't write into the symbol input as well. diff --git a/inputremapper/gui/reader.py b/inputremapper/gui/reader.py index ba6cef05..f87c56c0 100644 --- a/inputremapper/gui/reader.py +++ b/inputremapper/gui/reader.py @@ -24,17 +24,17 @@ see gui.helper.helper """ - -import evdev +from typing import Optional from evdev.ecodes import EV_REL +from inputremapper.input_event import InputEvent from inputremapper.logger import logger -from inputremapper.key import Key +from inputremapper.event_combination import EventCombination from inputremapper.groups import groups, GAMEPAD from inputremapper.ipc.pipe import Pipe from inputremapper.gui.helper import TERMINATE, REFRESH_GROUPS from inputremapper import utils -from inputremapper.gui.custom_mapping import custom_mapping +from inputremapper.gui.active_preset import active_preset from inputremapper.user import USER @@ -83,7 +83,7 @@ class Reader: self._groups_updated = False # assume the ui will react accordingly return outdated - def _get_event(self, message): + def _get_event(self, message) -> Optional[InputEvent]: """Return an InputEvent if the message contains one. None otherwise.""" message_type = message["type"] message_body = message["message"] @@ -96,13 +96,13 @@ class Reader: return None if message_type == "event": - return evdev.InputEvent(*message_body) + return InputEvent(*message_body) logger.error('Received unknown message "%s"', message) return None def read(self): - """Get the newest key/combination as Key object. + """Get the newest key/combination as EventCombination object. Only reports keys from down-events. @@ -135,32 +135,28 @@ class Reader: continue gamepad = GAMEPAD in self.group.types - if not utils.should_map_as_btn(event, custom_mapping, gamepad): + if not utils.should_map_as_btn(event, active_preset, gamepad): continue - event_tuple = (event.type, event.code, event.value) - - type_code = (event.type, event.code) - if event.value == 0: - logger.debug_key(event_tuple, "release") - self._release(type_code) + logger.debug_key(event.event_tuple, "release") + self._release(event.type_and_code) continue - if self._unreleased.get(type_code) == event_tuple: - logger.debug_key(event_tuple, "duplicate key down") - self._debounce_start(event_tuple) + if self._unreleased.get(event.type_and_code) == event.event_tuple: + logger.debug_key(event.event_tuple, "duplicate key down") + self._debounce_start(event.event_tuple) continue # to keep track of combinations. # "I have got this release event, what was this for?" A release # event for a D-Pad axis might be any direction, hence this maps # from release to input in order to remember it. Since all release - # events have value 0, the value is not used in the key. + # events have value 0, the value is not used in the combination. key_down_received = True - logger.debug_key(event_tuple, "down") - self._unreleased[type_code] = event_tuple - self._debounce_start(event_tuple) + logger.debug_key(event.event_tuple, "down") + self._unreleased[event.type_and_code] = event.event_tuple + self._debounce_start(event.event_tuple) previous_event = event if not key_down_received: @@ -172,13 +168,13 @@ class Reader: self.previous_event = previous_event if len(self._unreleased) > 0: - result = Key(*self._unreleased.values()) + result = EventCombination.from_events(self._unreleased.values()) if result == self.previous_result: # don't return the same stuff twice return None self.previous_result = result - logger.debug_key(result.keys, "read result") + logger.debug_key(result, "read result") return result @@ -214,13 +210,13 @@ class Reader: self.previous_result = None def get_unreleased_keys(self): - """Get a Key object of the current keyboard state.""" + """Get a EventCombination object of the current keyboard state.""" unreleased = list(self._unreleased.values()) if len(unreleased) == 0: return None - return Key(*unreleased) + return EventCombination.from_events(unreleased) def _release(self, type_code): """Modify the state to recognize the releasing of the key.""" diff --git a/inputremapper/gui/user_interface.py b/inputremapper/gui/user_interface.py index bb8498e7..76102ec5 100644 --- a/inputremapper/gui/user_interface.py +++ b/inputremapper/gui/user_interface.py @@ -29,13 +29,14 @@ import sys from evdev._ecodes import EV_KEY from gi.repository import Gtk, GtkSource, Gdk, GLib, GObject +from inputremapper.input_event import InputEvent -from inputremapper.data import get_data_path -from inputremapper.paths import get_config_path -from inputremapper.system_mapping import system_mapping -from inputremapper.gui.custom_mapping import custom_mapping +from inputremapper.configs.data import get_data_path +from inputremapper.configs.paths import get_config_path +from inputremapper.configs.system_mapping import system_mapping +from inputremapper.gui.active_preset import active_preset from inputremapper.gui.utils import HandlerDisabled -from inputremapper.presets import ( +from inputremapper.configs.preset import ( find_newest_preset, get_presets, delete_preset, @@ -53,12 +54,12 @@ from inputremapper.groups import ( MOUSE, ) from inputremapper.gui.editor.editor import Editor -from inputremapper.key import Key +from inputremapper.event_combination import EventCombination from inputremapper.gui.reader import reader from inputremapper.gui.helper import is_helper_running from inputremapper.injection.injector import RUNNING, FAILED, NO_GRAB from inputremapper.daemon import Daemon -from inputremapper.config import config +from inputremapper.configs.global_config import global_config from inputremapper.injection.macros.parse import is_this_a_macro, parse from inputremapper.injection.global_uinputs import global_uinputs from inputremapper.gui.utils import ( @@ -124,7 +125,7 @@ def on_close_about(about, _): def ensure_everything_saved(func): - """Make sure the editor has written its changes to custom_mapping and save.""" + """Make sure the editor has written its changes to active_preset and save.""" def wrapped(self, *args, **kwargs): if self.preset_name: @@ -136,7 +137,7 @@ def ensure_everything_saved(func): class UserInterface: - """The key mapper gtk window.""" + """The input-remapper gtk window.""" def __init__(self): self.dbus = None @@ -304,15 +305,15 @@ class UserInterface: speed = self.get("joystick_mouse_speed") with HandlerDisabled(left_purpose, self.on_left_joystick_changed): - value = custom_mapping.get("gamepad.joystick.left_purpose") + value = active_preset.get("gamepad.joystick.left_purpose") left_purpose.set_active_id(value) with HandlerDisabled(right_purpose, self.on_right_joystick_changed): - value = custom_mapping.get("gamepad.joystick.right_purpose") + value = active_preset.get("gamepad.joystick.right_purpose") right_purpose.set_active_id(value) with HandlerDisabled(speed, self.on_joystick_mouse_speed_changed): - value = custom_mapping.get("gamepad.joystick.pointer_speed") + value = active_preset.get("gamepad.joystick.pointer_speed") range_value = math.log(value, 2) speed.set_value(range_value) @@ -364,15 +365,15 @@ class UserInterface: def populate_presets(self): """Show the available presets for the selected device. - This will destroy unsaved changes in the custom_mapping. + This will destroy unsaved changes in the active_preset. """ presets = get_presets(self.group.name) if len(presets) == 0: new_preset = get_available_preset_name(self.group.name) - custom_mapping.empty() + active_preset.empty() path = self.group.get_preset_path(new_preset) - custom_mapping.save(path) + active_preset.save(path) presets = [new_preset] else: logger.debug('"%s" presets: "%s"', self.group.name, '", "'.join(presets)) @@ -389,8 +390,8 @@ class UserInterface: # and select the newest one (on the top). triggers on_select_preset preset_selection.set_active(0) - def can_modify_mapping(self, *_) -> bool: - """if changing the mapping is possible.""" + def can_modify_preset(self, *_) -> bool: + """if changing the preset is possible.""" return self.dbus.get_state(self.group.key) != RUNNING def consume_newest_keycode(self): @@ -413,7 +414,7 @@ class UserInterface: @if_group_selected def on_restore_defaults_clicked(self, *_): - """Stop injecting the mapping.""" + """Stop injecting the preset.""" self.dbus.stop_injecting(self.group.key) self.show_status(CTX_APPLY, "Applied the system default") GLib.timeout_add(100, self.show_device_mapping_status) @@ -459,12 +460,12 @@ class UserInterface: def check_macro_syntax(self): """Check if the programmed macros are allright.""" self.show_status(CTX_MAPPING, None) - for key, output in custom_mapping: + for key, output in active_preset: output = output[0] if not is_this_a_macro(output): continue - error = parse(output, custom_mapping, return_errors=True) + error = parse(output, active_preset, return_errors=True) if error is None: continue @@ -484,9 +485,9 @@ class UserInterface: # if the old preset was being autoloaded, change the # name there as well - is_autoloaded = config.is_autoloaded(self.group.key, self.preset_name) + is_autoloaded = global_config.is_autoloaded(self.group.key, self.preset_name) if is_autoloaded: - config.set_autoload_preset(self.group.key, new_name) + global_config.set_autoload_preset(self.group.key, new_name) self.get("preset_name_input").set_text("") self.populate_presets() @@ -495,10 +496,10 @@ class UserInterface: def on_delete_preset_clicked(self, *_): """Delete a preset from the file system.""" accept = Gtk.ResponseType.ACCEPT - if len(custom_mapping) > 0 and self.show_confirm_delete() != accept: + if len(active_preset) > 0 and self.show_confirm_delete() != accept: return - # avoid having the text of the symbol input leak into the custom_mapping again + # avoid having the text of the symbol input leak into the active_preset again # via a gazillion hooks, causing the preset to be saved again after deleting. self.editor.clear() @@ -511,7 +512,7 @@ class UserInterface: """Apply a preset without saving changes.""" self.save_preset() - if custom_mapping.num_saved_keys == 0: + if active_preset.num_saved_keys == 0: logger.error("Cannot apply empty preset file") # also helpful for first time use self.show_status(CTX_ERROR, "You need to add keys and save first") @@ -521,7 +522,7 @@ class UserInterface: logger.info('Applying preset "%s" for "%s"', preset, self.group.key) if not self.button_left_warn: - if custom_mapping.dangerously_mapped_btn_left(): + if active_preset.dangerously_mapped_btn_left(): self.show_status( CTX_ERROR, "This would disable your click button", @@ -533,9 +534,11 @@ class UserInterface: if not self.unreleased_warn: unreleased = reader.get_unreleased_keys() - if unreleased is not None and unreleased != Key.btn_left(): + if unreleased is not None and unreleased != EventCombination( + InputEvent.btn_left() + ): # it's super annoying if that happens and may break the user - # input in such a way to prevent disabling the mapping + # input in such a way to prevent disabling the preset logger.error( "Tried to apply a preset while keys were held down: %s", unreleased ) @@ -561,7 +564,7 @@ class UserInterface: """Load the preset automatically next time the user logs in.""" key = self.group.key preset = self.preset_name - config.set_autoload_preset(key, preset if active else None) + global_config.set_autoload_preset(key, preset if active else None) # tell the service to refresh its config self.dbus.set_config_dir(get_config_path()) @@ -594,7 +597,7 @@ class UserInterface: if state == RUNNING: msg = f'Applied preset "{self.preset_name}"' - if custom_mapping.get_mapping(Key.btn_left()): + if active_preset.get_mapping(EventCombination(InputEvent.btn_left())): msg += ", CTRL + DEL to stop" self.show_status(CTX_APPLY, msg) @@ -651,10 +654,10 @@ class UserInterface: else: new_preset = get_available_preset_name(name) self.editor.clear() - custom_mapping.empty() + active_preset.empty() path = self.group.get_preset_path(new_preset) - custom_mapping.save(path) + active_preset.save(path) self.get("preset_selection").append(new_preset, new_preset) # triggers on_select_preset self.get("preset_selection").set_active_id(new_preset) @@ -683,52 +686,54 @@ class UserInterface: self.editor.clear_mapping_list() self.preset_name = preset - custom_mapping.load(self.group.get_preset_path(preset)) + active_preset.load(self.group.get_preset_path(preset)) self.editor.load_custom_mapping() autoload_switch = self.get("preset_autoload_switch") with HandlerDisabled(autoload_switch, self.on_autoload_switch): - is_autoloaded = config.is_autoloaded(self.group.key, self.preset_name) + is_autoloaded = global_config.is_autoloaded( + self.group.key, self.preset_name + ) autoload_switch.set_active(is_autoloaded) self.get("preset_name_input").set_text("") self.initialize_gamepad_config() - custom_mapping.set_has_unsaved_changes(False) + active_preset.set_has_unsaved_changes(False) def on_left_joystick_changed(self, dropdown): """Set the purpose of the left joystick.""" purpose = dropdown.get_active_id() - custom_mapping.set("gamepad.joystick.left_purpose", purpose) + active_preset.set("gamepad.joystick.left_purpose", purpose) self.save_preset() def on_right_joystick_changed(self, dropdown): """Set the purpose of the right joystick.""" purpose = dropdown.get_active_id() - custom_mapping.set("gamepad.joystick.right_purpose", purpose) + active_preset.set("gamepad.joystick.right_purpose", purpose) self.save_preset() def on_joystick_mouse_speed_changed(self, gtk_range): """Set how fast the joystick moves the mouse.""" speed = 2 ** gtk_range.get_value() - custom_mapping.set("gamepad.joystick.pointer_speed", speed) + active_preset.set("gamepad.joystick.pointer_speed", speed) def save_preset(self, *_): - """Write changes in the custom_mapping to disk.""" - if not custom_mapping.has_unsaved_changes(): + """Write changes in the active_preset to disk.""" + if not active_preset.has_unsaved_changes(): # optimization, and also avoids tons of redundant logs - logger.debug("Not saving because mapping did not change") + logger.debug("Not saving because preset did not change") return try: assert self.preset_name is not None path = self.group.get_preset_path(self.preset_name) - custom_mapping.save(path) + active_preset.save(path) - # after saving the config, its modification date will be the + # after saving the preset, its modification date will be the # newest, so populate_presets will automatically select the # right one again. self.populate_presets() @@ -737,7 +742,7 @@ class UserInterface: self.show_status(CTX_ERROR, "Permission denied!", error) logger.error(error) - for _, mapping in custom_mapping: + for _, mapping in active_preset: if not mapping: continue diff --git a/inputremapper/injection/consumer_control.py b/inputremapper/injection/consumer_control.py index f06b7765..73a6c21d 100644 --- a/inputremapper/injection/consumer_control.py +++ b/inputremapper/injection/consumer_control.py @@ -20,15 +20,13 @@ """Because multiple calls to async_read_loop won't work.""" - - import asyncio - import evdev from inputremapper.injection.consumers.joystick_to_mouse import JoystickToMouse from inputremapper.injection.consumers.keycode_mapper import KeycodeMapper from inputremapper.logger import logger +from inputremapper.injection.context import Context consumer_classes = [ @@ -48,7 +46,12 @@ class ConsumerControl: needs to be created multiple times. """ - def __init__(self, context, source, forward_to): + def __init__( + self, + context: Context, + source: evdev.InputDevice, + forward_to: evdev.UInput, + ) -> None: """Initialize all consumers Parameters diff --git a/inputremapper/injection/consumers/joystick_to_mouse.py b/inputremapper/injection/consumers/joystick_to_mouse.py index ef05d77f..5fdf7454 100644 --- a/inputremapper/injection/consumers/joystick_to_mouse.py +++ b/inputremapper/injection/consumers/joystick_to_mouse.py @@ -39,7 +39,7 @@ from evdev.ecodes import ( ) from inputremapper.logger import logger -from inputremapper.config import MOUSE, WHEEL +from inputremapper.configs.global_config import MOUSE, WHEEL from inputremapper import utils from inputremapper.injection.consumers.consumer import Consumer from inputremapper.groups import classify, GAMEPAD @@ -208,11 +208,11 @@ class JoystickToMouse(Consumer): its position, this will keep injecting the mouse movement events. """ abs_range = self._abs_range - mapping = self.context.mapping - pointer_speed = mapping.get("gamepad.joystick.pointer_speed") - non_linearity = mapping.get("gamepad.joystick.non_linearity") - x_scroll_speed = mapping.get("gamepad.joystick.x_scroll_speed") - y_scroll_speed = mapping.get("gamepad.joystick.y_scroll_speed") + preset = self.context.preset + pointer_speed = preset.get("gamepad.joystick.pointer_speed") + non_linearity = preset.get("gamepad.joystick.non_linearity") + x_scroll_speed = preset.get("gamepad.joystick.x_scroll_speed") + y_scroll_speed = preset.get("gamepad.joystick.y_scroll_speed") max_speed = 2 ** 0.5 # for normalized abs event values if abs_range is not None: diff --git a/inputremapper/injection/consumers/keycode_mapper.py b/inputremapper/injection/consumers/keycode_mapper.py index 1db86d5c..201a2d9c 100644 --- a/inputremapper/injection/consumers/keycode_mapper.py +++ b/inputremapper/injection/consumers/keycode_mapper.py @@ -32,7 +32,7 @@ from evdev.ecodes import EV_KEY, EV_ABS import inputremapper.exceptions from inputremapper.logger import logger -from inputremapper.system_mapping import DISABLE_CODE +from inputremapper.configs.system_mapping import DISABLE_CODE from inputremapper import utils from inputremapper.injection.consumers.consumer import Consumer from inputremapper.utils import RELEASE @@ -219,11 +219,12 @@ class KeycodeMapper(Consumer): # some type checking, prevents me from forgetting what that stuff # is supposed to be when writing tests. - for key in self.context.key_to_code: - for sub_key in key: - if abs(sub_key[2]) > 1: + for combination in self.context.key_to_code: + for event in combination: + if abs(event.value) > 1: raise ValueError( - f"Expected values to be one of -1, 0 or 1, " f"but got {key}" + f"Expected values to be one of -1, 0 or 1, " + f"but got {combination}" ) def is_enabled(self): @@ -232,7 +233,7 @@ class KeycodeMapper(Consumer): return len(self.context.key_to_code) > 0 or len(self.context.macros) > 0 def is_handled(self, event): - return utils.should_map_as_btn(event, self.context.mapping, self._gamepad) + return utils.should_map_as_btn(event, self.context.preset, self._gamepad) async def run(self): """Provide a debouncer to inject wheel releases.""" diff --git a/inputremapper/injection/context.py b/inputremapper/injection/context.py index 21ae6c90..39ba6379 100644 --- a/inputremapper/injection/context.py +++ b/inputremapper/injection/context.py @@ -20,18 +20,19 @@ """Stores injection-process wide information.""" - +from typing import Awaitable, List, Dict, Tuple, Protocol, Set from inputremapper.logger import logger from inputremapper.injection.macros.parse import parse, is_this_a_macro -from inputremapper.system_mapping import system_mapping -from inputremapper.config import NONE, MOUSE, WHEEL, BUTTONS +from inputremapper.configs.system_mapping import system_mapping +from inputremapper.event_combination import EventCombination +from inputremapper.configs.global_config import NONE, MOUSE, WHEEL, BUTTONS class Context: """Stores injection-process wide information. - In some ways this is a wrapper for the mapping that derives some + In some ways this is a wrapper for the preset that derives some information that is specifically important to the injection. The information in the context does not change during the injection. @@ -48,23 +49,23 @@ class Context: Members ------- - mapping : Mapping - The mapping that is the source of key_to_code and macros, + preset : Preset + The preset that is the source of key_to_code and macros, only used to query config values. key_to_code : dict - Mapping of ((type, code, value),) to linux-keycode + Preset of ((type, code, value),) to linux-keycode or multiple of those like ((...), (...), ...) for combinations. Combinations need to be present in every possible valid ordering. e.g. shift + alt + a and alt + shift + a. This is needed to query keycodes more efficiently without having - to search mapping each time. + to search preset each time. macros : dict - Mapping of ((type, code, value),) to Macro objects. + Preset of ((type, code, value),) to Macro objects. Combinations work similar as in key_to_code """ - def __init__(self, mapping): - self.mapping = mapping + def __init__(self, preset): + self.preset = preset # avoid searching through the mapping at runtime, # might be a bit expensive @@ -81,21 +82,21 @@ class Context: For efficiency, so that the config doesn't have to be read during runtime repeatedly. """ - self.left_purpose = self.mapping.get("gamepad.joystick.left_purpose") - self.right_purpose = self.mapping.get("gamepad.joystick.right_purpose") + self.left_purpose = self.preset.get("gamepad.joystick.left_purpose") + self.right_purpose = self.preset.get("gamepad.joystick.right_purpose") def _parse_macros(self): """To quickly get the target macro during operation.""" logger.debug("Parsing macros") macros = {} - for key, output in self.mapping: + for combination, output in self.preset: if is_this_a_macro(output[0]): macro = parse(output[0], self) if macro is None: continue - for permutation in key.get_permutations(): - macros[permutation.keys] = (macro, output[1]) + for permutation in combination.get_permutations(): + macros[permutation] = (macro, output[1]) if len(macros) == 0: logger.debug("No macros configured") @@ -111,7 +112,7 @@ class Context: ((1, 5, 1), (1, 4, 1)): (4, "gamepad") """ key_to_code = {} - for key, output in self.mapping: + for combination, output in self.preset: if is_this_a_macro(output[0]): continue @@ -120,27 +121,27 @@ class Context: logger.error('Don\'t know what "%s" is', output[0]) continue - for permutation in key.get_permutations(): - if permutation.keys[-1][-1] not in [-1, 1]: + for permutation in combination.get_permutations(): + if permutation[-1].value not in [-1, 1]: logger.error( "Expected values to be -1 or 1 at this point: %s", - permutation.keys, + permutation, ) - key_to_code[permutation.keys] = (target_code, output[1]) + key_to_code[permutation] = (target_code, output[1]) return key_to_code - def is_mapped(self, key): - """Check if this key is used for macros or mappings. + def is_mapped(self, combination): + """Check if this combination is used for macros or mappings. Parameters ---------- - key : tuple of tuple of int + combination : tuple of tuple of int One or more 3-tuples of type, code, action, for example ((EV_KEY, KEY_A, 1), (EV_ABS, ABS_X, -1)) or ((EV_KEY, KEY_B, 1),) """ - return key in self.macros or key in self.key_to_code + return combination in self.macros or combination in self.key_to_code def maps_joystick(self): """If at least one of the joysticks will serve a special purpose.""" diff --git a/inputremapper/injection/injector.py b/inputremapper/injection/injector.py index 55e4c191..d4ef7629 100644 --- a/inputremapper/injection/injector.py +++ b/inputremapper/injection/injector.py @@ -19,21 +19,29 @@ # along with input-remapper. If not, see . -"""Keeps injecting keycodes in the background based on the mapping.""" - +"""Keeps injecting keycodes in the background based on the preset.""" +import os import asyncio import time import multiprocessing import evdev +from typing import Dict, List, Optional + +from inputremapper.configs.preset import Preset + from inputremapper.logger import logger -from inputremapper.groups import classify, GAMEPAD +from inputremapper.groups import classify, GAMEPAD, _Group from inputremapper.injection.context import Context from inputremapper.injection.numlock import set_numlock, is_numlock_on, ensure_numlock from inputremapper.injection.consumer_control import ConsumerControl +from inputremapper.event_combination import EventCombination + +CapabilitiesDict = Dict[int, List[int]] +GroupSources = List[evdev.InputDevice] DEV_NAME = "input-remapper" @@ -52,21 +60,18 @@ STOPPED = 5 NO_GRAB = 6 -def is_in_capabilities(key, capabilities): - """Are this key or one of its sub keys in the capabilities? - - Parameters - ---------- - key : Key - """ - for sub_key in key: - if sub_key[1] in capabilities.get(sub_key[0], []): +def is_in_capabilities( + combination: EventCombination, capabilities: CapabilitiesDict +) -> bool: + """Are this combination or one of its sub keys in the capabilities?""" + for event in combination: + if event.code in capabilities.get(event.type, []): return True return False -def get_udev_name(name, suffix): +def get_udev_name(name: str, suffix: str) -> str: """Make sure the generated name is not longer than 80 chars.""" max_len = 80 # based on error messages remaining_len = max_len - len(DEV_NAME) - len(suffix) - 2 @@ -83,16 +88,23 @@ class Injector(multiprocessing.Process): hardware-device that is being mapped. """ + group: _Group + preset: Preset + context: Optional[Context] + _state: int + _msg_pipe: multiprocessing.Pipe + _consumer_controls: List[ConsumerControl] + regrab_timeout = 0.2 - def __init__(self, group, mapping): + def __init__(self, group: _Group, preset: Preset) -> None: """ Parameters ---------- group : _Group the device group - mapping : Mapping + preset : Preset """ self.group = group self._state = UNKNOWN @@ -101,7 +113,7 @@ class Injector(multiprocessing.Process): # the new process self._msg_pipe = multiprocessing.Pipe() - self.mapping = mapping + self.preset = preset self.context = None # only needed inside the injection process self._consumer_controls = [] @@ -110,7 +122,7 @@ class Injector(multiprocessing.Process): """Functions to interact with the running process""" - def get_state(self): + def get_state(self) -> int: """Get the state of the injection. Can be safely called from the main process. @@ -142,7 +154,7 @@ class Injector(multiprocessing.Process): return self._state @ensure_numlock - def stop_injecting(self): + def stop_injecting(self) -> None: """Stop injecting keycodes. Can be safely called from the main procss. @@ -153,20 +165,20 @@ class Injector(multiprocessing.Process): """Process internal stuff""" - def _grab_devices(self): + def _grab_devices(self) -> GroupSources: """Grab all devices that are needed for the injection.""" sources = [] for path in self.group.paths: source = self._grab_device(path) if source is None: # this path doesn't need to be grabbed for injection, because - # it doesn't provide the events needed to execute the mapping + # it doesn't provide the events needed to execute the preset continue sources.append(source) return sources - def _grab_device(self, path): + def _grab_device(self, path: os.PathLike) -> Optional[evdev.InputDevice]: """Try to grab the device, return None if not needed/possible. Without grab, original events from it would reach the display server @@ -181,7 +193,7 @@ class Injector(multiprocessing.Process): capabilities = device.capabilities(absinfo=False) needed = False - for key, _ in self.context.mapping: + for key, _ in self.context.preset: if is_in_capabilities(key, capabilities): logger.debug('Grabbing "%s" because of "%s"', path, key) needed = True @@ -221,7 +233,7 @@ class Injector(multiprocessing.Process): return device - def _copy_capabilities(self, input_device): + def _copy_capabilities(self, input_device: evdev.InputDevice) -> CapabilitiesDict: """Copy capabilities for a new device.""" ecodes = evdev.ecodes @@ -243,7 +255,7 @@ class Injector(multiprocessing.Process): return capabilities - async def _msg_listener(self): + async def _msg_listener(self) -> None: """Wait for messages from the main process to do special stuff.""" loop = asyncio.get_event_loop() while True: @@ -259,7 +271,7 @@ class Injector(multiprocessing.Process): loop.stop() return - def run(self): + def run(self) -> None: """The injection worker that keeps injecting until terminated. Stuff is non-blocking by using asyncio in order to do multiple things @@ -286,7 +298,7 @@ class Injector(multiprocessing.Process): # called. # - benefit: writing macros that listen for events from other devices - logger.info('Starting injecting the mapping for "%s"', self.group.key) + logger.info('Starting injecting the preset for "%s"', self.group.key) # create a new event loop, because somehow running an infinite loop # that sleeps on iterations (joystick_to_mouse) in one process causes @@ -297,7 +309,7 @@ class Injector(multiprocessing.Process): # create this within the process after the event loop creation, # so that the macros use the correct loop - self.context = Context(self.mapping) + self.context = Context(self.preset) # grab devices as early as possible. If events appear that won't get # released anymore before the grab they appear to be held down @@ -337,9 +349,11 @@ class Injector(multiprocessing.Process): try: loop.run_until_complete(asyncio.gather(*coroutines)) - except RuntimeError: - # stopped event loop most likely - pass + except RuntimeError as error: + # the loop might have been stopped via a `CLOSE` message, + # which causes the error message below. This is expected behavior + if str(error) != "Event loop stopped before Future completed.": + raise error except OSError as error: logger.error("Failed to run injector coroutines: %s", str(error)) diff --git a/inputremapper/injection/macros/macro.py b/inputremapper/injection/macros/macro.py index 31d45a71..1321e9ae 100644 --- a/inputremapper/injection/macros/macro.py +++ b/inputremapper/injection/macros/macro.py @@ -42,7 +42,7 @@ import re from evdev.ecodes import ecodes, EV_KEY, EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL from inputremapper.logger import logger -from inputremapper.system_mapping import system_mapping +from inputremapper.configs.system_mapping import system_mapping from inputremapper.ipc.shared_dict import SharedDict from inputremapper.utils import PRESS, PRESS_NEGATIVE @@ -280,8 +280,7 @@ class Macro: # newly arriving events are only interesting if they arrive after the # macro started self._new_event_arrived.clear() - - self.keystroke_sleep_ms = self.context.mapping.get("macros.keystroke_sleep_ms") + self.keystroke_sleep_ms = self.context.preset.get("macros.keystroke_sleep_ms") self.running = True for task in self.tasks: diff --git a/inputremapper/input_event.py b/inputremapper/input_event.py new file mode 100644 index 00000000..8edd8da0 --- /dev/null +++ b/inputremapper/input_event.py @@ -0,0 +1,135 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# input-remapper - GUI for device specific keyboard mappings +# Copyright (C) 2022 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 . +from __future__ import annotations + +import evdev + +from dataclasses import dataclass +from typing import Tuple + +from inputremapper.exceptions import InputEventCreationError + + +@dataclass(frozen=True, slots=True) +class InputEvent: + """ + the evnet used by inputremapper + + as a drop in replacement for evdev.InputEvent + """ + + sec: int + usec: int + type: int + code: int + value: int + + def __hash__(self): + return hash((self.type, self.code, self.value)) + + def __eq__(self, other): + if isinstance(other, InputEvent) or isinstance(other, evdev.InputEvent): + return self.event_tuple == (other.type, other.code, other.value) + if isinstance(other, tuple): + return self.event_tuple == other + return False + + @classmethod + def __get_validators__(cls): + """used by pydantic and EventCombination to create InputEvent objects""" + yield cls.from_event + yield cls.from_tuple + yield cls.from_string + + @classmethod + def from_event(cls, event: evdev.InputEvent) -> InputEvent: + """create a InputEvent from another InputEvent or evdev.InputEvent""" + try: + return cls(event.sec, event.usec, event.type, event.code, event.value) + except AttributeError: + raise InputEventCreationError( + f"failed to create InputEvent from {event = }" + ) + + @classmethod + def from_string(cls, string: str) -> InputEvent: + """create a InputEvent from a string like 'type, code, value'""" + try: + t, c, v = string.split(",") + return cls(0, 0, int(t), int(c), int(v)) + except (ValueError, AttributeError): + raise InputEventCreationError( + f"failed to create InputEvent from {string = !r}" + ) + + @classmethod + def from_tuple(cls, event_tuple: Tuple[int, int, int]) -> InputEvent: + """create a InputEvent from a (type, code, value) tuple""" + try: + if len(event_tuple) != 3: + raise InputEventCreationError( + f"failed to create InputEvent {event_tuple = }" + f" must have length 3" + ) + return cls( + 0, 0, int(event_tuple[0]), int(event_tuple[1]), int(event_tuple[2]) + ) + except ValueError: + raise InputEventCreationError( + f"failed to create InputEvent from {event_tuple = }" + ) + except TypeError: + raise InputEventCreationError( + f"failed to create InputEvent from {type(event_tuple) = }" + ) + + @classmethod + def btn_left(cls): + return cls(0, 0, evdev.ecodes.EV_KEY, evdev.ecodes.BTN_LEFT, 1) + + @property + def type_and_code(self) -> Tuple[int, int]: + """event type, code""" + return self.type, self.code + + @property + def event_tuple(self) -> Tuple[int, int, int]: + """event type, code, value""" + return self.type, self.code, self.value + + def modify( + self, + sec: int = None, + usec: int = None, + type: int = None, + code: int = None, + value: int = None, + ) -> InputEvent: + """return a new modified event""" + return InputEvent( + sec if sec is not None else self.sec, + usec if usec is not None else self.usec, + type if type is not None else self.type, + code if code is not None else self.code, + value if value is not None else self.value, + ) + + def json_str(self) -> str: + return ",".join([str(self.type), str(self.code), str(self.value)]) diff --git a/inputremapper/ipc/pipe.py b/inputremapper/ipc/pipe.py index c82c350b..7b3e2e58 100644 --- a/inputremapper/ipc/pipe.py +++ b/inputremapper/ipc/pipe.py @@ -42,7 +42,7 @@ import time import json from inputremapper.logger import logger -from inputremapper.paths import mkdir, chown +from inputremapper.configs.paths import mkdir, chown class Pipe: diff --git a/inputremapper/ipc/socket.py b/inputremapper/ipc/socket.py index 206af97e..ed4f72be 100644 --- a/inputremapper/ipc/socket.py +++ b/inputremapper/ipc/socket.py @@ -57,7 +57,7 @@ import time import json from inputremapper.logger import logger -from inputremapper.paths import mkdir, chown +from inputremapper.configs.paths import mkdir, chown # something funny that most likely won't appear in messages. diff --git a/inputremapper/key.py b/inputremapper/key.py deleted file mode 100644 index 1285b2e8..00000000 --- a/inputremapper/key.py +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -# input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2022 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 . - - -"""A button or a key combination.""" - - -import itertools - -import evdev -from evdev import ecodes - -from inputremapper.system_mapping import system_mapping -from inputremapper.logger import logger - - -def verify(key): - """Check if the key is an int 3-tuple of type, code, value""" - if not isinstance(key, tuple) or len(key) != 3: - raise ValueError(f"Expected key to be a 3-tuple, but got {key}") - if sum([not isinstance(value, int) for value in key]) != 0: - raise ValueError(f"Can only use integers, but got {key}") - - -# having shift in combinations modifies the configured output, -# ctrl might not work at all -DIFFICULT_COMBINATIONS = [ - ecodes.KEY_LEFTSHIFT, - ecodes.KEY_RIGHTSHIFT, - ecodes.KEY_LEFTCTRL, - ecodes.KEY_RIGHTCTRL, - ecodes.KEY_LEFTALT, - ecodes.KEY_RIGHTALT, -] - - -class Key: - """Represents one or more pressed down keys. - - Can be used in hashmaps/dicts as key - """ - - def __init__(self, *keys): - """ - Parameters - ---------- - Takes an arbitrary number of tuples as arguments. Each one should - be in the format of - 0: type, one of evdev.events, taken from the original source - event. Everything will be mapped to EV_KEY. - 1: The source keycode, what the mouse would report without any - modification. - 2. The value. 1 (down), 0 (up) or any - other value that the device reports. Gamepads use a continuous - space of values for joysticks and triggers. - - or Key objects, which will flatten all of them into one combination - """ - if len(keys) == 0: - raise ValueError("At least one key is required") - - if isinstance(keys[0], int): - # type, code, value was provided instead of a tuple - keys = (keys,) - - # multiple objects of Key get flattened into one tuple - flattened = () - for key in keys: - if isinstance(key, Key): - flattened += key.keys # pylint: disable=no-member - else: - flattened += (key,) - keys = flattened - - for key in keys: - verify(key) - - self.keys = tuple(keys) - self.release = (*self.keys[-1][:2], 0) - - @classmethod - def btn_left(cls): - """Construct a Key object representing a left click on a mouse.""" - return cls(ecodes.EV_KEY, ecodes.BTN_LEFT, 1) - - def __iter__(self): - return iter(self.keys) - - def __getitem__(self, item): - return self.keys[item] - - def __len__(self): - """Get the number of pressed down kes.""" - return len(self.keys) - - def __str__(self): - return f"Key{str(self.keys)}" - - def __repr__(self): - # used in the AssertionError output of tests - return self.__str__() - - def __hash__(self): - if len(self.keys) == 1: - return hash(self.keys[0]) - - return hash(self.keys) - - def __eq__(self, other): - if isinstance(other, tuple): - if isinstance(other[0], tuple): - # a combination ((1, 5, 1), (1, 3, 1)) - return self.keys == other - - # otherwise, self needs to represent a single key as well - return len(self.keys) == 1 and self.keys[0] == other - - if not isinstance(other, Key): - return False - - # compare two instances of Key - return self.keys == other.keys - - def is_problematic(self): - """Is this combination going to work properly on all systems?""" - if len(self.keys) <= 1: - return False - - for sub_key in self.keys: - if sub_key[0] != ecodes.EV_KEY: - continue - - if sub_key[1] in DIFFICULT_COMBINATIONS: - return True - - return False - - def get_permutations(self): - """Get a list of Key objects representing all possible permutations. - - combining a + b + c should have the same result as b + a + c. - Only the last key remains the same in the returned result. - """ - if len(self.keys) <= 2: - return [self] - - permutations = [] - for permutation in itertools.permutations(self.keys[:-1]): - permutations.append(Key(*permutation, self.keys[-1])) - - return permutations - - def beautify(self): - """Get a human readable string representation.""" - result = [] - - for sub_key in self: - if isinstance(sub_key[0], tuple): - raise Exception("deprecated stuff") - - ev_type, code, value = sub_key - - if ev_type not in evdev.ecodes.bytype: - logger.error("Unknown key type for %s", sub_key) - result.append(str(code)) - continue - - if code not in evdev.ecodes.bytype[ev_type]: - logger.error("Unknown key code for %s", sub_key) - result.append(str(code)) - continue - - key_name = None - - # first try to find the name in xmodmap to not display wrong - # names due to the keyboard layout - if ev_type == evdev.ecodes.EV_KEY: - key_name = system_mapping.get_name(code) - - if key_name is None: - # if no result, look in the linux key constants. On a german - # keyboard for example z and y are switched, which will therefore - # cause the wrong letter to be displayed. - key_name = evdev.ecodes.bytype[ev_type][code] - if isinstance(key_name, list): - key_name = key_name[0] - - if ev_type != evdev.ecodes.EV_KEY: - direction = { - # D-Pad - (evdev.ecodes.ABS_HAT0X, -1): "Left", - (evdev.ecodes.ABS_HAT0X, 1): "Right", - (evdev.ecodes.ABS_HAT0Y, -1): "Up", - (evdev.ecodes.ABS_HAT0Y, 1): "Down", - (evdev.ecodes.ABS_HAT1X, -1): "Left", - (evdev.ecodes.ABS_HAT1X, 1): "Right", - (evdev.ecodes.ABS_HAT1Y, -1): "Up", - (evdev.ecodes.ABS_HAT1Y, 1): "Down", - (evdev.ecodes.ABS_HAT2X, -1): "Left", - (evdev.ecodes.ABS_HAT2X, 1): "Right", - (evdev.ecodes.ABS_HAT2Y, -1): "Up", - (evdev.ecodes.ABS_HAT2Y, 1): "Down", - # joystick - (evdev.ecodes.ABS_X, 1): "Right", - (evdev.ecodes.ABS_X, -1): "Left", - (evdev.ecodes.ABS_Y, 1): "Down", - (evdev.ecodes.ABS_Y, -1): "Up", - (evdev.ecodes.ABS_RX, 1): "Right", - (evdev.ecodes.ABS_RX, -1): "Left", - (evdev.ecodes.ABS_RY, 1): "Down", - (evdev.ecodes.ABS_RY, -1): "Up", - # wheel - (evdev.ecodes.REL_WHEEL, -1): "Down", - (evdev.ecodes.REL_WHEEL, 1): "Up", - (evdev.ecodes.REL_HWHEEL, -1): "Left", - (evdev.ecodes.REL_HWHEEL, 1): "Right", - }.get((code, value)) - if direction is not None: - key_name += f" {direction}" - - key_name = key_name.replace("ABS_Z", "Trigger Left") - key_name = key_name.replace("ABS_RZ", "Trigger Right") - - key_name = key_name.replace("ABS_HAT0X", "DPad") - key_name = key_name.replace("ABS_HAT0Y", "DPad") - key_name = key_name.replace("ABS_HAT1X", "DPad 2") - key_name = key_name.replace("ABS_HAT1Y", "DPad 2") - key_name = key_name.replace("ABS_HAT2X", "DPad 3") - key_name = key_name.replace("ABS_HAT2Y", "DPad 3") - - key_name = key_name.replace("ABS_X", "Joystick") - key_name = key_name.replace("ABS_Y", "Joystick") - key_name = key_name.replace("ABS_RX", "Joystick 2") - key_name = key_name.replace("ABS_RY", "Joystick 2") - - key_name = key_name.replace("BTN_", "Button ") - key_name = key_name.replace("KEY_", "") - - key_name = key_name.replace("REL_", "") - key_name = key_name.replace("HWHEEL", "Wheel") - key_name = key_name.replace("WHEEL", "Wheel") - - key_name = key_name.replace("_", " ") - key_name = key_name.replace(" ", " ") - - result.append(key_name) - - return " + ".join(result) diff --git a/inputremapper/mapping.py b/inputremapper/mapping.py deleted file mode 100644 index 01f3399e..00000000 --- a/inputremapper/mapping.py +++ /dev/null @@ -1,290 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -# input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2022 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 . - - -"""Contains and manages mappings.""" - - -import os -import json -import copy - -from evdev.ecodes import EV_KEY, BTN_LEFT - -from inputremapper.logger import logger -from inputremapper.paths import touch -from inputremapper.config import ConfigBase, config -from inputremapper.key import Key -from inputremapper.injection.macros.parse import clean - - -def split_key(key): - """Take a key like "1,2,3" and return a 3-tuple of ints.""" - key = key.strip() - - if key.count(",") != 2: - logger.error('Found invalid key: "%s"', key) - return None - - ev_type, code, value = key.split(",") - try: - key = (int(ev_type), int(code), int(value)) - except ValueError: - logger.error('Found non-int in: "%s"', key) - return None - - return key - - -class Mapping(ConfigBase): - """Contains and manages mappings and config of a single preset.""" - - def __init__(self): - self._mapping = {} # a mapping of Key objects to strings - self._changed = False - - # are there actually any keys set in the mapping file? - self.num_saved_keys = 0 - - super().__init__(fallback=config) - - def __iter__(self): - """Iterate over Key objects and their mappings.""" - return iter(self._mapping.items()) - - def __len__(self): - return len(self._mapping) - - def set(self, *args): - """Set a config value. See `ConfigBase.set`.""" - self._changed = True - return super().set(*args) - - def remove(self, *args): - """Remove a config value. See `ConfigBase.remove`.""" - self._changed = True - return super().remove(*args) - - def change(self, new_key, target, symbol, previous_key=None): - """Replace the mapping of a keycode with a different one. - - Parameters - ---------- - new_key : Key - target : string - name of target uinput - symbol : string - A single symbol known to xkb or linux. - Examples: KEY_KP1, Shift_L, a, B, BTN_LEFT. - previous_key : Key or None - the previous key - - If not set, will not remove any previous mapping. If you recently - used (1, 10, 1) for new_key and want to overwrite that with - (1, 11, 1), provide (1, 10, 1) here. - """ - if not isinstance(new_key, Key): - raise TypeError(f"Expected {new_key} to be a Key object") - - if symbol is None or symbol.strip() == "": - raise ValueError("Expected `symbol` not to be empty") - - if target is None or target.strip() == "": - raise ValueError("Expected `target` not to be None") - - target = target.strip() - symbol = symbol.strip() - output = (symbol, target) - - if previous_key is None and self._mapping.get(new_key): - # the key didn't change - previous_key = new_key - - key_changed = new_key != previous_key - if not key_changed and (symbol, target) == self._mapping.get(new_key): - # nothing was changed, no need to act - return - - self.clear(new_key) # this also clears all equivalent keys - - logger.debug('changing %s to "%s"', new_key, clean(symbol)) - - self._mapping[new_key] = output - - if key_changed and previous_key is not None: - # clear previous mapping of that code, because the line - # representing that one will now represent a different one - self.clear(previous_key) - - self._changed = True - - def has_unsaved_changes(self): - """Check if there are unsaved changed.""" - return self._changed - - def set_has_unsaved_changes(self, changed): - """Write down if there are unsaved changes, or if they have been saved.""" - self._changed = changed - - def clear(self, key): - """Remove a keycode from the mapping. - - Parameters - ---------- - key : Key - """ - if not isinstance(key, Key): - raise TypeError(f"Expected key to be a Key object but got {key}") - - for permutation in key.get_permutations(): - if permutation in self._mapping: - logger.debug("%s cleared", permutation) - del self._mapping[permutation] - self._changed = True - # there should be only one variation of the permutations - # in the mapping actually - - def empty(self): - """Remove all mappings and custom configs without saving.""" - self._mapping = {} - self._changed = True - self.clear_config() - - def load(self, path): - """Load a dumped JSON from home to overwrite the mappings. - - Parameters - path : string - Path of the preset file - """ - logger.info('Loading preset from "%s"', path) - - if not os.path.exists(path): - raise FileNotFoundError(f'Tried to load non-existing preset "{path}"') - - self.empty() - self._changed = False - - with open(path, "r") as file: - preset_dict = json.load(file) - - if not isinstance(preset_dict.get("mapping"), dict): - logger.error( - "Expected mapping to be a dict, but was %s. " - 'Invalid preset config at "%s"', - preset_dict.get("mapping"), - path, - ) - return - - for key, symbol in preset_dict["mapping"].items(): - try: - key = Key( - *[ - split_key(chunk) - for chunk in key.split("+") - if chunk.strip() != "" - ] - ) - except ValueError as error: - logger.error(str(error)) - continue - - if None in key: - continue - - if isinstance(symbol, list): - symbol = tuple(symbol) # use a immutable type - - logger.debug("%s maps to %s", key, symbol) - self._mapping[key] = symbol - - # add any metadata of the mapping - for key in preset_dict: - if key == "mapping": - continue - self._config[key] = preset_dict[key] - - self._changed = False - self.num_saved_keys = len(self) - - def clone(self): - """Create a copy of the mapping.""" - mapping = Mapping() - mapping._mapping = copy.deepcopy(self._mapping) - mapping.set_has_unsaved_changes(self._changed) - return mapping - - def save(self, path): - """Dump as JSON into home.""" - logger.info("Saving preset to %s", path) - - touch(path) - - with open(path, "w") as file: - if self._config.get("mapping") is not None: - logger.error( - '"mapping" is reserved and cannot be used as config ' "key: %s", - self._config.get("mapping"), - ) - - preset_dict = self._config.copy() # shallow copy - - # make sure to keep the option to add metadata if ever needed, - # so put the mapping into a special key - json_ready_mapping = {} - # tuple keys are not possible in json, encode them as string - for key, value in self._mapping.items(): - new_key = "+".join( - [",".join([str(value) for value in sub_key]) for sub_key in key] - ) - json_ready_mapping[new_key] = value - - preset_dict["mapping"] = json_ready_mapping - json.dump(preset_dict, file, indent=4) - file.write("\n") - - self._changed = False - self.num_saved_keys = len(self) - - def get_mapping(self, key): - """Read the (symbol, target)-tuple that is mapped to this keycode. - - Parameters - ---------- - key : Key - """ - if not isinstance(key, Key): - raise TypeError(f"Expected key to be a Key object but got {key}") - - for permutation in key.get_permutations(): - existing = self._mapping.get(permutation) - if existing is not None: - return existing - - return None - - def dangerously_mapped_btn_left(self): - """Return True if this mapping disables BTN_Left.""" - if self.get_mapping(Key(EV_KEY, BTN_LEFT, 1)) is not None: - values = [value[0].lower() for value in self._mapping.values()] - return "btn_left" not in values - - return False diff --git a/inputremapper/presets.py b/inputremapper/presets.py deleted file mode 100644 index 3991a865..00000000 --- a/inputremapper/presets.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -# input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2022 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 . - - -"""Helperfunctions to find device ids, names, and to load presets.""" - - -import os -import time -import glob -import re - -from inputremapper.paths import get_preset_path, mkdir -from inputremapper.logger import logger -from inputremapper.groups import groups - - -def get_available_preset_name(group_name, preset="new preset", copy=False): - """Increment the preset name until it is available.""" - if group_name is None: - # endless loop otherwise - raise ValueError("group_name may not be None") - - preset = preset.strip() - - if copy and not re.match(r"^.+\scopy( \d+)?$", preset): - preset = f"{preset} copy" - - # find a name that is not already taken - if os.path.exists(get_preset_path(group_name, preset)): - # if there already is a trailing number, increment it instead of - # adding another one - match = re.match(r"^(.+) (\d+)$", preset) - if match: - preset = match[1] - i = int(match[2]) + 1 - else: - i = 2 - - while os.path.exists(get_preset_path(group_name, f"{preset} {i}")): - i += 1 - - return f"{preset} {i}" - - return preset - - -def get_presets(group_name): - """Get all presets for the device and user, starting with the newest. - - Parameters - ---------- - group_name : string - """ - device_folder = get_preset_path(group_name) - mkdir(device_folder) - - paths = glob.glob(os.path.join(device_folder, "*.json")) - presets = [ - os.path.splitext(os.path.basename(path))[0] - for path in sorted(paths, key=os.path.getmtime) - ] - # the highest timestamp to the front - presets.reverse() - return presets - - -def get_any_preset(): - """Return the first found tuple of (device, preset).""" - group_names = groups.list_group_names() - if len(group_names) == 0: - return None, None - any_device = list(group_names)[0] - any_preset = (get_presets(any_device) or [None])[0] - return any_device, any_preset - - -def find_newest_preset(group_name=None): - """Get a tuple of (device, preset) that was most recently modified - in the users home directory. - - If no device has been configured yet, return an arbitrary device. - - Parameters - ---------- - group_name : string - If set, will return the newest preset for the device or None - """ - # sort the oldest files to the front in order to use pop to get the newest - if group_name is None: - paths = sorted( - glob.glob(os.path.join(get_preset_path(), "*/*.json")), key=os.path.getmtime - ) - else: - paths = sorted( - glob.glob(os.path.join(get_preset_path(group_name), "*.json")), - key=os.path.getmtime, - ) - - if len(paths) == 0: - logger.debug("No presets found") - return get_any_preset() - - group_names = groups.list_group_names() - - newest_path = None - while len(paths) > 0: - # take the newest path - path = paths.pop() - preset = os.path.split(path)[1] - group_name = os.path.split(os.path.split(path)[0])[1] - if group_name in group_names: - newest_path = path - break - - if newest_path is None: - return get_any_preset() - - preset = os.path.splitext(preset)[0] - logger.debug('The newest preset is "%s", "%s"', group_name, preset) - - return group_name, preset - - -def delete_preset(group_name, preset): - """Delete one of the users presets.""" - preset_path = get_preset_path(group_name, preset) - if not os.path.exists(preset_path): - logger.debug('Cannot remove non existing path "%s"', preset_path) - return - - logger.info('Removing "%s"', preset_path) - os.remove(preset_path) - - device_path = get_preset_path(group_name) - if os.path.exists(device_path) and len(os.listdir(device_path)) == 0: - logger.debug('Removing empty dir "%s"', device_path) - os.rmdir(device_path) - - -def rename_preset(group_name, old_preset_name, new_preset_name): - """Rename one of the users presets while avoiding name conflicts.""" - if new_preset_name == old_preset_name: - return None - - new_preset_name = get_available_preset_name(group_name, new_preset_name) - logger.info('Moving "%s" to "%s"', old_preset_name, new_preset_name) - os.rename( - get_preset_path(group_name, old_preset_name), - get_preset_path(group_name, new_preset_name), - ) - # set the modification date to now - now = time.time() - os.utime(get_preset_path(group_name, new_preset_name), (now, now)) - return new_preset_name diff --git a/inputremapper/utils.py b/inputremapper/utils.py index 05ae1827..8957c0bd 100644 --- a/inputremapper/utils.py +++ b/inputremapper/utils.py @@ -39,7 +39,7 @@ from evdev.ecodes import ( ) from inputremapper.logger import logger -from inputremapper.config import BUTTONS +from inputremapper.configs.global_config import BUTTONS # other events for ABS include buttons @@ -137,7 +137,7 @@ def will_report_key_up(event): return not is_wheel(event) -def should_map_as_btn(event, mapping, gamepad): +def should_map_as_btn(event, preset, gamepad): """Does this event describe a button that is or can be mapped. If a new kind of event should be mappable to buttons, this is the place @@ -149,7 +149,7 @@ def should_map_as_btn(event, mapping, gamepad): Parameters ---------- event : evdev.InputEvent - mapping : Mapping + preset : Preset gamepad : bool If the device is treated as gamepad """ @@ -170,8 +170,8 @@ def should_map_as_btn(event, mapping, gamepad): if not gamepad: return False - l_purpose = mapping.get("gamepad.joystick.left_purpose") - r_purpose = mapping.get("gamepad.joystick.right_purpose") + l_purpose = preset.get("gamepad.joystick.left_purpose") + r_purpose = preset.get("gamepad.joystick.right_purpose") if event.code in [ABS_X, ABS_Y] and l_purpose == BUTTONS: return True diff --git a/tests/integration/test_data.py b/tests/integration/test_data.py index a4424aa9..4edd05c3 100644 --- a/tests/integration/test_data.py +++ b/tests/integration/test_data.py @@ -23,7 +23,7 @@ import unittest import os import pkg_resources -from inputremapper.data import get_data_path +from inputremapper.configs.data import get_data_path class TestData(unittest.TestCase): diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index 68f53c92..541bbff7 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -42,21 +42,22 @@ from importlib.util import spec_from_loader, module_from_spec from importlib.machinery import SourceFileLoader import gi +from inputremapper.input_event import InputEvent gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gdk, GtkSource +from gi.repository import Gtk, GLib, Gdk -from inputremapper.system_mapping import system_mapping, XMODMAP_FILENAME -from inputremapper.gui.custom_mapping import custom_mapping -from inputremapper.paths import CONFIG_PATH, get_preset_path, get_config_path -from inputremapper.config import config, WHEEL, MOUSE, BUTTONS +from inputremapper.configs.system_mapping import system_mapping, XMODMAP_FILENAME +from inputremapper.gui.active_preset import active_preset +from inputremapper.configs.paths import CONFIG_PATH, get_preset_path, get_config_path +from inputremapper.configs.global_config import global_config, WHEEL, MOUSE, BUTTONS from inputremapper.gui.reader import reader from inputremapper.gui.helper import RootHelper from inputremapper.gui.utils import gtk_iteration from inputremapper.gui.user_interface import UserInterface from inputremapper.gui.editor.editor import SET_KEY_FIRST from inputremapper.injection.injector import RUNNING, FAILED, UNKNOWN -from inputremapper.key import Key +from inputremapper.event_combination import EventCombination from inputremapper.daemon import Daemon from inputremapper.groups import groups @@ -304,7 +305,7 @@ class GuiTestBase(unittest.TestCase): evdev.InputDevice.grab = grab - config._save_config() + global_config._save_config() self.throttle() @@ -356,7 +357,7 @@ class GuiTestBase(unittest.TestCase): Parameters ---------- - key : Key or None + key : EventCombination or None expect_success : boolean If the key can be stored in the selection label. False if this change is going to cause a duplicate. @@ -367,7 +368,7 @@ class GuiTestBase(unittest.TestCase): self.assertIsNone(reader.get_unreleased_keys()) - changed = custom_mapping.has_unsaved_changes() + changed = active_preset.has_unsaved_changes() # wait for the window to create a new empty selection_label if needed time.sleep(0.1) @@ -376,7 +377,7 @@ class GuiTestBase(unittest.TestCase): # the empty selection_label is expected to be the last one selection_label = self.get_selection_labels()[-1] self.selection_label_listbox.select_row(selection_label) - self.assertIsNone(selection_label.get_key()) + self.assertIsNone(selection_label.get_combination()) self.assertFalse(self.editor._input_has_arrived) if self.toggle.get_active(): @@ -389,14 +390,14 @@ class GuiTestBase(unittest.TestCase): self.toggle.set_active(True) gtk_iteration() gtk_iteration() - self.assertIsNone(selection_label.get_key()) + self.assertIsNone(selection_label.get_combination()) self.assertEqual(self.toggle.get_label(), "Press Key") if key: # modifies the keycode in the selection_label not by writing into the input, # but by sending an event. press down all the keys of a combination for sub_key in key: - send_event_to_reader(new_event(*sub_key)) + send_event_to_reader(new_event(*sub_key.event_tuple)) # this will be consumed all at once, since no gtk_iteration # is done @@ -411,7 +412,7 @@ class GuiTestBase(unittest.TestCase): # release all the keys for sub_key in key: - send_event_to_reader(new_event(*sub_key[:2], 0)) + send_event_to_reader(new_event(*sub_key.type_and_code, 0)) # wait for the window to consume the keycode self.sleep(len(key)) @@ -421,7 +422,7 @@ class GuiTestBase(unittest.TestCase): self.assertFalse(self.editor._input_has_arrived) if expect_success: - self.assertEqual(self.editor.get_key(), key) + self.assertEqual(self.editor.get_combination(), key) # the previously new entry, which has been edited now, is still the # selected one self.assertEqual(self.editor.active_selection_label, selection_label) @@ -433,12 +434,12 @@ class GuiTestBase(unittest.TestCase): self.assertEqual(len(reader._unreleased), 0) if not expect_success: - self.assertIsNone(selection_label.get_key()) + self.assertIsNone(selection_label.get_combination()) self.assertEqual(self.editor.get_symbol_input_text(), "") self.assertFalse(self.editor._input_has_arrived) # it won't switch the focus to the symbol input self.assertTrue(self.toggle.get_active()) - self.assertEqual(custom_mapping.has_unsaved_changes(), changed) + self.assertEqual(active_preset.has_unsaved_changes(), changed) return selection_label if key is None: @@ -460,7 +461,7 @@ class GuiTestBase(unittest.TestCase): self.set_focus(None) correct_case = system_mapping.correct_case(symbol) self.assertEqual(self.editor.get_symbol_input_text(), correct_case) - self.assertFalse(custom_mapping.has_unsaved_changes()) + self.assertFalse(active_preset.has_unsaved_changes()) self.set_focus(self.editor.get_text_input()) self.set_focus(None) @@ -496,12 +497,12 @@ class TestGui(GuiTestBase): self.selection_label_listbox.get_selected_row(), selection_labels[0], ) - self.assertEqual(len(custom_mapping), 0) + self.assertEqual(len(active_preset), 0) self.assertEqual(selection_labels[0].get_label(), "new entry") self.assertEqual(self.editor.get_symbol_input_text(), "") preset_selection = self.user_interface.get("preset_selection") self.assertEqual(preset_selection.get_active_id(), "new preset") - self.assertEqual(len(custom_mapping), 0) + self.assertEqual(len(active_preset), 0) self.assertEqual(self.editor.get_recording_toggle().get_label(), "Change Key") self.assertEqual(self.get_unfiltered_symbol_input_text(), SET_KEY_FIRST) @@ -592,7 +593,7 @@ class TestGui(GuiTestBase): def test_autoload(self): self.assertFalse( - config.is_autoloaded( + global_config.is_autoloaded( self.user_interface.group.key, self.user_interface.preset_name ) ) @@ -602,7 +603,7 @@ class TestGui(GuiTestBase): set_config_dir.assert_called_once() self.assertFalse( - config.is_autoloaded( + global_config.is_autoloaded( self.user_interface.group.key, self.user_interface.preset_name ) ) @@ -618,56 +619,57 @@ class TestGui(GuiTestBase): self.assertEqual(self.user_interface.group.key, "Foo Device 2") self.assertEqual(self.user_interface.group.name, "Foo Device") self.assertTrue( - config.is_autoloaded(self.user_interface.group.key, "new preset") + global_config.is_autoloaded(self.user_interface.group.key, "new preset") ) - self.assertFalse(config.is_autoloaded("Bar Device", "new preset")) + self.assertFalse(global_config.is_autoloaded("Bar Device", "new preset")) self.assertListEqual( - list(config.iterate_autoload_presets()), [("Foo Device 2", "new preset")] + list(global_config.iterate_autoload_presets()), + [("Foo Device 2", "new preset")], ) # create a new preset, the switch should be correctly off and the - # config not changed. + # global_config not changed. self.user_interface.on_create_preset_clicked() gtk_iteration() self.assertEqual(self.user_interface.preset_name, "new preset 2") self.assertFalse(self.user_interface.get("preset_autoload_switch").get_active()) - self.assertTrue(config.is_autoloaded("Foo Device 2", "new preset")) - self.assertFalse(config.is_autoloaded("Foo Device", "new preset")) - self.assertFalse(config.is_autoloaded("Foo Device", "new preset 2")) - self.assertFalse(config.is_autoloaded("Foo Device 2", "new preset 2")) + self.assertTrue(global_config.is_autoloaded("Foo Device 2", "new preset")) + self.assertFalse(global_config.is_autoloaded("Foo Device", "new preset")) + self.assertFalse(global_config.is_autoloaded("Foo Device", "new preset 2")) + self.assertFalse(global_config.is_autoloaded("Foo Device 2", "new preset 2")) # select a preset for the second device self.user_interface.on_select_device(FakeDeviceDropdown("Bar Device")) self.user_interface.get("preset_autoload_switch").set_active(True) gtk_iteration() - self.assertTrue(config.is_autoloaded("Foo Device 2", "new preset")) - self.assertFalse(config.is_autoloaded("Foo Device", "new preset")) - self.assertTrue(config.is_autoloaded("Bar Device", "new preset")) + self.assertTrue(global_config.is_autoloaded("Foo Device 2", "new preset")) + self.assertFalse(global_config.is_autoloaded("Foo Device", "new preset")) + self.assertTrue(global_config.is_autoloaded("Bar Device", "new preset")) self.assertListEqual( - list(config.iterate_autoload_presets()), + list(global_config.iterate_autoload_presets()), [("Foo Device 2", "new preset"), ("Bar Device", "new preset")], ) # disable autoloading for the second device self.user_interface.get("preset_autoload_switch").set_active(False) gtk_iteration() - self.assertTrue(config.is_autoloaded("Foo Device 2", "new preset")) - self.assertFalse(config.is_autoloaded("Foo Device", "new preset")) - self.assertFalse(config.is_autoloaded("Bar Device", "new preset")) + self.assertTrue(global_config.is_autoloaded("Foo Device 2", "new preset")) + self.assertFalse(global_config.is_autoloaded("Foo Device", "new preset")) + self.assertFalse(global_config.is_autoloaded("Bar Device", "new preset")) self.assertListEqual( - list(config.iterate_autoload_presets()), + list(global_config.iterate_autoload_presets()), [("Foo Device 2", "new preset")], ) def test_select_device(self): # creates a new empty preset when no preset exists for the device self.user_interface.on_select_device(FakeDeviceDropdown("Foo Device")) - custom_mapping.change(Key(EV_KEY, 50, 1), "keyboard", "q") - custom_mapping.change(Key(EV_KEY, 51, 1), "keyboard", "u") - custom_mapping.change(Key(EV_KEY, 52, 1), "keyboard", "x") - self.assertEqual(len(custom_mapping), 3) + active_preset.change(EventCombination([EV_KEY, 50, 1]), "keyboard", "q") + active_preset.change(EventCombination([EV_KEY, 51, 1]), "keyboard", "u") + active_preset.change(EventCombination([EV_KEY, 52, 1]), "keyboard", "x") + self.assertEqual(len(active_preset), 3) self.user_interface.on_select_device(FakeDeviceDropdown("Bar Device")) - self.assertEqual(len(custom_mapping), 0) + self.assertEqual(len(active_preset), 0) # it creates the file for that right away. It may have been possible # to write it such that it doesn't (its empty anyway), but it does, # so use that to test it in more detail. @@ -681,7 +683,7 @@ class TestGui(GuiTestBase): def save(_=None): raise PermissionError - with patch.object(custom_mapping, "save", save): + with patch.object(active_preset, "save", save): self.user_interface.on_create_preset_clicked() status = self.get_status_text() self.assertIn("Permission denied", status) @@ -697,29 +699,44 @@ class TestGui(GuiTestBase): def test_editor_keycode_to_string(self): # not an integration test, but I have all the selection_label tests here already - self.assertEqual(Key(EV_KEY, evdev.ecodes.KEY_A, 1).beautify(), "a") + self.assertEqual(EventCombination((EV_KEY, evdev.ecodes.KEY_A, 1)).beautify(), "a") + self.assertEqual( + EventCombination([EV_KEY, evdev.ecodes.KEY_A, 1]).beautify(), "a" + ) + self.assertEqual(EventCombination((EV_ABS, evdev.ecodes.ABS_HAT0Y, -1)).beautify(), "DPad Up") + self.assertEqual(EventCombination((EV_KEY, evdev.ecodes.BTN_A, 1)).beautify(), "Button A") + self.assertEqual(EventCombination((EV_KEY, 1234, 1)).beautify(), "1234") + self.assertEqual( + EventCombination([EV_ABS, evdev.ecodes.ABS_HAT0X, -1]).beautify(), + "DPad Left", + ) + self.assertEqual( + EventCombination([EV_ABS, evdev.ecodes.ABS_HAT0Y, -1]).beautify(), "DPad Up" + ) self.assertEqual( - Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1).beautify(), "DPad Left" + EventCombination([EV_KEY, evdev.ecodes.BTN_A, 1]).beautify(), "Button A" ) - self.assertEqual(Key(EV_ABS, evdev.ecodes.ABS_HAT0Y, -1).beautify(), "DPad Up") - self.assertEqual(Key(EV_KEY, evdev.ecodes.BTN_A, 1).beautify(), "Button A") - self.assertEqual(Key(EV_KEY, 1234, 1).beautify(), "1234") + self.assertEqual(EventCombination([EV_KEY, 1234, 1]).beautify(), "1234") self.assertEqual( - Key(EV_ABS, evdev.ecodes.ABS_X, 1).beautify(), "Joystick Right" + EventCombination([EV_ABS, evdev.ecodes.ABS_X, 1]).beautify(), + "Joystick Right", ) self.assertEqual( - Key(EV_ABS, evdev.ecodes.ABS_RY, 1).beautify(), "Joystick 2 Down" + EventCombination([EV_ABS, evdev.ecodes.ABS_RY, 1]).beautify(), + "Joystick 2 Down", ) self.assertEqual( - Key(EV_REL, evdev.ecodes.REL_HWHEEL, 1).beautify(), "Wheel Right" + EventCombination([EV_REL, evdev.ecodes.REL_HWHEEL, 1]).beautify(), + "Wheel Right", ) self.assertEqual( - Key(EV_REL, evdev.ecodes.REL_WHEEL, -1).beautify(), "Wheel Down" + EventCombination([EV_REL, evdev.ecodes.REL_WHEEL, -1]).beautify(), + "Wheel Down", ) # combinations self.assertEqual( - Key( + EventCombination( (EV_KEY, evdev.ecodes.BTN_A, 1), (EV_KEY, evdev.ecodes.BTN_B, 1), (EV_KEY, evdev.ecodes.BTN_C, 1), @@ -739,23 +756,25 @@ class TestGui(GuiTestBase): self.editor.consume_newest_keycode(None) # nothing happens - self.assertIsNone(selection_label.get_key()) - self.assertEqual(len(custom_mapping), 0) + self.assertIsNone(selection_label.get_combination()) + self.assertEqual(len(active_preset), 0) self.assertEqual(self.toggle.get_label(), "Press Key") - self.editor.consume_newest_keycode(Key(EV_KEY, 30, 1)) - # no symbol configured yet, so the custom_mapping remains empty - self.assertEqual(len(custom_mapping), 0) - self.assertEqual(selection_label.get_key(), (EV_KEY, 30, 1)) + self.editor.consume_newest_keycode(EventCombination([EV_KEY, 30, 1])) + # no symbol configured yet, so the active_preset remains empty + self.assertEqual(len(active_preset), 0) + self.assertEqual(len(selection_label.get_combination()), 1) + self.assertEqual(selection_label.get_combination()[0], (EV_KEY, 30, 1)) # this is KEY_A in linux/input-event-codes.h, # but KEY_ is removed from the text for display purposes self.assertEqual(selection_label.get_label(), "a") # providing the same key again (Maybe this could happen for gamepads or # something, idk) doesn't do any harm - self.editor.consume_newest_keycode(Key(EV_KEY, 30, 1)) - self.assertEqual(len(custom_mapping), 0) # not released yet - self.assertEqual(selection_label.get_key(), (EV_KEY, 30, 1)) + self.editor.consume_newest_keycode(EventCombination([EV_KEY, 30, 1])) + self.assertEqual(len(active_preset), 0) # not released yet + self.assertEqual(len(selection_label.get_combination()), 1) + self.assertEqual(selection_label.get_combination()[0], (EV_KEY, 30, 1)) time.sleep(0.11) # new empty entry was added @@ -769,7 +788,7 @@ class TestGui(GuiTestBase): self.editor.set_symbol_input_text("Shift_L") self.set_focus(None) - self.assertEqual(len(custom_mapping), 1) + self.assertEqual(len(active_preset), 1) time.sleep(0.1) gtk_iteration() @@ -779,11 +798,13 @@ class TestGui(GuiTestBase): ) self.assertEqual( - custom_mapping.get_mapping(Key(EV_KEY, 30, 1)), ("Shift_L", "keyboard") + active_preset.get_mapping(EventCombination([EV_KEY, 30, 1])), + ("Shift_L", "keyboard"), ) self.assertEqual(self.editor.get_target_selection(), "keyboard") self.assertEqual(self.editor.get_symbol_input_text(), "Shift_L") - self.assertEqual(selection_label.get_key(), (EV_KEY, 30, 1)) + self.assertEqual(len(selection_label.get_combination()), 1) + self.assertEqual(selection_label.get_combination()[0], (EV_KEY, 30, 1)) self.editor.set_target_selection("mouse") time.sleep(0.1) @@ -793,11 +814,12 @@ class TestGui(GuiTestBase): 2, ) self.assertEqual( - custom_mapping.get_mapping(Key(EV_KEY, 30, 1)), ("Shift_L", "mouse") + active_preset.get_mapping(EventCombination([EV_KEY, 30, 1])), + ("Shift_L", "mouse"), ) self.assertEqual(self.editor.get_target_selection(), "mouse") self.assertEqual(self.editor.get_symbol_input_text(), "Shift_L") - self.assertEqual(selection_label.get_key(), (EV_KEY, 30, 1)) + self.assertEqual(selection_label.get_combination()[0], (EV_KEY, 30, 1)) def test_editor_not_focused(self): # focus anything that is not the selection_label, @@ -810,8 +832,8 @@ class TestGui(GuiTestBase): self.assertEqual(len(selection_labels), 1) selection_label = selection_labels[0] - # the empty selection_label has this key not set - self.assertIsNone(selection_label.get_key()) + # the empty selection_label has this combination not set + self.assertIsNone(selection_label.get_combination()) # focus the text input instead self.set_focus(self.editor.get_text_input()) @@ -819,7 +841,7 @@ class TestGui(GuiTestBase): self.user_interface.consume_newest_keycode() # still nothing set - self.assertIsNone(selection_label.get_key()) + self.assertIsNone(selection_label.get_combination()) def test_show_status(self): self.user_interface.show_status(0, "a" * 100) @@ -831,11 +853,11 @@ class TestGui(GuiTestBase): self.assertNotIn("...", text) def test_clears_unreleased_on_focus_change(self): - ev_1 = Key(EV_KEY, 41, 1) + ev_1 = EventCombination([EV_KEY, 41, 1]) # focus self.set_focus(self.toggle) - send_event_to_reader(new_event(*ev_1.keys[0])) + send_event_to_reader(new_event(*ev_1[0].event_tuple)) reader.read() self.assertEqual(reader.get_unreleased_keys(), ev_1) @@ -867,8 +889,8 @@ class TestGui(GuiTestBase): # how many selection_labels there should be in the end num_selection_labels_target = 3 - ev_1 = Key(EV_KEY, 10, 1) - ev_2 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1) + ev_1 = EventCombination([EV_KEY, 10, 1]) + ev_2 = EventCombination([EV_ABS, evdev.ecodes.ABS_HAT0X, -1]) """edit""" @@ -883,15 +905,15 @@ class TestGui(GuiTestBase): gtk_iteration() self.assertEqual(len(self.get_selection_labels()), num_selection_labels_target) - self.assertEqual(custom_mapping.get_mapping(ev_1), ("Foo_BAR", "mouse")) - self.assertEqual(custom_mapping.get_mapping(ev_2), ("k(b).k(c)", "keyboard")) + self.assertEqual(active_preset.get_mapping(ev_1), ("Foo_BAR", "mouse")) + self.assertEqual(active_preset.get_mapping(ev_2), ("k(b).k(c)", "keyboard")) """edit first selection_label""" self.selection_label_listbox.select_row( self.selection_label_listbox.get_children()[0] ) - self.assertEqual(self.editor.get_key(), ev_1) + self.assertEqual(self.editor.get_combination(), ev_1) self.set_focus(self.editor.get_text_input()) self.editor.set_symbol_input_text("c") self.set_focus(None) @@ -900,35 +922,35 @@ class TestGui(GuiTestBase): # the mapping that was used preset_name = self.user_interface.preset_name preset_path = self.user_interface.group.get_preset_path(preset_name) - custom_mapping.load(preset_path) + active_preset.load(preset_path) - self.assertEqual(custom_mapping.get_mapping(ev_1), ("c", "mouse")) - self.assertEqual(custom_mapping.get_mapping(ev_2), ("k(b).k(c)", "keyboard")) + self.assertEqual(active_preset.get_mapping(ev_1), ("c", "mouse")) + self.assertEqual(active_preset.get_mapping(ev_2), ("k(b).k(c)", "keyboard")) """add duplicate""" # try to add a duplicate keycode, it should be ignored self.add_mapping_via_ui(ev_2, "d", expect_success=False) - self.assertEqual(custom_mapping.get_mapping(ev_2), ("k(b).k(c)", "keyboard")) + self.assertEqual(active_preset.get_mapping(ev_2), ("k(b).k(c)", "keyboard")) # and the number of selection_labels shouldn't change self.assertEqual(len(self.get_selection_labels()), num_selection_labels_target) def test_hat0x(self): # it should be possible to add all of them - ev_1 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1) - ev_2 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, 1) - ev_3 = Key(EV_ABS, evdev.ecodes.ABS_HAT0Y, -1) - ev_4 = Key(EV_ABS, evdev.ecodes.ABS_HAT0Y, 1) + ev_1 = EventCombination([EV_ABS, evdev.ecodes.ABS_HAT0X, -1]) + ev_2 = EventCombination([EV_ABS, evdev.ecodes.ABS_HAT0X, 1]) + ev_3 = EventCombination([EV_ABS, evdev.ecodes.ABS_HAT0Y, -1]) + ev_4 = EventCombination([EV_ABS, evdev.ecodes.ABS_HAT0Y, 1]) self.add_mapping_via_ui(ev_1, "a") self.add_mapping_via_ui(ev_2, "b") self.add_mapping_via_ui(ev_3, "c") self.add_mapping_via_ui(ev_4, "d") - self.assertEqual(custom_mapping.get_mapping(ev_1), ("a", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(ev_2), ("b", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(ev_3), ("c", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(ev_4), ("d", "keyboard")) + self.assertEqual(active_preset.get_mapping(ev_1), ("a", "keyboard")) + self.assertEqual(active_preset.get_mapping(ev_2), ("b", "keyboard")) + self.assertEqual(active_preset.get_mapping(ev_3), ("c", "keyboard")) + self.assertEqual(active_preset.get_mapping(ev_4), ("d", "keyboard")) # and trying to add them as duplicate selection_labels will be ignored for each # of them @@ -937,72 +959,72 @@ class TestGui(GuiTestBase): self.add_mapping_via_ui(ev_3, "g", expect_success=False) self.add_mapping_via_ui(ev_4, "h", expect_success=False) - self.assertEqual(custom_mapping.get_mapping(ev_1), ("a", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(ev_2), ("b", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(ev_3), ("c", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(ev_4), ("d", "keyboard")) + self.assertEqual(active_preset.get_mapping(ev_1), ("a", "keyboard")) + self.assertEqual(active_preset.get_mapping(ev_2), ("b", "keyboard")) + self.assertEqual(active_preset.get_mapping(ev_3), ("c", "keyboard")) + self.assertEqual(active_preset.get_mapping(ev_4), ("d", "keyboard")) def test_combination(self): - # it should be possible to write a key combination - ev_1 = Key(EV_KEY, evdev.ecodes.KEY_A, 1) - ev_2 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, 1) - ev_3 = Key(EV_KEY, evdev.ecodes.KEY_C, 1) - ev_4 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1) - combination_1 = Key(ev_1, ev_2, ev_3) - combination_2 = Key(ev_2, ev_1, ev_3) + # it should be possible to write a combination combination + ev_1 = InputEvent.from_tuple((EV_KEY, evdev.ecodes.KEY_A, 1)) + ev_2 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, 1)) + ev_3 = InputEvent.from_tuple((EV_KEY, evdev.ecodes.KEY_C, 1)) + ev_4 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, -1)) + combination_1 = EventCombination(ev_1, ev_2, ev_3) + combination_2 = EventCombination(ev_2, ev_1, ev_3) # same as 1, but different D-Pad direction - combination_3 = Key(ev_1, ev_4, ev_3) - combination_4 = Key(ev_4, ev_1, ev_3) + combination_3 = EventCombination(ev_1, ev_4, ev_3) + combination_4 = EventCombination(ev_4, ev_1, ev_3) - # same as 1, but the last key is different - combination_5 = Key(ev_1, ev_3, ev_2) - combination_6 = Key(ev_3, ev_1, ev_2) + # same as 1, but the last combination is different + combination_5 = EventCombination(ev_1, ev_3, ev_2) + combination_6 = EventCombination(ev_3, ev_1, ev_2) self.add_mapping_via_ui(combination_1, "a") - self.assertEqual(custom_mapping.get_mapping(combination_1), ("a", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(combination_2), ("a", "keyboard")) - self.assertIsNone(custom_mapping.get_mapping(combination_3)) - self.assertIsNone(custom_mapping.get_mapping(combination_4)) - self.assertIsNone(custom_mapping.get_mapping(combination_5)) - self.assertIsNone(custom_mapping.get_mapping(combination_6)) + self.assertEqual(active_preset.get_mapping(combination_1), ("a", "keyboard")) + self.assertEqual(active_preset.get_mapping(combination_2), ("a", "keyboard")) + self.assertIsNone(active_preset.get_mapping(combination_3)) + self.assertIsNone(active_preset.get_mapping(combination_4)) + self.assertIsNone(active_preset.get_mapping(combination_5)) + self.assertIsNone(active_preset.get_mapping(combination_6)) # it won't write the same combination again, even if the # first two events are in a different order self.add_mapping_via_ui(combination_2, "b", expect_success=False) - self.assertEqual(custom_mapping.get_mapping(combination_1), ("a", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(combination_2), ("a", "keyboard")) - self.assertIsNone(custom_mapping.get_mapping(combination_3)) - self.assertIsNone(custom_mapping.get_mapping(combination_4)) - self.assertIsNone(custom_mapping.get_mapping(combination_5)) - self.assertIsNone(custom_mapping.get_mapping(combination_6)) + self.assertEqual(active_preset.get_mapping(combination_1), ("a", "keyboard")) + self.assertEqual(active_preset.get_mapping(combination_2), ("a", "keyboard")) + self.assertIsNone(active_preset.get_mapping(combination_3)) + self.assertIsNone(active_preset.get_mapping(combination_4)) + self.assertIsNone(active_preset.get_mapping(combination_5)) + self.assertIsNone(active_preset.get_mapping(combination_6)) self.add_mapping_via_ui(combination_3, "c") - self.assertEqual(custom_mapping.get_mapping(combination_1), ("a", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(combination_2), ("a", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(combination_3), ("c", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(combination_4), ("c", "keyboard")) - self.assertIsNone(custom_mapping.get_mapping(combination_5)) - self.assertIsNone(custom_mapping.get_mapping(combination_6)) + self.assertEqual(active_preset.get_mapping(combination_1), ("a", "keyboard")) + self.assertEqual(active_preset.get_mapping(combination_2), ("a", "keyboard")) + self.assertEqual(active_preset.get_mapping(combination_3), ("c", "keyboard")) + self.assertEqual(active_preset.get_mapping(combination_4), ("c", "keyboard")) + self.assertIsNone(active_preset.get_mapping(combination_5)) + self.assertIsNone(active_preset.get_mapping(combination_6)) # same as with combination_2, the existing combination_3 blocks # combination_4 because they have the same keys and end in the # same key. self.add_mapping_via_ui(combination_4, "d", expect_success=False) - self.assertEqual(custom_mapping.get_mapping(combination_1), ("a", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(combination_2), ("a", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(combination_3), ("c", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(combination_4), ("c", "keyboard")) - self.assertIsNone(custom_mapping.get_mapping(combination_5)) - self.assertIsNone(custom_mapping.get_mapping(combination_6)) + self.assertEqual(active_preset.get_mapping(combination_1), ("a", "keyboard")) + self.assertEqual(active_preset.get_mapping(combination_2), ("a", "keyboard")) + self.assertEqual(active_preset.get_mapping(combination_3), ("c", "keyboard")) + self.assertEqual(active_preset.get_mapping(combination_4), ("c", "keyboard")) + self.assertIsNone(active_preset.get_mapping(combination_5)) + self.assertIsNone(active_preset.get_mapping(combination_6)) self.add_mapping_via_ui(combination_5, "e") - self.assertEqual(custom_mapping.get_mapping(combination_1), ("a", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(combination_2), ("a", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(combination_3), ("c", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(combination_4), ("c", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(combination_5), ("e", "keyboard")) - self.assertEqual(custom_mapping.get_mapping(combination_6), ("e", "keyboard")) + self.assertEqual(active_preset.get_mapping(combination_1), ("a", "keyboard")) + self.assertEqual(active_preset.get_mapping(combination_2), ("a", "keyboard")) + self.assertEqual(active_preset.get_mapping(combination_3), ("c", "keyboard")) + self.assertEqual(active_preset.get_mapping(combination_4), ("c", "keyboard")) + self.assertEqual(active_preset.get_mapping(combination_5), ("e", "keyboard")) + self.assertEqual(active_preset.get_mapping(combination_6), ("e", "keyboard")) error_icon = self.user_interface.get("error_status_icon") warning_icon = self.user_interface.get("warning_status_icon") @@ -1034,7 +1056,8 @@ class TestGui(GuiTestBase): if code is not None and symbol is not None: self.assertEqual( - custom_mapping.get_mapping(Key(EV_KEY, code, 1)), (symbol, target) + active_preset.get_mapping(EventCombination([EV_KEY, code, 1])), + (symbol, target), ) if symbol is not None: @@ -1043,9 +1066,12 @@ class TestGui(GuiTestBase): self.assertEqual(self.editor.get_target_selection(), target) if code is None: - self.assertIsNone(selection_label.get_key()) + self.assertIsNone(selection_label.get_combination()) else: - self.assertEqual(selection_label.get_key(), Key(EV_KEY, code, 1)) + self.assertEqual( + selection_label.get_combination(), + EventCombination([EV_KEY, code, 1]), + ) with PatchedConfirmDelete(self.user_interface): self.editor._on_delete_button_clicked() @@ -1056,9 +1082,11 @@ class TestGui(GuiTestBase): # if a reference to the selection_label is held somewhere and it is # accidentally used again, make sure to not provide any outdated # information that is supposed to be deleted - self.assertIsNone(selection_label.get_key()) + self.assertIsNone(selection_label.get_combination()) if code is not None: - self.assertIsNone(custom_mapping.get_mapping(Key(EV_KEY, code, 1))) + self.assertIsNone( + active_preset.get_mapping(EventCombination([EV_KEY, code, 1])) + ) self.assertEqual( len(self.get_selection_labels()), @@ -1067,8 +1095,12 @@ class TestGui(GuiTestBase): # sleeps are added to be able to visually follow and debug the test. Add two # selection_labels by modifiying the one empty selection_label that exists - selection_label_1 = self.add_mapping_via_ui(Key(EV_KEY, 10, 1), "a") - selection_label_2 = self.add_mapping_via_ui(Key(EV_KEY, 11, 1), "b") + selection_label_1 = self.add_mapping_via_ui( + EventCombination([EV_KEY, 10, 1]), "a" + ) + selection_label_2 = self.add_mapping_via_ui( + EventCombination([EV_KEY, 11, 1]), "b" + ) # no empty selection_label added because one is unfinished time.sleep(0.2) @@ -1076,7 +1108,8 @@ class TestGui(GuiTestBase): self.assertEqual(len(self.get_selection_labels()), 3) self.assertEqual( - custom_mapping.get_mapping(Key(EV_KEY, 11, 1)), ("b", "keyboard") + active_preset.get_mapping(EventCombination([EV_KEY, 11, 1])), + ("b", "keyboard"), ) remove(selection_label_1, 10, "a", 2) @@ -1088,7 +1121,7 @@ class TestGui(GuiTestBase): remove(self.selection_label_listbox.get_children()[-1], None, None, 1) def test_problematic_combination(self): - combination = Key((EV_KEY, KEY_LEFTSHIFT, 1), (EV_KEY, 82, 1)) + combination = EventCombination((EV_KEY, KEY_LEFTSHIFT, 1), (EV_KEY, 82, 1)) self.add_mapping_via_ui(combination, "b") text = self.get_status_text() self.assertIn("shift", text) @@ -1101,18 +1134,19 @@ class TestGui(GuiTestBase): def test_rename_and_save(self): self.assertEqual(self.user_interface.group.name, "Foo Device") - self.assertFalse(config.is_autoloaded("Foo Device", "new preset")) + self.assertFalse(global_config.is_autoloaded("Foo Device", "new preset")) - custom_mapping.change(Key(EV_KEY, 14, 1), "keyboard", "a", None) + active_preset.change(EventCombination([EV_KEY, 14, 1]), "keyboard", "a", None) self.assertEqual(self.user_interface.preset_name, "new preset") self.user_interface.save_preset() self.assertEqual( - custom_mapping.get_mapping(Key(EV_KEY, 14, 1)), ("a", "keyboard") + active_preset.get_mapping(EventCombination([EV_KEY, 14, 1])), + ("a", "keyboard"), ) - config.set_autoload_preset("Foo Device", "new preset") - self.assertTrue(config.is_autoloaded("Foo Device", "new preset")) + global_config.set_autoload_preset("Foo Device", "new preset") + self.assertTrue(global_config.is_autoloaded("Foo Device", "new preset")) - custom_mapping.change(Key(EV_KEY, 14, 1), "keyboard", "b", None) + active_preset.change(EventCombination([EV_KEY, 14, 1]), "keyboard", "b", None) self.user_interface.get("preset_name_input").set_text("asdf") self.user_interface.save_preset() self.user_interface.on_rename_button_clicked(None) @@ -1120,26 +1154,27 @@ class TestGui(GuiTestBase): preset_path = f"{CONFIG_PATH}/presets/Foo Device/asdf.json" self.assertTrue(os.path.exists(preset_path)) self.assertEqual( - custom_mapping.get_mapping(Key(EV_KEY, 14, 1)), ("b", "keyboard") + active_preset.get_mapping(EventCombination([EV_KEY, 14, 1])), + ("b", "keyboard"), ) # after renaming the preset it is still set to autoload - self.assertTrue(config.is_autoloaded("Foo Device", "asdf")) + self.assertTrue(global_config.is_autoloaded("Foo Device", "asdf")) # ALSO IN THE ACTUAL CONFIG FILE! - config.load_config() - self.assertTrue(config.is_autoloaded("Foo Device", "asdf")) + global_config.load_config() + self.assertTrue(global_config.is_autoloaded("Foo Device", "asdf")) error_icon = self.user_interface.get("error_status_icon") self.assertFalse(error_icon.get_visible()) # otherwise save won't do anything - custom_mapping.change(Key(EV_KEY, 14, 1), "keyboard", "c", None) - self.assertTrue(custom_mapping.has_unsaved_changes()) + active_preset.change(EventCombination([EV_KEY, 14, 1]), "keyboard", "c", None) + self.assertTrue(active_preset.has_unsaved_changes()) def save(_): raise PermissionError - with patch.object(custom_mapping, "save", save): + with patch.object(active_preset, "save", save): self.user_interface.save_preset() status = self.get_status_text() self.assertIn("Permission denied", status) @@ -1151,32 +1186,33 @@ class TestGui(GuiTestBase): def test_rename_create_switch(self): # after renaming a preset and saving it, new presets # start with "new preset" again - custom_mapping.change(Key(EV_KEY, 14, 1), "keyboard", "a", None) + active_preset.change(EventCombination([EV_KEY, 14, 1]), "keyboard", "a", None) self.user_interface.get("preset_name_input").set_text("asdf") self.user_interface.save_preset() self.user_interface.on_rename_button_clicked(None) - self.assertEqual(len(custom_mapping), 1) + self.assertEqual(len(active_preset), 1) self.assertEqual(self.user_interface.preset_name, "asdf") self.user_interface.on_create_preset_clicked() self.assertEqual(self.user_interface.preset_name, "new preset") self.assertEqual(len(self.selection_label_listbox.get_children()), 1) - self.assertEqual(len(custom_mapping), 0) + self.assertEqual(len(active_preset), 0) self.user_interface.save_preset() # symbol and code in the gui won't be carried over after selecting a preset - self.editor.set_key(Key(EV_KEY, 15, 1)) + self.editor.set_combination(EventCombination([EV_KEY, 15, 1])) self.editor.set_symbol_input_text("b") # selecting the first preset again loads the saved mapping, and saves # the current changes in the gui self.user_interface.on_select_preset(FakePresetDropdown("asdf")) self.assertEqual( - custom_mapping.get_mapping(Key(EV_KEY, 14, 1)), ("a", "keyboard") + active_preset.get_mapping(EventCombination([EV_KEY, 14, 1])), + ("a", "keyboard"), ) - self.assertEqual(len(custom_mapping), 1) + self.assertEqual(len(active_preset), 1) self.assertEqual(len(self.selection_label_listbox.get_children()), 2) - config.set_autoload_preset("Foo Device", "new preset") + global_config.set_autoload_preset("Foo Device", "new preset") # renaming a preset to an existing name appends a number self.user_interface.on_select_preset(FakePresetDropdown("new preset")) @@ -1185,11 +1221,12 @@ class TestGui(GuiTestBase): self.assertEqual(self.user_interface.preset_name, "asdf 2") # and that added number is correctly used in the autoload # configuration as well - self.assertTrue(config.is_autoloaded("Foo Device", "asdf 2")) + self.assertTrue(global_config.is_autoloaded("Foo Device", "asdf 2")) self.assertEqual( - custom_mapping.get_mapping(Key(EV_KEY, 15, 1)), ("b", "keyboard") + active_preset.get_mapping(EventCombination([EV_KEY, 15, 1])), + ("b", "keyboard"), ) - self.assertEqual(len(custom_mapping), 1) + self.assertEqual(len(active_preset), 1) self.assertEqual(len(self.selection_label_listbox.get_children()), 2) self.assertEqual(self.user_interface.get("preset_name_input").get_text(), "") @@ -1210,16 +1247,18 @@ class TestGui(GuiTestBase): self.assertEqual(self.user_interface.preset_name, "asdf 2") def test_avoids_redundant_saves(self): - custom_mapping.change(Key(EV_KEY, 14, 1), "keyboard", "abcd", None) + active_preset.change( + EventCombination([EV_KEY, 14, 1]), "keyboard", "abcd", None + ) - custom_mapping.set_has_unsaved_changes(False) + active_preset.set_has_unsaved_changes(False) self.user_interface.save_preset() with open(get_preset_path("Foo Device", "new preset")) as f: content = f.read() self.assertNotIn("abcd", content) - custom_mapping.set_has_unsaved_changes(True) + active_preset.set_has_unsaved_changes(True) self.user_interface.save_preset() with open(get_preset_path("Foo Device", "new preset")) as f: @@ -1231,8 +1270,8 @@ class TestGui(GuiTestBase): error_icon = self.user_interface.get("error_status_icon") warning_icon = self.user_interface.get("warning_status_icon") - custom_mapping.change(Key(EV_KEY, 71, 1), "keyboard", "qux", None) - custom_mapping.change(Key(EV_KEY, 72, 1), "keyboard", "foo", None) + active_preset.change(EventCombination([EV_KEY, 71, 1]), "keyboard", "qux", None) + active_preset.change(EventCombination([EV_KEY, 72, 1]), "keyboard", "foo", None) self.user_interface.save_preset() tooltip = status.get_tooltip_text().lower() self.assertIn("qux", tooltip) @@ -1245,14 +1284,14 @@ class TestGui(GuiTestBase): self.assertIn("qux", content) self.assertIn("foo", content) - custom_mapping.change(Key(EV_KEY, 71, 1), "keyboard", "a", None) + active_preset.change(EventCombination([EV_KEY, 71, 1]), "keyboard", "a", None) self.user_interface.save_preset() tooltip = status.get_tooltip_text().lower() self.assertIn("foo", tooltip) self.assertTrue(error_icon.get_visible()) self.assertFalse(warning_icon.get_visible()) - custom_mapping.change(Key(EV_KEY, 72, 1), "keyboard", "b", None) + active_preset.change(EventCombination([EV_KEY, 72, 1]), "keyboard", "b", None) self.user_interface.save_preset() tooltip = status.get_tooltip_text() self.assertIsNone(tooltip) @@ -1264,14 +1303,16 @@ class TestGui(GuiTestBase): error_icon = self.user_interface.get("error_status_icon") warning_icon = self.user_interface.get("warning_status_icon") - custom_mapping.change(Key(EV_KEY, 9, 1), "keyboard", "k(1))", None) + active_preset.change( + EventCombination([EV_KEY, 9, 1]), "keyboard", "k(1))", None + ) self.user_interface.save_preset() tooltip = status.get_tooltip_text().lower() self.assertIn("brackets", tooltip) self.assertTrue(error_icon.get_visible()) self.assertFalse(warning_icon.get_visible()) - custom_mapping.change(Key(EV_KEY, 9, 1), "keyboard", "k(1)", None) + active_preset.change(EventCombination([EV_KEY, 9, 1]), "keyboard", "k(1)", None) self.user_interface.save_preset() tooltip = (status.get_tooltip_text() or "").lower() self.assertNotIn("brackets", tooltip) @@ -1279,13 +1320,14 @@ class TestGui(GuiTestBase): self.assertFalse(warning_icon.get_visible()) self.assertEqual( - custom_mapping.get_mapping(Key(EV_KEY, 9, 1)), ("k(1)", "keyboard") + active_preset.get_mapping(EventCombination([EV_KEY, 9, 1])), + ("k(1)", "keyboard"), ) def test_select_device_and_preset(self): foo_device_path = f"{CONFIG_PATH}/presets/Foo Device" - key_10 = Key(EV_KEY, 10, 1) - key_11 = Key(EV_KEY, 11, 1) + key_10 = EventCombination([EV_KEY, 10, 1]) + key_11 = EventCombination([EV_KEY, 11, 1]) # created on start because the first device is selected and some empty # preset prepared. @@ -1293,7 +1335,7 @@ class TestGui(GuiTestBase): self.assertEqual(self.user_interface.group.name, "Foo Device") self.assertEqual(self.user_interface.preset_name, "new preset") # change it to check if the gui loads presets correctly later - self.editor.set_key(key_10) + self.editor.set_combination(key_10) self.editor.set_symbol_input_text("a") # create another one @@ -1302,10 +1344,10 @@ class TestGui(GuiTestBase): self.assertTrue(os.path.exists(f"{foo_device_path}/new preset.json")) self.assertTrue(os.path.exists(f"{foo_device_path}/new preset 2.json")) self.assertEqual(self.user_interface.preset_name, "new preset 2") - self.assertEqual(len(custom_mapping), 0) + self.assertEqual(len(active_preset), 0) # this should not be loaded when "new preset" is selected, because it belongs # to "new preset 2": - self.editor.set_key(key_11) + self.editor.set_combination(key_11) self.editor.set_symbol_input_text("a") # select the first one again @@ -1313,8 +1355,8 @@ class TestGui(GuiTestBase): gtk_iteration() self.assertEqual(self.user_interface.preset_name, "new preset") - self.assertEqual(len(custom_mapping), 1) - self.assertEqual(custom_mapping.get_mapping(key_10), ("a", "keyboard")) + self.assertEqual(len(active_preset), 1) + self.assertEqual(active_preset.get_mapping(key_10), ("a", "keyboard")) self.assertListEqual( sorted(os.listdir(f"{foo_device_path}")), @@ -1331,7 +1373,7 @@ class TestGui(GuiTestBase): # putting new information into the editor does not lead to some weird # problems. when doing the rename everything will be saved and then moved # to the new path - self.editor.set_key(Key(EV_KEY, 10, 1)) + self.editor.set_combination(EventCombination([EV_KEY, 10, 1])) self.editor.set_symbol_input_text("1") self.assertEqual(self.user_interface.preset_name, "new preset") @@ -1352,7 +1394,7 @@ class TestGui(GuiTestBase): def test_copy_preset(self): selection_labels = self.selection_label_listbox - self.add_mapping_via_ui(Key(EV_KEY, 81, 1), "a") + self.add_mapping_via_ui(EventCombination([EV_KEY, 81, 1]), "a") time.sleep(0.1) gtk_iteration() self.user_interface.save_preset() @@ -1360,37 +1402,37 @@ class TestGui(GuiTestBase): self.assertEqual(len(selection_labels.get_children()), 2) # should be cleared when creating a new preset - custom_mapping.set("a.b", 3) - self.assertEqual(custom_mapping.get("a.b"), 3) + active_preset.set("a.b", 3) + self.assertEqual(active_preset.get("a.b"), 3) self.user_interface.on_create_preset_clicked() # the preset should be empty, only one empty selection_label present self.assertEqual(len(selection_labels.get_children()), 1) - self.assertIsNone(custom_mapping.get("a.b")) + self.assertIsNone(active_preset.get("a.b")) # add one new selection_label again and a setting - self.add_mapping_via_ui(Key(EV_KEY, 81, 1), "b") + self.add_mapping_via_ui(EventCombination([EV_KEY, 81, 1]), "b") time.sleep(0.1) gtk_iteration() self.user_interface.save_preset() self.assertEqual(len(selection_labels.get_children()), 2) - custom_mapping.set(["foo", "bar"], 2) + active_preset.set(["foo", "bar"], 2) # this time it should be copied self.user_interface.on_copy_preset_clicked() self.assertEqual(self.user_interface.preset_name, "new preset 2 copy") self.assertEqual(len(selection_labels.get_children()), 2) self.assertEqual(self.editor.get_symbol_input_text(), "b") - self.assertEqual(custom_mapping.get(["foo", "bar"]), 2) + self.assertEqual(active_preset.get(["foo", "bar"]), 2) # make another copy self.user_interface.on_copy_preset_clicked() self.assertEqual(self.user_interface.preset_name, "new preset 2 copy 2") self.assertEqual(len(selection_labels.get_children()), 2) self.assertEqual(self.editor.get_symbol_input_text(), "b") - self.assertEqual(len(custom_mapping), 1) - self.assertEqual(custom_mapping.get("foo.bar"), 2) + self.assertEqual(len(active_preset), 1) + self.assertEqual(active_preset.get("foo.bar"), 2) def test_gamepad_config(self): # set some stuff in the beginning, otherwise gtk fails to @@ -1401,17 +1443,17 @@ class TestGui(GuiTestBase): self.user_interface.get("left_joystick_purpose").set_active_id(BUTTONS) self.user_interface.get("right_joystick_purpose").set_active_id(BUTTONS) self.user_interface.get("joystick_mouse_speed").set_value(1) - custom_mapping.set_has_unsaved_changes(False) + active_preset.set_has_unsaved_changes(False) # select a device that is not a gamepad self.user_interface.on_select_device(FakeDeviceDropdown("Foo Device")) self.assertFalse(self.user_interface.get("gamepad_config").is_visible()) - self.assertFalse(custom_mapping.has_unsaved_changes()) + self.assertFalse(active_preset.has_unsaved_changes()) # select a gamepad self.user_interface.on_select_device(FakeDeviceDropdown("gamepad")) self.assertTrue(self.user_interface.get("gamepad_config").is_visible()) - self.assertFalse(custom_mapping.has_unsaved_changes()) + self.assertFalse(active_preset.has_unsaved_changes()) # set stuff gtk_iteration() @@ -1420,15 +1462,15 @@ class TestGui(GuiTestBase): joystick_mouse_speed = 5 self.user_interface.get("joystick_mouse_speed").set_value(joystick_mouse_speed) - # it should be stored in custom_mapping, which overwrites the - # global config - config.set("gamepad.joystick.left_purpose", MOUSE) - config.set("gamepad.joystick.right_purpose", MOUSE) - config.set("gamepad.joystick.pointer_speed", 50) - self.assertTrue(custom_mapping.has_unsaved_changes()) - left_purpose = custom_mapping.get("gamepad.joystick.left_purpose") - right_purpose = custom_mapping.get("gamepad.joystick.right_purpose") - pointer_speed = custom_mapping.get("gamepad.joystick.pointer_speed") + # it should be stored in active_preset, which overwrites the + # global_config + global_config.set("gamepad.joystick.left_purpose", MOUSE) + global_config.set("gamepad.joystick.right_purpose", MOUSE) + global_config.set("gamepad.joystick.pointer_speed", 50) + self.assertTrue(active_preset.has_unsaved_changes()) + left_purpose = active_preset.get("gamepad.joystick.left_purpose") + right_purpose = active_preset.get("gamepad.joystick.right_purpose") + pointer_speed = active_preset.get("gamepad.joystick.pointer_speed") self.assertEqual(left_purpose, WHEEL) self.assertEqual(right_purpose, WHEEL) self.assertEqual(pointer_speed, 2**joystick_mouse_speed) @@ -1436,7 +1478,7 @@ class TestGui(GuiTestBase): # select a device that is not a gamepad again self.user_interface.on_select_device(FakeDeviceDropdown("Foo Device")) self.assertFalse(self.user_interface.get("gamepad_config").is_visible()) - self.assertFalse(custom_mapping.has_unsaved_changes()) + self.assertFalse(active_preset.has_unsaved_changes()) def test_wont_start(self): error_icon = self.user_interface.get("error_status_icon") @@ -1447,7 +1489,7 @@ class TestGui(GuiTestBase): # empty - custom_mapping.empty() + active_preset.empty() self.user_interface.save_preset() self.user_interface.on_apply_preset_clicked(None) text = self.get_status_text() @@ -1459,7 +1501,7 @@ class TestGui(GuiTestBase): # not empty, but keys are held down - custom_mapping.change(Key(EV_KEY, KEY_A, 1), "keyboard", "a") + active_preset.change(EventCombination([EV_KEY, KEY_A, 1]), "keyboard", "a") self.user_interface.save_preset() send_event_to_reader(new_event(EV_KEY, KEY_A, 1)) reader.read() @@ -1513,7 +1555,7 @@ class TestGui(GuiTestBase): # this time work properly self.grab_fails = False - custom_mapping.save(get_preset_path(group_name, preset_name)) + active_preset.save(get_preset_path(group_name, preset_name)) self.user_interface.on_apply_preset_clicked(None) text = self.get_status_text() self.assertIn("Starting injection", text) @@ -1529,7 +1571,7 @@ class TestGui(GuiTestBase): ) # because this test managed to reproduce some minor bug: - self.assertNotIn("mapping", custom_mapping._config) + self.assertNotIn("mapping", active_preset._config) def test_wont_start_2(self): preset_name = "foo preset" @@ -1546,10 +1588,10 @@ class TestGui(GuiTestBase): return # btn_left mapped - custom_mapping.change(Key.btn_left(), "keyboard", "a") + active_preset.change(EventCombination(InputEvent.btn_left()), "keyboard", "a") self.user_interface.save_preset() - # and key held down + # and combination held down send_event_to_reader(new_event(EV_KEY, KEY_A, 1)) reader.read() self.assertEqual(len(reader._unreleased), 1) @@ -1591,12 +1633,12 @@ class TestGui(GuiTestBase): self.assertNotEqual( self.user_interface.dbus.get_state(self.user_interface.group.key), RUNNING ) - self.user_interface.can_modify_mapping() + self.user_interface.can_modify_preset() text = self.get_status_text() self.assertNotIn("Stop Injection", text) - custom_mapping.change(Key(EV_KEY, KEY_A, 1), "keyboard", "b") - custom_mapping.save(get_preset_path(group_name, preset_name)) + active_preset.change(EventCombination([EV_KEY, KEY_A, 1]), "keyboard", "b") + active_preset.save(get_preset_path(group_name, preset_name)) self.user_interface.on_apply_preset_clicked(None) # wait for the injector to start @@ -1610,8 +1652,8 @@ class TestGui(GuiTestBase): self.user_interface.dbus.get_state(self.user_interface.group.key), RUNNING ) - # the mapping cannot be changed anymore - self.assertFalse(self.user_interface.can_modify_mapping()) + # the preset cannot be changed anymore + self.assertFalse(self.user_interface.can_modify_preset()) # the toggle button should reset itself shortly self.user_interface.editor.get_recording_toggle().set_active(True) @@ -1629,7 +1671,7 @@ class TestGui(GuiTestBase): keycode_from = 9 keycode_to = 200 - self.add_mapping_via_ui(Key(EV_KEY, keycode_from, 1), "a") + self.add_mapping_via_ui(EventCombination([EV_KEY, keycode_from, 1]), "a") system_mapping.clear() system_mapping._set("a", keycode_to) @@ -1642,7 +1684,7 @@ class TestGui(GuiTestBase): ) # injecting for group.key will look at paths containing group.name - custom_mapping.save(get_preset_path("Foo Device", "foo preset")) + active_preset.save(get_preset_path("Foo Device", "foo preset")) # use only the manipulated system_mapping if os.path.exists(os.path.join(tmp, XMODMAP_FILENAME)): @@ -1692,8 +1734,8 @@ class TestGui(GuiTestBase): self.user_interface.get("left_joystick_purpose").set_active_id(BUTTONS) self.user_interface.get("joystick_mouse_speed").set_value(6) gtk_iteration() - speed = custom_mapping.get("gamepad.joystick.pointer_speed") - custom_mapping.set("gamepad.joystick.non_linearity", 1) + speed = active_preset.get("gamepad.joystick.pointer_speed") + active_preset.set("gamepad.joystick.non_linearity", 1) self.assertEqual(speed, 2 ** 6) # don't consume the events in the reader, they are used to test @@ -1707,7 +1749,7 @@ class TestGui(GuiTestBase): * 100, ) - custom_mapping.change(Key(EV_ABS, ABS_X, 1), "keyboard", "a") + active_preset.change(EventCombination([EV_ABS, ABS_X, 1]), "keyboard", "a") self.user_interface.save_preset() gtk_iteration() @@ -1731,7 +1773,7 @@ class TestGui(GuiTestBase): keycode_from = 16 keycode_to = 90 - self.add_mapping_via_ui(Key(EV_KEY, keycode_from, 1), "t") + self.add_mapping_via_ui(EventCombination([EV_KEY, keycode_from, 1]), "t") system_mapping.clear() system_mapping._set("t", keycode_to) @@ -1739,7 +1781,7 @@ class TestGui(GuiTestBase): # time due to time.sleep in the fakes and the injection is stopped. push_events("Bar Device", [new_event(1, keycode_from, 1)] * 100) - custom_mapping.save(get_preset_path("Bar Device", "foo preset")) + active_preset.save(get_preset_path("Bar Device", "foo preset")) self.user_interface.group = groups.find(name="Bar Device") self.user_interface.preset_name = "foo preset" @@ -1769,13 +1811,13 @@ class TestGui(GuiTestBase): self.assertEqual(len(write_history), len_before) def test_delete_preset(self): - self.editor.set_key(Key(EV_KEY, 71, 1)) + self.editor.set_combination(EventCombination([EV_KEY, 71, 1])) self.editor.set_symbol_input_text("a") self.user_interface.get("preset_name_input").set_text("asdf") self.user_interface.on_rename_button_clicked(None) gtk_iteration() self.assertEqual(self.user_interface.preset_name, "asdf") - self.assertEqual(len(custom_mapping), 1) + self.assertEqual(len(active_preset), 1) self.user_interface.save_preset() self.assertTrue(os.path.exists(get_preset_path("Foo Device", "asdf"))) @@ -1846,7 +1888,7 @@ class TestGui(GuiTestBase): # 1. create a preset self.user_interface.on_select_device(FakeDeviceDropdown("Foo Device 2")) self.user_interface.on_create_preset_clicked() - self.add_mapping_via_ui(Key(3, 2, 1), "qux") + self.add_mapping_via_ui(EventCombination([3, 2, 1]), "qux") self.user_interface.get("preset_name_input").set_text("asdf") self.user_interface.on_rename_button_clicked(None) self.user_interface.save_preset() @@ -1868,7 +1910,7 @@ class TestGui(GuiTestBase): with PatchedConfirmDelete(self.user_interface): # add some rows for code in range(3): - self.add_mapping_via_ui(Key(1, code, 1), "qux") + self.add_mapping_via_ui(EventCombination([1, code, 1]), "qux") self.user_interface.on_delete_preset_clicked() # the ui should be clear now @@ -1905,7 +1947,7 @@ class TestAutocompletion(GuiTestBase): self.editor.autocompletion.navigate(None, event) def test_autocomplete_key(self): - self.add_mapping_via_ui(Key(1, 99, 1), "") + self.add_mapping_via_ui(EventCombination([1, 99, 1]), "") source_view = self.editor.get_text_input() self.set_focus(source_view) @@ -1914,7 +1956,7 @@ class TestAutocompletion(GuiTestBase): system_mapping.clear() system_mapping._set(complete_key_name, 1) - # it can autocomplete a key inbetween other things + # it can autocomplete a combination inbetween other things incomplete = "qux_1\n + + qux_2" Gtk.TextView.do_insert_at_cursor(source_view, incomplete) Gtk.TextView.do_move_cursor( @@ -1948,7 +1990,7 @@ class TestAutocompletion(GuiTestBase): self.assertFalse(autocompletion.visible) def test_autocomplete_function(self): - self.add_mapping_via_ui(Key(1, 99, 1), "") + self.add_mapping_via_ui(EventCombination([1, 99, 1]), "") source_view = self.editor.get_text_input() self.set_focus(source_view) @@ -1969,7 +2011,7 @@ class TestAutocompletion(GuiTestBase): self.assertEqual(modified_symbol, "key(KEY_A).\nrepeat") def test_close_autocompletion(self): - self.add_mapping_via_ui(Key(1, 99, 1), "") + self.add_mapping_via_ui(EventCombination([1, 99, 1]), "") source_view = self.editor.get_text_input() self.set_focus(source_view) @@ -1990,7 +2032,7 @@ class TestAutocompletion(GuiTestBase): self.assertEqual(symbol, "KEY_") def test_writing_still_works(self): - self.add_mapping_via_ui(Key(1, 99, 1), "") + self.add_mapping_via_ui(EventCombination([1, 99, 1]), "") source_view = self.editor.get_text_input() self.set_focus(source_view) @@ -2019,7 +2061,7 @@ class TestAutocompletion(GuiTestBase): self.assertFalse(autocompletion.visible) def test_cycling(self): - self.add_mapping_via_ui(Key(1, 99, 1), "") + self.add_mapping_via_ui(EventCombination([1, 99, 1]), "") source_view = self.editor.get_text_input() self.set_focus(source_view) diff --git a/tests/test.py b/tests/test.py index 3a56fc23..f759429e 100644 --- a/tests/test.py +++ b/tests/test.py @@ -288,12 +288,12 @@ def new_event(type, code, value, timestamp=None, offset=0): sec = int(timestamp) usec = timestamp % 1 * 1000000 - event = evdev.InputEvent(sec, usec, type, code, value) + event = InputEvent(sec, usec, type, code, value) return event def patch_paths(): - from inputremapper import paths + from inputremapper.configs import paths paths.CONFIG_PATH = tmp @@ -527,12 +527,12 @@ from inputremapper.logger import update_verbosity update_verbosity(True) from inputremapper.injection.injector import Injector -from inputremapper.config import config +from inputremapper.configs.global_config import global_config from inputremapper.gui.reader import reader from inputremapper.groups import groups -from inputremapper.system_mapping import system_mapping -from inputremapper.gui.custom_mapping import custom_mapping -from inputremapper.paths import get_config_path +from inputremapper.configs.system_mapping import system_mapping +from inputremapper.gui.active_preset import active_preset +from inputremapper.configs.paths import get_config_path from inputremapper.injection.macros.macro import macro_variables from inputremapper.injection.consumers.keycode_mapper import active_macros, unreleased from inputremapper.injection.global_uinputs import global_uinputs @@ -600,15 +600,15 @@ def quick_cleanup(log=True): if os.path.exists(tmp): shutil.rmtree(tmp) - config.path = os.path.join(get_config_path(), "config.json") - config.clear_config() - config._save_config() + global_config.path = os.path.join(get_config_path(), "config.json") + global_config.clear_config() + global_config._save_config() system_mapping.populate() - custom_mapping.empty() - custom_mapping.clear_config() - custom_mapping.set_has_unsaved_changes(False) + active_preset.empty() + active_preset.clear_config() + active_preset.set_has_unsaved_changes(False) clear_write_history() diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index ff0ece85..4cbec555 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -22,8 +22,8 @@ import os import unittest -from inputremapper.config import config -from inputremapper.paths import touch +from inputremapper.configs.global_config import global_config +from inputremapper.configs.paths import touch from tests.test import quick_cleanup, tmp @@ -31,93 +31,95 @@ from tests.test import quick_cleanup, tmp class TestConfig(unittest.TestCase): def tearDown(self): quick_cleanup() - self.assertEqual(len(config.iterate_autoload_presets()), 0) + self.assertEqual(len(global_config.iterate_autoload_presets()), 0) def test_get_default(self): - config._config = {} - self.assertEqual(config.get("gamepad.joystick.non_linearity"), 4) + global_config._config = {} + self.assertEqual(global_config.get("gamepad.joystick.non_linearity"), 4) - config.set("gamepad.joystick.non_linearity", 3) - self.assertEqual(config.get("gamepad.joystick.non_linearity"), 3) + global_config.set("gamepad.joystick.non_linearity", 3) + self.assertEqual(global_config.get("gamepad.joystick.non_linearity"), 3) def test_basic(self): - self.assertEqual(config.get("a"), None) + self.assertEqual(global_config.get("a"), None) - config.set("a", 1) - self.assertEqual(config.get("a"), 1) + global_config.set("a", 1) + self.assertEqual(global_config.get("a"), 1) - config.remove("a") - config.set("a.b", 2) - self.assertEqual(config.get("a.b"), 2) - self.assertEqual(config._config["a"]["b"], 2) + global_config.remove("a") + global_config.set("a.b", 2) + self.assertEqual(global_config.get("a.b"), 2) + self.assertEqual(global_config._config["a"]["b"], 2) - config.remove("a.b") - config.set("a.b.c", 3) - self.assertEqual(config.get("a.b.c"), 3) - self.assertEqual(config._config["a"]["b"]["c"], 3) + global_config.remove("a.b") + global_config.set("a.b.c", 3) + self.assertEqual(global_config.get("a.b.c"), 3) + self.assertEqual(global_config._config["a"]["b"]["c"], 3) def test_autoload(self): - self.assertEqual(len(config.iterate_autoload_presets()), 0) - self.assertFalse(config.is_autoloaded("d1", "a")) - self.assertFalse(config.is_autoloaded("d2.foo", "b")) - self.assertEqual(config.get(["autoload", "d1"]), None) - self.assertEqual(config.get(["autoload", "d2.foo"]), None) - - config.set_autoload_preset("d1", "a") - self.assertEqual(len(config.iterate_autoload_presets()), 1) - self.assertTrue(config.is_autoloaded("d1", "a")) - self.assertFalse(config.is_autoloaded("d2.foo", "b")) - - config.set_autoload_preset("d2.foo", "b") - self.assertEqual(len(config.iterate_autoload_presets()), 2) - self.assertTrue(config.is_autoloaded("d1", "a")) - self.assertTrue(config.is_autoloaded("d2.foo", "b")) - self.assertEqual(config.get(["autoload", "d1"]), "a") - self.assertEqual(config.get("autoload.d1"), "a") - self.assertEqual(config.get(["autoload", "d2.foo"]), "b") - - config.set_autoload_preset("d2.foo", "c") - self.assertEqual(len(config.iterate_autoload_presets()), 2) - self.assertTrue(config.is_autoloaded("d1", "a")) - self.assertFalse(config.is_autoloaded("d2.foo", "b")) - self.assertTrue(config.is_autoloaded("d2.foo", "c")) - self.assertEqual(config._config["autoload"]["d2.foo"], "c") + 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")) + self.assertEqual(global_config.get(["autoload", "d1"]), None) + self.assertEqual(global_config.get(["autoload", "d2.foo"]), None) + + global_config.set_autoload_preset("d1", "a") + self.assertEqual(len(global_config.iterate_autoload_presets()), 1) + self.assertTrue(global_config.is_autoloaded("d1", "a")) + self.assertFalse(global_config.is_autoloaded("d2.foo", "b")) + + global_config.set_autoload_preset("d2.foo", "b") + self.assertEqual(len(global_config.iterate_autoload_presets()), 2) + self.assertTrue(global_config.is_autoloaded("d1", "a")) + self.assertTrue(global_config.is_autoloaded("d2.foo", "b")) + self.assertEqual(global_config.get(["autoload", "d1"]), "a") + self.assertEqual(global_config.get("autoload.d1"), "a") + self.assertEqual(global_config.get(["autoload", "d2.foo"]), "b") + + global_config.set_autoload_preset("d2.foo", "c") + self.assertEqual(len(global_config.iterate_autoload_presets()), 2) + self.assertTrue(global_config.is_autoloaded("d1", "a")) + self.assertFalse(global_config.is_autoloaded("d2.foo", "b")) + self.assertTrue(global_config.is_autoloaded("d2.foo", "c")) + self.assertEqual(global_config._config["autoload"]["d2.foo"], "c") self.assertListEqual( - list(config.iterate_autoload_presets()), + list(global_config.iterate_autoload_presets()), [("d1", "a"), ("d2.foo", "c")], ) - config.set_autoload_preset("d2.foo", None) - self.assertTrue(config.is_autoloaded("d1", "a")) - self.assertFalse(config.is_autoloaded("d2.foo", "b")) - self.assertFalse(config.is_autoloaded("d2.foo", "c")) - self.assertListEqual(list(config.iterate_autoload_presets()), [("d1", "a")]) - self.assertEqual(config.get(["autoload", "d1"]), "a") + global_config.set_autoload_preset("d2.foo", None) + self.assertTrue(global_config.is_autoloaded("d1", "a")) + self.assertFalse(global_config.is_autoloaded("d2.foo", "b")) + self.assertFalse(global_config.is_autoloaded("d2.foo", "c")) + self.assertListEqual( + list(global_config.iterate_autoload_presets()), [("d1", "a")] + ) + self.assertEqual(global_config.get(["autoload", "d1"]), "a") def test_initial(self): # when loading for the first time, create a config file with # the default values - os.remove(config.path) - self.assertFalse(os.path.exists(config.path)) - config.load_config() - self.assertTrue(os.path.exists(config.path)) + 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)) - with open(config.path, "r") as file: + with open(global_config.path, "r") as file: contents = file.read() self.assertIn('"keystroke_sleep_ms": 10', contents) def test_save_load(self): - self.assertEqual(len(config.iterate_autoload_presets()), 0) + self.assertEqual(len(global_config.iterate_autoload_presets()), 0) - config.load_config() - self.assertEqual(len(config.iterate_autoload_presets()), 0) + global_config.load_config() + self.assertEqual(len(global_config.iterate_autoload_presets()), 0) - config.set_autoload_preset("d1", "a") - config.set_autoload_preset("d2.foo", "b") + global_config.set_autoload_preset("d1", "a") + global_config.set_autoload_preset("d2.foo", "b") - config.load_config() + global_config.load_config() self.assertListEqual( - list(config.iterate_autoload_presets()), + list(global_config.iterate_autoload_presets()), [("d1", "a"), ("d2.foo", "b")], ) @@ -126,9 +128,9 @@ class TestConfig(unittest.TestCase): with open(config_2, "w") as f: f.write('{"a":"b"}') - config.load_config(config_2) - self.assertEqual(config.get("a"), "b") - self.assertEqual(config.get(["a"]), "b") + global_config.load_config(config_2) + self.assertEqual(global_config.get("a"), "b") + self.assertEqual(global_config.get(["a"]), "b") if __name__ == "__main__": diff --git a/tests/unit/test_consumer_control.py b/tests/unit/test_consumer_control.py index 7fd57715..aa36e61d 100644 --- a/tests/unit/test_consumer_control.py +++ b/tests/unit/test_consumer_control.py @@ -26,15 +26,15 @@ import evdev from evdev.ecodes import EV_KEY, EV_ABS, ABS_Y, EV_REL from inputremapper.injection.consumers.keycode_mapper import active_macros -from inputremapper.config import BUTTONS, MOUSE, WHEEL +from inputremapper.configs.global_config import BUTTONS, MOUSE, WHEEL from inputremapper.injection.context import Context -from inputremapper.mapping import Mapping -from inputremapper.key import Key +from inputremapper.configs.preset import Preset +from inputremapper.event_combination import EventCombination from inputremapper.injection.consumer_control import ConsumerControl, consumer_classes from inputremapper.injection.consumers.consumer import Consumer from inputremapper.injection.consumers.keycode_mapper import KeycodeMapper -from inputremapper.system_mapping import system_mapping +from inputremapper.configs.system_mapping import system_mapping from inputremapper.injection.global_uinputs import global_uinputs from tests.test import new_event, quick_cleanup @@ -61,7 +61,7 @@ class TestConsumerControl(unittest.IsolatedAsyncioTestCase): def setUp(self): consumer_classes.append(ExampleConsumer) self.gamepad_source = evdev.InputDevice("/dev/input/event30") - self.mapping = Mapping() + self.mapping = Preset() def tearDown(self): quick_cleanup() @@ -79,7 +79,7 @@ class TestConsumerControl(unittest.IsolatedAsyncioTestCase): return context, consumer_control async def test_no_keycode_mapper_needed(self): - self.mapping.change(Key(EV_KEY, 1, 1), "keyboard", "b") + self.mapping.change(EventCombination([EV_KEY, 1, 1]), "keyboard", "b") _, consumer_control = self.setup(self.gamepad_source, self.mapping) consumer_types = [type(consumer) for consumer in consumer_control._consumers] self.assertIn(KeycodeMapper, consumer_types) @@ -89,7 +89,7 @@ class TestConsumerControl(unittest.IsolatedAsyncioTestCase): consumer_types = [type(consumer) for consumer in consumer_control._consumers] self.assertNotIn(KeycodeMapper, consumer_types) - self.mapping.change(Key(EV_KEY, 1, 1), "keyboard", "k(a)") + self.mapping.change(EventCombination([EV_KEY, 1, 1]), "keyboard", "k(a)") _, consumer_control = self.setup(self.gamepad_source, self.mapping) consumer_types = [type(consumer) for consumer in consumer_control._consumers] self.assertIn(KeycodeMapper, consumer_types) @@ -101,9 +101,11 @@ class TestConsumerControl(unittest.IsolatedAsyncioTestCase): code_shift = system_mapping.get("KEY_LEFTSHIFT") trigger = 1 self.mapping.change( - Key(EV_KEY, trigger, 1), "keyboard", "if_single(k(a), k(KEY_LEFTSHIFT))" + EventCombination([EV_KEY, trigger, 1]), + "keyboard", + "if_single(k(a), k(KEY_LEFTSHIFT))", ) - self.mapping.change(Key(EV_ABS, ABS_Y, 1), "keyboard", "b") + self.mapping.change(EventCombination([EV_ABS, ABS_Y, 1]), "keyboard", "b") self.mapping.set("gamepad.joystick.left_purpose", MOUSE) self.mapping.set("gamepad.joystick.right_purpose", WHEEL) @@ -137,9 +139,11 @@ class TestConsumerControl(unittest.IsolatedAsyncioTestCase): code_shift = system_mapping.get("KEY_LEFTSHIFT") trigger = 1 self.mapping.change( - Key(EV_KEY, trigger, 1), "keyboard", "if_single(k(a), k(KEY_LEFTSHIFT))" + EventCombination([EV_KEY, trigger, 1]), + "keyboard", + "if_single(k(a), k(KEY_LEFTSHIFT))", ) - self.mapping.change(Key(EV_ABS, ABS_Y, 1), "keyboard", "b") + self.mapping.change(EventCombination([EV_ABS, ABS_Y, 1]), "keyboard", "b") self.mapping.set("gamepad.joystick.left_purpose", BUTTONS) self.mapping.set("gamepad.joystick.right_purpose", BUTTONS) @@ -173,9 +177,11 @@ class TestConsumerControl(unittest.IsolatedAsyncioTestCase): code_a = system_mapping.get("a") trigger = 1 self.mapping.change( - Key(EV_KEY, trigger, 1), "keyboard", "if_single(k(a), k(KEY_LEFTSHIFT))" + EventCombination([EV_KEY, trigger, 1]), + "keyboard", + "if_single(k(a), k(KEY_LEFTSHIFT))", ) - self.mapping.change(Key(EV_ABS, ABS_Y, 1), "keyboard", "b") + self.mapping.change(EventCombination([EV_ABS, ABS_Y, 1]), "keyboard", "b") self.mapping.set("gamepad.joystick.left_purpose", BUTTONS) self.mapping.set("gamepad.joystick.right_purpose", BUTTONS) diff --git a/tests/unit/test_context.py b/tests/unit/test_context.py index ec915629..0da17e46 100644 --- a/tests/unit/test_context.py +++ b/tests/unit/test_context.py @@ -22,10 +22,10 @@ import unittest from inputremapper.injection.context import Context -from inputremapper.mapping import Mapping -from inputremapper.key import Key -from inputremapper.config import NONE, MOUSE, WHEEL, BUTTONS -from inputremapper.system_mapping import system_mapping +from inputremapper.configs.preset import Preset +from inputremapper.event_combination import EventCombination +from inputremapper.configs.global_config import NONE, MOUSE, WHEEL, BUTTONS +from inputremapper.configs.system_mapping import system_mapping from tests.test import quick_cleanup @@ -35,12 +35,14 @@ class TestContext(unittest.TestCase): quick_cleanup() def setUp(self): - self.mapping = Mapping() + self.mapping = Preset() self.mapping.set("gamepad.joystick.left_purpose", WHEEL) self.mapping.set("gamepad.joystick.right_purpose", WHEEL) - self.mapping.change(Key(1, 31, 1), "keyboard", "k(a)") - self.mapping.change(Key(1, 32, 1), "keyboard", "b") - self.mapping.change(Key((1, 33, 1), (1, 34, 1), (1, 35, 1)), "keyboard", "c") + self.mapping.change(EventCombination([1, 31, 1]), "keyboard", "k(a)") + self.mapping.change(EventCombination([1, 32, 1]), "keyboard", "b") + self.mapping.change( + EventCombination((1, 33, 1), (1, 34, 1), (1, 35, 1)), "keyboard", "c" + ) self.context = Context(self.mapping) def test_update_purposes(self): @@ -120,7 +122,7 @@ class TestContext(unittest.TestCase): def test_writes_keys(self): self.assertTrue(self.context.writes_keys()) - self.assertFalse(Context(Mapping()).writes_keys()) + self.assertFalse(Context(Preset()).writes_keys()) if __name__ == "__main__": diff --git a/tests/unit/test_control.py b/tests/unit/test_control.py index 707ee0df..52f58149 100644 --- a/tests/unit/test_control.py +++ b/tests/unit/test_control.py @@ -30,11 +30,11 @@ import collections from importlib.util import spec_from_loader, module_from_spec from importlib.machinery import SourceFileLoader -from inputremapper.gui.custom_mapping import custom_mapping -from inputremapper.config import config +from inputremapper.gui.active_preset import active_preset +from inputremapper.configs.global_config import global_config from inputremapper.daemon import Daemon -from inputremapper.mapping import Mapping -from inputremapper.paths import get_preset_path +from inputremapper.configs.preset import Preset +from inputremapper.configs.paths import get_preset_path from inputremapper.groups import groups from tests.test import quick_cleanup, tmp @@ -42,7 +42,7 @@ from tests.test import quick_cleanup, tmp def import_control(): """Import the core function of the input-remapper-control command.""" - custom_mapping.empty() + active_preset.empty() bin_path = os.path.join(os.getcwd(), "bin", "input-remapper-control") @@ -77,9 +77,9 @@ class TestControl(unittest.TestCase): get_preset_path(groups_[1].name, presets[2]), ] - Mapping().save(paths[0]) - Mapping().save(paths[1]) - Mapping().save(paths[2]) + Preset().save(paths[0]) + Preset().save(paths[1]) + Preset().save(paths[2]) daemon = Daemon() @@ -98,8 +98,8 @@ class TestControl(unittest.TestCase): daemon.start_injecting = start_injecting - config.set_autoload_preset(groups_[0].key, presets[0]) - config.set_autoload_preset(groups_[1].key, presets[1]) + global_config.set_autoload_preset(groups_[0].key, presets[0]) + global_config.set_autoload_preset(groups_[1].key, presets[1]) communicate(options("autoload", None, None, None, False, False, False), daemon) self.assertEqual(len(start_history), 2) @@ -155,7 +155,7 @@ class TestControl(unittest.TestCase): daemon.autoload_history.may_autoload(groups_[1].key, presets[1]) ) self.assertEqual(stop_counter, 3) - config.set_autoload_preset(groups_[1].key, presets[2]) + global_config.set_autoload_preset(groups_[1].key, presets[2]) communicate( options("autoload", None, None, groups_[1].key, False, False, False), daemon ) @@ -201,18 +201,18 @@ class TestControl(unittest.TestCase): os.path.join(config_dir, "presets", device_names[1], presets[1] + ".json"), ] - Mapping().save(paths[0]) - Mapping().save(paths[1]) + Preset().save(paths[0]) + Preset().save(paths[1]) daemon = Daemon() start_history = [] daemon.start_injecting = lambda *args: start_history.append(args) - config.path = os.path.join(config_dir, "config.json") - config.load_config() - config.set_autoload_preset(device_names[0], presets[0]) - config.set_autoload_preset(device_names[1], presets[1]) + 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]) communicate( options("autoload", config_dir, None, None, False, False, False), @@ -282,19 +282,19 @@ class TestControl(unittest.TestCase): with open(os.path.join(path, "config.json"), "w") as file: file.write('{"foo":"bar"}') - self.assertIsNone(config.get("foo")) + self.assertIsNone(global_config.get("foo")) daemon.set_config_dir(path) - # since daemon and this test share the same memory, the config + # since daemon and this test share the same memory, the global_config # object that this test can access will be modified - self.assertEqual(config.get("foo"), "bar") + self.assertEqual(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(config.get("foo"), "bar") + self.assertEqual(global_config.get("foo"), "bar") daemon.set_config_dir(os.path.join(tmp, "qux")) - self.assertEqual(config.get("foo"), "bar") + self.assertEqual(global_config.get("foo"), "bar") def test_internals(self): with mock.patch("os.system") as os_system_patch: diff --git a/tests/unit/test_daemon.py b/tests/unit/test_daemon.py index e783f15f..da218063 100644 --- a/tests/unit/test_daemon.py +++ b/tests/unit/test_daemon.py @@ -30,13 +30,13 @@ from evdev.ecodes import EV_KEY, EV_ABS, KEY_B, KEY_A from gi.repository import Gtk from pydbus import SystemBus -from inputremapper.system_mapping import system_mapping -from inputremapper.gui.custom_mapping import custom_mapping -from inputremapper.config import config +from inputremapper.configs.system_mapping import system_mapping +from inputremapper.gui.active_preset import active_preset +from inputremapper.configs.global_config import global_config from inputremapper.groups import groups -from inputremapper.paths import get_config_path, mkdir, get_preset_path -from inputremapper.key import Key -from inputremapper.mapping import Mapping +from inputremapper.configs.paths import get_config_path, mkdir, get_preset_path +from inputremapper.event_combination import EventCombination +from inputremapper.configs.preset import Preset from inputremapper.injection.injector import STARTING, RUNNING, STOPPED, UNKNOWN from inputremapper.daemon import Daemon @@ -69,7 +69,7 @@ class TestDaemon(unittest.TestCase): self.grab = evdev.InputDevice.grab self.daemon = None mkdir(get_config_path()) - config._save_config() + global_config._save_config() def tearDown(self): # avoid race conditions with other tests, daemon may run processes @@ -126,13 +126,13 @@ class TestDaemon(unittest.TestCase): # unrelated group that shouldn't be affected at all group2 = groups.find(name="gamepad") - custom_mapping.change(Key(*ev_1, 1), "keyboard", "a") - custom_mapping.change(Key(*ev_2, -1), "keyboard", "b") + active_preset.change(EventCombination([*ev_1, 1]), "keyboard", "a") + active_preset.change(EventCombination([*ev_2, -1]), "keyboard", "b") preset = "foo" - custom_mapping.save(group.get_preset_path(preset)) - config.set_autoload_preset(group.key, preset) + active_preset.save(group.get_preset_path(preset)) + global_config.set_autoload_preset(group.key, preset) """injection 1""" @@ -183,15 +183,15 @@ class TestDaemon(unittest.TestCase): self.assertEqual(event.value, 1) def test_config_dir(self): - config.set("foo", "bar") - self.assertEqual(config.get("foo"), "bar") + global_config.set("foo", "bar") + self.assertEqual(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.assertEqual(self.daemon.config_dir, get_config_path()) - self.assertIsNone(config.get("foo")) + self.assertIsNone(global_config.get("foo")) def test_refresh_on_start(self): if os.path.exists(get_config_path("xmodmap.json")): @@ -206,7 +206,7 @@ class TestDaemon(unittest.TestCase): group = groups.find(name=group_name) # this test only makes sense if this device is unknown yet self.assertIsNone(group) - custom_mapping.change(Key(*ev, 1), "keyboard", "a") + active_preset.change(EventCombination([*ev, 1]), "keyboard", "a") system_mapping.clear() system_mapping._set("a", KEY_A) @@ -216,8 +216,8 @@ class TestDaemon(unittest.TestCase): system_mapping.clear() preset = "foo" - custom_mapping.save(get_preset_path(group_name, preset)) - config.set_autoload_preset(group_key, preset) + active_preset.save(get_preset_path(group_name, preset)) + global_config.set_autoload_preset(group_key, preset) push_events(group_key, [new_event(*ev, 1)]) self.daemon = Daemon() @@ -288,8 +288,8 @@ class TestDaemon(unittest.TestCase): path = os.path.join(config_dir, "presets", name, f"{preset}.json") - custom_mapping.change(Key(event), target, to_name) - custom_mapping.save(path) + active_preset.change(EventCombination(event), target, to_name) + active_preset.save(path) system_mapping.clear() @@ -298,8 +298,8 @@ class TestDaemon(unittest.TestCase): # an existing config file is needed otherwise set_config_dir refuses # to use the directory config_path = os.path.join(config_dir, "config.json") - config.path = config_path - config._save_config() + global_config.path = config_path + global_config._save_config() xmodmap_path = os.path.join(config_dir, "xmodmap.json") with open(xmodmap_path, "w") as file: @@ -325,8 +325,8 @@ class TestDaemon(unittest.TestCase): daemon = Daemon() self.daemon = daemon - mapping = Mapping() - mapping.change(Key(3, 2, 1), "keyboard", "a") + mapping = Preset() + mapping.change(EventCombination([3, 2, 1]), "keyboard", "a") mapping.save(group.get_preset_path(preset)) # start @@ -378,8 +378,8 @@ class TestDaemon(unittest.TestCase): daemon = Daemon() self.daemon = daemon - mapping = Mapping() - mapping.change(Key(3, 2, 1), "keyboard", "a") + mapping = Preset() + mapping.change(EventCombination([3, 2, 1]), "keyboard", "a") mapping.save(group.get_preset_path(preset)) # no autoloading is configured yet @@ -387,7 +387,7 @@ class TestDaemon(unittest.TestCase): self.assertNotIn(group.key, daemon.autoload_history._autoload_history) self.assertTrue(daemon.autoload_history.may_autoload(group.key, preset)) - config.set_autoload_preset(group.key, preset) + global_config.set_autoload_preset(group.key, preset) len_before = len(self.daemon.autoload_history._autoload_history) # now autoloading is configured, so it will autoload self.daemon._autoload(group.key) @@ -430,13 +430,13 @@ class TestDaemon(unittest.TestCase): # existing device preset = "preset7" group = groups.find(key="Foo Device 2") - mapping = Mapping() - mapping.change(Key(3, 2, 1), "keyboard", "a") + mapping = Preset() + mapping.change(EventCombination([3, 2, 1]), "keyboard", "a") mapping.save(group.get_preset_path(preset)) - config.set_autoload_preset(group.key, preset) + global_config.set_autoload_preset(group.key, preset) # ignored, won't cause problems: - config.set_autoload_preset("non-existant-key", "foo") + global_config.set_autoload_preset("non-existant-key", "foo") self.daemon.autoload() self.assertEqual(len(history), 1) @@ -447,11 +447,11 @@ class TestDaemon(unittest.TestCase): preset = "preset7" group = groups.find(key="Foo Device 2") - mapping = Mapping() - mapping.change(Key(3, 2, 1), "keyboard", "a") + mapping = Preset() + mapping.change(EventCombination([3, 2, 1]), "keyboard", "a") mapping.save(group.get_preset_path(preset)) - config.set_autoload_preset(group.key, preset) + global_config.set_autoload_preset(group.key, preset) self.daemon = Daemon() groups.set_groups([]) # caused the bug diff --git a/tests/unit/test_dev_utils.py b/tests/unit/test_dev_utils.py index 41e66307..b6f2eb8a 100644 --- a/tests/unit/test_dev_utils.py +++ b/tests/unit/test_dev_utils.py @@ -33,8 +33,8 @@ from evdev.ecodes import ( REL_HWHEEL, ) -from inputremapper.config import config, BUTTONS -from inputremapper.mapping import Mapping +from inputremapper.configs.global_config import global_config, BUTTONS +from inputremapper.configs.preset import Preset from inputremapper import utils from tests.test import new_event, InputDevice, MAX_ABS, MIN_ABS @@ -60,7 +60,7 @@ class TestDevUtils(unittest.TestCase): self.assertFalse(utils.is_wheel(new_event(EV_ABS, ABS_HAT0X, -1))) def test_should_map_as_btn(self): - mapping = Mapping() + mapping = Preset() def do(gamepad, event): return utils.should_map_as_btn(event, mapping, gamepad) @@ -121,7 +121,7 @@ class TestDevUtils(unittest.TestCase): self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_RY, -1))) mapping.set("gamepad.joystick.right_purpose", BUTTONS) - config.set("gamepad.joystick.left_purpose", BUTTONS) + global_config.set("gamepad.joystick.left_purpose", BUTTONS) # but only for gamepads self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_Y, -1))) self.assertTrue(do(1, new_event(EV_ABS, ecodes.ABS_Y, -1))) diff --git a/tests/unit/test_key.py b/tests/unit/test_event_combination.py similarity index 51% rename from tests/unit/test_key.py rename to tests/unit/test_event_combination.py index c741d0b6..d1458260 100644 --- a/tests/unit/test_key.py +++ b/tests/unit/test_event_combination.py @@ -23,41 +23,44 @@ import unittest from evdev.ecodes import KEY_LEFTSHIFT, KEY_RIGHTALT, KEY_LEFTCTRL -from inputremapper.key import Key +from inputremapper.event_combination import EventCombination +from inputremapper.input_event import InputEvent class TestKey(unittest.TestCase): def test_key(self): # its very similar to regular tuples, but with some extra stuff - key_1 = Key((1, 3, 1), (1, 5, 1)) - self.assertEqual(str(key_1), "Key((1, 3, 1), (1, 5, 1))") + key_1 = EventCombination((1, 3, 1), (1, 5, 1)) + self.assertEqual(str(key_1), "EventCombination((1, 3, 1), (1, 5, 1))") self.assertEqual(len(key_1), 2) self.assertEqual(key_1[0], (1, 3, 1)) self.assertEqual(key_1[1], (1, 5, 1)) self.assertEqual(hash(key_1), hash(((1, 3, 1), (1, 5, 1)))) - key_2 = Key((1, 3, 1)) - self.assertEqual(str(key_2), "Key((1, 3, 1),)") + key_2 = EventCombination((1, 3, 1)) + self.assertEqual(str(key_2), "EventCombination((1, 3, 1))") self.assertEqual(len(key_2), 1) self.assertNotEqual(key_2, key_1) self.assertNotEqual(hash(key_2), hash(key_1)) - key_3 = Key(1, 3, 1) - self.assertEqual(str(key_3), "Key((1, 3, 1),)") + key_3 = EventCombination((1, 3, 1)) + self.assertEqual(str(key_3), "EventCombination((1, 3, 1))") self.assertEqual(len(key_3), 1) self.assertEqual(key_3, key_2) - self.assertEqual(key_3, (1, 3, 1)) + self.assertNotEqual(key_3, (1, 3, 1)) self.assertEqual(hash(key_3), hash(key_2)) - self.assertEqual(hash(key_3), hash((1, 3, 1))) + self.assertEqual(hash(key_3), hash(((1, 3, 1),))) - key_4 = Key(key_3) - self.assertEqual(str(key_4), "Key((1, 3, 1),)") + key_4 = EventCombination(*key_3) + self.assertEqual(str(key_4), "EventCombination((1, 3, 1))") self.assertEqual(len(key_4), 1) self.assertEqual(key_4, key_3) self.assertEqual(hash(key_4), hash(key_3)) - key_5 = Key(key_4, key_4, (1, 7, 1)) - self.assertEqual(str(key_5), "Key((1, 3, 1), (1, 3, 1), (1, 7, 1))") + key_5 = EventCombination(*key_4, *key_4, (1, 7, 1)) + self.assertEqual( + str(key_5), "EventCombination((1, 3, 1), (1, 3, 1), (1, 7, 1))" + ) self.assertEqual(len(key_5), 3) self.assertNotEqual(key_5, key_4) self.assertNotEqual(hash(key_5), hash(key_4)) @@ -65,52 +68,63 @@ class TestKey(unittest.TestCase): self.assertEqual(hash(key_5), hash(((1, 3, 1), (1, 3, 1), (1, 7, 1)))) def test_get_permutations(self): - key_1 = Key((1, 3, 1)) + key_1 = EventCombination((1, 3, 1)) self.assertEqual(len(key_1.get_permutations()), 1) self.assertEqual(key_1.get_permutations()[0], key_1) - key_2 = Key((1, 3, 1), (1, 5, 1)) + key_2 = EventCombination((1, 3, 1), (1, 5, 1)) self.assertEqual(len(key_2.get_permutations()), 1) self.assertEqual(key_2.get_permutations()[0], key_2) - key_3 = Key((1, 3, 1), (1, 5, 1), (1, 7, 1)) + key_3 = EventCombination((1, 3, 1), (1, 5, 1), (1, 7, 1)) self.assertEqual(len(key_3.get_permutations()), 2) self.assertEqual( - key_3.get_permutations()[0], Key((1, 3, 1), (1, 5, 1), (1, 7, 1)) + key_3.get_permutations()[0], + EventCombination((1, 3, 1), (1, 5, 1), (1, 7, 1)), ) self.assertEqual(key_3.get_permutations()[1], ((1, 5, 1), (1, 3, 1), (1, 7, 1))) def test_is_problematic(self): - key_1 = Key((1, KEY_LEFTSHIFT, 1), (1, 5, 1)) + key_1 = EventCombination((1, KEY_LEFTSHIFT, 1), (1, 5, 1)) self.assertTrue(key_1.is_problematic()) - key_2 = Key((1, KEY_RIGHTALT, 1), (1, 5, 1)) + key_2 = EventCombination((1, KEY_RIGHTALT, 1), (1, 5, 1)) self.assertTrue(key_2.is_problematic()) - key_3 = Key((1, 3, 1), (1, KEY_LEFTCTRL, 1)) + key_3 = EventCombination((1, 3, 1), (1, KEY_LEFTCTRL, 1)) self.assertTrue(key_3.is_problematic()) - key_4 = Key(1, 3, 1) + key_4 = EventCombination((1, 3, 1)) self.assertFalse(key_4.is_problematic()) - key_5 = Key((1, 3, 1), (1, 5, 1)) + key_5 = EventCombination((1, 3, 1), (1, 5, 1)) self.assertFalse(key_5.is_problematic()) - def test_raises(self): - self.assertRaises(ValueError, lambda: Key(1)) - self.assertRaises(ValueError, lambda: Key(None)) - self.assertRaises(ValueError, lambda: Key([1])) - self.assertRaises(ValueError, lambda: Key((1,))) - self.assertRaises(ValueError, lambda: Key((1, 2))) - self.assertRaises(ValueError, lambda: Key(("1", "2", "3"))) - self.assertRaises(ValueError, lambda: Key("1")) - self.assertRaises(ValueError, lambda: Key("(1,2,3)")) - self.assertRaises(ValueError, lambda: Key((1, 2, 3), (1, 2, "3"))) - self.assertRaises(ValueError, lambda: Key((1, 2, 3), (1, 2, 3), None)) + def test_init(self): + self.assertRaises(ValueError, lambda: EventCombination(1)) + self.assertRaises(ValueError, lambda: EventCombination(None)) + self.assertRaises(ValueError, lambda: EventCombination([1])) + self.assertRaises(ValueError, lambda: EventCombination((1,))) + self.assertRaises(ValueError, lambda: EventCombination((1, 2))) + self.assertRaises(ValueError, lambda: EventCombination("1")) + self.assertRaises(ValueError, lambda: EventCombination("(1,2,3)")) + self.assertRaises( + ValueError, lambda: EventCombination((1, 2, 3), (1, 2, 3), None) + ) # those don't raise errors - Key((1, 2, 3), (1, 2, 3)) - Key((1, 2, 3)) + EventCombination((1, 2, 3), (1, 2, 3)) + EventCombination((1, 2, 3)) + EventCombination(("1", "2", "3")) + EventCombination("1, 2, 3") + EventCombination("1, 2, 3", (1, 3, 4), InputEvent.from_string(" 1,5 , 1 ")) + EventCombination((1, 2, 3), (1, 2, "3")) + + def test_json_str(self): + c1 = EventCombination((1, 2, 3)) + c2 = EventCombination((1, 2, 3), (4, 5, 6)) + self.assertEqual(c1.json_str(), "1,2,3") + self.assertEqual(c2.json_str(), "1,2,3+4,5,6") if __name__ == "__main__": diff --git a/tests/unit/test_event_producer.py b/tests/unit/test_event_producer.py index 34bd71e7..ead363f7 100644 --- a/tests/unit/test_event_producer.py +++ b/tests/unit/test_event_producer.py @@ -35,8 +35,8 @@ from evdev.ecodes import ( ABS_RY, ) -from inputremapper.config import config -from inputremapper.mapping import Mapping +from inputremapper.configs.global_config import global_config +from inputremapper.configs.preset import Preset from inputremapper.injection.context import Context from inputremapper.injection.consumers.joystick_to_mouse import ( JoystickToMouse, @@ -64,7 +64,7 @@ class TestJoystickToMouse(unittest.IsolatedAsyncioTestCase): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - self.mapping = Mapping() + self.mapping = Preset() self.context = Context(self.mapping) uinput = UInput() @@ -73,8 +73,8 @@ class TestJoystickToMouse(unittest.IsolatedAsyncioTestCase): source = InputDevice("/dev/input/event30") self.joystick_to_mouse = JoystickToMouse(self.context, source) - config.set("gamepad.joystick.x_scroll_speed", 1) - config.set("gamepad.joystick.y_scroll_speed", 1) + global_config.set("gamepad.joystick.x_scroll_speed", 1) + global_config.set("gamepad.joystick.y_scroll_speed", 1) def tearDown(self): quick_cleanup() @@ -155,12 +155,12 @@ class TestJoystickToMouse(unittest.IsolatedAsyncioTestCase): asyncio.ensure_future(self.joystick_to_mouse.run()) speed = 30 - config.set("gamepad.joystick.non_linearity", 1) - config.set("gamepad.joystick.pointer_speed", speed) - config.set("gamepad.joystick.left_purpose", WHEEL) - config.set("gamepad.joystick.right_purpose", MOUSE) - config.set("gamepad.joystick.x_scroll_speed", 1) - config.set("gamepad.joystick.y_scroll_speed", 2) + global_config.set("gamepad.joystick.non_linearity", 1) + global_config.set("gamepad.joystick.pointer_speed", speed) + global_config.set("gamepad.joystick.left_purpose", WHEEL) + global_config.set("gamepad.joystick.right_purpose", MOUSE) + global_config.set("gamepad.joystick.x_scroll_speed", 1) + global_config.set("gamepad.joystick.y_scroll_speed", 2) # vertical wheel event values are negative await self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, 1)) @@ -178,9 +178,9 @@ class TestJoystickToMouse(unittest.IsolatedAsyncioTestCase): speed = 40 self.mapping.set("gamepad.joystick.non_linearity", 1) - config.set("gamepad.joystick.pointer_speed", speed) + global_config.set("gamepad.joystick.pointer_speed", speed) self.mapping.set("gamepad.joystick.left_purpose", MOUSE) - config.set("gamepad.joystick.right_purpose", MOUSE) + global_config.set("gamepad.joystick.right_purpose", MOUSE) await self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_X, speed)) await self.do(MIN_ABS, 0, 0, 0, (EV_REL, REL_X, -speed)) @@ -195,8 +195,8 @@ class TestJoystickToMouse(unittest.IsolatedAsyncioTestCase): async def test_joystick_purpose_4(self): asyncio.ensure_future(self.joystick_to_mouse.run()) - config.set("gamepad.joystick.left_purpose", WHEEL) - config.set("gamepad.joystick.right_purpose", WHEEL) + global_config.set("gamepad.joystick.left_purpose", WHEEL) + global_config.set("gamepad.joystick.right_purpose", WHEEL) self.mapping.set("gamepad.joystick.x_scroll_speed", 2) self.mapping.set("gamepad.joystick.y_scroll_speed", 3) diff --git a/tests/unit/test_groups.py b/tests/unit/test_groups.py index 01b94425..bfc3f7ac 100644 --- a/tests/unit/test_groups.py +++ b/tests/unit/test_groups.py @@ -26,7 +26,7 @@ import json import evdev from evdev.ecodes import EV_KEY, KEY_A -from inputremapper.paths import CONFIG_PATH +from inputremapper.configs.paths import CONFIG_PATH from inputremapper.groups import ( _FindGroups, groups, diff --git a/tests/unit/test_injector.py b/tests/unit/test_injector.py index 5b097ac3..8991dfa0 100644 --- a/tests/unit/test_injector.py +++ b/tests/unit/test_injector.py @@ -55,11 +55,15 @@ from inputremapper.injection.injector import ( get_udev_name, ) from inputremapper.injection.numlock import is_numlock_on -from inputremapper.system_mapping import system_mapping, DISABLE_CODE, DISABLE_NAME -from inputremapper.gui.custom_mapping import custom_mapping -from inputremapper.mapping import Mapping -from inputremapper.config import config, NONE, MOUSE, WHEEL -from inputremapper.key import Key +from inputremapper.configs.system_mapping import ( + system_mapping, + DISABLE_CODE, + DISABLE_NAME, +) +from inputremapper.gui.active_preset import active_preset +from inputremapper.configs.preset import Preset +from inputremapper.configs.global_config import global_config, NONE, MOUSE, WHEEL +from inputremapper.event_combination import EventCombination from inputremapper.injection.macros.parse import parse from inputremapper.injection.context import Context from inputremapper.groups import groups, classify, GAMEPAD @@ -128,12 +132,12 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): # path is from the fixtures path = "/dev/input/event10" - custom_mapping.change(Key(EV_KEY, 10, 1), "keyboard", "a") + active_preset.change(EventCombination([EV_KEY, 10, 1]), "keyboard", "a") - self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping) + self.injector = Injector(groups.find(key="Foo Device 2"), active_preset) # this test needs to pass around all other constraints of # _grab_device - self.injector.context = Context(custom_mapping) + self.injector.context = Context(active_preset) device = self.injector._grab_device(path) gamepad = classify(device) == GAMEPAD self.assertFalse(gamepad) @@ -143,11 +147,11 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): def test_fail_grab(self): self.make_it_fail = 999 - custom_mapping.change(Key(EV_KEY, 10, 1), "keyboard", "a") + active_preset.change(EventCombination([EV_KEY, 10, 1]), "keyboard", "a") - self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping) + self.injector = Injector(groups.find(key="Foo Device 2"), active_preset) path = "/dev/input/event10" - self.injector.context = Context(custom_mapping) + self.injector.context = Context(active_preset) device = self.injector._grab_device(path) self.assertIsNone(device) self.assertGreaterEqual(self.failed, 1) @@ -162,9 +166,9 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): self.assertEqual(self.injector.get_state(), NO_GRAB) def test_grab_device_1(self): - custom_mapping.change(Key(EV_ABS, ABS_HAT0X, 1), "keyboard", "a") - self.injector = Injector(groups.find(name="gamepad"), custom_mapping) - self.injector.context = Context(custom_mapping) + active_preset.change(EventCombination([EV_ABS, ABS_HAT0X, 1]), "keyboard", "a") + self.injector = Injector(groups.find(name="gamepad"), active_preset) + self.injector.context = Context(active_preset) _grab_device = self.injector._grab_device # doesn't have the required capability @@ -176,17 +180,17 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): def test_gamepad_purpose_none(self): # forward abs joystick events - custom_mapping.set("gamepad.joystick.left_purpose", NONE) - config.set("gamepad.joystick.right_purpose", NONE) + active_preset.set("gamepad.joystick.left_purpose", NONE) + global_config.set("gamepad.joystick.right_purpose", NONE) - self.injector = Injector(groups.find(name="gamepad"), custom_mapping) - self.injector.context = Context(custom_mapping) + self.injector = Injector(groups.find(name="gamepad"), active_preset) + self.injector.context = Context(active_preset) path = "/dev/input/event30" device = self.injector._grab_device(path) self.assertIsNone(device) # no capability is used, so it won't grab - custom_mapping.change(Key(EV_KEY, BTN_A, 1), "keyboard", "a") + active_preset.change(EventCombination([EV_KEY, BTN_A, 1]), "keyboard", "a") device = self.injector._grab_device(path) self.assertIsNotNone(device) gamepad = classify(device) == GAMEPAD @@ -194,42 +198,42 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): def test_gamepad_purpose_none_2(self): # forward abs joystick events for the left joystick only - custom_mapping.set("gamepad.joystick.left_purpose", NONE) - config.set("gamepad.joystick.right_purpose", MOUSE) + active_preset.set("gamepad.joystick.left_purpose", NONE) + global_config.set("gamepad.joystick.right_purpose", MOUSE) - self.injector = Injector(groups.find(name="gamepad"), custom_mapping) - self.injector.context = Context(custom_mapping) + self.injector = Injector(groups.find(name="gamepad"), active_preset) + self.injector.context = Context(active_preset) path = "/dev/input/event30" device = self.injector._grab_device(path) # the right joystick maps as mouse, so it is grabbed - # even with an empty mapping + # even with an empty preset self.assertIsNotNone(device) gamepad = classify(device) == GAMEPAD self.assertTrue(gamepad) - custom_mapping.change(Key(EV_KEY, BTN_A, 1), "keyboard", "a") + active_preset.change(EventCombination([EV_KEY, BTN_A, 1]), "keyboard", "a") device = self.injector._grab_device(path) gamepad = classify(device) == GAMEPAD self.assertIsNotNone(device) self.assertTrue(gamepad) def test_skip_unused_device(self): - # skips a device because its capabilities are not used in the mapping - custom_mapping.change(Key(EV_KEY, 10, 1), "keyboard", "a") - self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping) - self.injector.context = Context(custom_mapping) + # skips a device because its capabilities are not used in the preset + active_preset.change(EventCombination([EV_KEY, 10, 1]), "keyboard", "a") + self.injector = Injector(groups.find(key="Foo Device 2"), active_preset) + self.injector.context = Context(active_preset) path = "/dev/input/event11" device = self.injector._grab_device(path) self.assertIsNone(device) self.assertEqual(self.failed, 0) def test_skip_unknown_device(self): - custom_mapping.change(Key(EV_KEY, 10, 1), "keyboard", "a") + active_preset.change(EventCombination([EV_KEY, 10, 1]), "keyboard", "a") - # skips a device because its capabilities are not used in the mapping - self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping) - self.injector.context = Context(custom_mapping) + # skips a device because its capabilities are not used in the preset + self.injector = Injector(groups.find(key="Foo Device 2"), active_preset) + self.injector.context = Context(active_preset) path = "/dev/input/event11" device = self.injector._grab_device(path) @@ -239,10 +243,10 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): def test_gamepad_to_mouse(self): # maps gamepad joystick events to mouse events - config.set("gamepad.joystick.non_linearity", 1) + global_config.set("gamepad.joystick.non_linearity", 1) pointer_speed = 80 - config.set("gamepad.joystick.pointer_speed", pointer_speed) - config.set("gamepad.joystick.left_purpose", MOUSE) + global_config.set("gamepad.joystick.pointer_speed", pointer_speed) + global_config.set("gamepad.joystick.left_purpose", MOUSE) # they need to sum up before something is written divisor = 10 @@ -258,7 +262,7 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): ], ) - self.injector = Injector(groups.find(name="gamepad"), custom_mapping) + self.injector = Injector(groups.find(name="gamepad"), active_preset) self.injector.start() # wait for the injector to start sending, at most 1s @@ -305,12 +309,12 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): * 2, ) - custom_mapping.set("gamepad.joystick.left_purpose", NONE) - custom_mapping.set("gamepad.joystick.right_purpose", NONE) + active_preset.set("gamepad.joystick.left_purpose", NONE) + active_preset.set("gamepad.joystick.right_purpose", NONE) # BTN_A -> 77 - custom_mapping.change(Key((1, BTN_A, 1)), "keyboard", "b") + active_preset.change(EventCombination([1, BTN_A, 1]), "keyboard", "b") system_mapping._set("b", 77) - self.injector = Injector(groups.find(name="gamepad"), custom_mapping) + self.injector = Injector(groups.find(name="gamepad"), active_preset) self.injector.start() # wait for the injector to start sending, at most 1s @@ -341,9 +345,9 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): # ABS_Z -> 77 # ABS_RZ is not mapped - custom_mapping.change(Key((EV_ABS, ABS_Z, 1)), "keyboard", "b") + active_preset.change(EventCombination((EV_ABS, ABS_Z, 1)), "keyboard", "b") system_mapping._set("b", 77) - self.injector = Injector(groups.find(name="gamepad"), custom_mapping) + self.injector = Injector(groups.find(name="gamepad"), active_preset) self.injector.start() # wait for the injector to start sending, at most 1s @@ -358,9 +362,9 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): @mock.patch("evdev.InputDevice.ungrab") def test_gamepad_to_mouse_joystick_to_mouse(self, ungrab_patch): - custom_mapping.set("gamepad.joystick.left_purpose", MOUSE) - custom_mapping.set("gamepad.joystick.right_purpose", NONE) - self.injector = Injector(groups.find(name="gamepad"), custom_mapping) + active_preset.set("gamepad.joystick.left_purpose", MOUSE) + active_preset.set("gamepad.joystick.right_purpose", NONE) + self.injector = Injector(groups.find(name="gamepad"), active_preset) # the stop message will be available in the pipe right away, # so run won't block and just stop. all the stuff # will be initialized though, so that stuff can be tested @@ -379,15 +383,15 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): self.assertEqual(joystick_to_mouse._abs_range[0], MIN_ABS) self.assertEqual(joystick_to_mouse._abs_range[1], MAX_ABS) self.assertEqual( - self.injector.context.mapping.get("gamepad.joystick.left_purpose"), MOUSE + self.injector.context.preset.get("gamepad.joystick.left_purpose"), MOUSE ) self.assertEqual(ungrab_patch.call_count, 1) def test_device1_not_a_gamepad(self): - custom_mapping.set("gamepad.joystick.left_purpose", MOUSE) - custom_mapping.set("gamepad.joystick.right_purpose", WHEEL) - self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping) + active_preset.set("gamepad.joystick.left_purpose", MOUSE) + active_preset.set("gamepad.joystick.right_purpose", WHEEL) + self.injector = Injector(groups.find(key="Foo Device 2"), active_preset) self.injector.stop_injecting() self.injector.run() @@ -395,7 +399,7 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): self.assertEqual(len(self.injector._consumer_controls), 0) def test_get_udev_name(self): - self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping) + self.injector = Injector(groups.find(key="Foo Device 2"), active_preset) suffix = "mapped" prefix = "input-remapper" expected = f'{prefix} {"a" * (80 - len(suffix) - len(prefix) - 2)} {suffix}' @@ -410,14 +414,18 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): @mock.patch("evdev.InputDevice.ungrab") def test_capabilities_and_uinput_presence(self, ungrab_patch): - custom_mapping.change(Key(EV_KEY, KEY_A, 1), "keyboard", "c") - custom_mapping.change(Key(EV_REL, REL_HWHEEL, 1), "keyboard", "k(b)") - self.injector = Injector(groups.find(key="Foo Device 2"), custom_mapping) + active_preset.change(EventCombination([EV_KEY, KEY_A, 1]), "keyboard", "c") + active_preset.change( + EventCombination([EV_REL, REL_HWHEEL, 1]), "keyboard", "k(b)" + ) + self.injector = Injector(groups.find(key="Foo Device 2"), active_preset) self.injector.stop_injecting() self.injector.run() self.assertEqual( - self.injector.context.mapping.get_mapping(Key(EV_KEY, KEY_A, 1)), + self.injector.context.preset.get_mapping( + EventCombination([EV_KEY, KEY_A, 1]) + ), ("c", "keyboard"), ) self.assertEqual( @@ -425,7 +433,9 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): (KEY_C, "keyboard"), ) self.assertEqual( - self.injector.context.mapping.get_mapping(Key(EV_REL, REL_HWHEEL, 1)), + self.injector.context.preset.get_mapping( + EventCombination([EV_REL, REL_HWHEEL, 1]) + ), ("k(b)", "keyboard"), ) self.assertEqual( @@ -461,14 +471,14 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): numlock_before = is_numlock_on() - combination = Key((EV_KEY, 8, 1), (EV_KEY, 9, 1)) - custom_mapping.change(combination, "keyboard", "k(KEY_Q).k(w)") - custom_mapping.change(Key(EV_ABS, ABS_HAT0X, -1), "keyboard", "a") + combination = EventCombination((EV_KEY, 8, 1), (EV_KEY, 9, 1)) + active_preset.change(combination, "keyboard", "k(KEY_Q).k(w)") + active_preset.change(EventCombination([EV_ABS, ABS_HAT0X, -1]), "keyboard", "a") # one mapping that is unknown in the system_mapping on purpose input_b = 10 - custom_mapping.change(Key(EV_KEY, input_b, 1), "keyboard", "b") + active_preset.change(EventCombination([EV_KEY, input_b, 1]), "keyboard", "b") - # stuff the custom_mapping outputs (except for the unknown b) + # stuff the active_preset outputs (except for the unknown b) system_mapping.clear() code_a = 100 code_q = 101 @@ -495,7 +505,7 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): ], ) - self.injector = Injector(groups.find(name="Bar Device"), custom_mapping) + self.injector = Injector(groups.find(name="Bar Device"), active_preset) self.assertEqual(self.injector.get_state(), UNKNOWN) self.injector.start() self.assertEqual(self.injector.get_state(), STARTING) @@ -575,8 +585,8 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): d_down = (EV_TYPE, CODE_2, 1) d_up = (EV_TYPE, CODE_2, 0) - custom_mapping.change(Key(*w_down[:2], -1), "keyboard", "w") - custom_mapping.change(Key(*d_down[:2], 1), "keyboard", "k(d)") + active_preset.change(EventCombination([*w_down[:2], -1]), "keyboard", "w") + active_preset.change(EventCombination([*d_down[:2], 1]), "keyboard", "k(d)") system_mapping.clear() code_w = 71 @@ -602,7 +612,7 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): ], ) - self.injector = Injector(groups.find(name="gamepad"), custom_mapping) + self.injector = Injector(groups.find(name="gamepad"), active_preset) # the injector will otherwise skip the device because # the capabilities don't contain EV_TYPE @@ -641,8 +651,8 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): # should be forwarded and present in the capabilities hw_left = (EV_REL, REL_HWHEEL, -1) - custom_mapping.change(Key(*hw_right), "keyboard", "k(b)") - custom_mapping.change(Key(*w_up), "keyboard", "c") + active_preset.change(EventCombination(hw_right), "keyboard", "k(b)") + active_preset.change(EventCombination(w_up), "keyboard", "c") system_mapping.clear() code_b = 91 @@ -660,7 +670,7 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): ) group = groups.find(key=group_key) - self.injector = Injector(group, custom_mapping) + self.injector = Injector(group, active_preset) device = InputDevice("/dev/input/event11") # make sure this test uses a device that has the needed capabilities @@ -711,12 +721,12 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): self.assertEqual(len(events), 3) def test_store_permutations_for_macros(self): - mapping = Mapping() + mapping = Preset() ev_1 = (EV_KEY, 41, 1) ev_2 = (EV_KEY, 42, 1) ev_3 = (EV_KEY, 43, 1) # a combination - mapping.change(Key(ev_1, ev_2, ev_3), "keyboard", "k(a)") + mapping.change(EventCombination(ev_1, ev_2, ev_3), "keyboard", "k(a)") self.injector = Injector(groups.find(key="Foo Device 2"), mapping) history = [] @@ -744,15 +754,17 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): self.assertEqual(macros[(ev_2, ev_1, ev_3)][0].code, "k(a)") def test_key_to_code(self): - mapping = Mapping() + mapping = Preset() ev_1 = (EV_KEY, 41, 1) ev_2 = (EV_KEY, 42, 1) ev_3 = (EV_KEY, 43, 1) ev_4 = (EV_KEY, 44, 1) - mapping.change(Key(ev_1), "keyboard", "a") + mapping.change(EventCombination(ev_1), "keyboard", "a") # a combination - mapping.change(Key(ev_2, ev_3, ev_4), "keyboard", "b") - self.assertEqual(mapping.get_mapping(Key(ev_2, ev_3, ev_4)), ("b", "keyboard")) + mapping.change(EventCombination(ev_2, ev_3, ev_4), "keyboard", "b") + self.assertEqual( + mapping.get_mapping(EventCombination(ev_2, ev_3, ev_4)), ("b", "keyboard") + ) system_mapping.clear() system_mapping._set("a", 51) @@ -771,18 +783,18 @@ class TestInjector(unittest.IsolatedAsyncioTestCase): self.assertEqual(len(injector.context.key_to_code), 3) def test_is_in_capabilities(self): - key = Key(1, 2, 1) + key = EventCombination([1, 2, 1]) capabilities = {1: [9, 2, 5]} self.assertTrue(is_in_capabilities(key, capabilities)) - key = Key((1, 2, 1), (1, 3, 1)) + key = EventCombination((1, 2, 1), (1, 3, 1)) capabilities = {1: [9, 2, 5]} # only one of the codes of the combination is required. - # The goal is to make combinations across those sub-devices possible, + # The goal is to make combinations= across those sub-devices possible, # that make up one hardware device self.assertTrue(is_in_capabilities(key, capabilities)) - key = Key((1, 2, 1), (1, 5, 1)) + key = EventCombination((1, 2, 1), (1, 5, 1)) capabilities = {1: [9, 2, 5]} self.assertTrue(is_in_capabilities(key, capabilities)) @@ -829,18 +841,18 @@ class TestModifyCapabilities(unittest.TestCase): assert absinfo is True return self._capabilities - mapping = Mapping() - mapping.change(Key(EV_KEY, 80, 1), "keyboard", "a") - mapping.change(Key(EV_KEY, 81, 1), "keyboard", DISABLE_NAME) + mapping = Preset() + mapping.change(EventCombination([EV_KEY, 80, 1]), "keyboard", "a") + mapping.change(EventCombination([EV_KEY, 81, 1]), "keyboard", DISABLE_NAME) macro_code = "r(2, m(sHiFt_l, r(2, k(1).k(2))))" macro = parse(macro_code, mapping) - mapping.change(Key(EV_KEY, 60, 111), "keyboard", macro_code) + mapping.change(EventCombination([EV_KEY, 60, 111]), "keyboard", macro_code) # going to be ignored, because EV_REL cannot be mapped, that's # mouse movements. - mapping.change(Key(EV_REL, 1234, 3), "keyboard", "b") + mapping.change(EventCombination([EV_REL, 1234, 3]), "keyboard", "b") self.a = system_mapping.get("a") self.shift_l = system_mapping.get("ShIfT_L") @@ -865,7 +877,9 @@ class TestModifyCapabilities(unittest.TestCase): quick_cleanup() def test_copy_capabilities(self): - self.mapping.change(Key(EV_KEY, 60, 1), "keyboard", self.macro.code) + self.mapping.change( + EventCombination([EV_KEY, 60, 1]), "keyboard", self.macro.code + ) # 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 diff --git a/tests/unit/test_input_event.py b/tests/unit/test_input_event.py new file mode 100644 index 00000000..bb8d0e01 --- /dev/null +++ b/tests/unit/test_input_event.py @@ -0,0 +1,118 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# input-remapper - GUI for device specific keyboard mappings +# Copyright (C) 2022 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 unittest + +import evdev +from dataclasses import FrozenInstanceError +from inputremapper.input_event import InputEvent +from inputremapper.exceptions import InputEventCreationError + + +class TestInputEvent(unittest.TestCase): + def test_from_event(self): + e1 = InputEvent.from_event(evdev.InputEvent(1, 2, 3, 4, 5)) + e2 = InputEvent.from_event(e1) + + self.assertEqual(e1, e2) + self.assertEqual(e1.sec, 1) + self.assertEqual(e1.usec, 2) + self.assertEqual(e1.type, 3) + self.assertEqual(e1.code, 4) + self.assertEqual(e1.value, 5) + + self.assertEqual(e1.sec, e2.sec) + self.assertEqual(e1.usec, e2.usec) + self.assertEqual(e1.type, e2.type) + self.assertEqual(e1.code, e2.code) + self.assertEqual(e1.value, e2.value) + + self.assertRaises(InputEventCreationError, InputEvent.from_event, "1,2,3") + + def test_from_string(self): + s1 = "1,2,3" + s2 = "1 ,2, 3 " + s3 = (1, 2, 3) + s4 = "1,2,3,4" + s5 = "1,2,_3" + + e1 = InputEvent.from_string(s1) + e2 = InputEvent.from_string(s2) + self.assertEqual(e1, e2) + self.assertEqual(e1.sec, 0) + self.assertEqual(e1.usec, 0) + self.assertEqual(e1.type, 1) + self.assertEqual(e1.code, 2) + self.assertEqual(e1.value, 3) + + self.assertRaises(InputEventCreationError, InputEvent.from_string, s3) + self.assertRaises(InputEventCreationError, InputEvent.from_string, s4) + self.assertRaises(InputEventCreationError, InputEvent.from_string, s5) + + def test_from_event_tuple(self): + t1 = (1, 2, 3) + t2 = (1, "2", 3) + t3 = (1, 2, 3, 4, 5) + t4 = (1, "b", 3) + + e1 = InputEvent.from_tuple(t1) + e2 = InputEvent.from_tuple(t2) + self.assertEqual(e1, e2) + self.assertEqual(e1.sec, 0) + self.assertEqual(e1.usec, 0) + self.assertEqual(e1.type, 1) + self.assertEqual(e1.code, 2) + self.assertEqual(e1.value, 3) + + self.assertRaises(InputEventCreationError, InputEvent.from_string, t3) + self.assertRaises(InputEventCreationError, InputEvent.from_string, t4) + + def test_properties(self): + e1 = InputEvent.btn_left() + self.assertEqual( + e1.event_tuple, (evdev.ecodes.EV_KEY, evdev.ecodes.BTN_LEFT, 1) + ) + self.assertEqual(e1.type_and_code, (evdev.ecodes.EV_KEY, evdev.ecodes.BTN_LEFT)) + + with self.assertRaises(TypeError): + e1.event_tuple = (1, 2, 3) + + with self.assertRaises(TypeError): + e1.type_and_code = (1, 2) + + with self.assertRaises(FrozenInstanceError): + e1.value = 5 + + def test_modify(self): + e1 = InputEvent(1, 2, 3, 4, 5) + e2 = e1.modify(value=6) + e3 = e1.modify(sec=0, usec=0, type=0, code=0, value=0) + + self.assertNotEqual(e1, e2) + self.assertEqual(e1.sec, e2.sec) + self.assertEqual(e1.usec, e2.usec) + self.assertEqual(e1.type, e2.type) + self.assertEqual(e1.code, e2.code) + self.assertNotEqual(e1.value, e2.value) + self.assertEqual(e3.sec, 0) + self.assertEqual(e3.usec, 0) + self.assertEqual(e3.type, 0) + self.assertEqual(e3.code, 0) + self.assertEqual(e3.value, 0) diff --git a/tests/unit/test_keycode_mapper.py b/tests/unit/test_keycode_mapper.py index 4400bdf4..666f3ce5 100644 --- a/tests/unit/test_keycode_mapper.py +++ b/tests/unit/test_keycode_mapper.py @@ -36,6 +36,7 @@ from evdev.ecodes import ( ABS_HAT1Y, ABS_Y, ) +from inputremapper.event_combination import EventCombination from inputremapper.injection.consumers.keycode_mapper import ( active_macros, @@ -43,13 +44,13 @@ from inputremapper.injection.consumers.keycode_mapper import ( unreleased, subsets, ) -from inputremapper.system_mapping import system_mapping +from inputremapper.configs.system_mapping import system_mapping from inputremapper.injection.macros.parse import parse from inputremapper.injection.context import Context from inputremapper.utils import RELEASE, PRESS -from inputremapper.config import config, BUTTONS -from inputremapper.mapping import Mapping -from inputremapper.system_mapping import DISABLE_CODE +from inputremapper.configs.global_config import global_config, BUTTONS +from inputremapper.configs.preset import Preset +from inputremapper.configs.system_mapping import DISABLE_CODE from inputremapper.injection.global_uinputs import global_uinputs from tests.test import ( @@ -85,7 +86,7 @@ def calculate_event_number(holdtime, before, after): after : int how many extra k() calls are executed after h() """ - keystroke_sleep = config.get("macros.keystroke_sleep_ms", 10) + keystroke_sleep = global_config.get("macros.keystroke_sleep_ms", 10) # down and up: two sleeps per k # one initial k(a): events = before * 2 @@ -99,7 +100,7 @@ def calculate_event_number(holdtime, before, after): class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): def setUp(self): - self.mapping = Mapping() + self.mapping = Preset() self.context = Context(self.mapping) self.source = InputDevice("/dev/input/event11") self.history = [] @@ -171,10 +172,10 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): uinput = UInput() self.context.uinput = uinput self.context.key_to_code = { - (ev_1,): (51, "keyboard"), - (ev_2,): (52, "keyboard"), - (ev_4,): (54, "keyboard"), - (ev_5,): (55, "keyboard"), + EventCombination(ev_1): (51, "keyboard"), + EventCombination(ev_2): (52, "keyboard"), + EventCombination(ev_4): (54, "keyboard"), + EventCombination(ev_5): (55, "keyboard"), } keycode_mapper = KeycodeMapper(self.context, self.source, uinput) @@ -265,7 +266,7 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): uinput = UInput() - _key_to_code = {((3, 0, -1),): (73, "keyboard")} + _key_to_code = {EventCombination((3, 0, -1)): (73, "keyboard")} self.mapping.set("gamepad.joystick.left_purpose", BUTTONS) @@ -328,7 +329,7 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): output = 71 - key_to_code = {(down_1, down_2): (71, "keyboard")} + key_to_code = {EventCombination(down_1, down_2): (71, "keyboard")} self.context.key_to_code = key_to_code keycode_mapper = KeycodeMapper(self.context, self.source, forward) @@ -358,8 +359,8 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): ev_4 = (EV_ABS, ABS_HAT0Y, 0) _key_to_code = { - (ev_1, ev_2): (51, "keyboard"), - (ev_2,): (52, "keyboard"), + EventCombination(ev_1, ev_2): (51, "keyboard"), + EventCombination(ev_2): (52, "keyboard"), } uinput = UInput() @@ -398,8 +399,8 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): # when input and output codes are the same (because it at some point # screwed it up because of that) _key_to_code = { - ((EV_KEY, 1, 1),): (101, "keyboard"), - ((EV_KEY, code_2, 1),): (code_2, "keyboard"), + EventCombination((EV_KEY, 1, 1)): (101, "keyboard"), + EventCombination((EV_KEY, code_2, 1)): (code_2, "keyboard"), } uinput_mapped = global_uinputs.devices["keyboard"] @@ -421,7 +422,7 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): self.assertEqual(uinput_forwarded.write_history[0].t, (EV_KEY, 3, 1)) async def test_combination_keycode(self): - combination = ((EV_KEY, 1, 1), (EV_KEY, 2, 1)) + combination = EventCombination((EV_KEY, 1, 1), (EV_KEY, 2, 1)) _key_to_code = {combination: (101, "keyboard")} uinput = UInput() @@ -430,8 +431,8 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): self.context.key_to_code = _key_to_code keycode_mapper = KeycodeMapper(self.context, self.source, uinput) - await keycode_mapper.notify(new_event(*combination[0])) - await keycode_mapper.notify(new_event(*combination[1])) + await keycode_mapper.notify(combination[0]) + await keycode_mapper.notify(combination[1]) self.assertEqual(len(uinput_write_history), 2) # the first event is written and then the triggered combination @@ -439,8 +440,8 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): self.assertEqual(uinput_write_history[1].t, (EV_KEY, 101, 1)) # release them - await keycode_mapper.notify(new_event(*combination[0][:2], 0)) - await keycode_mapper.notify(new_event(*combination[1][:2], 0)) + await keycode_mapper.notify(combination[0].modify(value=0)) + await keycode_mapper.notify(combination[1].modify(value=0)) # the first key writes its release event. The second key is hidden # behind the executed combination. The result of the combination is # also released, because it acts like a key. @@ -450,20 +451,20 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): # press them in the wrong order (the wrong key at the end, the order # of all other keys won't matter). no combination should be triggered - await keycode_mapper.notify(new_event(*combination[1])) - await keycode_mapper.notify(new_event(*combination[0])) + await keycode_mapper.notify(combination[1]) + await keycode_mapper.notify(combination[0]) self.assertEqual(len(uinput_write_history), 6) self.assertEqual(uinput_write_history[4].t, (EV_KEY, 2, 1)) self.assertEqual(uinput_write_history[5].t, (EV_KEY, 1, 1)) async def test_combination_keycode_2(self): - combination_1 = ( + combination_1 = EventCombination( (EV_KEY, 1, 1), (EV_ABS, ABS_Y, MIN_ABS), (EV_KEY, 3, 1), (EV_KEY, 4, 1), ) - combination_2 = ( + combination_2 = EventCombination( # should not be triggered, combination_1 should be prioritized # when all of its keys are down (EV_KEY, 2, 1), @@ -475,15 +476,18 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): up_5 = (EV_KEY, 5, 0) up_4 = (EV_KEY, 4, 0) - def sign_value(key): - return key[0], key[1], key[2] / abs(key[2]) + def sign_value(event): + return event.modify(value=event.value / abs(event.value)) _key_to_code = { # key_to_code is supposed to only contain values classified into PRESS, # PRESS_NEGATIVE and RELEASE - tuple([sign_value(a) for a in combination_1]): (101, "keyboard"), + EventCombination(*[sign_value(a) for a in combination_1]): ( + 101, + "keyboard", + ), combination_2: (102, "keyboard"), - (down_5,): (103, "keyboard"), + EventCombination(down_5): (103, "keyboard"), } uinput = UInput() @@ -501,11 +505,11 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): # 10 and 11: insert some more arbitrary key-down events, # they should not break the combinations await keycode_mapper.notify(new_event(EV_KEY, 10, 1)) - await keycode_mapper.notify(new_event(*combination_1[0])) - await keycode_mapper.notify(new_event(*combination_1[1])) - await keycode_mapper.notify(new_event(*combination_1[2])) + await keycode_mapper.notify(combination_1[0]) + await keycode_mapper.notify(combination_1[1]) + await keycode_mapper.notify(combination_1[2]) await keycode_mapper.notify(new_event(EV_KEY, 11, 1)) - await keycode_mapper.notify(new_event(*combination_1[3])) + await keycode_mapper.notify(combination_1[3]) # combination_1 should have been triggered now self.assertEqual(len(uinput_write_history), 6) @@ -544,7 +548,7 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): await keycode_mapper.notify(new_event(EV_KEY, 1, 1)) - sleeptime = config.get("macros.keystroke_sleep_ms", 10) * 12 + sleeptime = global_config.get("macros.keystroke_sleep_ms", 10) * 12 await asyncio.sleep(sleeptime / 1000 + 0.1) self.assertEqual( @@ -568,7 +572,7 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): await keycode_mapper.notify(new_event(EV_KEY, 1, 1)) await keycode_mapper.notify(new_event(EV_KEY, 2, 1)) - sleeptime = config.get("macros.keystroke_sleep_ms", 10) * 12 + sleeptime = global_config.get("macros.keystroke_sleep_ms", 10) * 12 # let the mainloop run for some time so that the macro does its stuff await asyncio.sleep(sleeptime / 1000 + 0.1) @@ -645,7 +649,7 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): # let the mainloop run for some time so that the macro does its stuff sleeptime = 500 - keystroke_sleep = config.get("macros.keystroke_sleep_ms", 10) + keystroke_sleep = global_config.get("macros.keystroke_sleep_ms", 10) await asyncio.sleep(sleeptime / 1000) self.assertTrue(active_macros[(EV_KEY, 1)].is_holding()) @@ -874,7 +878,7 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): # let the mainloop run for some time so that the macro does its stuff sleeptime = 500 - keystroke_sleep = config.get("macros.keystroke_sleep_ms", 10) + keystroke_sleep = global_config.get("macros.keystroke_sleep_ms", 10) await asyncio.sleep(sleeptime / 1000) # test that two macros are really running at the same time @@ -940,8 +944,8 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): trigger = (EV_KEY, BTN_TL) _key_to_code = { - ((*trigger, 1),): (51, "keyboard"), - ((*trigger, -1),): (52, "keyboard"), + EventCombination((*trigger, 1)): (51, "keyboard"), + EventCombination((*trigger, -1)): (52, "keyboard"), } uinput = UInput() @@ -987,7 +991,7 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): ev_3 = (*key, 0) _key_to_code = { - ((*key, 1),): (21, "keyboard"), + EventCombination((*key, 1)): (21, "keyboard"), } uinput = UInput() @@ -1019,12 +1023,12 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): ev_5 = (EV_KEY, KEY_A, 1) ev_6 = (EV_KEY, KEY_A, 0) - combi_1 = (ev_5, ev_3) - combi_2 = (ev_3, ev_5) + combi_1 = EventCombination(ev_5, ev_3) + combi_2 = EventCombination(ev_3, ev_5) _key_to_code = { - (ev_1,): (61, "keyboard"), - (ev_3,): (DISABLE_CODE, "keyboard"), + EventCombination(ev_1): (61, "keyboard"), + EventCombination(ev_3): (DISABLE_CODE, "keyboard"), combi_1: (62, "keyboard"), combi_2: (63, "keyboard"), } @@ -1062,49 +1066,49 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): """a combination that ends in a disabled key""" # ev_5 should be forwarded and the combination triggered - await keycode_mapper.notify(new_event(*combi_1[0])) # ev_5 - await keycode_mapper.notify(new_event(*combi_1[1])) # ev_3 + await keycode_mapper.notify(new_event(*combi_1[0].event_tuple)) # ev_5 + await keycode_mapper.notify(new_event(*combi_1[1].event_tuple)) # ev_3 expect_writecounts(3, 1) self.assertEqual(len(uinput_write_history), 4) self.assertEqual(uinput_write_history[2].t, (EV_KEY, KEY_A, 1)) self.assertEqual(uinput_write_history[3].t, (EV_KEY, 62, 1)) - self.assertIn(combi_1[0][:2], unreleased) - self.assertIn(combi_1[1][:2], unreleased) + self.assertIn(combi_1[0].type_and_code, unreleased) + self.assertIn(combi_1[1].type_and_code, unreleased) # since this event did not trigger anything, key is None - self.assertEqual(unreleased[combi_1[0][:2]].triggered_key, None) + self.assertEqual(unreleased[combi_1[0].type_and_code].triggered_key, None) # that one triggered something from _key_to_code, so the key is that - self.assertEqual(unreleased[combi_1[1][:2]].triggered_key, combi_1) + self.assertEqual(unreleased[combi_1[1].type_and_code].triggered_key, combi_1) # release the last key of the combi first, it should # release what the combination maps to - event = new_event(combi_1[1][0], combi_1[1][1], 0) + event = combi_1[1].modify(value=0) await keycode_mapper.notify(event) expect_writecounts(4, 1) self.assertEqual(len(uinput_write_history), 5) self.assertEqual(uinput_write_history[-1].t, (EV_KEY, 62, 0)) - self.assertIn(combi_1[0][:2], unreleased) - self.assertNotIn(combi_1[1][:2], unreleased) + self.assertIn(combi_1[0].type_and_code, unreleased) + self.assertNotIn(combi_1[1].type_and_code, unreleased) - event = new_event(combi_1[0][0], combi_1[0][1], 0) + event = combi_1[0].modify(value=0) await keycode_mapper.notify(event) expect_writecounts(4, 2) self.assertEqual(len(uinput_write_history), 6) self.assertEqual(uinput_write_history[-1].t, (EV_KEY, KEY_A, 0)) - self.assertNotIn(combi_1[0][:2], unreleased) - self.assertNotIn(combi_1[1][:2], unreleased) + self.assertNotIn(combi_1[0].type_and_code, unreleased) + self.assertNotIn(combi_1[1].type_and_code, unreleased) """a combination that starts with a disabled key""" # only the combination should get triggered - await keycode_mapper.notify(new_event(*combi_2[0])) - await keycode_mapper.notify(new_event(*combi_2[1])) + await keycode_mapper.notify(combi_2[0]) + await keycode_mapper.notify(combi_2[1]) expect_writecounts(5, 2) self.assertEqual(len(uinput_write_history), 7) self.assertEqual(uinput_write_history[-1].t, (EV_KEY, 63, 1)) # release the last key of the combi first, it should # release what the combination maps to - event = new_event(combi_2[1][0], combi_2[1][1], 0) + event = combi_2[1].modify(value=0) await keycode_mapper.notify(event) self.assertEqual(len(uinput_write_history), 8) self.assertEqual(uinput_write_history[-1].t, (EV_KEY, 63, 0)) @@ -1112,7 +1116,7 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): # the first key of combi_2 is disabled, so it won't write another # key-up event - event = new_event(combi_2[0][0], combi_2[0][1], 0) + event = combi_2[0].modify(value=0) await keycode_mapper.notify(event) self.assertEqual(len(uinput_write_history), 8) expect_writecounts(6, 2) @@ -1183,7 +1187,7 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): scroll_release = (2, 8, 0) btn_down = (1, 276, 1) btn_up = (1, 276, 0) - combination = ((1, 276, 1), (2, 8, -1)) + combination = EventCombination((1, 276, 1), (2, 8, -1)) system_mapping.clear() system_mapping._set("a", 30) @@ -1300,9 +1304,9 @@ class TestKeycodeMapper(unittest.IsolatedAsyncioTestCase): ev_6 = (EV_KEY, KEY_C, 0) self.context.key_to_code = { - (ev_1,): (51, "foo"), # invalid - (ev_2,): (BTN_TL, "keyboard"), # invalid - (ev_3,): (KEY_A, "keyboard"), # valid + EventCombination(ev_1): (51, "foo"), # invalid + EventCombination(ev_2): (BTN_TL, "keyboard"), # invalid + EventCombination(ev_3): (KEY_A, "keyboard"), # valid } keyboard = global_uinputs.get_uinput("keyboard") diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py index 0ce385c7..c198aea5 100644 --- a/tests/unit/test_logger.py +++ b/tests/unit/test_logger.py @@ -25,7 +25,7 @@ import unittest import logging from inputremapper.logger import logger, add_filehandler, update_verbosity, log_info -from inputremapper.paths import remove +from inputremapper.configs.paths import remove from tests.test import tmp diff --git a/tests/unit/test_macros.py b/tests/unit/test_macros.py index 42f4a314..21e18c05 100644 --- a/tests/unit/test_macros.py +++ b/tests/unit/test_macros.py @@ -59,9 +59,9 @@ from inputremapper.injection.macros.parse import ( remove_comments, ) from inputremapper.injection.context import Context -from inputremapper.config import config -from inputremapper.mapping import Mapping -from inputremapper.system_mapping import system_mapping +from inputremapper.configs.global_config import global_config +from inputremapper.configs.preset import Preset +from inputremapper.configs.system_mapping import system_mapping from inputremapper.utils import PRESS, RELEASE from tests.test import quick_cleanup, new_event @@ -79,11 +79,11 @@ class MacroTestBase(unittest.IsolatedAsyncioTestCase): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) - self.context = Context(Mapping()) + self.context = Context(Preset()) def tearDown(self): self.result = [] - self.context.mapping.clear_config() + self.context.preset.clear_config() quick_cleanup() def handler(self, ev_type, code, value): @@ -652,7 +652,7 @@ class TestMacros(MacroTestBase): self.assertSetEqual(macro.get_capabilities()[EV_KEY], {k_code}) await macro.run(self.handler) - keystroke_sleep = self.context.mapping.get("macros.keystroke_sleep_ms") + keystroke_sleep = self.context.preset.get("macros.keystroke_sleep_ms") sleep_time = 2 * repeats * keystroke_sleep / 1000 self.assertGreater(time.time() - start, sleep_time * 0.9) self.assertLess(time.time() - start, sleep_time * 1.2) @@ -671,7 +671,7 @@ class TestMacros(MacroTestBase): self.assertSetEqual(macro.get_capabilities()[EV_KEY], {m_code}) await macro.run(self.handler) - keystroke_time = 6 * self.context.mapping.get("macros.keystroke_sleep_ms") + keystroke_time = 6 * self.context.preset.get("macros.keystroke_sleep_ms") total_time = keystroke_time + 300 total_time /= 1000 @@ -735,7 +735,7 @@ class TestMacros(MacroTestBase): await macro.run(self.handler) num_pauses = 8 + 6 + 4 - keystroke_time = num_pauses * self.context.mapping.get( + keystroke_time = num_pauses * self.context.preset.get( "macros.keystroke_sleep_ms" ) wait_time = 220 @@ -757,8 +757,8 @@ class TestMacros(MacroTestBase): self.assertListEqual(self.result, []) async def test_keystroke_sleep_config(self): - # global config as fallback - config.set("macros.keystroke_sleep_ms", 100) + # global_config as fallback + global_config.set("macros.keystroke_sleep_ms", 100) start = time.time() macro = parse("k(a).k(b)", self.context) await macro.run(self.handler) @@ -767,8 +767,8 @@ class TestMacros(MacroTestBase): # that doesn't do anything self.assertGreater(delta, 0.300) - # now set the value in the mapping, which is prioritized - self.context.mapping.set("macros.keystroke_sleep_ms", 50) + # now set the value in the preset, which is prioritized + self.context.preset.set("macros.keystroke_sleep_ms", 50) start = time.time() macro = parse("k(a).k(b)", self.context) await macro.run(self.handler) diff --git a/tests/unit/test_mapping.py b/tests/unit/test_mapping.py deleted file mode 100644 index e3afd85a..00000000 --- a/tests/unit/test_mapping.py +++ /dev/null @@ -1,373 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -# input-remapper - GUI for device specific keyboard mappings -# Copyright (C) 2022 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 os -import unittest -import json -from unittest.mock import patch - -from evdev.ecodes import EV_KEY, EV_ABS, KEY_A - -from inputremapper.mapping import Mapping, split_key -from inputremapper.system_mapping import SystemMapping, XMODMAP_FILENAME -from inputremapper.config import config -from inputremapper.paths import get_preset_path -from inputremapper.key import Key - -from tests.test import tmp, quick_cleanup - - -class TestSystemMapping(unittest.TestCase): - def tearDown(self): - quick_cleanup() - - def test_split_key(self): - self.assertEqual(split_key("1,2,3"), (1, 2, 3)) - self.assertIsNone(split_key("1,2"), (1, 2, 1)) - self.assertIsNone(split_key("1")) - self.assertIsNone(split_key("1,a,2")) - self.assertIsNone(split_key("1,a")) - - 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) - - def test_xmodmap_file(self): - system_mapping = SystemMapping() - path = os.path.join(tmp, XMODMAP_FILENAME) - os.remove(path) - - system_mapping.populate() - self.assertTrue(os.path.exists(path)) - with open(path, "r") as file: - content = json.load(file) - self.assertEqual(content["a"], KEY_A) - # only xmodmap stuff should be present - self.assertNotIn("key_a", content) - self.assertNotIn("KEY_A", content) - self.assertNotIn("disable", content) - - 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") - # unknown stuff is returned as is - self.assertEqual(system_mapping.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) - - def test_system_mapping(self): - system_mapping = SystemMapping() - system_mapping.populate() - self.assertGreater(len(system_mapping._mapping), 100) - - # this is case-insensitive - self.assertEqual(system_mapping.get("1"), 2) - self.assertEqual(system_mapping.get("KeY_1"), 2) - - self.assertEqual(system_mapping.get("AlT_L"), 56) - self.assertEqual(system_mapping.get("KEy_LEFtALT"), 56) - - self.assertEqual(system_mapping.get("kEY_LeFTSHIFT"), 42) - self.assertEqual(system_mapping.get("ShiFt_L"), 42) - - self.assertEqual(system_mapping.get("BTN_left"), 272) - - self.assertIsNotNone(system_mapping.get("KEY_KP4")) - self.assertEqual(system_mapping.get("KP_Left"), system_mapping.get("KEY_KP4")) - - # this only lists the correct casing, - # includes linux constants and xmodmap symbols - names = system_mapping.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() - self.assertIn("F4", names) - self.assertNotIn("f4", names) - self.assertIn("BTN_RIGHT", names) - self.assertNotIn("btn_right", names) - self.assertIn("KEY_KP7", names) - self.assertIn("KP_Home", names) - self.assertNotIn("kp_home", names) - - self.assertEqual(system_mapping.get("disable"), -1) - - -class TestMapping(unittest.TestCase): - def setUp(self): - self.mapping = Mapping() - self.assertFalse(self.mapping.has_unsaved_changes()) - - def tearDown(self): - quick_cleanup() - - def test_config(self): - self.mapping.save(get_preset_path("foo", "bar2")) - - self.assertEqual(self.mapping.get("a"), None) - - self.assertFalse(self.mapping.has_unsaved_changes()) - - self.mapping.set("a", 1) - self.assertEqual(self.mapping.get("a"), 1) - self.assertTrue(self.mapping.has_unsaved_changes()) - - self.mapping.remove("a") - self.mapping.set("a.b", 2) - self.assertEqual(self.mapping.get("a.b"), 2) - self.assertEqual(self.mapping._config["a"]["b"], 2) - - self.mapping.remove("a.b") - self.mapping.set("a.b.c", 3) - self.assertEqual(self.mapping.get("a.b.c"), 3) - self.assertEqual(self.mapping._config["a"]["b"]["c"], 3) - - # setting mapping.whatever does not overwrite the mapping - # after saving. It should be ignored. - self.mapping.change(Key(EV_KEY, 81, 1), "keyboard", " a ") - self.mapping.set("mapping.a", 2) - self.assertEqual(self.mapping.num_saved_keys, 0) - self.mapping.save(get_preset_path("foo", "bar")) - self.assertEqual(self.mapping.num_saved_keys, len(self.mapping)) - self.assertFalse(self.mapping.has_unsaved_changes()) - self.mapping.load(get_preset_path("foo", "bar")) - self.assertEqual( - self.mapping.get_mapping(Key(EV_KEY, 81, 1)), ("a", "keyboard") - ) - self.assertIsNone(self.mapping.get("mapping.a")) - self.assertFalse(self.mapping.has_unsaved_changes()) - - # loading a different preset also removes the configs from memory - self.mapping.remove("a") - self.assertTrue(self.mapping.has_unsaved_changes()) - self.mapping.set("a.b.c", 6) - self.mapping.load(get_preset_path("foo", "bar2")) - self.assertIsNone(self.mapping.get("a.b.c")) - - def test_fallback(self): - config.set("d.e.f", 5) - self.assertEqual(self.mapping.get("d.e.f"), 5) - self.mapping.set("d.e.f", 3) - self.assertEqual(self.mapping.get("d.e.f"), 3) - - def test_clone(self): - ev_1 = Key(EV_KEY, 1, 1) - ev_2 = Key(EV_KEY, 2, 0) - - mapping1 = Mapping() - mapping1.change(ev_1, "keyboard", " a") - mapping2 = mapping1.clone() - mapping1.change(ev_2, "keyboard", "b ") - - self.assertEqual(mapping1.get_mapping(ev_1), ("a", "keyboard")) - self.assertEqual(mapping1.get_mapping(ev_2), ("b", "keyboard")) - - self.assertEqual(mapping2.get_mapping(ev_1), ("a", "keyboard")) - self.assertIsNone(mapping2.get_mapping(ev_2)) - - self.assertIsNone(mapping2.get_mapping(Key(EV_KEY, 2, 3))) - self.assertIsNone(mapping2.get_mapping(Key(EV_KEY, 1, 3))) - - def test_save_load(self): - one = Key(EV_KEY, 10, 1) - two = Key(EV_KEY, 11, 1) - three = Key(EV_KEY, 12, 1) - - self.mapping.change(one, "keyboard", "1") - self.mapping.change(two, "keyboard", "2") - self.mapping.change(Key(two, three), "keyboard", "3") - self.mapping._config["foo"] = "bar" - self.mapping.save(get_preset_path("Foo Device", "test")) - - path = os.path.join(tmp, "presets", "Foo Device", "test.json") - self.assertTrue(os.path.exists(path)) - - loaded = Mapping() - self.assertEqual(len(loaded), 0) - loaded.load(get_preset_path("Foo Device", "test")) - - self.assertEqual(len(loaded), 3) - self.assertEqual(loaded.get_mapping(one), ("1", "keyboard")) - self.assertEqual(loaded.get_mapping(two), ("2", "keyboard")) - self.assertEqual(loaded.get_mapping(Key(two, three)), ("3", "keyboard")) - self.assertEqual(loaded._config["foo"], "bar") - - def test_change(self): - # the reader would not report values like 111 or 222, only 1 or -1. - # the mapping just does what it is told, so it accepts them. - ev_1 = Key(EV_KEY, 1, 111) - ev_2 = Key(EV_KEY, 1, 222) - ev_3 = Key(EV_KEY, 2, 111) - ev_4 = Key(EV_ABS, 1, 111) - - # 1 is not assigned yet, ignore it - self.mapping.change(ev_1, "keyboard", "a", ev_2) - self.assertTrue(self.mapping.has_unsaved_changes()) - self.assertIsNone(self.mapping.get_mapping(ev_2)) - self.assertEqual(self.mapping.get_mapping(ev_1), ("a", "keyboard")) - self.assertEqual(len(self.mapping), 1) - - # change ev_1 to ev_3 and change a to b - self.mapping.change(ev_3, "keyboard", "b", ev_1) - self.assertIsNone(self.mapping.get_mapping(ev_1)) - self.assertEqual(self.mapping.get_mapping(ev_3), ("b", "keyboard")) - self.assertEqual(len(self.mapping), 1) - - # add 4 - self.mapping.change(ev_4, "keyboard", "c", None) - self.assertEqual(self.mapping.get_mapping(ev_3), ("b", "keyboard")) - self.assertEqual(self.mapping.get_mapping(ev_4), ("c", "keyboard")) - self.assertEqual(len(self.mapping), 2) - - # change the mapping of 4 to d - self.mapping.change(ev_4, "keyboard", "d", None) - self.assertEqual(self.mapping.get_mapping(ev_4), ("d", "keyboard")) - self.assertEqual(len(self.mapping), 2) - - # this also works in the same way - self.mapping.change(ev_4, "keyboard", "e", ev_4) - self.assertEqual(self.mapping.get_mapping(ev_4), ("e", "keyboard")) - self.assertEqual(len(self.mapping), 2) - - self.assertEqual(self.mapping.num_saved_keys, 0) - - def test_rejects_empty(self): - key = Key(EV_KEY, 1, 111) - self.assertEqual(len(self.mapping), 0) - self.assertRaises( - ValueError, lambda: self.mapping.change(key, "keyboard", " \n ") - ) - self.assertRaises(ValueError, lambda: self.mapping.change(key, " \n ", "b")) - self.assertEqual(len(self.mapping), 0) - - def test_avoids_redundant_changes(self): - # to avoid logs that don't add any value - def clear(*_): - # should not be called - raise AssertionError - - key = Key(EV_KEY, 987, 1) - target = "keyboard" - symbol = "foo" - - self.mapping.change(key, target, symbol) - with patch.object(self.mapping, "clear", clear): - self.mapping.change(key, target, symbol) - self.mapping.change(key, target, symbol, previous_key=key) - - def test_combinations(self): - ev_1 = Key(EV_KEY, 1, 111) - ev_2 = Key(EV_KEY, 1, 222) - ev_3 = Key(EV_KEY, 2, 111) - ev_4 = Key(EV_ABS, 1, 111) - combi_1 = Key(ev_1, ev_2, ev_3) - combi_2 = Key(ev_2, ev_1, ev_3) - combi_3 = Key(ev_1, ev_2, ev_4) - - self.mapping.change(combi_1, "keyboard", "a") - self.assertEqual(self.mapping.get_mapping(combi_1), ("a", "keyboard")) - self.assertEqual(self.mapping.get_mapping(combi_2), ("a", "keyboard")) - # since combi_1 and combi_2 are equivalent, a changes to b - self.mapping.change(combi_2, "keyboard", "b") - self.assertEqual(self.mapping.get_mapping(combi_1), ("b", "keyboard")) - self.assertEqual(self.mapping.get_mapping(combi_2), ("b", "keyboard")) - - self.mapping.change(combi_3, "keyboard", "c") - self.assertEqual(self.mapping.get_mapping(combi_1), ("b", "keyboard")) - self.assertEqual(self.mapping.get_mapping(combi_2), ("b", "keyboard")) - self.assertEqual(self.mapping.get_mapping(combi_3), ("c", "keyboard")) - - self.mapping.change(combi_3, "keyboard", "c", combi_1) - self.assertIsNone(self.mapping.get_mapping(combi_1)) - self.assertIsNone(self.mapping.get_mapping(combi_2)) - self.assertEqual(self.mapping.get_mapping(combi_3), ("c", "keyboard")) - - def test_clear(self): - # does nothing - ev_1 = Key(EV_KEY, 40, 1) - ev_2 = Key(EV_KEY, 30, 1) - ev_3 = Key(EV_KEY, 20, 1) - ev_4 = Key(EV_KEY, 10, 1) - - self.mapping.clear(ev_1) - self.assertFalse(self.mapping.has_unsaved_changes()) - self.assertEqual(len(self.mapping), 0) - - self.mapping._mapping[ev_1] = "b" - self.assertEqual(len(self.mapping), 1) - self.mapping.clear(ev_1) - self.assertEqual(len(self.mapping), 0) - self.assertTrue(self.mapping.has_unsaved_changes()) - - self.mapping.change(ev_4, "keyboard", "KEY_KP1", None) - self.assertTrue(self.mapping.has_unsaved_changes()) - self.mapping.change(ev_3, "keyboard", "KEY_KP2", None) - self.mapping.change(ev_2, "keyboard", "KEY_KP3", None) - self.assertEqual(len(self.mapping), 3) - self.mapping.clear(ev_3) - self.assertEqual(len(self.mapping), 2) - self.assertEqual(self.mapping.get_mapping(ev_4), ("KEY_KP1", "keyboard")) - self.assertIsNone(self.mapping.get_mapping(ev_3)) - self.assertEqual(self.mapping.get_mapping(ev_2), ("KEY_KP3", "keyboard")) - - def test_empty(self): - self.mapping.change(Key(EV_KEY, 10, 1), "keyboard", "1") - self.mapping.change(Key(EV_KEY, 11, 1), "keyboard", "2") - self.mapping.change(Key(EV_KEY, 12, 1), "keyboard", "3") - self.assertEqual(len(self.mapping), 3) - self.mapping.empty() - self.assertEqual(len(self.mapping), 0) - - def test_dangerously_mapped_btn_left(self): - self.mapping.change(Key.btn_left(), "keyboard", "1") - self.assertTrue(self.mapping.dangerously_mapped_btn_left()) - - self.mapping.change(Key(EV_KEY, 41, 1), "keyboard", "2") - self.assertTrue(self.mapping.dangerously_mapped_btn_left()) - - self.mapping.change(Key(EV_KEY, 42, 1), "gamepad", "btn_left") - self.assertFalse(self.mapping.dangerously_mapped_btn_left()) - - self.mapping.change(Key(EV_KEY, 42, 1), "gamepad", "BTN_Left") - self.assertFalse(self.mapping.dangerously_mapped_btn_left()) - - self.mapping.change(Key(EV_KEY, 42, 1), "keyboard", "3") - self.assertTrue(self.mapping.dangerously_mapped_btn_left()) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/unit/test_migrations.py b/tests/unit/test_migrations.py index 327e3fad..dd12cd27 100644 --- a/tests/unit/test_migrations.py +++ b/tests/unit/test_migrations.py @@ -20,11 +20,11 @@ import json from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X -from inputremapper.migrations import migrate, config_version -from inputremapper.mapping import Mapping -from inputremapper.config import config -from inputremapper.paths import touch, CONFIG_PATH, mkdir, get_preset_path -from inputremapper.key import Key +from inputremapper.configs.migrations import migrate, config_version +from inputremapper.configs.preset import Preset +from inputremapper.configs.global_config import global_config +from inputremapper.configs.paths import touch, CONFIG_PATH, mkdir, get_preset_path +from inputremapper.event_combination import EventCombination from inputremapper.user import HOME from inputremapper.logger import VERSION @@ -35,7 +35,7 @@ from tests.test import quick_cleanup, tmp class TestMigrations(unittest.TestCase): def tearDown(self): quick_cleanup() - self.assertEqual(len(config.iterate_autoload_presets()), 0) + self.assertEqual(len(global_config.iterate_autoload_presets()), 0) def test_migrate_suffix(self): old = os.path.join(CONFIG_PATH, "config") @@ -185,27 +185,36 @@ class TestMigrations(unittest.TestCase): file, ) migrate() - loaded = Mapping() + loaded = Preset() self.assertEqual(loaded.num_saved_keys, 0) loaded.load(get_preset_path("Foo Device", "test")) self.assertEqual(len(loaded), 6) self.assertEqual(loaded.num_saved_keys, 6) - self.assertEqual(loaded.get_mapping(Key(EV_KEY, 1, 1)), ("a", "keyboard")) - self.assertEqual(loaded.get_mapping(Key(EV_KEY, 2, 1)), ("BTN_B", "gamepad")) self.assertEqual( - loaded.get_mapping(Key(EV_KEY, 3, 1)), + loaded.get_mapping(EventCombination([EV_KEY, 1, 1])), ("a", "keyboard") + ) + self.assertEqual( + loaded.get_mapping(EventCombination([EV_KEY, 2, 1])), ("BTN_B", "gamepad") + ) + self.assertEqual( + loaded.get_mapping(EventCombination([EV_KEY, 3, 1])), ( "BTN_1\n# Broken mapping:\n# No target can handle all specified keycodes", "keyboard", ), ) - self.assertEqual(loaded.get_mapping(Key(EV_KEY, 4, 1)), ("a", "foo")) self.assertEqual( - loaded.get_mapping(Key(EV_ABS, ABS_HAT0X, -1)), ("b", "keyboard") + loaded.get_mapping(EventCombination([EV_KEY, 4, 1])), ("a", "foo") ) self.assertEqual( - loaded.get_mapping(Key((EV_ABS, 1, 1), (EV_ABS, 2, -1), Key(EV_ABS, 3, 1))), + loaded.get_mapping(EventCombination([EV_ABS, ABS_HAT0X, -1])), + ("b", "keyboard"), + ) + self.assertEqual( + loaded.get_mapping( + EventCombination((EV_ABS, 1, 1), (EV_ABS, 2, -1), (EV_ABS, 3, 1)) + ), ("c", "keyboard"), ) diff --git a/tests/unit/test_paths.py b/tests/unit/test_paths.py index d80fafdb..c2b1e925 100644 --- a/tests/unit/test_paths.py +++ b/tests/unit/test_paths.py @@ -23,7 +23,7 @@ import os import unittest import tempfile -from inputremapper.paths import touch, mkdir, get_preset_path, get_config_path +from inputremapper.configs.paths import touch, mkdir, get_preset_path, get_config_path from tests.test import quick_cleanup, tmp diff --git a/tests/unit/test_preset.py b/tests/unit/test_preset.py new file mode 100644 index 00000000..d78c4451 --- /dev/null +++ b/tests/unit/test_preset.py @@ -0,0 +1,361 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# input-remapper - GUI for device specific keyboard mappings +# Copyright (C) 2022 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 os +import unittest +import json +from unittest.mock import patch + +from evdev.ecodes import EV_KEY, EV_ABS, KEY_A +from inputremapper.input_event import InputEvent + +from inputremapper.configs.preset import Preset +from inputremapper.configs.system_mapping import SystemMapping, XMODMAP_FILENAME +from inputremapper.configs.global_config import global_config +from inputremapper.configs.paths import get_preset_path +from inputremapper.event_combination import EventCombination + +from tests.test import tmp, quick_cleanup + + +class TestSystemMapping(unittest.TestCase): + def tearDown(self): + quick_cleanup() + + 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) + + def test_xmodmap_file(self): + system_mapping = SystemMapping() + path = os.path.join(tmp, XMODMAP_FILENAME) + os.remove(path) + + system_mapping.populate() + self.assertTrue(os.path.exists(path)) + with open(path, "r") as file: + content = json.load(file) + self.assertEqual(content["a"], KEY_A) + # only xmodmap stuff should be present + self.assertNotIn("key_a", content) + self.assertNotIn("KEY_A", content) + self.assertNotIn("disable", content) + + 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") + # unknown stuff is returned as is + self.assertEqual(system_mapping.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) + + def test_system_mapping(self): + system_mapping = SystemMapping() + system_mapping.populate() + self.assertGreater(len(system_mapping._mapping), 100) + + # this is case-insensitive + self.assertEqual(system_mapping.get("1"), 2) + self.assertEqual(system_mapping.get("KeY_1"), 2) + + self.assertEqual(system_mapping.get("AlT_L"), 56) + self.assertEqual(system_mapping.get("KEy_LEFtALT"), 56) + + self.assertEqual(system_mapping.get("kEY_LeFTSHIFT"), 42) + self.assertEqual(system_mapping.get("ShiFt_L"), 42) + + self.assertEqual(system_mapping.get("BTN_left"), 272) + + self.assertIsNotNone(system_mapping.get("KEY_KP4")) + self.assertEqual(system_mapping.get("KP_Left"), system_mapping.get("KEY_KP4")) + + # this only lists the correct casing, + # includes linux constants and xmodmap symbols + names = system_mapping.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() + self.assertIn("F4", names) + self.assertNotIn("f4", names) + self.assertIn("BTN_RIGHT", names) + self.assertNotIn("btn_right", names) + self.assertIn("KEY_KP7", names) + self.assertIn("KP_Home", names) + self.assertNotIn("kp_home", names) + + self.assertEqual(system_mapping.get("disable"), -1) + + +class TestMapping(unittest.TestCase): + def setUp(self): + self.preset = Preset() + self.assertFalse(self.preset.has_unsaved_changes()) + + def tearDown(self): + quick_cleanup() + + def test_config(self): + self.preset.save(get_preset_path("foo", "bar2")) + + self.assertEqual(self.preset.get("a"), None) + + self.assertFalse(self.preset.has_unsaved_changes()) + + self.preset.set("a", 1) + self.assertEqual(self.preset.get("a"), 1) + self.assertTrue(self.preset.has_unsaved_changes()) + + self.preset.remove("a") + self.preset.set("a.b", 2) + self.assertEqual(self.preset.get("a.b"), 2) + self.assertEqual(self.preset._config["a"]["b"], 2) + + self.preset.remove("a.b") + self.preset.set("a.b.c", 3) + self.assertEqual(self.preset.get("a.b.c"), 3) + self.assertEqual(self.preset._config["a"]["b"]["c"], 3) + + # setting preset.whatever does not overwrite the preset + # after saving. It should be ignored. + self.preset.change(EventCombination([EV_KEY, 81, 1]), "keyboard", " a ") + self.preset.set("mapping.a", 2) + self.assertEqual(self.preset.num_saved_keys, 0) + self.preset.save(get_preset_path("foo", "bar")) + self.assertEqual(self.preset.num_saved_keys, len(self.preset)) + self.assertFalse(self.preset.has_unsaved_changes()) + self.preset.load(get_preset_path("foo", "bar")) + self.assertEqual( + self.preset.get_mapping(EventCombination([EV_KEY, 81, 1])), + ("a", "keyboard"), + ) + self.assertIsNone(self.preset.get("mapping.a")) + self.assertFalse(self.preset.has_unsaved_changes()) + + # loading a different preset also removes the configs from memory + self.preset.remove("a") + self.assertTrue(self.preset.has_unsaved_changes()) + self.preset.set("a.b.c", 6) + self.preset.load(get_preset_path("foo", "bar2")) + self.assertIsNone(self.preset.get("a.b.c")) + + def test_fallback(self): + global_config.set("d.e.f", 5) + self.assertEqual(self.preset.get("d.e.f"), 5) + self.preset.set("d.e.f", 3) + self.assertEqual(self.preset.get("d.e.f"), 3) + + def test_save_load(self): + one = InputEvent.from_tuple((EV_KEY, 10, 1)) + two = InputEvent.from_tuple((EV_KEY, 11, 1)) + three = InputEvent.from_tuple((EV_KEY, 12, 1)) + + self.preset.change(EventCombination(one), "keyboard", "1") + self.preset.change(EventCombination(two), "keyboard", "2") + self.preset.change(EventCombination(two, three), "keyboard", "3") + self.preset._config["foo"] = "bar" + self.preset.save(get_preset_path("Foo Device", "test")) + + path = os.path.join(tmp, "presets", "Foo Device", "test.json") + self.assertTrue(os.path.exists(path)) + + loaded = Preset() + self.assertEqual(len(loaded), 0) + loaded.load(get_preset_path("Foo Device", "test")) + + self.assertEqual(len(loaded), 3) + self.assertRaises(TypeError, loaded.get_mapping, one) + self.assertEqual(loaded.get_mapping(EventCombination(one)), ("1", "keyboard")) + self.assertEqual(loaded.get_mapping(EventCombination(two)), ("2", "keyboard")) + self.assertEqual( + loaded.get_mapping(EventCombination(two, three)), ("3", "keyboard") + ) + self.assertEqual(loaded._config["foo"], "bar") + + def test_change(self): + # the reader would not report values like 111 or 222, only 1 or -1. + # the preset just does what it is told, so it accepts them. + ev_1 = EventCombination((EV_KEY, 1, 111)) + ev_2 = EventCombination((EV_KEY, 1, 222)) + ev_3 = EventCombination((EV_KEY, 2, 111)) + ev_4 = EventCombination((EV_ABS, 1, 111)) + + self.assertRaises( + TypeError, self.preset.change, [(EV_KEY, 10, 1), "keyboard", "a", ev_2] + ) + self.assertRaises( + TypeError, self.preset.change, [ev_1, "keyboard", "a", (EV_KEY, 1, 222)] + ) + + # 1 is not assigned yet, ignore it + self.preset.change(ev_1, "keyboard", "a", ev_2) + self.assertTrue(self.preset.has_unsaved_changes()) + self.assertIsNone(self.preset.get_mapping(ev_2)) + self.assertEqual(self.preset.get_mapping(ev_1), ("a", "keyboard")) + self.assertEqual(len(self.preset), 1) + + # change ev_1 to ev_3 and change a to b + self.preset.change(ev_3, "keyboard", "b", ev_1) + self.assertIsNone(self.preset.get_mapping(ev_1)) + self.assertEqual(self.preset.get_mapping(ev_3), ("b", "keyboard")) + self.assertEqual(len(self.preset), 1) + + # add 4 + self.preset.change(ev_4, "keyboard", "c", None) + self.assertEqual(self.preset.get_mapping(ev_3), ("b", "keyboard")) + self.assertEqual(self.preset.get_mapping(ev_4), ("c", "keyboard")) + self.assertEqual(len(self.preset), 2) + + # change the preset of 4 to d + self.preset.change(ev_4, "keyboard", "d", None) + self.assertEqual(self.preset.get_mapping(ev_4), ("d", "keyboard")) + self.assertEqual(len(self.preset), 2) + + # this also works in the same way + self.preset.change(ev_4, "keyboard", "e", ev_4) + self.assertEqual(self.preset.get_mapping(ev_4), ("e", "keyboard")) + self.assertEqual(len(self.preset), 2) + + self.assertEqual(self.preset.num_saved_keys, 0) + + def test_rejects_empty(self): + key = EventCombination([EV_KEY, 1, 111]) + self.assertEqual(len(self.preset), 0) + self.assertRaises( + ValueError, lambda: self.preset.change(key, "keyboard", " \n ") + ) + self.assertRaises(ValueError, lambda: self.preset.change(key, " \n ", "b")) + self.assertEqual(len(self.preset), 0) + + def test_avoids_redundant_changes(self): + # to avoid logs that don't add any value + def clear(*_): + # should not be called + raise AssertionError + + key = EventCombination([EV_KEY, 987, 1]) + target = "keyboard" + symbol = "foo" + + self.preset.change(key, target, symbol) + with patch.object(self.preset, "clear", clear): + self.preset.change(key, target, symbol) + self.preset.change(key, target, symbol, previous_combination=key) + + def test_combinations(self): + ev_1 = InputEvent.from_tuple((EV_KEY, 1, 111)) + ev_2 = InputEvent.from_tuple((EV_KEY, 1, 222)) + ev_3 = InputEvent.from_tuple((EV_KEY, 2, 111)) + ev_4 = InputEvent.from_tuple((EV_ABS, 1, 111)) + combi_1 = EventCombination(ev_1, ev_2, ev_3) + combi_2 = EventCombination(ev_2, ev_1, ev_3) + combi_3 = EventCombination(ev_1, ev_2, ev_4) + + self.preset.change(combi_1, "keyboard", "a") + self.assertEqual(self.preset.get_mapping(combi_1), ("a", "keyboard")) + self.assertEqual(self.preset.get_mapping(combi_2), ("a", "keyboard")) + # since combi_1 and combi_2 are equivalent, a changes to b + self.preset.change(combi_2, "keyboard", "b") + self.assertEqual(self.preset.get_mapping(combi_1), ("b", "keyboard")) + self.assertEqual(self.preset.get_mapping(combi_2), ("b", "keyboard")) + + self.preset.change(combi_3, "keyboard", "c") + self.assertEqual(self.preset.get_mapping(combi_1), ("b", "keyboard")) + self.assertEqual(self.preset.get_mapping(combi_2), ("b", "keyboard")) + self.assertEqual(self.preset.get_mapping(combi_3), ("c", "keyboard")) + + self.preset.change(combi_3, "keyboard", "c", combi_1) + self.assertIsNone(self.preset.get_mapping(combi_1)) + self.assertIsNone(self.preset.get_mapping(combi_2)) + self.assertEqual(self.preset.get_mapping(combi_3), ("c", "keyboard")) + + def test_clear(self): + # does nothing + ev_1 = EventCombination((EV_KEY, 40, 1)) + ev_2 = EventCombination((EV_KEY, 30, 1)) + ev_3 = EventCombination((EV_KEY, 20, 1)) + ev_4 = EventCombination((EV_KEY, 10, 1)) + + self.assertRaises(TypeError, self.preset.clear, (EV_KEY, 10, 1)) + self.preset.clear(ev_1) + self.assertFalse(self.preset.has_unsaved_changes()) + self.assertEqual(len(self.preset), 0) + + self.preset._mapping[ev_1] = "b" + self.assertEqual(len(self.preset), 1) + self.preset.clear(ev_1) + self.assertEqual(len(self.preset), 0) + self.assertTrue(self.preset.has_unsaved_changes()) + + self.preset.change(ev_4, "keyboard", "KEY_KP1", None) + self.assertTrue(self.preset.has_unsaved_changes()) + self.preset.change(ev_3, "keyboard", "KEY_KP2", None) + self.preset.change(ev_2, "keyboard", "KEY_KP3", None) + self.assertEqual(len(self.preset), 3) + self.preset.clear(ev_3) + self.assertEqual(len(self.preset), 2) + self.assertEqual(self.preset.get_mapping(ev_4), ("KEY_KP1", "keyboard")) + self.assertIsNone(self.preset.get_mapping(ev_3)) + self.assertEqual(self.preset.get_mapping(ev_2), ("KEY_KP3", "keyboard")) + + def test_empty(self): + self.preset.change(EventCombination([EV_KEY, 10, 1]), "keyboard", "1") + self.preset.change(EventCombination([EV_KEY, 11, 1]), "keyboard", "2") + self.preset.change(EventCombination([EV_KEY, 12, 1]), "keyboard", "3") + self.assertEqual(len(self.preset), 3) + self.preset.empty() + self.assertEqual(len(self.preset), 0) + + def test_dangerously_mapped_btn_left(self): + self.preset.change(EventCombination(InputEvent.btn_left()), "keyboard", "1") + self.assertTrue(self.preset.dangerously_mapped_btn_left()) + + self.preset.change(EventCombination([EV_KEY, 41, 1]), "keyboard", "2") + self.assertTrue(self.preset.dangerously_mapped_btn_left()) + + self.preset.change(EventCombination([EV_KEY, 42, 1]), "gamepad", "btn_left") + self.assertFalse(self.preset.dangerously_mapped_btn_left()) + + self.preset.change(EventCombination([EV_KEY, 42, 1]), "gamepad", "BTN_Left") + self.assertFalse(self.preset.dangerously_mapped_btn_left()) + + self.preset.change(EventCombination([EV_KEY, 42, 1]), "keyboard", "3") + self.assertTrue(self.preset.dangerously_mapped_btn_left()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/test_presets.py b/tests/unit/test_presets.py index 21c9745a..ca9407e9 100644 --- a/tests/unit/test_presets.py +++ b/tests/unit/test_presets.py @@ -24,7 +24,7 @@ import unittest import shutil import time -from inputremapper.presets import ( +from inputremapper.configs.preset import ( find_newest_preset, rename_preset, get_any_preset, @@ -32,16 +32,16 @@ from inputremapper.presets import ( get_available_preset_name, get_presets, ) -from inputremapper.paths import CONFIG_PATH, get_preset_path, touch, mkdir -from inputremapper.gui.custom_mapping import custom_mapping +from inputremapper.configs.paths import CONFIG_PATH, get_preset_path, touch +from inputremapper.gui.active_preset import active_preset from tests.test import tmp def create_preset(group_name, name="new preset"): name = get_available_preset_name(group_name, name) - custom_mapping.empty() - custom_mapping.save(get_preset_path(group_name, name)) + active_preset.empty() + active_preset.save(get_preset_path(group_name, name)) PRESETS = os.path.join(CONFIG_PATH, "presets") diff --git a/tests/unit/test_reader.py b/tests/unit/test_reader.py index 22db901e..f0217f96 100644 --- a/tests/unit/test_reader.py +++ b/tests/unit/test_reader.py @@ -41,9 +41,9 @@ from evdev.ecodes import ( ) from inputremapper.gui.reader import reader, will_report_up -from inputremapper.gui.custom_mapping import custom_mapping -from inputremapper.config import BUTTONS, MOUSE -from inputremapper.key import Key +from inputremapper.gui.active_preset import active_preset +from inputremapper.configs.global_config import BUTTONS, MOUSE +from inputremapper.event_combination import EventCombination from inputremapper.gui.helper import RootHelper from inputremapper.groups import groups @@ -109,7 +109,7 @@ class TestReader(unittest.TestCase): self.create_helper() reader.start_reading(groups.find(key="Foo Device 2")) time.sleep(0.2) - self.assertEqual(reader.read(), (EV_ABS, ABS_HAT0X, 1)) + self.assertEqual(reader.read(), EventCombination((EV_ABS, ABS_HAT0X, 1))) self.assertEqual(reader.read(), None) self.assertEqual(len(reader._unreleased), 1) @@ -123,19 +123,21 @@ class TestReader(unittest.TestCase): send_event_to_reader(new_event(EV_REL, REL_WHEEL, 1)) result = reader.read() - self.assertIsInstance(result, Key) - self.assertEqual(result, (EV_REL, REL_WHEEL, 1)) + self.assertIsInstance(result, EventCombination) + self.assertIsInstance(result, tuple) + self.assertEqual(result, EventCombination((EV_REL, REL_WHEEL, 1))) self.assertEqual(result, ((EV_REL, REL_WHEEL, 1),)) - self.assertNotEqual(result, ((EV_REL, REL_WHEEL, 1), (1, 1, 1))) - self.assertEqual(result.keys, ((EV_REL, REL_WHEEL, 1),)) + self.assertNotEqual(result, EventCombination((EV_REL, REL_WHEEL, 1), (1, 1, 1))) # it won't return the same event twice self.assertEqual(reader.read(), None) # but it is still remembered unreleased self.assertEqual(len(reader._unreleased), 1) - self.assertEqual(reader.get_unreleased_keys(), (EV_REL, REL_WHEEL, 1)) - self.assertIsInstance(reader.get_unreleased_keys(), Key) + self.assertEqual( + reader.get_unreleased_keys(), EventCombination((EV_REL, REL_WHEEL, 1)) + ) + self.assertIsInstance(reader.get_unreleased_keys(), EventCombination) # as long as new wheel events arrive, it is considered unreleased for _ in range(10): @@ -153,8 +155,8 @@ class TestReader(unittest.TestCase): send_event_to_reader(new_event(EV_REL, REL_WHEEL, 1, 1000)) send_event_to_reader(new_event(EV_KEY, KEY_COMMA, 1, 1001)) - combi_1 = ((EV_REL, REL_WHEEL, 1), (EV_KEY, KEY_COMMA, 1)) - combi_2 = ((EV_KEY, KEY_COMMA, 1), (EV_KEY, KEY_A, 1)) + combi_1 = EventCombination((EV_REL, REL_WHEEL, 1), (EV_KEY, KEY_COMMA, 1)) + combi_2 = EventCombination((EV_KEY, KEY_COMMA, 1), (EV_KEY, KEY_A, 1)) read = reader.read() self.assertEqual(read, combi_1) self.assertEqual(reader.read(), None) @@ -174,7 +176,7 @@ class TestReader(unittest.TestCase): self.assertEqual(read, None) self.assertEqual(reader.read(), None) self.assertEqual(len(reader._unreleased), 1) - self.assertEqual(reader.get_unreleased_keys(), combi_1[1]) + self.assertEqual(reader.get_unreleased_keys(), EventCombination(combi_1[1])) # press down a new key, now it will return a different combination send_event_to_reader(new_event(EV_KEY, KEY_A, 1, 1002)) @@ -199,12 +201,12 @@ class TestReader(unittest.TestCase): self.assertEqual(reader.read(), None) send_event_to_reader(new_event(EV_REL, REL_WHEEL, 1)) - self.assertEqual(reader.read(), (EV_REL, REL_WHEEL, 1)) + self.assertEqual(reader.read(), EventCombination((EV_REL, REL_WHEEL, 1))) self.assertEqual(len(reader._unreleased), 1) self.assertEqual(reader.read(), None) send_event_to_reader(new_event(EV_REL, REL_WHEEL, -1)) - self.assertEqual(reader.read(), (EV_REL, REL_WHEEL, -1)) + self.assertEqual(reader.read(), EventCombination((EV_REL, REL_WHEEL, -1))) # notice that this is no combination of two sides, the previous # entry in unreleased has to get overwritten. So there is still only # one element in it. @@ -232,7 +234,7 @@ class TestReader(unittest.TestCase): reader.start_reading(groups.find(key="Foo Device 2")) time.sleep(0.1) - self.assertEqual(reader.read(), Key(EV_KEY, 1, 1)) + self.assertEqual(reader.read(), EventCombination((EV_KEY, 1, 1))) reader.start_reading(groups.find(name="Bar Device")) @@ -243,7 +245,7 @@ class TestReader(unittest.TestCase): reader.clear() time.sleep(0.1) - self.assertEqual(reader.read(), Key(EV_KEY, 2, 1)) + self.assertEqual(reader.read(), EventCombination((EV_KEY, 2, 1))) def test_reading_2(self): # a combination of events @@ -289,16 +291,20 @@ class TestReader(unittest.TestCase): reader.start_reading(groups.find(name="gamepad")) send_event_to_reader(new_event(EV_KEY, CODE_1, 1, 1001)) - self.assertEqual(reader.read(), ((EV_KEY, CODE_1, 1))) + self.assertEqual(reader.read(), EventCombination((EV_KEY, CODE_1, 1))) - custom_mapping.set("gamepad.joystick.left_purpose", BUTTONS) + active_preset.set("gamepad.joystick.left_purpose", BUTTONS) send_event_to_reader(new_event(EV_ABS, ABS_Y, 1, 1002)) - self.assertEqual(reader.read(), ((EV_KEY, CODE_1, 1), (EV_ABS, ABS_Y, 1))) + self.assertEqual( + reader.read(), EventCombination((EV_KEY, CODE_1, 1), (EV_ABS, ABS_Y, 1)) + ) send_event_to_reader(new_event(EV_ABS, ABS_HAT0X, -1, 1003)) self.assertEqual( reader.read(), - ((EV_KEY, CODE_1, 1), (EV_ABS, ABS_Y, 1), (EV_ABS, ABS_HAT0X, -1)), + EventCombination( + (EV_KEY, CODE_1, 1), (EV_ABS, ABS_Y, 1), (EV_ABS, ABS_HAT0X, -1) + ), ) # adding duplicate down events won't report a different combination. @@ -321,7 +327,7 @@ class TestReader(unittest.TestCase): def test_reads_joysticks(self): # if their purpose is "buttons" - custom_mapping.set("gamepad.joystick.left_purpose", BUTTONS) + active_preset.set("gamepad.joystick.left_purpose", BUTTONS) push_events( "gamepad", [ @@ -335,12 +341,12 @@ class TestReader(unittest.TestCase): reader.start_reading(groups.find(name="gamepad")) time.sleep(0.2) - self.assertEqual(reader.read(), (EV_ABS, ABS_Y, 1)) + self.assertEqual(reader.read(), EventCombination((EV_ABS, ABS_Y, 1))) self.assertEqual(reader.read(), None) self.assertEqual(len(reader._unreleased), 1) reader._unreleased = {} - custom_mapping.set("gamepad.joystick.left_purpose", MOUSE) + active_preset.set("gamepad.joystick.left_purpose", MOUSE) push_events("gamepad", [new_event(EV_ABS, ABS_Y, MAX_ABS)]) self.create_helper() @@ -363,10 +369,12 @@ class TestReader(unittest.TestCase): send_event_to_reader(new_event(3, 1, 0, next_timestamp())) send_event_to_reader(new_event(3, 0, 0, next_timestamp())) send_event_to_reader(new_event(3, 2, 1, next_timestamp())) - self.assertEqual(reader.read(), (EV_ABS, ABS_Z, 1)) + self.assertEqual(reader.read(), EventCombination((EV_ABS, ABS_Z, 1))) send_event_to_reader(new_event(3, 0, 0, next_timestamp())) send_event_to_reader(new_event(3, 5, 1, next_timestamp())) - self.assertEqual(reader.read(), ((EV_ABS, ABS_Z, 1), (EV_ABS, ABS_RZ, 1))) + self.assertEqual( + reader.read(), EventCombination((EV_ABS, ABS_Z, 1), (EV_ABS, ABS_RZ, 1)) + ) send_event_to_reader(new_event(3, 5, 0, next_timestamp())) send_event_to_reader(new_event(3, 0, 0, next_timestamp())) send_event_to_reader(new_event(3, 1, 0, next_timestamp())) @@ -391,7 +399,7 @@ class TestReader(unittest.TestCase): self.create_helper() reader.start_reading(groups.find(key="Foo Device 2")) time.sleep(0.1) - self.assertEqual(reader.read(), (EV_KEY, CODE_2, 1)) + self.assertEqual(reader.read(), EventCombination((EV_KEY, CODE_2, 1))) self.assertEqual(reader.read(), None) self.assertEqual(len(reader._unreleased), 1) @@ -404,7 +412,7 @@ class TestReader(unittest.TestCase): self.create_helper() reader.start_reading(groups.find(key="Foo Device 2")) time.sleep(0.2) - self.assertEqual(reader.read(), (EV_ABS, ABS_HAT0X, 1)) + self.assertEqual(reader.read(), EventCombination((EV_ABS, ABS_HAT0X, 1))) self.assertEqual(reader.read(), None) self.assertEqual(len(reader._unreleased), 1) @@ -420,14 +428,14 @@ class TestReader(unittest.TestCase): self.create_helper() reader.start_reading(groups.find(key="Foo Device 2")) time.sleep(0.1) - self.assertEqual(reader.read(), (EV_KEY, CODE_2, 1)) + self.assertEqual(reader.read(), EventCombination((EV_KEY, CODE_2, 1))) self.assertEqual(reader.read(), None) self.assertEqual(len(reader._unreleased), 1) def test_reading_ignore_duplicate_down(self): send_event_to_reader(new_event(EV_ABS, ABS_Z, 1, 10)) - self.assertEqual(reader.read(), (EV_ABS, ABS_Z, 1)) + self.assertEqual(reader.read(), EventCombination((EV_ABS, ABS_Z, 1))) self.assertEqual(reader.read(), None) # duplicate @@ -435,7 +443,7 @@ class TestReader(unittest.TestCase): self.assertEqual(reader.read(), None) self.assertEqual(len(reader._unreleased), 1) self.assertEqual(len(reader.get_unreleased_keys()), 1) - self.assertIsInstance(reader.get_unreleased_keys(), Key) + self.assertIsInstance(reader.get_unreleased_keys(), EventCombination) # release send_event_to_reader(new_event(EV_ABS, ABS_Z, 0, 10)) @@ -528,7 +536,7 @@ class TestReader(unittest.TestCase): time.sleep(EVENT_READ_TIMEOUT * 5) self.assertTrue(reader._results.poll()) - self.assertEqual(reader.read(), (EV_KEY, CODE_3, 1)) + self.assertEqual(reader.read(), EventCombination((EV_KEY, CODE_3, 1))) self.assertEqual(reader.read(), None) self.assertEqual(len(reader._unreleased), 1)