Refactorings (#275)

pull/280/head
jonasBoss 2 years ago committed by GitHub
parent ae56f2c0e3
commit b3e1e4ca19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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(

@ -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()

@ -18,20 +18,10 @@
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
"""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()

@ -0,0 +1,129 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""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()

@ -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

@ -0,0 +1,414 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
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

@ -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"

@ -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:

@ -0,0 +1,220 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
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)

@ -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)

@ -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

@ -19,10 +19,10 @@
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
"""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()

@ -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,

@ -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.

@ -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."""

@ -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

@ -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

@ -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:

@ -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."""

@ -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."""

@ -19,21 +19,29 @@
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
"""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))

@ -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:

@ -0,0 +1,135 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
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)])

@ -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:

@ -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.

@ -1,265 +0,0 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""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)

@ -1,290 +0,0 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""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

@ -1,172 +0,0 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""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

@ -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

@ -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):

File diff suppressed because it is too large Load Diff

@ -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()

@ -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__":

@ -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)

@ -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__":

@ -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:

@ -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

@ -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)))

@ -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__":

@ -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)

@ -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,

@ -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

@ -0,0 +1,118 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
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)

@ -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")

@ -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

@ -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)

@ -1,373 +0,0 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
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()

@ -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"),
)

@ -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

@ -0,0 +1,361 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
#
# 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 <https://www.gnu.org/licenses/>.
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()

@ -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")

@ -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)

Loading…
Cancel
Save