Input event with origin (#550)

pull/595/head
jonasBoss 1 year ago committed by GitHub
parent bb6e4d23fe
commit 0f10d06ab6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests" type="tests" factoryName="Autodetect">
<configuration default="false" name="All Tests" type="tests" factoryName="Unittests">
<module name="input-remapper" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
@ -8,6 +8,7 @@
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="_new_pattern" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;&quot;" />
<option name="_new_target" value="&quot;$PROJECT_DIR$/tests&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Only Integration Tests" type="tests" factoryName="Autodetect">
<configuration default="false" name="Only Integration Tests" type="tests" factoryName="Unittests">
<module name="input-remapper" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
@ -8,8 +8,9 @@
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="_new_additionalArguments" value="&quot;--start-dir integration&quot;" />
<option name="_new_target" value="&quot;$PROJECT_DIR$/tests&quot;" />
<option name="_new_pattern" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;&quot;" />
<option name="_new_target" value="&quot;$PROJECT_DIR$/tests/integration&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />
<method v="2" />
</configuration>

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Only Unit Tests" type="tests" factoryName="Autodetect">
<configuration default="false" name="Only Unit Tests" type="tests" factoryName="Unittests">
<module name="input-remapper" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
@ -8,8 +8,9 @@
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="_new_additionalArguments" value="&quot;--start-dir unit&quot;" />
<option name="_new_target" value="&quot;$PROJECT_DIR$/tests&quot;" />
<option name="_new_pattern" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;&quot;" />
<option name="_new_target" value="&quot;$PROJECT_DIR$/tests/unit&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />
<method v="2" />
</configuration>

@ -0,0 +1,353 @@
# -*- 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, Union, List, Dict, Optional, Hashable
from evdev import ecodes
from inputremapper.input_event import InputEvent
from pydantic import BaseModel, root_validator, validator, constr
from inputremapper.configs.system_mapping import system_mapping
from inputremapper.gui.messages.message_types import MessageType
from inputremapper.logger import logger
# 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,
]
DeviceHash = constr(to_lower=True)
class InputConfig(BaseModel):
"""Describes a single input within a combination, to configure mappings."""
message_type = MessageType.selected_event
type: int
code: int
# origin_hash is a hash to identify a specific /dev/input/eventXX device.
# This solves a number of bugs when multiple devices have overlapping capabilities.
# see utils.get_device_hash for the exact hashing function
origin_hash: Optional[DeviceHash] = None # type: ignore
analog_threshold: Optional[int] = None
@property
def input_match_hash(self) -> Hashable:
"""a Hashable object which is intended to match the InputConfig with a
InputEvent.
InputConfig itself is hashable, but can not be used to match InputEvent's
because its hash includes the analog_threshold
"""
return self.type, self.code, self.origin_hash
@property
def defines_analog_input(self) -> bool:
"""Whether this defines an analog input"""
return not self.analog_threshold and self.type != ecodes.EV_KEY
@property
def type_and_code(self) -> Tuple[int, int]:
"""Event type, code."""
return self.type, self.code
@classmethod
def btn_left(cls):
return cls(type=ecodes.EV_KEY, code=ecodes.BTN_LEFT)
@classmethod
def from_input_event(cls, event: InputEvent) -> InputConfig:
"""create an input confing from the given InputEvent, uses the value as
analog threshold"""
return cls(
type=event.type,
code=event.code,
origin_hash=event.origin_hash,
analog_threshold=event.value,
)
def description(self, exclude_threshold=False, exclude_direction=False) -> str:
"""Get a human-readable description of the event."""
return (
f"{self._get_name()} "
f"{self._get_direction() if not exclude_direction else ''} "
f"{self._get_threshold_value() if not exclude_threshold else ''}".strip()
)
def _get_name(self) -> Optional[str]:
"""Human-readable name (e.g. KEY_A) of the specified input event."""
if self.type not in ecodes.bytype:
logger.warning("Unknown type for %s", self)
return f"unknown {self.type, self.code}"
if self.code not in ecodes.bytype[self.type]:
logger.warning("Unknown code for %s", self)
return f"unknown {self.type, self.code}"
key_name = None
# first try to find the name in xmodmap to not display wrong
# names due to the keyboard layout
if self.type == ecodes.EV_KEY:
key_name = system_mapping.get_name(self.code)
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[self.type][self.code]
if isinstance(key_name, list):
key_name = key_name[0]
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-X")
key_name = key_name.replace("ABS_HAT0Y", "DPad-Y")
key_name = key_name.replace("ABS_HAT1X", "DPad-2-X")
key_name = key_name.replace("ABS_HAT1Y", "DPad-2-Y")
key_name = key_name.replace("ABS_HAT2X", "DPad-3-X")
key_name = key_name.replace("ABS_HAT2Y", "DPad-3-Y")
key_name = key_name.replace("ABS_X", "Joystick-X")
key_name = key_name.replace("ABS_Y", "Joystick-Y")
key_name = key_name.replace("ABS_RX", "Joystick-RX")
key_name = key_name.replace("ABS_RY", "Joystick-RY")
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(" ", " ")
return key_name
def _get_direction(self) -> str:
"""human-readable direction description for the analog_threshold"""
if self.type == ecodes.EV_KEY or self.defines_analog_input:
return ""
assert self.analog_threshold
threshold_direction = self.analog_threshold // abs(self.analog_threshold)
return {
# 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((self.code, threshold_direction)) or (
"+" if threshold_direction > 0 else "-"
)
def _get_threshold_value(self) -> str:
"""human-readable value of the analog_threshold e.g. '20%'"""
if self.analog_threshold is None:
return ""
return {
ecodes.EV_REL: f"{abs(self.analog_threshold)}",
ecodes.EV_ABS: f"{abs(self.analog_threshold)}%",
}.get(self.type) or ""
def modify(
self,
type_: Optional[int] = None,
code: Optional[int] = None,
origin_hash: Optional[str] = None,
analog_threshold: Optional[int] = None,
) -> InputConfig:
"""Return a new modified event."""
return InputConfig(
type=type_ if type_ is not None else self.type,
code=code if code is not None else self.code,
origin_hash=origin_hash if origin_hash is not None else self.origin_hash,
analog_threshold=analog_threshold
if analog_threshold is not None
else self.analog_threshold,
)
def __hash__(self):
return hash((self.type, self.code, self.origin_hash, self.analog_threshold))
@validator("analog_threshold")
def _ensure_analog_threshold_is_none(cls, analog_threshold):
"""ensure the analog threshold is none, not zero."""
if analog_threshold:
return analog_threshold
return None
@root_validator
def _remove_analog_threshold_for_key_input(cls, values):
"""remove the analog threshold if the type is a EV_KEY"""
type_ = values.get("type")
if type_ == ecodes.EV_KEY:
values["analog_threshold"] = None
return values
class Config:
allow_mutation = False
underscore_attrs_are_private = True
InputCombinationInit = Union[
InputConfig,
Iterable[Dict[str, Union[str, int]]],
Iterable[InputConfig],
]
class InputCombination(Tuple[InputConfig, ...]):
"""One or more InputConfig's used to trigger a mapping"""
# tuple is immutable, therefore we need to override __new__()
# https://jfine-python-classes.readthedocs.io/en/latest/subclass-tuple.html
def __new__(cls, configs: InputCombinationInit) -> InputCombination:
if isinstance(configs, InputCombination):
return super().__new__(cls, configs) # type: ignore
if isinstance(configs, InputConfig):
return super().__new__(cls, [configs]) # type: ignore
validated_configs = []
for cfg in configs:
if isinstance(cfg, InputConfig):
validated_configs.append(cfg)
else:
validated_configs.append(InputConfig(**cfg))
if len(validated_configs) == 0:
raise ValueError(f"failed to create InputCombination with {configs = }")
# mypy bug: https://github.com/python/mypy/issues/8957
# https://github.com/python/mypy/issues/8541
return super().__new__(cls, validated_configs) # type: ignore
def __str__(self):
return " + ".join(event.description(exclude_threshold=True) for event in self)
def __repr__(self):
return f"<InputCombination {', '.join([str((*e.type_and_code, e.analog_threshold)) for e in self])}>"
@classmethod
def __get_validators__(cls):
"""Used by pydantic to create InputCombination objects."""
yield cls.validate
@classmethod
def validate(cls, init_arg) -> InputCombination:
"""The only valid option is from_config"""
if isinstance(init_arg, InputCombination):
return init_arg
return cls(init_arg)
def to_config(self) -> Tuple[Dict[str, int], ...]:
return tuple(input_config.dict(exclude_defaults=True) for input_config in self)
@classmethod
def empty_combination(cls) -> InputCombination:
"""A combination that has default invalid (to evdev) values.
Useful for the UI to indicate that this combination is not set
"""
return cls([{"type": 99, "code": 99, "analog_threshold": 99}])
def is_problematic(self) -> bool:
"""Is this combination going to work properly on all systems?"""
if len(self) <= 1:
return False
for input_config in self:
if input_config.type != ecodes.EV_KEY:
continue
if input_config.code in DIFFICULT_COMBINATIONS:
return True
return False
@property
def defines_analog_input(self) -> bool:
"""Check if there is any analog input in self."""
return True in tuple(i.defines_analog_input for i in self)
def find_analog_input_config(
self, type_: Optional[int] = None
) -> Optional[InputConfig]:
"""Return the first event that defines an analog input"""
for input_config in self:
if input_config.defines_analog_input and (
type_ is None or input_config.type == type_
):
return input_config
return None
def get_permutations(self) -> List[InputCombination]:
"""Get a list of EventCombinations 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(InputCombination((*permutation, self[-1])))
return permutations
def beautify(self) -> str:
"""Get a human-readable string representation."""
if self == InputCombination.empty_combination():
return "empty_combination"
return " + ".join(event.description(exclude_threshold=True) for event in self)

@ -20,7 +20,7 @@
from __future__ import annotations
import enum
from typing import Optional, Callable, Tuple, TypeVar, Literal, Union, Any
from typing import Optional, Callable, Tuple, TypeVar, Literal, Union, Any, Dict
import evdev
import pkg_resources
@ -46,13 +46,12 @@ from pydantic import (
BaseConfig,
)
from inputremapper.configs.input_config import InputCombination
from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME
from inputremapper.event_combination import EventCombination
from inputremapper.exceptions import MacroParsingError
from inputremapper.gui.gettext import _
from inputremapper.gui.messages.message_types import MessageType
from inputremapper.injection.macros.parse import is_this_a_macro, parse
from inputremapper.input_event import InputEvent, EventActions, USE_AS_ANALOG_VALUE
# TODO: remove pydantic VERSION check as soon as we no longer support
# Ubuntu 20.04 and with it the ancient pydantic 1.2
@ -86,16 +85,16 @@ class KnownUinput(str, enum.Enum):
CombinationChangedCallback = Optional[
Callable[[EventCombination, EventCombination], None]
Callable[[InputCombination, InputCombination], None]
]
MappingModel = TypeVar("MappingModel", bound="Mapping")
MappingModel = TypeVar("MappingModel", bound="UIMapping")
class Cfg(BaseConfig):
validate_assignment = True
use_enum_values = True
underscore_attrs_are_private = True
json_encoders = {EventCombination: lambda v: v.json_key()}
json_encoders = {InputCombination: lambda v: v.json_key()}
class ImmutableCfg(Cfg):
@ -116,7 +115,7 @@ class UIMapping(BaseModel):
# Required attributes
# The InputEvent or InputEvent combination which is mapped
event_combination: EventCombination = EventCombination.empty_combination()
input_combination: InputCombination = InputCombination.empty_combination()
# The UInput to which the mapped event will be sent
target_uinput: Optional[Union[str, KnownUinput]] = None
@ -157,7 +156,7 @@ class UIMapping(BaseModel):
# instead wait until it dropped for loger than release_timeout below the threshold
force_release_timeout: bool = False
# callback which gets called if the event_combination is updated
# callback which gets called if the input_combination is updated
if not needs_workaround:
_combination_changed: Optional[CombinationChangedCallback] = None
@ -170,9 +169,9 @@ class UIMapping(BaseModel):
def __setattr__(self, key: str, value: Any):
"""Call the combination changed callback
if we are about to update the event_combination
if we are about to update the input_combination
"""
if key != "event_combination" or self._combination_changed is None:
if key != "input_combination" or self._combination_changed is None:
if key == "_combination_changed" and needs_workaround:
object.__setattr__(self, "_combination_changed", value)
return
@ -181,23 +180,23 @@ class UIMapping(BaseModel):
# the new combination is not yet validated
try:
new_combi = EventCombination.validate(value)
except ValueError as exception:
new_combi = InputCombination.validate(value)
except (ValueError, TypeError) as exception:
raise ValidationError(
f"failed to Validate {value} as EventCombination", UIMapping
f"failed to Validate {value} as InputCombination", UIMapping
) from exception
if new_combi == self.event_combination:
if new_combi == self.input_combination:
return
# raises a keyError if the combination or a permutation is already mapped
self._combination_changed(new_combi, self.event_combination)
super().__setattr__(key, value)
self._combination_changed(new_combi, self.input_combination)
super().__setattr__("input_combination", new_combi)
def __str__(self):
return str(
self.dict(
exclude_defaults=True, include={"event_combination", "target_uinput"}
exclude_defaults=True, include={"input_combination", "target_uinput"}
)
)
@ -215,34 +214,21 @@ class UIMapping(BaseModel):
return self.name
if (
self.event_combination == EventCombination.empty_combination()
or self.event_combination is None
self.input_combination == InputCombination.empty_combination()
or self.input_combination is None
):
return EMPTY_MAPPING_NAME
return self.event_combination.beautify()
return self.input_combination.beautify()
def has_input_defined(self) -> bool:
"""Whether this mapping defines an event-input."""
return self.event_combination != EventCombination.empty_combination()
return self.input_combination != InputCombination.empty_combination()
def is_axis_mapping(self) -> bool:
"""Whether this mapping specifies an output axis."""
return self.output_type in [EV_ABS, EV_REL]
def find_analog_input_event(
self, type_: Optional[int] = None
) -> Optional[InputEvent]:
"""Return the first event that is configured with "Use as analog"."""
for event in self.event_combination:
if event.value == USE_AS_ANALOG_VALUE:
if type_ is not None and event.type != type_:
continue
return event
return None
def is_wheel_output(self) -> bool:
"""Check if this maps to wheel output."""
return self.output_code in (
@ -270,7 +256,7 @@ class UIMapping(BaseModel):
"""
if self.output_code and self.output_type:
return self.output_type, self.output_code
if not is_this_a_macro(self.output_symbol):
if self.output_symbol and not is_this_a_macro(self.output_symbol):
return EV_KEY, system_mapping.get(self.output_symbol)
return None
@ -319,7 +305,7 @@ class Mapping(UIMapping):
"""
# Override Required attributes to enforce they are set
event_combination: EventCombination
input_combination: InputCombination
target_uinput: KnownUinput
def is_valid(self) -> bool:
@ -349,15 +335,12 @@ class Mapping(UIMapping):
f'The output_symbol "{symbol}" is not a macro and not a valid keycode-name'
)
@validator("event_combination")
def only_one_analog_input(cls, combination) -> EventCombination:
"""Check that the event_combination specifies a maximum of one
@validator("input_combination")
def only_one_analog_input(cls, combination) -> InputCombination:
"""Check that the input_combination specifies a maximum of one
analog to analog mapping
"""
# any event with a value of 0 is considered an analog input (even key events)
# any event with a non-zero value is considered a binary input
analog_events = [event for event in combination if event.value == 0]
analog_events = [event for event in combination if event.defines_analog_input]
if len(analog_events) > 1:
raise ValueError(
f"Cannot map a combination of multiple analog inputs: {analog_events}"
@ -366,27 +349,21 @@ class Mapping(UIMapping):
return combination
@validator("event_combination")
def trigger_point_in_range(cls, combination) -> EventCombination:
@validator("input_combination")
def trigger_point_in_range(cls, combination: InputCombination) -> InputCombination:
"""Check if the trigger point for mapping analog axis to buttons is valid."""
for event in combination:
if event.type == EV_ABS and abs(event.value) >= 100:
for input_config in combination:
if (
input_config.type == EV_ABS
and input_config.analog_threshold
and abs(input_config.analog_threshold) >= 100
):
raise ValueError(
f"{event = } maps an absolute axis to a button, but the trigger "
"point (event.value) is not between -100[%] and 100[%]"
f"{input_config = } maps an absolute axis to a button, but the trigger "
"point (event.analog_threshold) is not between -100[%] and 100[%]"
)
return combination
@validator("event_combination")
def set_event_actions(cls, combination):
"""Sets the correct actions for each event."""
new_combination = []
for event in combination:
if event.value != 0:
event = event.modify(actions=(EventActions.as_key,))
new_combination.append(event)
return EventCombination(new_combination)
@root_validator
def validate_output_symbol_variant(cls, values):
"""Validate that either type and code or symbol are set for key output."""
@ -428,17 +405,16 @@ class Mapping(UIMapping):
return values
@root_validator
def output_matches_input(cls, values):
def output_matches_input(cls, values: Dict[str, Any]) -> Dict[str, Any]:
"""Validate that an output type is an axis if we have an input axis.
And vice versa."""
combination: EventCombination = values.get("event_combination")
event_values = [event.value for event in combination]
assert isinstance(values.get("input_combination"), InputCombination)
combination: InputCombination = values["input_combination"]
use_as_analog = True in [event.defines_analog_input for event in combination]
output_type = values.get("output_type")
output_symbol = values.get("output_symbol")
use_as_analog = USE_AS_ANALOG_VALUE in event_values
if not use_as_analog and not output_symbol and output_type != EV_KEY:
raise ValueError(
"missing macro or key: "

@ -22,6 +22,7 @@
Only write changes to disk, if there actually are changes. Otherwise file-modification
dates are destroyed.
"""
from __future__ import annotations
import copy
import json
@ -29,7 +30,7 @@ import os
import re
import shutil
from pathlib import Path
from typing import Iterator, Tuple, Dict
from typing import Iterator, Tuple, Dict, List
import pkg_resources
from evdev.ecodes import (
@ -46,18 +47,18 @@ from evdev.ecodes import (
REL_HWHEEL_HI_RES,
)
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.mapping import Mapping, UIMapping
from inputremapper.configs.paths import get_preset_path, mkdir, CONFIG_PATH, remove
from inputremapper.configs.preset import Preset
from inputremapper.configs.system_mapping import system_mapping
from inputremapper.event_combination import EventCombination
from inputremapper.injection.global_uinputs import global_uinputs
from inputremapper.injection.macros.parse import is_this_a_macro
from inputremapper.logger import logger, VERSION, IS_BETA
from inputremapper.user import HOME
def all_presets() -> Iterator[Tuple[os.PathLike, Dict]]:
def all_presets() -> Iterator[Tuple[os.PathLike, Dict | List]]:
"""Get all presets for all groups as list."""
if not os.path.exists(get_preset_path()):
return
@ -73,8 +74,8 @@ def all_presets() -> Iterator[Tuple[os.PathLike, Dict]]:
try:
with open(preset, "r") as f:
preset_dict = json.load(f)
yield preset, preset_dict
preset_structure = json.load(f)
yield preset, preset_structure
except json.decoder.JSONDecodeError:
logger.warning('Invalid json format in preset "%s"', preset)
continue
@ -132,19 +133,24 @@ def _mapping_keys():
Update all keys in preset to include value e.g.: '1,5'->'1,5,1'
"""
for preset, preset_dict in all_presets():
for preset, preset_structure in all_presets():
if isinstance(preset_structure, list):
continue # the preset must be at least 1.6-beta version
changes = 0
if "mapping" in preset_dict.keys():
mapping = copy.deepcopy(preset_dict["mapping"])
if "mapping" in preset_structure.keys():
mapping = copy.deepcopy(preset_structure["mapping"])
for key in mapping.keys():
if key.count(",") == 1:
preset_dict["mapping"][f"{key},1"] = preset_dict["mapping"].pop(key)
preset_structure["mapping"][f"{key},1"] = preset_structure[
"mapping"
].pop(key)
changes += 1
if changes:
with open(preset, "w") as file:
logger.info('Updating mapping keys of "%s"', preset)
json.dump(preset_dict, file, indent=4)
json.dump(preset_structure, file, indent=4)
file.write("\n")
@ -195,12 +201,15 @@ def _find_target(symbol):
def _add_target():
"""Add the target field to each preset mapping."""
for preset, preset_dict in all_presets():
if "mapping" not in preset_dict.keys():
for preset, preset_structure in all_presets():
if isinstance(preset_structure, list):
continue
if "mapping" not in preset_structure.keys():
continue
changed = False
for key, symbol in preset_dict["mapping"].copy().items():
for key, symbol in preset_structure["mapping"].copy().items():
if isinstance(symbol, list):
continue
@ -220,7 +229,7 @@ def _add_target():
target,
)
symbol = [symbol, target]
preset_dict["mapping"][key] = symbol
preset_structure["mapping"][key] = symbol
changed = True
if not changed:
@ -228,18 +237,21 @@ def _add_target():
with open(preset, "w") as file:
logger.info('Adding targets for "%s"', preset)
json.dump(preset_dict, file, indent=4)
json.dump(preset_structure, file, indent=4)
file.write("\n")
def _otherwise_to_else():
"""Conditional macros should use an "else" parameter instead of "otherwise"."""
for preset, preset_dict in all_presets():
if "mapping" not in preset_dict.keys():
for preset, preset_structure in all_presets():
if isinstance(preset_structure, list):
continue
if "mapping" not in preset_structure.keys():
continue
changed = False
for key, symbol in preset_dict["mapping"].copy().items():
for key, symbol in preset_structure["mapping"].copy().items():
if not is_this_a_macro(symbol[0]):
continue
@ -258,24 +270,38 @@ def _otherwise_to_else():
symbol[0],
)
preset_dict["mapping"][key] = symbol
preset_structure["mapping"][key] = symbol
if not changed:
continue
with open(preset, "w") as file:
logger.info('Changing otherwise to else for "%s"', preset)
json.dump(preset_dict, file, indent=4)
json.dump(preset_structure, file, indent=4)
file.write("\n")
def _input_combination_from_string(combination_string: str) -> InputCombination:
configs = []
for event_str in combination_string.split("+"):
type_, code, analog_threshold = event_str.split(",")
configs.append(
{"type": type_, "code": code, "analog_threshold": analog_threshold}
)
return InputCombination(configs)
def _convert_to_individual_mappings():
"""Convert preset.json
from {key: [symbol, target]}
to {key: {target: target, symbol: symbol, ...}}
to [{input_combination: ..., output_symbol: symbol, ...}]
"""
for preset_path, old_preset in all_presets():
if isinstance(old_preset, list):
continue
preset = Preset(preset_path, UIMapping)
if "mapping" in old_preset.keys():
for combination, symbol_target in old_preset["mapping"].items():
@ -285,7 +311,7 @@ def _convert_to_individual_mappings():
symbol_target,
)
try:
combination = EventCombination.from_string(combination)
combination = _input_combination_from_string(combination)
except ValueError:
logger.error(
"unable to migrate mapping with invalid combination %s",
@ -294,7 +320,7 @@ def _convert_to_individual_mappings():
continue
mapping = UIMapping(
event_combination=combination,
input_combination=combination,
target_uinput=symbol_target[1],
output_symbol=symbol_target[0],
)
@ -316,7 +342,7 @@ def _convert_to_individual_mappings():
y_scroll_speed = joystick_dict.get("y_scroll_speed")
cfg = {
"event_combination": None,
"input_combination": None,
"target_uinput": "mouse",
"output_type": EV_REL,
"output_code": None,
@ -325,8 +351,12 @@ def _convert_to_individual_mappings():
if left_purpose == "mouse":
x_config = cfg.copy()
y_config = cfg.copy()
x_config["event_combination"] = ",".join((str(EV_ABS), str(ABS_X), "0"))
y_config["event_combination"] = ",".join((str(EV_ABS), str(ABS_Y), "0"))
x_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_X)
)
y_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_Y)
)
x_config["output_code"] = REL_X
y_config["output_code"] = REL_Y
mapping_x = Mapping(**x_config)
@ -340,11 +370,11 @@ def _convert_to_individual_mappings():
if right_purpose == "mouse":
x_config = cfg.copy()
y_config = cfg.copy()
x_config["event_combination"] = ",".join(
(str(EV_ABS), str(ABS_RX), "0")
x_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_RX)
)
y_config["event_combination"] = ",".join(
(str(EV_ABS), str(ABS_RY), "0")
y_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_RY)
)
x_config["output_code"] = REL_X
y_config["output_code"] = REL_Y
@ -359,8 +389,12 @@ def _convert_to_individual_mappings():
if left_purpose == "wheel":
x_config = cfg.copy()
y_config = cfg.copy()
x_config["event_combination"] = ",".join((str(EV_ABS), str(ABS_X), "0"))
y_config["event_combination"] = ",".join((str(EV_ABS), str(ABS_Y), "0"))
x_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_X)
)
y_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_Y)
)
x_config["output_code"] = REL_HWHEEL_HI_RES
y_config["output_code"] = REL_WHEEL_HI_RES
mapping_x = Mapping(**x_config)
@ -375,11 +409,11 @@ def _convert_to_individual_mappings():
if right_purpose == "wheel":
x_config = cfg.copy()
y_config = cfg.copy()
x_config["event_combination"] = ",".join(
(str(EV_ABS), str(ABS_RX), "0")
x_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_RX)
)
y_config["event_combination"] = ",".join(
(str(EV_ABS), str(ABS_RY), "0")
y_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_RY)
)
x_config["output_code"] = REL_HWHEEL_HI_RES
y_config["output_code"] = REL_WHEEL_HI_RES

@ -21,9 +21,8 @@
from __future__ import annotations
import os
import json
import os
from typing import (
Tuple,
Dict,
@ -37,13 +36,11 @@ from typing import (
)
from pydantic import ValidationError
from inputremapper.logger import logger
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.mapping import Mapping, UIMapping
from inputremapper.configs.paths import touch
from inputremapper.input_event import InputEvent
from inputremapper.event_combination import EventCombination
from inputremapper.logger import logger
MappingModel = TypeVar("MappingModel", bound=UIMapping)
@ -69,9 +66,9 @@ class Preset(Generic[MappingModel]):
path: Optional[os.PathLike] = None,
mapping_factory=Mapping,
) -> None:
self._mappings: Dict[EventCombination, MappingModel] = {}
self._mappings: Dict[InputCombination, MappingModel] = {}
# a copy of mappings for keeping track of changes
self._saved_mappings: Dict[EventCombination, MappingModel] = {}
self._saved_mappings: Dict[InputCombination, MappingModel] = {}
self._path: Optional[os.PathLike] = path
# the mapping class which is used by load()
@ -79,7 +76,7 @@ class Preset(Generic[MappingModel]):
def __iter__(self) -> Iterator[MappingModel]:
"""Iterate over Mapping objects."""
return iter(self._mappings.values())
return iter(self._mappings.copy().values())
def __len__(self) -> int:
return len(self._mappings)
@ -93,12 +90,12 @@ class Preset(Generic[MappingModel]):
"""Check if there are unsaved changed."""
return self._mappings != self._saved_mappings
def remove(self, combination: EventCombination) -> None:
"""Remove a mapping from the preset by providing the EventCombination."""
def remove(self, combination: InputCombination) -> None:
"""Remove a mapping from the preset by providing the InputCombination."""
if not isinstance(combination, EventCombination):
if not isinstance(combination, InputCombination):
raise TypeError(
f"combination must by of type EventCombination, got {type(combination)}"
f"combination must by of type InputCombination, got {type(combination)}"
)
for permutation in combination.get_permutations():
@ -117,15 +114,15 @@ class Preset(Generic[MappingModel]):
def add(self, mapping: MappingModel) -> None:
"""Add a mapping to the preset."""
for permutation in mapping.event_combination.get_permutations():
for permutation in mapping.input_combination.get_permutations():
if permutation in self._mappings:
raise KeyError(
"A mapping with this event_combination: "
"A mapping with this input_combination: "
f"{permutation} already exists",
)
mapping.set_combination_changed_callback(self._combination_changed_callback)
self._mappings[mapping.event_combination] = mapping
self._mappings[mapping.input_combination] = mapping
def empty(self) -> None:
"""Remove all mappings and custom configs without saving.
@ -155,18 +152,18 @@ class Preset(Generic[MappingModel]):
# the _combination_changed_callback is attached
self.add(mapping.copy())
def _is_mapped_multiple_times(self, event_combination: EventCombination) -> bool:
def _is_mapped_multiple_times(self, input_combination: InputCombination) -> bool:
"""Check if the event combination maps to multiple mappings."""
all_input_combinations = {mapping.event_combination for mapping in self}
permutations = set(event_combination.get_permutations())
all_input_combinations = {mapping.input_combination for mapping in self}
permutations = set(input_combination.get_permutations())
union = permutations & all_input_combinations
# if there are more than one matches, then there is a duplicate
return len(union) > 1
def _has_valid_event_combination(self, mapping: UIMapping) -> bool:
def _has_valid_input_combination(self, mapping: UIMapping) -> bool:
"""Check if the mapping has a valid input event combination."""
is_a_combination = isinstance(mapping.event_combination, EventCombination)
is_empty = mapping.event_combination == EventCombination.empty_combination()
is_a_combination = isinstance(mapping.input_combination, InputCombination)
is_empty = mapping.input_combination == InputCombination.empty_combination()
return is_a_combination and not is_empty
def save(self) -> None:
@ -183,17 +180,19 @@ class Preset(Generic[MappingModel]):
logger.info("Saving preset to %s", self.path)
preset_dict = {}
preset_list = []
saved_mappings = {}
for mapping in self:
if not mapping.is_valid():
if not self._has_valid_event_combination(mapping):
if not self._has_valid_input_combination(mapping):
# we save invalid mappings except for those with an invalid
# event_combination
# input_combination
logger.debug("skipping invalid mapping %s", mapping)
continue
if self._is_mapped_multiple_times(mapping.event_combination):
if self._is_mapped_multiple_times(mapping.input_combination):
# todo: is this ever executed? it should not be possible to
# reach this
logger.debug(
"skipping mapping with duplicate event combination %s",
mapping,
@ -201,17 +200,15 @@ class Preset(Generic[MappingModel]):
continue
mapping_dict = mapping.dict(exclude_defaults=True)
combination = mapping.event_combination
if "event_combination" in mapping_dict:
# used as key, don't store it redundantly
del mapping_dict["event_combination"]
preset_dict[combination.json_key()] = mapping_dict
mapping_dict["input_combination"] = mapping.input_combination.to_config()
combination = mapping.input_combination
preset_list.append(mapping_dict)
saved_mappings[combination] = mapping.copy()
saved_mappings[combination].remove_combination_changed_callback()
with open(self.path, "w") as file:
json.dump(preset_dict, file, indent=4)
json.dump(preset_list, file, indent=4)
file.write("\n")
self._saved_mappings = saved_mappings
@ -220,15 +217,15 @@ class Preset(Generic[MappingModel]):
return False not in [mapping.is_valid() for mapping in self]
def get_mapping(
self, combination: Optional[EventCombination]
self, combination: Optional[InputCombination]
) -> Optional[MappingModel]:
"""Return the Mapping that is mapped to this EventCombination."""
"""Return the Mapping that is mapped to this InputCombination."""
if not combination:
return None
if not isinstance(combination, EventCombination):
if not isinstance(combination, InputCombination):
raise TypeError(
f"combination must by of type EventCombination, got {type(combination)}"
f"combination must by of type InputCombination, got {type(combination)}"
)
for permutation in combination.get_permutations():
@ -240,8 +237,8 @@ class Preset(Generic[MappingModel]):
def dangerously_mapped_btn_left(self) -> bool:
"""Return True if this mapping disables BTN_Left."""
if EventCombination(InputEvent.btn_left()) not in [
m.event_combination for m in self
if InputCombination(InputConfig.btn_left()) not in [
m.input_combination for m in self
]:
return False
@ -254,11 +251,11 @@ class Preset(Generic[MappingModel]):
return (
"btn_left" not in values
or InputEvent.btn_left().type_and_code not in values
or InputConfig.btn_left().type_and_code not in values
)
def _combination_changed_callback(
self, new: EventCombination, old: EventCombination
self, new: InputCombination, old: InputCombination
) -> None:
for permutation in new.get_permutations():
if permutation in self._mappings.keys() and permutation != old:
@ -274,8 +271,8 @@ class Preset(Generic[MappingModel]):
return
self._saved_mappings = self._get_mappings_from_disc()
def _get_mappings_from_disc(self) -> Dict[EventCombination, MappingModel]:
mappings: Dict[EventCombination, MappingModel] = {}
def _get_mappings_from_disc(self) -> Dict[InputCombination, MappingModel]:
mappings: Dict[InputCombination, MappingModel] = {}
if not self.path:
logger.debug("unable to read preset without a path set Preset.path first")
return mappings
@ -286,25 +283,44 @@ class Preset(Generic[MappingModel]):
with open(self.path, "r") as file:
try:
preset_dict = json.load(file)
preset_list = json.load(file)
except json.JSONDecodeError:
logger.error("unable to decode json file: %s", self.path)
return mappings
for combination, mapping_dict in preset_dict.items():
if isinstance(preset_list, dict):
# todo: remove this before merge into main
# adds compatibility with older beta versions
def str_to_cfg(string):
config = []
for event_str in string.split("+"):
type_, code, analog_threshold = event_str.split(",")
config.append(
{
"type": type_,
"code": code,
"analog_threshold": analog_threshold,
}
)
return config
for combination_string, mapping_dict in preset_list.items():
mapping_dict["input_combination"] = str_to_cfg(combination_string)
preset_list = list(preset_list.values())
for mapping_dict in preset_list:
try:
mapping = self._mapping_factory(
event_combination=combination, **mapping_dict
)
mapping = self._mapping_factory(**mapping_dict)
except ValidationError as error:
print(mapping_dict)
logger.error(
"failed to Validate mapping for %s: %s",
combination,
mapping_dict["input_combination"],
error,
)
continue
mappings[mapping.event_combination] = mapping
mappings[mapping.input_combination] = mapping
return mappings
@property

@ -1,163 +0,0 @@
# -*- 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, Union, Callable, Sequence, List
from evdev import ecodes
from inputremapper.input_event import (
InputEvent,
InputEventValidationType,
)
# 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,
]
EventCombinationInitType = Union[
InputEventValidationType,
Iterable[InputEventValidationType],
]
EventCombinationValidatorType = Union[EventCombinationInitType, str]
class EventCombination(Tuple[InputEvent]):
"""One or more InputEvents for use as a 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, events: EventCombinationInitType) -> EventCombination:
validated_events = []
try:
validated_events.append(InputEvent.validate(events))
except ValueError:
for event in events:
validated_events.append(InputEvent.validate(event))
if len(validated_events) == 0:
raise ValueError(f"failed to create EventCombination with {events = }")
# mypy bug: https://github.com/python/mypy/issues/8957
# https://github.com/python/mypy/issues/8541
return super().__new__(cls, validated_events) # type: ignore
def __str__(self):
return " + ".join(event.description(exclude_threshold=True) for event in self)
def __repr__(self):
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.validate
@classmethod
def validate(cls, init_arg: EventCombinationValidatorType) -> EventCombination:
"""Try all the different methods, and raise an error if none succeed."""
if isinstance(init_arg, EventCombination):
return init_arg
combi = None
validators: Sequence[Callable[..., EventCombination]] = (cls.from_string, cls)
for validator in validators:
try:
combi = validator(init_arg)
break
except ValueError:
pass
if combi:
return combi
raise ValueError(f"failed to create EventCombination with {init_arg = }")
@classmethod
def from_string(cls, init_string: str) -> EventCombination:
"""Create a EventCombination form a string like '1,2,3+4,5,6'."""
try:
init_strs = init_string.split("+")
return cls(init_strs)
except AttributeError as exception:
raise ValueError(
f"failed to create EventCombination from {init_string = }"
) from exception
@classmethod
def empty_combination(cls) -> EventCombination:
"""A combination that has default invalid (to evdev) values.
Useful for the UI to indicate that this combination is not set
"""
return cls("99,99,99")
def is_problematic(self) -> bool:
"""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 has_input_axis(self) -> bool:
"""Check if there is any analog event in self."""
return False in (event.is_key_event for event in self)
def get_permutations(self) -> List[EventCombination]:
"""Get a list of EventCombinations 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_key(self) -> str:
"""Get a representation of the input that works as key in a json object."""
return "+".join([event.json_key() for event in self])
def beautify(self) -> str:
"""Get a human readable string representation."""
if self == EventCombination.empty_combination():
return "empty_combination"
return " + ".join(event.description(exclude_threshold=True) for event in self)

@ -298,6 +298,16 @@ class _Group:
"""
return get_preset_path(self.name, preset)
def get_devices(self) -> List[evdev.InputDevice]:
devices: List[evdev.InputDevice] = []
for path in self.paths:
try:
devices.append(evdev.InputDevice(path))
except (FileNotFoundError, OSError):
logger.error('Could not find "%s"', path)
continue
return devices
def dumps(self):
"""Return a string representing this object."""
return json.dumps(

@ -0,0 +1,6 @@
import gi
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
gi.require_version("GLib", "2.0")
gi.require_version("GtkSource", "4")

@ -25,16 +25,10 @@ import re
from typing import Dict, Optional, List, Tuple
from evdev.ecodes import EV_KEY
import gi
from inputremapper.gui.controller import Controller
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
gi.require_version("GLib", "2.0")
from gi.repository import Gdk, Gtk, GLib, GObject
from inputremapper.configs.mapping import MappingData, KnownUinput
from inputremapper.gui.controller import Controller
from inputremapper.configs.mapping import MappingData
from inputremapper.configs.system_mapping import system_mapping
from inputremapper.gui.components.editor import CodeEditor
from inputremapper.gui.messages.message_broker import MessageBroker, MessageType

@ -25,7 +25,6 @@ from __future__ import annotations
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
from typing import Optional

@ -18,11 +18,9 @@
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Optional
import gi
from typing import Optional
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
from inputremapper.gui.components.common import FlowBoxEntry, FlowBoxWrapper

@ -40,13 +40,10 @@ from evdev.ecodes import (
BTN_SIDE,
)
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
gi.require_version("GtkSource", "4")
from gi.repository import Gtk, GtkSource, Gdk
from inputremapper.configs.mapping import MappingData
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.groups import DeviceType
from inputremapper.gui.controller import Controller
from inputremapper.gui.gettext import _
@ -159,9 +156,9 @@ class MappingListBox:
@staticmethod
def _sort_func(row1: MappingSelectionLabel, row2: MappingSelectionLabel) -> int:
"""Sort alphanumerical by name."""
if row1.combination == EventCombination.empty_combination():
if row1.combination == InputCombination.empty_combination():
return 1
if row2.combination == EventCombination.empty_combination():
if row2.combination == InputCombination.empty_combination():
return 0
return 0 if row1.name < row2.name else 1
@ -180,14 +177,14 @@ class MappingListBox:
self._message_broker,
self._controller,
mapping.format_name(),
mapping.event_combination,
mapping.input_combination,
)
self._gui.insert(selection_label, -1)
self._gui.invalidate_sort()
def _on_mapping_changed(self, mapping: MappingData):
with HandlerDisabled(self._gui, self._on_gtk_mapping_selected):
combination = mapping.event_combination
combination = mapping.input_combination
for row in self._gui.get_children():
if row.combination == combination:
@ -209,7 +206,7 @@ class MappingSelectionLabel(Gtk.ListBoxRow):
message_broker: MessageBroker,
controller: Controller,
name: Optional[str],
combination: EventCombination,
combination: InputCombination,
):
super().__init__()
self._message_broker = message_broker
@ -290,7 +287,7 @@ class MappingSelectionLabel(Gtk.ListBoxRow):
self._controller.set_focus(self.name_input)
def _on_mapping_changed(self, mapping: MappingData):
if mapping.event_combination != self.combination:
if mapping.input_combination != self.combination:
self._set_not_selected()
return
self.name = mapping.format_name()
@ -497,7 +494,7 @@ class RequireActiveMapping:
self,
message_broker: MessageBroker,
widget: Gtk.ToggleButton,
require_recorded_input: False,
require_recorded_input: bool,
):
self._widget = widget
self._default_tooltip = self._widget.get_tooltip_text()
@ -653,12 +650,12 @@ class ReleaseCombinationSwitch:
self._controller.update_mapping(release_combination_keys=self._gui.get_active())
class EventEntry(Gtk.ListBoxRow):
"""The ListBoxRow representing a single event inside the CombinationListBox."""
class InputConfigEntry(Gtk.ListBoxRow):
"""The ListBoxRow representing a single input config inside the CombinationListBox."""
__gtype_name__ = "EventEntry"
__gtype_name__ = "InputConfigEntry"
def __init__(self, event: InputEvent, controller: Controller):
def __init__(self, event: InputConfig, controller: Controller):
super().__init__()
self.input_event = event
@ -692,13 +689,13 @@ class EventEntry(Gtk.ListBoxRow):
up_btn.connect(
"clicked",
lambda *_: self._controller.move_event_in_combination(
lambda *_: self._controller.move_input_config_in_combination(
self.input_event, "up"
),
)
down_btn.connect(
"clicked",
lambda *_: self._controller.move_event_in_combination(
lambda *_: self._controller.move_input_config_in_combination(
self.input_event, "down"
),
)
@ -711,7 +708,7 @@ class EventEntry(Gtk.ListBoxRow):
class CombinationListbox:
"""The ListBox with all the events inside active_mapping.event_combination."""
"""The ListBox with all the events inside active_mapping.input_combination."""
def __init__(
self,
@ -722,7 +719,7 @@ class CombinationListbox:
self._message_broker = message_broker
self._controller = controller
self._gui = listbox
self._combination: Optional[EventCombination] = None
self._combination: Optional[InputCombination] = None
self._message_broker.subscribe(
MessageType.mapping,
@ -741,7 +738,7 @@ class CombinationListbox:
self._gui.select_row(row)
def _on_mapping_changed(self, mapping: MappingData):
if self._combination == mapping.event_combination:
if self._combination == mapping.input_combination:
return
event_entries = self._gui.get_children()
@ -751,9 +748,9 @@ class CombinationListbox:
if self._controller.is_empty_mapping():
self._combination = None
else:
self._combination = mapping.event_combination
self._combination = mapping.input_combination
for event in self._combination:
self._gui.insert(EventEntry(event, self._controller), -1)
self._gui.insert(InputConfigEntry(event, self._controller), -1)
def _on_event_changed(self, event: InputEvent):
with HandlerDisabled(self._gui, self._on_gtk_row_selected):
@ -762,12 +759,12 @@ class CombinationListbox:
def _on_gtk_row_selected(self, *_):
for row in self._gui.get_children():
if row.is_selected():
self._controller.load_event(row.input_event)
self._controller.load_input_config(row.input_event)
break
class AnalogInputSwitch:
"""The switch that marks the active_event as analog input."""
"""The switch that marks the active_input_config as analog input."""
def __init__(
self,
@ -778,17 +775,17 @@ class AnalogInputSwitch:
self._message_broker = message_broker
self._controller = controller
self._gui = gui
self._event: Optional[InputEvent] = None
self._input_config: Optional[InputConfig] = None
self._gui.connect("state-set", self._on_gtk_toggle)
self._message_broker.subscribe(MessageType.selected_event, self._on_event)
def _on_event(self, event: InputEvent):
def _on_event(self, input_cfg: InputConfig):
with HandlerDisabled(self._gui, self._on_gtk_toggle):
self._gui.set_active(event.value == 0)
self._event = event
self._gui.set_active(input_cfg.defines_analog_input)
self._input_config = input_cfg
if event.type == EV_KEY:
if input_cfg.type == EV_KEY:
self._gui.set_sensitive(False)
self._gui.set_opacity(0.5)
else:
@ -801,7 +798,7 @@ class AnalogInputSwitch:
class TriggerThresholdInput:
"""The number selection used to set the speed or position threshold of the
active_event when it is an ABS or REL event used as a key."""
active_input_config when it is an ABS or REL event used as a key."""
def __init__(
self,
@ -812,17 +809,17 @@ class TriggerThresholdInput:
self._message_broker = message_broker
self._controller = controller
self._gui = gui
self._event: Optional[InputEvent] = None
self._input_config: Optional[InputConfig] = None
self._gui.set_increments(1, 1)
self._gui.connect("value-changed", self._on_gtk_changed)
self._message_broker.subscribe(MessageType.selected_event, self._on_event)
def _on_event(self, event: InputEvent):
if event.type == EV_KEY:
def _on_event(self, input_config: InputConfig):
if input_config.type == EV_KEY:
self._gui.set_sensitive(False)
self._gui.set_opacity(0.5)
elif event.type == EV_ABS:
elif input_config.type == EV_ABS:
self._gui.set_sensitive(True)
self._gui.set_opacity(1)
self._gui.set_range(-99, 99)
@ -832,12 +829,12 @@ class TriggerThresholdInput:
self._gui.set_range(-999, 999)
with HandlerDisabled(self._gui, self._on_gtk_changed):
self._gui.set_value(event.value)
self._event = event
self._gui.set_value(input_config.analog_threshold or 0)
self._input_config = input_config
def _on_gtk_changed(self, *_):
self._controller.update_event(
self._event.modify(value=int(self._gui.get_value()))
self._controller.update_input_config(
self._input_config.modify(analog_threshold=int(self._gui.get_value()))
)
@ -860,7 +857,7 @@ class ReleaseTimeoutInput:
self._message_broker.subscribe(MessageType.mapping, self._on_mapping_message)
def _on_mapping_message(self, mapping: MappingData):
if EV_REL in [event.type for event in mapping.event_combination]:
if EV_REL in [event.type for event in mapping.input_combination]:
self._gui.set_sensitive(True)
self._gui.set_opacity(1)
else:
@ -894,7 +891,7 @@ class RelativeInputCutoffInput:
def _on_mapping_message(self, mapping: MappingData):
if (
EV_REL in [event.type for event in mapping.event_combination]
EV_REL in [event.type for event in mapping.input_combination]
and mapping.output_type == EV_ABS
):
self._gui.set_sensitive(True)

@ -25,8 +25,7 @@ from __future__ import annotations
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
from gi.repository import Gtk
from inputremapper.gui.controller import Controller

@ -23,9 +23,6 @@
from __future__ import annotations
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
from inputremapper.gui.components.common import FlowBoxEntry, FlowBoxWrapper

@ -29,19 +29,17 @@ from typing import (
Dict,
Callable,
List,
Any,
)
from evdev.ecodes import EV_KEY, EV_REL, EV_ABS
import gi
from evdev.ecodes import EV_KEY, EV_REL, EV_ABS
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
from inputremapper.configs.mapping import MappingData, UIMapping
from inputremapper.configs.paths import sanitize_path_component
from inputremapper.input_event import USE_AS_ANALOG_VALUE
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.exceptions import DataManagementError
from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME
from inputremapper.gui.gettext import _
@ -61,7 +59,6 @@ from inputremapper.injection.injector import (
InjectorState,
InjectorStateMessage,
)
from inputremapper.input_event import InputEvent
from inputremapper.logger import logger
if TYPE_CHECKING:
@ -129,12 +126,12 @@ class Controller:
mappings = list(data.mappings)
mappings.sort(
key=lambda mapping: (
mapping.format_name() or mapping.event_combination.beautify()
mapping.format_name() or mapping.input_combination.beautify()
)
)
combination = mappings[0].event_combination
combination = mappings[0].input_combination
self.load_mapping(combination)
self.load_event(combination[0])
self.load_input_config(combination[0])
else:
# send an empty mapping to make sure the ui is reset to default values
self.message_broker.publish(MappingData(**MAPPING_DEFAULTS))
@ -167,7 +164,7 @@ class Controller:
if (
"output_symbol is a macro:" in error_string
or "output_symbol and output_code mismatch:" in error_string
) and mapping.event_combination.has_input_axis():
) and mapping.input_combination.defines_analog_input:
return _(
"Remove the macro or key from the macro input field "
"when specifying an analog output"
@ -176,7 +173,7 @@ class Controller:
if (
"output_symbol is a macro:" in error_string
or "output_symbol and output_code mismatch:" in error_string
) and not mapping.event_combination.has_input_axis():
) and not mapping.input_combination.defines_analog_input:
return _(
"Remove the Analog Output Axis when specifying a macro or key output"
)
@ -187,7 +184,9 @@ class Controller:
)
if mapping.output_symbol is not None:
event = [
event for event in mapping.event_combination if event.value == 0
event
for event in mapping.input_combination
if event.defines_analog_input
][0]
message += _(
"\nIf you mean to create a key or macro mapping "
@ -244,10 +243,10 @@ class Controller:
)
self.message_broker.publish(DoStackSwitch(1))
def update_combination(self, combination: EventCombination):
"""Update the event_combination of the active mapping."""
def update_combination(self, combination: InputCombination):
"""Update the input_combination of the active mapping."""
try:
self.data_manager.update_mapping(event_combination=combination)
self.data_manager.update_mapping(input_combination=combination)
self.save()
except KeyError:
self.show_status(
@ -265,21 +264,23 @@ class Controller:
+ _("break them."),
)
def move_event_in_combination(
self, event: InputEvent, direction: Union[Literal["up"], Literal["down"]]
def move_input_config_in_combination(
self,
input_config: InputConfig,
direction: Union[Literal["up"], Literal["down"]],
):
"""Move the active_event up or down in the event_combination of the
"""Move the active_input_config up or down in the input_combination of the
active_mapping."""
if (
not self.data_manager.active_mapping
or len(self.data_manager.active_mapping.event_combination) == 1
or len(self.data_manager.active_mapping.input_combination) == 1
):
return
combination: Sequence[
InputEvent
] = self.data_manager.active_mapping.event_combination
InputConfig
] = self.data_manager.active_mapping.input_combination
i = combination.index(event)
i = combination.index(input_config)
if (
i + 1 == len(combination)
and direction == "down"
@ -291,7 +292,7 @@ class Controller:
if direction == "up":
combination = (
list(combination[: i - 1])
+ [event]
+ [input_config]
+ [combination[i - 1]]
+ list(combination[i + 1 :])
)
@ -299,22 +300,22 @@ class Controller:
combination = (
list(combination[:i])
+ [combination[i + 1]]
+ [event]
+ [input_config]
+ list(combination[i + 2 :])
)
else:
raise ValueError(f"unknown direction: {direction}")
self.update_combination(EventCombination(combination))
self.load_event(event)
self.update_combination(InputCombination(combination))
self.load_input_config(input_config)
def load_event(self, event: InputEvent):
"""Load an InputEvent form the active mapping event combination."""
self.data_manager.load_event(event)
def load_input_config(self, input_config: InputConfig):
"""Load an InputConfig form the active mapping input combination."""
self.data_manager.load_input_config(input_config)
def update_event(self, new_event: InputEvent):
"""Modify the active event."""
def update_input_config(self, new_input_config: InputConfig):
"""Modify the active input configuration."""
try:
self.data_manager.update_event(new_event)
self.data_manager.update_input_config(new_input_config)
except KeyError:
# we need to synchronize the gui
self.data_manager.publish_mapping()
@ -322,16 +323,19 @@ class Controller:
def remove_event(self):
"""Remove the active InputEvent from the active mapping event combination."""
if not self.data_manager.active_mapping or not self.data_manager.active_event:
if (
not self.data_manager.active_mapping
or not self.data_manager.active_input_config
):
return
combination = list(self.data_manager.active_mapping.event_combination)
combination.remove(self.data_manager.active_event)
combination = list(self.data_manager.active_mapping.input_combination)
combination.remove(self.data_manager.active_input_config)
try:
self.data_manager.update_mapping(
event_combination=EventCombination(combination)
input_combination=InputCombination(combination)
)
self.load_event(combination[0])
self.load_input_config(combination[0])
self.save()
except (KeyError, ValueError):
# we need to synchronize the gui
@ -340,14 +344,14 @@ class Controller:
def set_event_as_analog(self, analog: bool):
"""Use the active event as an analog input."""
assert self.data_manager.active_event is not None
event = self.data_manager.active_event
assert self.data_manager.active_input_config is not None
event = self.data_manager.active_input_config
if event.type != EV_KEY:
if analog:
try:
self.data_manager.update_event(
event.modify(value=USE_AS_ANALOG_VALUE)
self.data_manager.update_input_config(
event.modify(analog_threshold=0)
)
self.save()
return
@ -357,7 +361,9 @@ class Controller:
try_values = {EV_REL: [1, -1], EV_ABS: [10, -10]}
for value in try_values[event.type]:
try:
self.data_manager.update_event(event.modify(value=value))
self.data_manager.update_input_config(
event.modify(analog_threshold=value)
)
self.save()
return
except KeyError:
@ -421,10 +427,10 @@ class Controller:
)
self.message_broker.publish(UserConfirmRequest(msg, f))
def load_mapping(self, event_combination: EventCombination):
"""Load the mapping with the given event_combination form the active_preset."""
self.data_manager.load_mapping(event_combination)
self.load_event(event_combination[0])
def load_mapping(self, input_combination: InputCombination):
"""Load the mapping with the given input_combination form the active_preset."""
self.data_manager.load_mapping(input_combination)
self.load_input_config(input_combination[0])
def update_mapping(self, **kwargs):
"""Update the active_mapping with the given keywords and values."""
@ -446,7 +452,7 @@ class Controller:
# there is already an empty mapping
return
self.data_manager.load_mapping(combination=EventCombination.empty_combination())
self.data_manager.load_mapping(combination=InputCombination.empty_combination())
self.data_manager.update_mapping(**MAPPING_DEFAULTS)
def delete_mapping(self):
@ -481,7 +487,7 @@ class Controller:
def start_key_recording(self):
"""Record the input of the active_group
Updates the active_mapping.event_combination with the recorded events.
Updates the active_mapping.input_combination with the recorded events.
"""
state = self.data_manager.get_state()
if state == InjectorState.RUNNING or state == InjectorState.STARTING:
@ -553,7 +559,7 @@ class Controller:
def running():
msg = _("Applied preset %s") % self.data_manager.active_preset.name
if self.data_manager.active_preset.get_mapping(
EventCombination(InputEvent.btn_left())
InputCombination(InputConfig.btn_left())
):
msg += _(", CTRL + DEL to stop")
self.show_status(CTX_APPLY, msg)
@ -644,7 +650,7 @@ class Controller:
"""Focus the given component."""
self.gui.window.set_focus(component)
def _change_mapping_type(self, kwargs):
def _change_mapping_type(self, kwargs: Dict[str, Any]):
"""Query the user to update the mapping in order to change the mapping type."""
mapping = self.data_manager.active_mapping
@ -661,13 +667,17 @@ class Controller:
mapping.output_symbol
)
if not [event for event in mapping.event_combination if event.value == 0]:
if not [
input_config
for input_config in mapping.input_combination
if input_config.defines_analog_input
]:
# there is no analog input configured, let's try to autoconfigure it
events: List[InputEvent] = list(mapping.event_combination)
for i, e in enumerate(events):
inputs: List[InputConfig] = list(mapping.input_combination)
for i, e in enumerate(inputs):
if e.type in [EV_ABS, EV_REL]:
events[i] = e.modify(value=0)
kwargs["event_combination"] = EventCombination(events)
inputs[i] = e.modify(analog_threshold=0)
kwargs["input_combination"] = InputCombination(inputs)
msg += _(
'\nThe input "{}" will be used as analog input.'
).format(e.description())
@ -694,7 +704,10 @@ class Controller:
if kwargs["mapping_type"] == "key_macro":
try:
analog_input = [e for e in mapping.event_combination if e.value == 0][0]
analog_input = tuple(
filter(lambda i: i.defines_analog_input, mapping.input_combination)
)
analog_input = analog_input[0]
except IndexError:
kwargs["output_type"] = None
kwargs["output_code"] = None

@ -24,8 +24,6 @@ import time
from typing import Optional, List, Tuple, Set
import gi
gi.require_version("GLib", "2.0")
from gi.repository import GLib
from inputremapper.configs.global_config import GlobalConfig
@ -34,7 +32,7 @@ from inputremapper.configs.paths import get_preset_path, mkdir, split_all
from inputremapper.configs.preset import Preset
from inputremapper.configs.system_mapping import SystemMapping
from inputremapper.daemon import DaemonProxy
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.exceptions import DataManagementError
from inputremapper.gui.gettext import _
from inputremapper.groups import _Group
@ -53,7 +51,6 @@ from inputremapper.injection.injector import (
InjectorState,
InjectorStateMessage,
)
from inputremapper.input_event import InputEvent
from inputremapper.logger import logger
DEFAULT_PRESET_NAME = _("new preset")
@ -91,7 +88,7 @@ class DataManager:
self._active_preset: Optional[Preset[UIMapping]] = None
self._active_mapping: Optional[UIMapping] = None
self._active_event: Optional[InputEvent] = None
self._active_input_config: Optional[InputConfig] = None
def publish_group(self):
"""Send active group to the MessageBroker.
@ -134,9 +131,9 @@ class DataManager:
It is usually not necessary to call this explicitly from
outside DataManager
"""
if self.active_event:
assert self.active_event in self.active_mapping.event_combination
self.message_broker.publish(self.active_event)
if self.active_input_config:
assert self.active_input_config in self.active_mapping.input_combination
self.message_broker.publish(self.active_input_config)
def publish_uinputs(self):
"""Send the "uinputs" message on the MessageBroker."""
@ -176,9 +173,9 @@ class DataManager:
return self._active_mapping
@property
def active_event(self) -> Optional[InputEvent]:
def active_input_config(self) -> Optional[InputConfig]:
"""The currently loaded event."""
return self._active_event
return self._active_input_config
def get_group_keys(self) -> Tuple[GroupKey, ...]:
"""Get all group keys (plugged devices)."""
@ -299,7 +296,7 @@ class DataManager:
logger.info('Loading group "%s"', group_key)
self._active_event = None
self._active_input_config = None
self._active_mapping = None
self._active_preset = None
group = self._reader_client.groups.find(key=group_key)
@ -320,12 +317,12 @@ class DataManager:
preset_path = get_preset_path(self.active_group.name, name)
preset = Preset(preset_path, mapping_factory=UIMapping)
preset.load()
self._active_event = None
self._active_input_config = None
self._active_mapping = None
self._active_preset = preset
self.publish_preset()
def load_mapping(self, combination: EventCombination):
def load_mapping(self, combination: InputCombination):
"""Load a mapping. Will send "mapping" message on the MessageBroker."""
if not self._active_preset:
raise DataManagementError("Unable to load mapping. Preset is not set")
@ -336,23 +333,23 @@ class DataManager:
f"the mapping with {combination = } does not "
f"exist in the {self._active_preset.path}"
)
self._active_event = None
self._active_input_config = None
self._active_mapping = mapping
self.publish_mapping()
def load_event(self, event: InputEvent):
"""Load a InputEvent from the combination in the active mapping.
def load_input_config(self, input_config: InputConfig):
"""Load a InputConfig from the combination in the active mapping.
Will send "event" message on the MessageBroker,
"""
if not self.active_mapping:
raise DataManagementError("Unable to load event. Mapping is not set")
if event not in self.active_mapping.event_combination:
if input_config not in self.active_mapping.input_combination:
raise ValueError(
f"{event} is not member of active_mapping.event_combination: "
f"{self.active_mapping.event_combination}"
f"{input_config} is not member of active_mapping.input_combination: "
f"{self.active_mapping.input_combination}"
)
self._active_event = event
self._active_input_config = input_config
self.publish_event()
def rename_preset(self, new_name: str):
@ -441,7 +438,7 @@ class DataManager:
"""Update the active mapping with the given keywords and values.
Will send "mapping" message to the MessageBroker. In case of a new
event_combination. This will first send a "combination_update" message.
input_combination. This will first send a "combination_update" message.
"""
if not self._active_mapping:
raise DataManagementError("Cannot modify Mapping: Mapping is not set")
@ -449,17 +446,17 @@ class DataManager:
if symbol := kwargs.get("output_symbol"):
kwargs["output_symbol"] = self._system_mapping.correct_case(symbol)
combination = self.active_mapping.event_combination
combination = self.active_mapping.input_combination
for key, value in kwargs.items():
setattr(self._active_mapping, key, value)
if (
"event_combination" in kwargs
and combination != self.active_mapping.event_combination
"input_combination" in kwargs
and combination != self.active_mapping.input_combination
):
self._active_event = None
self._active_input_config = None
self.message_broker.publish(
CombinationUpdate(combination, self._active_mapping.event_combination)
CombinationUpdate(combination, self._active_mapping.input_combination)
)
if "mapping_type" in kwargs:
@ -469,19 +466,19 @@ class DataManager:
self.publish_mapping()
def update_event(self, new_event: InputEvent):
"""Update the active event.
def update_input_config(self, new_input_config: InputConfig):
"""Update the active input configuration.
Will send "combination_update", "mapping" and "event" messages to the
MessageBroker (in that order)
"""
if not self.active_mapping or not self.active_event:
if not self.active_mapping or not self.active_input_config:
raise DataManagementError("Cannot modify event: Event is not set")
combination = list(self.active_mapping.event_combination)
combination[combination.index(self.active_event)] = new_event
self.update_mapping(event_combination=EventCombination(combination))
self._active_event = new_event
combination = list(self.active_mapping.input_combination)
combination[combination.index(self.active_input_config)] = new_input_config
self.update_mapping(input_combination=InputCombination(combination))
self._active_input_config = new_input_config
self.publish_event()
def create_mapping(self):
@ -504,7 +501,7 @@ class DataManager:
"cannot delete active mapping: active mapping is not set"
)
self._active_preset.remove(self._active_mapping.event_combination)
self._active_preset.remove(self._active_mapping.input_combination)
self._active_mapping = None
self.publish_preset()

@ -42,7 +42,9 @@ if TYPE_CHECKING:
class Message(Protocol):
"""The protocol any message must follow to be sent with the MessageBroker."""
message_type: MessageType
@property
def message_type(self) -> MessageType:
...
# useful type aliases
@ -65,7 +67,10 @@ class MessageBroker:
def signal(self, signal: MessageType):
"""Send a signal without any data payload."""
self.publish(Signal(signal))
# This is different from calling self.publish because self.get_caller()
# looks back at the current stack 3 frames
self._messages.append((Signal(signal), *self.get_caller()))
self._publish_all()
def _publish(self, data: Message, file: str, line: int):
logger.debug(
@ -107,7 +112,7 @@ class MessageBroker:
pass
class Signal(Message):
class Signal:
"""Send a Message without any associated data over the MassageBus."""
def __init__(self, message_type: MessageType):

@ -21,8 +21,8 @@ import re
from dataclasses import dataclass
from typing import Dict, Tuple, Optional, Callable
from inputremapper.configs.input_config import InputCombination
from inputremapper.configs.mapping import MappingData
from inputremapper.event_combination import EventCombination
from inputremapper.gui.messages.message_types import (
MessageType,
Name,
@ -97,7 +97,7 @@ class CombinationRecorded:
"""Message with the latest recoded combination."""
message_type = MessageType.combination_recorded
combination: "EventCombination"
combination: "InputCombination"
@dataclass(frozen=True)
@ -105,8 +105,8 @@ class CombinationUpdate:
"""Message with the old and new combination (hash for a mapping) when it changed."""
message_type = MessageType.combination_update
old_combination: "EventCombination"
new_combination: "EventCombination"
old_combination: "InputCombination"
new_combination: "InputCombination"
@dataclass(frozen=True)

@ -23,17 +23,15 @@
see gui.reader_service.ReaderService
"""
from typing import Optional, List, Generator, Dict, Tuple, Set
import time
from typing import Optional, List, Generator, Dict, Tuple, Set
import evdev
import gi
gi.require_version("GLib", "2.0")
from gi.repository import GLib
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination
from inputremapper.groups import _Groups, _Group
from inputremapper.gui.reader_service import (
MSG_EVENT,
@ -153,7 +151,7 @@ class ReaderClient:
# update the generator
try:
if self._recording_generator is not None:
self._recording_generator.send(InputEvent(*message_body))
self._recording_generator.send(InputEvent(**message_body))
else:
# the ReaderService should only send events while the gui
# is recording, so this is unexpected.
@ -198,13 +196,22 @@ class ReaderClient:
self.message_broker.signal(MessageType.recording_finished)
@staticmethod
def _input_event_to_config(event: InputEvent):
return {
"type": event.type,
"code": event.code,
"analog_threshold": event.value,
"origin_hash": event.origin_hash,
}
def _recorder(self) -> RecordingGenerator:
"""Generator which receives InputEvents.
It accumulates them into EventCombinations and sends those on the
message_broker. It will stop once all keys or inputs are released.
"""
active: Set[Tuple[int, int]] = set()
active: Set = set()
accumulator: List[InputEvent] = []
while True:
event: InputEvent = yield
@ -213,7 +220,7 @@ class ReaderClient:
if event.value == 0:
try:
active.remove((event.type, event.code))
active.remove(event.input_match_hash)
except KeyError:
# we haven't seen this before probably a key got released which
# was pressed before we started recording. ignore it.
@ -224,21 +231,25 @@ class ReaderClient:
return
continue
active.add(event.type_and_code)
accu_type_code = [e.type_and_code for e in accumulator]
if event.type_and_code in accu_type_code and event not in accumulator:
active.add(event.input_match_hash)
accu_input_hashes = [e.input_match_hash for e in accumulator]
if event.input_match_hash in accu_input_hashes and event not in accumulator:
# the value has changed but the event is already in the accumulator
# update the event
i = accu_type_code.index(event.type_and_code)
i = accu_input_hashes.index(event.input_match_hash)
accumulator[i] = event
self.message_broker.publish(
CombinationRecorded(EventCombination(accumulator))
CombinationRecorded(
InputCombination(map(self._input_event_to_config, accumulator))
)
)
if event not in accumulator:
accumulator.append(event)
self.message_broker.publish(
CombinationRecorded(EventCombination(accumulator))
CombinationRecorded(
InputCombination(map(self._input_event_to_config, accumulator))
)
)
def set_group(self, group: _Group):

@ -38,25 +38,30 @@ different input-events into simple on/off events and sends them to the gui.
from __future__ import annotations
import time
import logging
import os
import asyncio
import logging
import multiprocessing
import os
import subprocess
import sys
import time
from collections import defaultdict
from typing import Set, List
import evdev
from evdev.ecodes import EV_KEY, EV_ABS, EV_REL, REL_HWHEEL, REL_WHEEL
from inputremapper.utils import get_device_hash
from inputremapper.configs.mapping import UIMapping
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.mapping import Mapping
from inputremapper.groups import _Groups, _Group
from inputremapper.injection.event_reader import EventReader
from inputremapper.injection.mapping_handlers.abs_to_btn_handler import AbsToBtnHandler
from inputremapper.injection.mapping_handlers.mapping_handler import MappingHandler
from inputremapper.injection.mapping_handlers.mapping_handler import (
NotifyCallback,
InputEventHandler,
MappingHandler,
)
from inputremapper.injection.mapping_handlers.rel_to_btn_handler import RelToBtnHandler
from inputremapper.input_event import InputEvent, EventActions
from inputremapper.ipc.pipe import Pipe
@ -267,68 +272,80 @@ class ReaderService:
context = ContextDummy()
# create a context for each source
for device in sources:
device_hash = get_device_hash(device)
capabilities = device.capabilities(absinfo=False)
for ev_code in capabilities.get(EV_KEY) or ():
context.notify_callbacks[(EV_KEY, ev_code)].append(
ForwardToUIHandler(self._results_pipe).notify
input_config = InputConfig(
type=EV_KEY, code=ev_code, origin_hash=device_hash
)
context.add_handler(
input_config, ForwardToUIHandler(self._results_pipe)
)
for ev_code in capabilities.get(EV_ABS) or ():
# positive direction
mapping = UIMapping(
event_combination=EventCombination((EV_ABS, ev_code, 30)),
input_config = InputConfig(
type=EV_ABS,
code=ev_code,
analog_threshold=30,
origin_hash=device_hash,
)
mapping = Mapping(
input_combination=InputCombination(input_config),
target_uinput="keyboard",
output_symbol="KEY_A",
)
handler: MappingHandler = AbsToBtnHandler(
EventCombination((EV_ABS, ev_code, 30)), mapping
InputCombination(input_config), mapping
)
handler.set_sub_handler(ForwardToUIHandler(self._results_pipe))
context.notify_callbacks[(EV_ABS, ev_code)].append(handler.notify)
context.add_handler(input_config, handler)
# negative direction
mapping = UIMapping(
event_combination=EventCombination((EV_ABS, ev_code, -30)),
input_config = input_config.modify(analog_threshold=-30)
mapping = Mapping(
input_combination=InputCombination(input_config),
target_uinput="keyboard",
output_symbol="KEY_A",
)
handler = AbsToBtnHandler(
EventCombination((EV_ABS, ev_code, -30)), mapping
)
handler = AbsToBtnHandler(InputCombination(input_config), mapping)
handler.set_sub_handler(ForwardToUIHandler(self._results_pipe))
context.notify_callbacks[(EV_ABS, ev_code)].append(handler.notify)
context.add_handler(input_config, handler)
for ev_code in capabilities.get(EV_REL) or ():
# positive direction
mapping = UIMapping(
event_combination=EventCombination(
(EV_REL, ev_code, self.rel_xy_speed[ev_code])
),
input_config = InputConfig(
type=EV_REL,
code=ev_code,
analog_threshold=self.rel_xy_speed[ev_code],
origin_hash=device_hash,
)
mapping = Mapping(
input_combination=InputCombination(input_config),
target_uinput="keyboard",
output_symbol="KEY_A",
release_timeout=0.3,
force_release_timeout=True,
)
handler = RelToBtnHandler(
EventCombination((EV_REL, ev_code, self.rel_xy_speed[ev_code])),
mapping,
)
handler = RelToBtnHandler(InputCombination(input_config), mapping)
handler.set_sub_handler(ForwardToUIHandler(self._results_pipe))
context.notify_callbacks[(EV_REL, ev_code)].append(handler.notify)
context.add_handler(input_config, handler)
# negative direction
mapping = UIMapping(
event_combination=EventCombination(
(EV_REL, ev_code, -self.rel_xy_speed[ev_code])
),
input_config = input_config.modify(
analog_threshold=-self.rel_xy_speed[ev_code]
)
mapping = Mapping(
input_combination=InputCombination(input_config),
target_uinput="keyboard",
output_symbol="KEY_A",
release_timeout=0.3,
force_release_timeout=True,
)
handler = RelToBtnHandler(
EventCombination((EV_REL, ev_code, -self.rel_xy_speed[ev_code])),
mapping,
)
handler = RelToBtnHandler(InputCombination(input_config), mapping)
handler.set_sub_handler(ForwardToUIHandler(self._results_pipe))
context.notify_callbacks[(EV_REL, ev_code)].append(handler.notify)
context.add_handler(input_config, handler)
return context
@ -336,7 +353,13 @@ class ReaderService:
class ContextDummy:
def __init__(self):
self.listeners = set()
self.notify_callbacks = defaultdict(list)
self._notify_callbacks = defaultdict(list)
def add_handler(self, input_config: InputConfig, handler: InputEventHandler):
self._notify_callbacks[input_config.input_match_hash].append(handler.notify)
def get_entry_points(self, input_event: InputEvent) -> List[NotifyCallback]:
return self._notify_callbacks[input_event.input_match_hash]
def reset(self):
pass
@ -372,13 +395,14 @@ class ForwardToUIHandler:
self.pipe.send(
{
"type": MSG_EVENT,
"message": (
event.sec,
event.usec,
event.type,
event.code,
event.value,
),
"message": {
"sec": event.sec,
"usec": event.usec,
"type": event.type,
"code": event.code,
"value": event.value,
"origin_hash": event.origin_hash,
},
}
)
return True

@ -23,14 +23,11 @@ from typing import Dict, Callable
import gi
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
gi.require_version("GtkSource", "4")
from gi.repository import Gtk, GtkSource, Gdk, GObject
from inputremapper.configs.data import get_data_path
from inputremapper.configs.mapping import MappingData
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination
from inputremapper.gui.autocompletion import Autocompletion
from inputremapper.gui.components.editor import (
MappingListBox,
@ -381,15 +378,15 @@ class UserInterface:
def update_combination_label(self, mapping: MappingData):
"""Listens for mapping and updates the combination label."""
label: Gtk.Label = self.get("combination-label")
if mapping.event_combination.beautify() == label.get_label():
if mapping.input_combination.beautify() == label.get_label():
return
if mapping.event_combination == EventCombination.empty_combination():
if mapping.input_combination == InputCombination.empty_combination():
label.set_opacity(0.5)
label.set_label(_("no input configured"))
return
label.set_opacity(1)
label.set_label(mapping.event_combination.beautify())
label.set_label(mapping.input_combination.beautify())
def on_gtk_shortcut(self, _, event: Gdk.EventKey):
"""Execute shortcuts."""

@ -25,9 +25,6 @@ from typing import List, Callable, Dict, Optional
import gi
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
gi.require_version("GLib", "2.0")
from gi.repository import Gtk, GLib, Gdk
from inputremapper.logger import logger

@ -20,16 +20,19 @@
"""Stores injection-process wide information."""
from collections import defaultdict
from typing import List, Dict, Tuple, Set
from typing import List, Dict, Tuple, Set, Hashable
from inputremapper.input_event import InputEvent
from inputremapper.configs.preset import Preset
from inputremapper.injection.mapping_handlers.mapping_handler import (
InputEventHandler,
EventListener,
NotifyCallback,
)
from inputremapper.injection.mapping_handlers.mapping_parser import parse_mappings
from inputremapper.input_event import InputEvent
from inputremapper.injection.mapping_handlers.mapping_parser import (
parse_mappings,
EventPipelines,
)
class Context:
@ -61,12 +64,12 @@ class Context:
"""
listeners: Set[EventListener]
notify_callbacks: Dict[Tuple[int, int], List[NotifyCallback]]
_handlers: Dict[InputEvent, Set[InputEventHandler]]
_notify_callbacks: Dict[Hashable, List[NotifyCallback]]
_handlers: EventPipelines
def __init__(self, preset: Preset):
self.listeners = set()
self.notify_callbacks = defaultdict(list)
self._notify_callbacks = defaultdict(list)
self._handlers = parse_mappings(preset, self)
self._create_callbacks()
@ -79,7 +82,10 @@ class Context:
def _create_callbacks(self) -> None:
"""Add the notify method from all _handlers to self.callbacks."""
for event, handler_list in self._handlers.items():
self.notify_callbacks[event.type_and_code].extend(
for input_config, handler_list in self._handlers.items():
self._notify_callbacks[input_config.input_match_hash].extend(
handler.notify for handler in handler_list
)
def get_entry_points(self, input_event: InputEvent) -> List[NotifyCallback]:
return self._notify_callbacks[input_event.input_match_hash]

@ -22,10 +22,11 @@
import asyncio
import os
from typing import AsyncIterator, Protocol, Set, Dict, Tuple, List
from typing import AsyncIterator, Protocol, Set, List
import evdev
from inputremapper.utils import get_device_hash
from inputremapper.injection.mapping_handlers.mapping_handler import (
EventListener,
NotifyCallback,
@ -36,11 +37,13 @@ from inputremapper.logger import logger
class Context(Protocol):
listeners: Set[EventListener]
notify_callbacks: Dict[Tuple[int, int], List[NotifyCallback]]
def reset(self):
...
def get_entry_points(self, input_event: InputEvent) -> List[NotifyCallback]:
...
class EventReader:
"""Reads input events from a single device and distributes them.
@ -71,6 +74,7 @@ class EventReader:
Should be an UInput with capabilities that work for all forwarded
events, so ideally they should be copied from source.
"""
self._device_hash = get_device_hash(source)
self._source = source
self._forward_to = forward_to
self.context = context
@ -119,7 +123,7 @@ class EventReader:
return False
results = set()
notify_callbacks = self.context.notify_callbacks.get(event.type_and_code)
notify_callbacks = self.context.get_entry_points(event)
if notify_callbacks:
for notify_callback in notify_callbacks:
results.add(
@ -191,7 +195,9 @@ class EventReader:
self._source.fd,
)
async for event in self.read_loop():
await self.handle(InputEvent.from_event(event))
await self.handle(
InputEvent.from_event(event, origin_hash=self._device_hash)
)
self.context.reset()
logger.info("read loop for %s stopped", self._source.path)

@ -20,19 +20,21 @@
"""Keeps injecting keycodes in the background based on the preset."""
from __future__ import annotations
import asyncio
import enum
import multiprocessing
import sys
import time
from collections import defaultdict
from dataclasses import dataclass
from multiprocessing.connection import Connection
from typing import Dict, List, Optional, Tuple, Union
import evdev
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.preset import Preset
from inputremapper.event_combination import EventCombination
from inputremapper.groups import (
_Group,
classify,
@ -43,6 +45,7 @@ from inputremapper.injection.context import Context
from inputremapper.injection.event_reader import EventReader
from inputremapper.injection.numlock import set_numlock, is_numlock_on, ensure_numlock
from inputremapper.logger import logger
from inputremapper.utils import get_device_hash
CapabilitiesDict = Dict[int, List[int]]
GroupSources = List[evdev.InputDevice]
@ -67,7 +70,7 @@ class InjectorState(str, enum.Enum):
def is_in_capabilities(
combination: EventCombination, capabilities: CapabilitiesDict
combination: InputCombination, capabilities: CapabilitiesDict
) -> bool:
"""Are this combination or one of its sub keys in the capabilities?"""
for event in combination:
@ -109,6 +112,7 @@ class Injector(multiprocessing.Process):
group: _Group
preset: Preset
context: Optional[Context]
_devices: List[evdev.InputDevice]
_state: InjectorState
_msg_pipe: Tuple[Connection, Connection]
_event_readers: List[EventReader]
@ -187,7 +191,26 @@ class Injector(multiprocessing.Process):
"""Process internal stuff."""
def _grab_devices(self) -> GroupSources:
def _find_input_device(
self, input_config: InputConfig
) -> Optional[evdev.InputDevice]:
"""find the InputDevice specified by the InputConfig
ensures the devices supports the type and code specified by the InputConfig"""
devices_by_hash = {get_device_hash(device): device for device in self._devices}
# mypy thinks None is the wrong type for dict.get()
if device := devices_by_hash.get(input_config.origin_hash): # type: ignore
if input_config.code in device.capabilities(absinfo=False).get(
input_config.type, []
):
return device
return None
def _find_input_device_fallback(
self, input_config: InputConfig
) -> Optional[evdev.InputDevice]:
"""find the InputDevice specified by the InputConfig fallback logic"""
ranking = [
DeviceType.KEYBOARD,
DeviceType.GAMEPAD,
@ -197,40 +220,41 @@ class Injector(multiprocessing.Process):
DeviceType.CAMERA,
DeviceType.UNKNOWN,
]
candidates: List[evdev.InputDevice] = [
device
for device in self._devices
if input_config.code
in device.capabilities(absinfo=False).get(input_config.type, [])
]
# all devices in this group
devices: List[evdev.InputDevice] = []
for path in self.group.paths:
try:
devices.append(evdev.InputDevice(path))
except (FileNotFoundError, OSError):
logger.error('Could not find "%s"', path)
continue
if len(candidates) > 1:
# there is more than on input device which can be used for this
# event we choose only one determined by the ranking
return sorted(candidates, key=lambda d: ranking.index(classify(d)))[0]
if len(candidates) == 1:
return candidates.pop()
logger.error(f"Could not find input for {input_config}")
return None
def _grab_devices(self) -> GroupSources:
# find all devices which have an associated mapping
# use a dict because the InputDevice is not directly hashable
needed_devices = {}
input_configs = set()
# find all unique input_config's
for mapping in self.preset:
for event in mapping.event_combination:
candidates: List[evdev.InputDevice] = [
device
for device in devices
if event.code
in device.capabilities(absinfo=False).get(event.type, [])
]
if len(candidates) > 1:
# there is more than on input device which can be used for this
# event we choose only one determined by the ranking
device = sorted(
candidates, key=lambda d: ranking.index(classify(d))
)[0]
elif len(candidates) == 1:
device = candidates.pop()
else:
logger.error("Could not find input for %s in %s", event, mapping)
continue
needed_devices[device.path] = device
for input_config in mapping.input_combination:
input_configs.add(input_config)
# find all unique input_device's
for input_config in input_configs:
if not (device := self._find_input_device(input_config)):
# there is no point in trying the fallback because
# self._update_preset already did that.
continue
needed_devices[device.path] = device
grabbed_devices = []
for device in needed_devices.values():
@ -238,6 +262,29 @@ class Injector(multiprocessing.Process):
grabbed_devices.append(device)
return grabbed_devices
def _update_preset(self):
"""Update all InputConfigs in the preset to include correct origin_hash
information."""
mappings_by_input = defaultdict(list)
for mapping in self.preset:
for input_config in mapping.input_combination:
mappings_by_input[input_config].append(mapping)
for input_config in mappings_by_input:
if self._find_input_device(input_config):
continue
if not (device := self._find_input_device_fallback(input_config)):
# fallback failed, this mapping will be ignored
continue
for mapping in mappings_by_input[input_config]:
combination: List[InputConfig] = list(mapping.input_combination)
device_hash = get_device_hash(device)
idx = combination.index(input_config)
combination[idx] = combination[idx].modify(origin_hash=device_hash)
mapping.input_combination = combination
def _grab_device(self, device: evdev.InputDevice) -> Optional[evdev.InputDevice]:
"""Try to grab the device, return None if not possible.
@ -261,7 +308,8 @@ class Injector(multiprocessing.Process):
logger.error(str(error))
return None
def _copy_capabilities(self, input_device: evdev.InputDevice) -> CapabilitiesDict:
@staticmethod
def _copy_capabilities(input_device: evdev.InputDevice) -> CapabilitiesDict:
"""Copy capabilities for a new device."""
ecodes = evdev.ecodes
@ -305,6 +353,35 @@ class Injector(multiprocessing.Process):
self._msg_pipe[0].send(InjectorState.STOPPED)
return
def _create_forwarding_device(self, source: evdev.InputDevice) -> evdev.UInput:
# copy as much information as possible, because libinput uses the extra
# information to enable certain features like "Disable touchpad while
# typing"
try:
forward_to = evdev.UInput(
name=get_udev_name(source.name, "forwarded"),
events=self._copy_capabilities(source),
# phys=source.phys, # this leads to confusion. the appearance of
# a uinput with this "phys" property causes the udev rule to
# autoload for the original device, overwriting our previous
# attempts at starting an injection.
vendor=source.info.vendor,
product=source.info.product,
version=source.info.version,
bustype=source.info.bustype,
input_props=source.input_props(),
)
except TypeError as e:
if "input_props" in str(e):
# UInput constructor doesn't support input_props and
# source.input_props doesn't exist with old python-evdev versions.
logger.error("Please upgrade your python-evdev version. Exiting")
self._msg_pipe[0].send(InjectorState.UPGRADE_EVDEV)
sys.exit(12)
raise e
return forward_to
def run(self) -> None:
"""The injection worker that keeps injecting until terminated.
@ -323,16 +400,21 @@ class Injector(multiprocessing.Process):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self._devices = self.group.get_devices()
# InputConfigs may not contain the origin_hash information, this will try to make a
# good guess if the origin_hash information is missing or invalid.
self._update_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 forever
sources = self._grab_devices()
# create this within the process after the event loop creation,
# so that the macros use the correct loop
self.context = Context(self.preset)
self._stop_event = asyncio.Event()
# grab devices as early as possible. If events appear that won't get
# released anymore before the grab they appear to be held down
# forever
sources = self._grab_devices()
if len(sources) == 0:
# maybe the preset was empty or something
logger.error("Did not grab any device")
@ -343,33 +425,7 @@ class Injector(multiprocessing.Process):
coroutines = []
for source in sources:
# copy as much information as possible, because libinput uses the extra
# information to enable certain features like "Disable touchpad while
# typing"
try:
forward_to = evdev.UInput(
name=get_udev_name(source.name, "forwarded"),
events=self._copy_capabilities(source),
# phys=source.phys, # this leads to confusion. the appearance of
# a uinput with this "phys" property causes the udev rule to
# autoload for the original device, overwriting our previous
# attempts at starting an injection.
vendor=source.info.vendor,
product=source.info.product,
version=source.info.version,
bustype=source.info.bustype,
input_props=source.input_props(),
)
except TypeError as e:
if "input_props" in str(e):
# UInput constructor doesn't support input_props and
# source.input_props doesn't exist with old python-evdev versions.
logger.error("Please upgrade your python-evdev version. Exiting")
self._msg_pipe[0].send(InjectorState.UPGRADE_EVDEV)
sys.exit(12)
raise e
forward_to = self._create_forwarding_device(source)
# actually doing things
event_reader = EventReader(
self.context,

@ -22,9 +22,9 @@ from typing import Tuple, Optional, Dict
import evdev
from evdev.ecodes import EV_ABS
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper import exceptions
from inputremapper.configs.mapping import Mapping
from inputremapper.event_combination import EventCombination
from inputremapper.injection.global_uinputs import global_uinputs
from inputremapper.injection.mapping_handlers.axis_transform import Transformation
from inputremapper.injection.mapping_handlers.mapping_handler import (
@ -40,14 +40,14 @@ from inputremapper.utils import get_evdev_constant_name
class AbsToAbsHandler(MappingHandler):
"""Handler which transforms EV_ABS to EV_ABS events."""
_map_axis: Tuple[int, int] # the (type, code) of the axis we map
_map_axis: InputConfig # the InputConfig for the axis we map
_output_axis: Tuple[int, int] # the (type, code) of the output axis
_transform: Optional[Transformation]
_target_absinfo: evdev.AbsInfo
def __init__(
self,
combination: EventCombination,
combination: InputCombination,
mapping: Mapping,
**_,
) -> None:
@ -55,11 +55,8 @@ class AbsToAbsHandler(MappingHandler):
# find the input event we are supposed to map. If the input combination is
# BTN_A + ABS_X + BTN_B, then use the value of ABS_X for the transformation
for event in combination:
if event.value == 0:
assert event.type == EV_ABS
self._map_axis = event.type_and_code
break
assert (map_axis := combination.find_analog_input_config(type_=EV_ABS))
self._map_axis = map_axis
assert mapping.output_code is not None
assert mapping.output_type == EV_ABS
@ -72,7 +69,7 @@ class AbsToAbsHandler(MappingHandler):
self._transform = None
def __str__(self):
name = get_evdev_constant_name(*self._map_axis)
name = get_evdev_constant_name(*self._map_axis.type_and_code)
return f'AbsToAbsHandler for "{name}" {self._map_axis} <{id(self)}>:'
def __repr__(self):
@ -94,7 +91,7 @@ class AbsToAbsHandler(MappingHandler):
suppress: bool = False,
) -> bool:
if event.type_and_code != self._map_axis:
if event.input_match_hash != self._map_axis.input_match_hash:
return False
if EventActions.recenter in event.actions:
@ -145,12 +142,12 @@ class AbsToAbsHandler(MappingHandler):
logger.error("OverflowError (%s, %s, %s)", *self._output_axis, value)
def needs_wrapping(self) -> bool:
return len(self.input_events) > 1
return len(self.input_configs) > 1
def set_sub_handler(self, handler: InputEventHandler) -> None:
assert False # cannot have a sub-handler
def wrap_with(self) -> Dict[EventCombination, HandlerEnums]:
def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
if self.needs_wrapping():
return {EventCombination(self.input_events): HandlerEnums.axisswitch}
return {InputCombination(self.input_configs): HandlerEnums.axisswitch}
return {}

@ -18,44 +18,46 @@
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
from typing import Tuple
from typing import Tuple, Optional
import evdev
from evdev.ecodes import EV_ABS
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.mapping import Mapping
from inputremapper.event_combination import EventCombination
from inputremapper.injection.mapping_handlers.mapping_handler import (
MappingHandler,
InputEventHandler,
)
from inputremapper.input_event import InputEvent, EventActions
from inputremapper.utils import get_evdev_constant_name
class AbsToBtnHandler(MappingHandler):
"""Handler which transforms an EV_ABS to a button event."""
_input_event: InputEvent
_input_config: InputConfig
_active: bool
_sub_handler: InputEventHandler
def __init__(
self,
combination: EventCombination,
combination: InputCombination,
mapping: Mapping,
**_,
):
super().__init__(combination, mapping)
self._active = False
self._input_event = combination[0]
assert self._input_event.value != 0
self._input_config = combination[0]
assert self._input_config.analog_threshold
assert len(combination) == 1
def __str__(self):
name = get_evdev_constant_name(*self._input_config.type_and_code)
return (
f'AbsToBtnHandler for "{self._input_event.get_name()}" '
f"{self._input_event.event_tuple} <{id(self)}>:"
f'AbsToBtnHandler for "{name}" '
f"{self._input_config.type_and_code} <{id(self)}>:"
)
def __repr__(self):
@ -68,17 +70,19 @@ class AbsToBtnHandler(MappingHandler):
def _trigger_point(self, abs_min: int, abs_max: int) -> Tuple[float, float]:
"""Calculate the axis mid and trigger point."""
# TODO: potentially cache this function
assert self._input_config.analog_threshold
if abs_min == -1 and abs_max == 1:
# this is a hat switch
# return +-1
return (
self._input_event.value // abs(self._input_event.value),
self._input_config.analog_threshold
// abs(self._input_config.analog_threshold),
0,
)
half_range = (abs_max - abs_min) / 2
middle = half_range + abs_min
trigger_offset = half_range * self._input_event.value / 100
trigger_offset = half_range * self._input_config.analog_threshold / 100
# threshold, middle
return middle + trigger_offset, middle
@ -90,7 +94,7 @@ class AbsToBtnHandler(MappingHandler):
forward: evdev.UInput,
suppress: bool = False,
) -> bool:
if event.type_and_code != self._input_event.type_and_code:
if event.input_match_hash != self._input_config.input_match_hash:
return False
absinfo = {

@ -33,6 +33,7 @@ from evdev.ecodes import (
REL_HWHEEL_HI_RES,
)
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.mapping import (
Mapping,
REL_XY_SCALING,
@ -40,7 +41,6 @@ from inputremapper.configs.mapping import (
WHEEL_HI_RES_SCALING,
DEFAULT_REL_RATE,
)
from inputremapper.event_combination import EventCombination
from inputremapper.injection.global_uinputs import global_uinputs
from inputremapper.injection.mapping_handlers.axis_transform import Transformation
from inputremapper.injection.mapping_handlers.mapping_handler import (
@ -124,7 +124,7 @@ async def _run_wheel_output(self, codes: Tuple[int, int]) -> None:
class AbsToRelHandler(MappingHandler):
"""Handler which transforms an EV_ABS to EV_REL events."""
_map_axis: Tuple[int, int] # the input (type, code) of the axis we map
_map_axis: InputConfig # the InputConfig for the axis we map
_value: float # the current output value
_running: bool # if the run method is active
_stop: bool # if the run loop should return
@ -132,18 +132,15 @@ class AbsToRelHandler(MappingHandler):
def __init__(
self,
combination: EventCombination,
combination: InputCombination,
mapping: Mapping,
**_,
) -> None:
super().__init__(combination, mapping)
# find the input event we are supposed to map
for event in combination:
if event.value == 0:
assert event.type == EV_ABS
self._map_axis = event.type_and_code
break
assert (map_axis := combination.find_analog_input_config(type_=EV_ABS))
self._map_axis = map_axis
self._value = 0
self._running = False
@ -168,7 +165,7 @@ class AbsToRelHandler(MappingHandler):
self._run = partial(_run_normal_output, self)
def __str__(self):
name = get_evdev_constant_name(*self._map_axis)
name = get_evdev_constant_name(*self._map_axis.type_and_code)
return f'AbsToRelHandler for "{name}" {self._map_axis} <{id(self)}>:'
def __repr__(self):
@ -189,7 +186,7 @@ class AbsToRelHandler(MappingHandler):
forward: evdev.UInput = None,
suppress: bool = False,
) -> bool:
if event.type_and_code != self._map_axis:
if event.input_match_hash != self._map_axis.input_match_hash:
return False
if EventActions.recenter in event.actions:
@ -238,12 +235,12 @@ class AbsToRelHandler(MappingHandler):
logger.error("OverflowError (%s, %s, %s)", type_, keycode, value)
def needs_wrapping(self) -> bool:
return len(self.input_events) > 1
return len(self.input_configs) > 1
def set_sub_handler(self, handler: InputEventHandler) -> None:
assert False # cannot have a sub-handler
def wrap_with(self) -> Dict[EventCombination, HandlerEnums]:
def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
if self.needs_wrapping():
return {EventCombination(self.input_events): HandlerEnums.axisswitch}
return {InputCombination(self.input_configs): HandlerEnums.axisswitch}
return {}

@ -16,12 +16,13 @@
#
# 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 typing import Dict, Tuple
from typing import Dict, Tuple, Hashable
import evdev
from inputremapper.configs.input_config import InputConfig
from inputremapper.configs.input_config import InputCombination
from inputremapper.configs.mapping import Mapping
from inputremapper.event_combination import EventCombination
from inputremapper.injection.mapping_handlers.mapping_handler import (
MappingHandler,
HandlerEnums,
@ -42,8 +43,8 @@ class AxisSwitchHandler(MappingHandler):
output.
"""
_map_axis: Tuple[int, int] # the axis we switch on or off (type and code)
_trigger_key: Tuple[Tuple[int, int]] # all events that can switch the axis
_map_axis: InputConfig # the InputConfig for the axis we switch on or off
_trigger_keys: Tuple[Hashable, ...] # all events that can switch the axis
_active: bool # whether the axis is on or off
_last_value: int # the value of the last axis event that arrived
_axis_source: evdev.InputDevice # the cached source of the axis input events
@ -52,21 +53,20 @@ class AxisSwitchHandler(MappingHandler):
def __init__(
self,
combination: EventCombination,
combination: InputCombination,
mapping: Mapping,
**_,
):
super().__init__(combination, mapping)
map_axis = [
event.type_and_code for event in combination if not event.is_key_event
]
trigger_keys = [
event.type_and_code for event in combination if event.is_key_event
]
assert len(map_axis) != 0
trigger_keys = tuple(
event.input_match_hash
for event in combination
if not event.defines_analog_input
)
assert len(trigger_keys) >= 1
self._map_axis = map_axis[0]
self._trigger_keys = tuple(trigger_keys)
assert (map_axis := combination.find_analog_input_config())
self._map_axis = map_axis
self._trigger_keys = trigger_keys
self._active = False
self._last_value = 0
@ -74,7 +74,7 @@ class AxisSwitchHandler(MappingHandler):
self._forward_device = None
def __str__(self):
return f"AxisSwitchHandler for {self._map_axis} <{id(self)}>"
return f"AxisSwitchHandler for {self._map_axis.type_and_code} <{id(self)}>"
def __repr__(self):
return self.__str__()
@ -101,26 +101,28 @@ class AxisSwitchHandler(MappingHandler):
if not key_is_pressed:
# recenter the axis
logger.debug_key(self.mapping.event_combination, "stopping axis")
logger.debug_key(self.mapping.input_combination, "stopping axis")
event = InputEvent(
0,
0,
*self._map_axis,
*self._map_axis.type_and_code,
0,
actions=(EventActions.recenter,),
origin_hash=self._map_axis.origin_hash,
)
self._sub_handler.notify(event, self._axis_source, self._forward_device)
return True
if self._map_axis[0] == evdev.ecodes.EV_ABS:
if self._map_axis.type == evdev.ecodes.EV_ABS:
# send the last cached value so that the abs axis
# is at the correct position
logger.debug_key(self.mapping.event_combination, "starting axis")
logger.debug_key(self.mapping.input_combination, "starting axis")
event = InputEvent(
0,
0,
*self._map_axis,
*self._map_axis.type_and_code,
self._last_value,
origin_hash=self._map_axis.origin_hash,
)
self._sub_handler.notify(event, self._axis_source, self._forward_device)
return True
@ -129,8 +131,8 @@ class AxisSwitchHandler(MappingHandler):
def _should_map(self, event: InputEvent):
return (
event.type_and_code in self._trigger_keys
or event.type_and_code == self._map_axis
event.input_match_hash in self._trigger_keys
or event.input_match_hash == self._map_axis.input_match_hash
)
def notify(
@ -169,6 +171,8 @@ class AxisSwitchHandler(MappingHandler):
def needs_wrapping(self) -> bool:
return True
def wrap_with(self) -> Dict[EventCombination, HandlerEnums]:
combination = [event for event in self.input_events if event.is_key_event]
return {EventCombination(combination): HandlerEnums.combination}
def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
combination = [
config for config in self.input_configs if not config.defines_analog_input
]
return {InputCombination(combination): HandlerEnums.combination}

@ -17,13 +17,13 @@
# 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 typing import Dict, Tuple
from typing import Dict, Tuple, Hashable
import evdev
from evdev.ecodes import EV_ABS, EV_REL
from inputremapper.configs.input_config import InputCombination
from inputremapper.configs.mapping import Mapping
from inputremapper.event_combination import EventCombination
from inputremapper.injection.mapping_handlers.mapping_handler import (
MappingHandler,
InputEventHandler,
@ -36,14 +36,14 @@ from inputremapper.logger import logger
class CombinationHandler(MappingHandler):
"""Keeps track of a combination and notifies a sub handler."""
# map of (event.type, event.code) -> bool , keep track of the combination state
_pressed_keys: Dict[Tuple[int, int], bool]
# map of InputEvent.input_match_hash -> bool , keep track of the combination state
_pressed_keys: Dict[Hashable, bool]
_output_state: bool # the last update we sent to a sub-handler
_sub_handler: InputEventHandler
def __init__(
self,
combination: EventCombination,
combination: InputCombination,
mapping: Mapping,
**_,
) -> None:
@ -53,15 +53,16 @@ class CombinationHandler(MappingHandler):
self._output_state = False
# prepare a key map for all events with non-zero value
for event in combination:
assert event.is_key_event
self._pressed_keys[event.type_and_code] = False
for input_config in combination:
assert not input_config.defines_analog_input
self._pressed_keys[input_config.input_match_hash] = False
assert len(self._pressed_keys) > 0 # no combination handler without a key
def __str__(self):
return (
f'CombinationHandler for "{self.mapping.event_combination}" <{id(self)}>:'
f'CombinationHandler for "{self.mapping.input_combination}" '
f"{tuple(t for t in self._pressed_keys.keys())} <{id(self)}>:"
)
def __repr__(self):
@ -78,12 +79,11 @@ class CombinationHandler(MappingHandler):
forward: evdev.UInput,
suppress: bool = False,
) -> bool:
type_code = event.type_and_code
if type_code not in self._pressed_keys.keys():
if event.input_match_hash not in self._pressed_keys.keys():
return False # we are not responsible for the event
last_state = self.get_active()
self._pressed_keys[type_code] = event.value == 1
self._pressed_keys[event.input_match_hash] = event.value == 1
if self.get_active() == last_state or self.get_active() == self._output_state:
# nothing changed
@ -113,7 +113,7 @@ class CombinationHandler(MappingHandler):
return False
logger.debug_key(
self.mapping.event_combination, "triggered: sending to sub-handler"
self.mapping.input_combination, "triggered: sending to sub-handler"
)
self._output_state = bool(event.value)
return self._sub_handler.notify(event, source, forward, suppress)
@ -135,25 +135,30 @@ class CombinationHandler(MappingHandler):
"""
if len(self._pressed_keys) == 1 or not self.mapping.release_combination_keys:
return
for type_and_code in self._pressed_keys:
forward.write(*type_and_code, 0)
keys_to_release = filter(
lambda cfg: self._pressed_keys.get(cfg.input_match_hash),
self.mapping.input_combination,
)
for input_config in keys_to_release:
forward.write(*input_config.type_and_code, 0)
forward.syn()
def needs_ranking(self) -> bool:
return bool(self.input_events)
return bool(self.input_configs)
def rank_by(self) -> EventCombination:
return EventCombination(
event for event in self.input_events if event.value != 0
def rank_by(self) -> InputCombination:
return InputCombination(
event for event in self.input_configs if not event.defines_analog_input
)
def wrap_with(self) -> Dict[EventCombination, HandlerEnums]:
def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
return_dict = {}
for event in self.input_events:
if event.type == EV_ABS and event.value != 0:
return_dict[EventCombination(event)] = HandlerEnums.abs2btn
for config in self.input_configs:
if config.type == EV_ABS and not config.defines_analog_input:
return_dict[InputCombination(config)] = HandlerEnums.abs2btn
if event.type == EV_REL and event.value != 0:
return_dict[EventCombination(event)] = HandlerEnums.rel2btn
if config.type == EV_REL and not config.defines_analog_input:
return_dict[InputCombination(config)] = HandlerEnums.rel2btn
return return_dict

@ -21,7 +21,7 @@ from typing import List, Dict
import evdev
from evdev.ecodes import EV_ABS, EV_REL
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.injection.mapping_handlers.mapping_handler import (
MappingHandler,
InputEventHandler,
@ -37,18 +37,20 @@ class HierarchyHandler(MappingHandler):
all other handlers will be notified, but suppressed
"""
_input_event: InputEvent
_input_config: InputConfig
def __init__(self, handlers: List[MappingHandler], event: InputEvent) -> None:
def __init__(
self, handlers: List[MappingHandler], input_config: InputConfig
) -> None:
self.handlers = handlers
self._input_event = event
combination = EventCombination(event)
self._input_config = input_config
combination = InputCombination(input_config)
# use the mapping from the first child TODO: find a better solution
mapping = handlers[0].mapping
super().__init__(combination, mapping)
def __str__(self):
return f"HierarchyHandler for {self._input_event} <{id(self)}>:"
return f"HierarchyHandler for {self._input_config} <{id(self)}>:"
def __repr__(self):
return self.__str__()
@ -64,7 +66,7 @@ class HierarchyHandler(MappingHandler):
forward: evdev.UInput = None,
suppress: bool = False,
) -> bool:
if event.type_and_code != self._input_event.type_and_code:
if event.input_match_hash != self._input_config.input_match_hash:
return False
success = False
@ -79,11 +81,17 @@ class HierarchyHandler(MappingHandler):
for sub_handler in self.handlers:
sub_handler.reset()
def wrap_with(self) -> Dict[EventCombination, HandlerEnums]:
if self._input_event.type == EV_ABS and self._input_event.value != 0:
return {EventCombination(self._input_event): HandlerEnums.abs2btn}
if self._input_event.type == EV_REL and self._input_event.value != 0:
return {EventCombination(self._input_event): HandlerEnums.rel2btn}
def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
if (
self._input_config.type == EV_ABS
and not self._input_config.defines_analog_input
):
return {InputCombination(self._input_config): HandlerEnums.abs2btn}
if (
self._input_config.type == EV_REL
and not self._input_config.defines_analog_input
):
return {InputCombination(self._input_config): HandlerEnums.rel2btn}
return {}
def set_sub_handler(self, handler: InputEventHandler) -> None:

@ -19,9 +19,9 @@
from typing import Tuple, Dict
from inputremapper.configs.input_config import InputCombination
from inputremapper import exceptions
from inputremapper.configs.mapping import Mapping
from inputremapper.event_combination import EventCombination
from inputremapper.exceptions import MappingParsingError
from inputremapper.injection.global_uinputs import global_uinputs
from inputremapper.injection.mapping_handlers.mapping_handler import (
@ -41,7 +41,7 @@ class KeyHandler(MappingHandler):
def __init__(
self,
combination: EventCombination,
combination: InputCombination,
mapping: Mapping,
**_,
):
@ -63,7 +63,7 @@ class KeyHandler(MappingHandler):
@property
def child(self): # used for logging
name = get_evdev_constant_name(*self._map_axis)
name = get_evdev_constant_name(*self._maps_to)
return f"maps to: {name} {self._maps_to} on {self.mapping.target_uinput}"
def notify(self, event: InputEvent, *_, **__) -> bool:
@ -88,5 +88,5 @@ class KeyHandler(MappingHandler):
def needs_wrapping(self) -> bool:
return True
def wrap_with(self) -> Dict[EventCombination, HandlerEnums]:
return {EventCombination(self.input_events): HandlerEnums.combination}
def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
return {InputCombination(self.input_configs): HandlerEnums.combination}

@ -20,8 +20,8 @@
import asyncio
from typing import Dict, Callable
from inputremapper.configs.input_config import InputCombination
from inputremapper.configs.mapping import Mapping
from inputremapper.event_combination import EventCombination
from inputremapper.injection.global_uinputs import global_uinputs
from inputremapper.injection.macros.macro import Macro
from inputremapper.injection.macros.parse import parse
@ -43,7 +43,7 @@ class MacroHandler(MappingHandler):
def __init__(
self,
combination: EventCombination,
combination: InputCombination,
mapping: Mapping,
*,
context: ContextProtocol,
@ -70,7 +70,6 @@ class MacroHandler(MappingHandler):
logger.error('Macro "%s" failed: %s', self._macro.code, exception)
def notify(self, event: InputEvent, *_, **__) -> bool:
if event.value == 1:
self._active = True
self._macro.press_trigger()
@ -103,5 +102,5 @@ class MacroHandler(MappingHandler):
def needs_wrapping(self) -> bool:
return True
def wrap_with(self) -> Dict[EventCombination, HandlerEnums]:
return {EventCombination(self.input_events): HandlerEnums.combination}
def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
return {InputCombination(self.input_configs): HandlerEnums.combination}

@ -65,10 +65,10 @@ from typing import Dict, Protocol, Set, Optional, List
import evdev
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.mapping import Mapping
from inputremapper.event_combination import EventCombination
from inputremapper.exceptions import MappingParsingError
from inputremapper.input_event import InputEvent, EventActions
from inputremapper.input_event import InputEvent
from inputremapper.logger import logger
@ -147,14 +147,14 @@ class MappingHandler:
mapping: Mapping
# all input events this handler cares about
# should always be a subset of mapping.event_combination
input_events: List[InputEvent]
# should always be a subset of mapping.input_combination
input_configs: List[InputConfig]
_sub_handler: Optional[InputEventHandler]
# https://bugs.python.org/issue44807
def __init__(
self,
combination: EventCombination,
combination: InputCombination,
mapping: Mapping,
**_,
) -> None:
@ -166,14 +166,8 @@ class MappingHandler:
the combination from sub_handler.wrap_with()
mapping
"""
new_combination = []
for event in combination:
if event.value != 0:
event = event.modify(actions=(EventActions.as_key,))
new_combination.append(event)
self.mapping = mapping
self.input_events = new_combination
self.input_configs = list(combination)
self._sub_handler = None
def notify(
@ -209,13 +203,13 @@ class MappingHandler:
"""If this handler needs ranking and wrapping with a HierarchyHandler."""
return False
def rank_by(self) -> Optional[EventCombination]:
def rank_by(self) -> Optional[InputCombination]:
"""The combination for which this handler needs ranking."""
def wrap_with(self) -> Dict[EventCombination, HandlerEnums]:
"""A dict of EventCombination -> HandlerEnums.
def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
"""A dict of InputCombination -> HandlerEnums.
for each EventCombination this handler should be wrapped
for each InputCombination this handler should be wrapped
with the given MappingHandler.
"""
return {}
@ -224,14 +218,14 @@ class MappingHandler:
"""Give this handler a sub_handler."""
self._sub_handler = handler
def occlude_input_event(self, event: InputEvent) -> None:
"""Remove the event from self.input_events."""
if not self.input_events:
def occlude_input_event(self, input_config: InputConfig) -> None:
"""Remove the config from self.input_configs."""
if not self.input_configs:
logger.debug_mapping_handler(self)
raise MappingParsingError(
"Cannot remove a non existing event", mapping_handler=self
"Cannot remove a non existing config", mapping_handler=self
)
# should be called for each event a wrapping-handler
# has in its input_events EventCombination
self.input_events.remove(event)
# has in its input_configs InputCombination
self.input_configs.remove(input_config)

@ -24,16 +24,15 @@ from typing import Dict, List, Type, Optional, Set, Iterable, Sized, Tuple, Sequ
from evdev.ecodes import EV_KEY, EV_ABS, EV_REL
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.mapping import Mapping
from inputremapper.configs.preset import Preset
from inputremapper.configs.system_mapping import DISABLE_CODE, DISABLE_NAME
from inputremapper.event_combination import EventCombination
from inputremapper.exceptions import MappingParsingError
from inputremapper.injection.macros.parse import is_this_a_macro
from inputremapper.injection.mapping_handlers.abs_to_abs_handler import AbsToAbsHandler
from inputremapper.injection.mapping_handlers.abs_to_btn_handler import AbsToBtnHandler
from inputremapper.injection.mapping_handlers.abs_to_rel_handler import AbsToRelHandler
from inputremapper.injection.mapping_handlers.rel_to_rel_handler import RelToRelHandler
from inputremapper.injection.mapping_handlers.axis_switch_handler import (
AxisSwitchHandler,
)
@ -52,11 +51,11 @@ from inputremapper.injection.mapping_handlers.mapping_handler import (
from inputremapper.injection.mapping_handlers.null_handler import NullHandler
from inputremapper.injection.mapping_handlers.rel_to_abs_handler import RelToAbsHandler
from inputremapper.injection.mapping_handlers.rel_to_btn_handler import RelToBtnHandler
from inputremapper.input_event import InputEvent, USE_AS_ANALOG_VALUE
from inputremapper.injection.mapping_handlers.rel_to_rel_handler import RelToRelHandler
from inputremapper.logger import logger
from inputremapper.utils import get_evdev_constant_name
EventPipelines = Dict[InputEvent, Set[InputEventHandler]]
EventPipelines = Dict[InputConfig, Set[InputEventHandler]]
mapping_handler_classes: Dict[HandlerEnums, Optional[Type[MappingHandler]]] = {
# all available mapping_handlers
@ -95,7 +94,7 @@ def parse_mappings(preset: Preset, context: ContextProtocol) -> EventPipelines:
continue
output_handler = constructor(
mapping.event_combination,
mapping.input_combination,
mapping,
context=context,
)
@ -129,15 +128,15 @@ def parse_mappings(preset: Preset, context: ContextProtocol) -> EventPipelines:
# up in multiple groups if it takes care of multiple InputEvents
event_pipelines: EventPipelines = defaultdict(set)
for handler in handlers:
assert handler.input_events
for event in handler.input_events:
assert handler.input_configs
for input_config in handler.input_configs:
logger.debug(
"event-pipeline with entry point: %s %s",
get_evdev_constant_name(*event.type_and_code),
event.type_and_code,
get_evdev_constant_name(*input_config.type_and_code),
input_config.input_match_hash,
)
logger.debug_mapping_handler(handler)
event_pipelines[event].add(handler)
event_pipelines[input_config].add(handler)
return event_pipelines
@ -168,7 +167,7 @@ def _create_event_pipeline(
handlers.extend(_create_event_pipeline(super_handler, context))
if handler.input_events:
if handler.input_configs:
# the handler was only partially wrapped,
# we need to return it as a toplevel handler
handlers.append(handler)
@ -193,7 +192,7 @@ def _get_output_handler(mapping: Mapping) -> HandlerEnums:
if mapping.output_type == EV_KEY:
return HandlerEnums.key
input_event = _maps_axis(mapping.event_combination)
input_event = _maps_axis(mapping.input_combination)
if not input_event:
raise MappingParsingError(
f"This {mapping = } does not map to an axis, key or macro",
@ -219,18 +218,18 @@ def _get_output_handler(mapping: Mapping) -> HandlerEnums:
raise MappingParsingError(f"the output of {mapping = } is unknown", mapping=Mapping)
def _maps_axis(combination: EventCombination) -> Optional[InputEvent]:
"""Whether this EventCombination contains an InputEvent that is treated as
def _maps_axis(combination: InputCombination) -> Optional[InputConfig]:
"""Whether this InputCombination contains an InputEvent that is treated as
an axis and not a binary (key or button) event.
"""
for event in combination:
if event.value == USE_AS_ANALOG_VALUE:
if event.defines_analog_input:
return event
return None
def _create_hierarchy_handlers(
handlers: Dict[EventCombination, Set[MappingHandler]]
handlers: Dict[InputCombination, Set[MappingHandler]]
) -> Set[MappingHandler]:
"""Sort handlers by input events and create Hierarchy handlers."""
sorted_handlers = set()
@ -271,8 +270,8 @@ def _create_hierarchy_handlers(
def _order_combinations(
combinations: List[EventCombination], common_event: InputEvent
) -> List[EventCombination]:
combinations: List[InputCombination], common_config: InputConfig
) -> List[InputCombination]:
"""Reorder the keys according to some rules.
such that a combination a+b+c is in front of a+b which is in front of b
@ -285,21 +284,21 @@ def _order_combinations(
----------
combinations
the list which needs ordering
common_event
the Key all members of Keys have in common
common_config
the InputConfig all InputCombination's in combinations have in common
"""
combinations.sort(key=len)
for start, end in ranges_with_constant_length(combinations.copy()):
for start, end in _ranges_with_constant_length(combinations.copy()):
sub_list = combinations[start:end]
sub_list.sort(key=lambda x: x.index(common_event))
sub_list.sort(key=lambda x: x.index(common_config))
combinations[start:end] = sub_list
combinations.reverse()
return combinations
def ranges_with_constant_length(x: Sequence[Sized]) -> Iterable[Tuple[int, int]]:
def _ranges_with_constant_length(x: Sequence[Sized]) -> Iterable[Tuple[int, int]]:
"""Get all ranges of x for which the elements have constant length
Parameters

@ -21,7 +21,7 @@ from typing import Dict
import evdev
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination
from inputremapper.injection.mapping_handlers.mapping_handler import (
MappingHandler,
HandlerEnums,
@ -33,17 +33,17 @@ class NullHandler(MappingHandler):
"""Handler which consumes the event and does nothing."""
def __str__(self):
return f"NullHandler for {self.mapping.event_combination}<{id(self)}>"
return f"NullHandler for {self.mapping.input_combination}<{id(self)}>"
@property
def child(self):
return "Voids all events"
def needs_wrapping(self) -> bool:
return True in [event.value != 0 for event in self.input_events]
return False in [input_.defines_analog_input for input_ in self.input_configs]
def wrap_with(self) -> Dict[EventCombination, HandlerEnums]:
return {EventCombination(self.input_events): HandlerEnums.combination}
def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
return {InputCombination(self.input_configs): HandlerEnums.combination}
def notify(
self,

@ -30,6 +30,7 @@ from evdev.ecodes import (
REL_WHEEL_HI_RES,
)
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper import exceptions
from inputremapper.configs.mapping import (
Mapping,
@ -38,7 +39,6 @@ from inputremapper.configs.mapping import (
REL_XY_SCALING,
DEFAULT_REL_RATE,
)
from inputremapper.event_combination import EventCombination
from inputremapper.injection.global_uinputs import global_uinputs
from inputremapper.injection.mapping_handlers.axis_transform import Transformation
from inputremapper.injection.mapping_handlers.mapping_handler import (
@ -58,7 +58,7 @@ class RelToAbsHandler(MappingHandler):
release_timeout.
"""
_input_movement: Tuple[int, int] # (type, code) of the relative movement we map
_map_axis: InputConfig # InputConfig for the relative movement we map
_output_axis: Tuple[int, int] # the (type, code) of the output axis
_transform: Transformation
_target_absinfo: evdev.AbsInfo
@ -72,7 +72,7 @@ class RelToAbsHandler(MappingHandler):
def __init__(
self,
combination: EventCombination,
combination: InputCombination,
mapping: Mapping,
**_,
) -> None:
@ -80,9 +80,8 @@ class RelToAbsHandler(MappingHandler):
# find the input event we are supposed to map. If the input combination is
# BTN_A + REL_X + BTN_B, then use the value of REL_X for the transformation
analog_input = mapping.find_analog_input_event(type_=EV_REL)
assert analog_input is not None
self._input_movement = analog_input.type_and_code
assert (map_axis := combination.find_analog_input_config(type_=EV_REL))
self._map_axis = map_axis
assert mapping.output_code is not None
assert mapping.output_type == EV_ABS
@ -107,7 +106,7 @@ class RelToAbsHandler(MappingHandler):
self._observed_rate = DEFAULT_REL_RATE
def __str__(self):
return f"RelToAbsHandler for {self._input_movement} <{id(self)}>:"
return f"RelToAbsHandler for {self._map_axis} <{id(self)}>:"
def __repr__(self):
return self.__str__()
@ -140,10 +139,10 @@ class RelToAbsHandler(MappingHandler):
def _get_default_cutoff(self):
"""Get the cutoff value assuming the default input rate."""
if self._input_movement[1] in [REL_WHEEL, REL_HWHEEL]:
if self._map_axis.code in [REL_WHEEL, REL_HWHEEL]:
return self.mapping.rel_to_abs_input_cutoff * WHEEL_SCALING
if self._input_movement[1] in [REL_WHEEL_HI_RES, REL_HWHEEL_HI_RES]:
if self._map_axis.code in [REL_WHEEL_HI_RES, REL_HWHEEL_HI_RES]:
return self.mapping.rel_to_abs_input_cutoff * WHEEL_HI_RES_SCALING
return self.mapping.rel_to_abs_input_cutoff * REL_XY_SCALING
@ -167,7 +166,7 @@ class RelToAbsHandler(MappingHandler):
) -> bool:
self._observe_rate(event)
if event.type_and_code != self._input_movement:
if event.input_match_hash != self._map_axis.input_match_hash:
return False
if EventActions.recenter in event.actions:
@ -236,12 +235,12 @@ class RelToAbsHandler(MappingHandler):
logger.error("OverflowError (%s, %s, %s)", *self._output_axis, value)
def needs_wrapping(self) -> bool:
return len(self.input_events) > 1
return len(self.input_configs) > 1
def set_sub_handler(self, handler: InputEventHandler) -> None:
assert False # cannot have a sub-handler
def wrap_with(self) -> Dict[EventCombination, HandlerEnums]:
def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
if self.needs_wrapping():
return {EventCombination(self.input_events): HandlerEnums.axisswitch}
return {InputCombination(self.input_configs): HandlerEnums.axisswitch}
return {}

@ -23,8 +23,8 @@ import time
import evdev
from evdev.ecodes import EV_REL
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.mapping import Mapping
from inputremapper.event_combination import EventCombination
from inputremapper.injection.mapping_handlers.mapping_handler import (
MappingHandler,
InputEventHandler,
@ -41,30 +41,27 @@ class RelToBtnHandler(MappingHandler):
"""
_active: bool
_input_event: InputEvent
_input_config: InputConfig
_last_activation: float
_sub_handler: InputEventHandler
def __init__(
self,
combination: EventCombination,
combination: InputCombination,
mapping: Mapping,
**_,
) -> None:
super().__init__(combination, mapping)
self._active = False
self._input_event = combination[0]
self._input_config = combination[0]
self._last_activation = time.time()
self._abort_release = False
assert self._input_event.value != 0
assert self._input_config.analog_threshold != 0
assert len(combination) == 1
def __str__(self):
return (
f'RelToBtnHandler for "{self._input_event.get_name()}" '
f"{self._input_event.event_tuple} <{id(self)}>:"
)
return f'RelToBtnHandler for "{self._input_config}" <{id(self)}>:'
def __repr__(self):
return self.__str__()
@ -86,7 +83,14 @@ class RelToBtnHandler(MappingHandler):
self._abort_release = False
return
event = self._input_event.modify(value=0, actions=(EventActions.as_key,))
event = InputEvent(
0,
0,
*self._input_config.type_and_code,
value=0,
actions=(EventActions.as_key,),
origin_hash=self._input_config.origin_hash,
)
logger.debug_key(event.event_tuple, "sending to sub_handler")
self._sub_handler.notify(event, source, forward, suppress)
self._active = False
@ -100,10 +104,10 @@ class RelToBtnHandler(MappingHandler):
) -> bool:
assert event.type == EV_REL
if event.type_and_code != self._input_event.type_and_code:
if event.input_match_hash != self._input_config.input_match_hash:
return False
threshold = self._input_event.value
assert (threshold := self._input_config.analog_threshold)
value = event.value
if (value < threshold > 0) or (value > threshold < 0):
if self._active:

@ -29,6 +29,7 @@ from evdev.ecodes import (
REL_HWHEEL_HI_RES,
)
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper import exceptions
from inputremapper.configs.mapping import (
Mapping,
@ -36,7 +37,6 @@ from inputremapper.configs.mapping import (
WHEEL_SCALING,
WHEEL_HI_RES_SCALING,
)
from inputremapper.event_combination import EventCombination
from inputremapper.injection.global_uinputs import global_uinputs
from inputremapper.injection.mapping_handlers.axis_transform import Transformation
from inputremapper.injection.mapping_handlers.mapping_handler import (
@ -77,7 +77,7 @@ class Remainder:
class RelToRelHandler(MappingHandler):
"""Handler which transforms EV_REL to EV_REL events."""
_input_event: InputEvent # the relative movement we map
_input_config: InputConfig # the relative movement we map
_max_observed_input: float
@ -89,7 +89,7 @@ class RelToRelHandler(MappingHandler):
def __init__(
self,
combination: EventCombination,
combination: InputCombination,
mapping: Mapping,
**_,
) -> None:
@ -97,9 +97,9 @@ class RelToRelHandler(MappingHandler):
# find the input event we are supposed to map. If the input combination is
# BTN_A + REL_X + BTN_B, then use the value of REL_X for the transformation
input_event = mapping.find_analog_input_event(type_=EV_REL)
assert input_event is not None
self._input_event = input_event
input_config = combination.find_analog_input_config(type_=EV_REL)
assert input_config is not None
self._input_config = input_config
self._max_observed_input = 1
@ -116,7 +116,7 @@ class RelToRelHandler(MappingHandler):
)
def __str__(self):
return f"RelToRelHandler for {self._input_event} <{id(self)}>:"
return f"RelToRelHandler for {self._input_config} <{id(self)}>:"
def __repr__(self):
return self.__str__()
@ -127,10 +127,7 @@ class RelToRelHandler(MappingHandler):
def _should_map(self, event: InputEvent):
"""Check if this input event is relevant for this handler."""
if event.type_and_code == (self._input_event.type, self._input_event.code):
return True
return False
return event.input_match_hash == self._input_config.input_match_hash
def notify(
self,
@ -264,12 +261,12 @@ class RelToRelHandler(MappingHandler):
)
def needs_wrapping(self) -> bool:
return len(self.input_events) > 1
return len(self.input_configs) > 1
def set_sub_handler(self, handler: InputEventHandler) -> None:
assert False # cannot have a sub-handler
def wrap_with(self) -> Dict[EventCombination, HandlerEnums]:
def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
if self.needs_wrapping():
return {EventCombination(self.input_events): HandlerEnums.axisswitch}
return {InputCombination(self.input_configs): HandlerEnums.axisswitch}
return {}

@ -21,26 +21,11 @@ from __future__ import annotations
import enum
from dataclasses import dataclass
from typing import Tuple, Union, Sequence, Callable, Optional, Any
from typing import Tuple, Optional, Hashable
import evdev
from evdev import ecodes
from inputremapper.configs.system_mapping import system_mapping
from inputremapper.exceptions import InputEventCreationError
from inputremapper.gui.messages.message_broker import MessageType
from inputremapper.logger import logger
InputEventValidationType = Union[
str,
Tuple[int, int, int],
evdev.InputEvent,
]
# if "Use as analog" is set in the advanced mapping editor, the value will be set to 0
USE_AS_ANALOG_VALUE = 0
class EventActions(enum.Enum):
"""Additional information an InputEvent can send through the event pipeline."""
@ -57,108 +42,67 @@ class EventActions(enum.Enum):
# Todo: add slots=True as soon as python 3.10 is in common distros
@dataclass(frozen=True)
class InputEvent:
"""The evnet used by inputremapper
"""Events that are generated during runtime.
as a drop in replacement for evdev.InputEvent
Is a drop-in replacement for evdev.InputEvent
"""
message_type = MessageType.selected_event
sec: int
usec: int
type: int
code: int
value: int
actions: Tuple[EventActions, ...] = ()
origin_hash: Optional[str] = None
def __hash__(self):
return hash((self.type, self.code, self.value))
def __eq__(self, other: Any):
def __eq__(self, other: InputEvent | evdev.InputEvent | Tuple[int, int, int]):
# useful in tests
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.validate
@classmethod
def validate(cls, init_arg: InputEventValidationType) -> InputEvent:
"""Try all the different methods, and raise an error if none succeed."""
if isinstance(init_arg, InputEvent):
return init_arg
event = None
validators: Sequence[Callable[..., InputEvent]] = (
cls.from_event,
cls.from_string,
cls.from_tuple,
)
for validator in validators:
try:
event = validator(init_arg)
break
except InputEventCreationError:
pass
raise TypeError(f"cannot compare {type(other)} with InputEvent")
if event:
return event
raise ValueError(f"failed to create InputEvent with {init_arg = }")
@property
def input_match_hash(self) -> Hashable:
"""a Hashable object which is intended to match the InputEvent with a
InputConfig.
"""
return self.type, self.code, self.origin_hash
@classmethod
def from_event(cls, event: evdev.InputEvent) -> InputEvent:
def from_event(
cls, event: evdev.InputEvent, origin_hash: Optional[str] = None
) -> InputEvent:
"""Create a InputEvent from another InputEvent or evdev.InputEvent."""
try:
return cls(event.sec, event.usec, event.type, event.code, event.value)
return cls(
event.sec,
event.usec,
event.type,
event.code,
event.value,
origin_hash=origin_hash,
)
except AttributeError as exception:
raise InputEventCreationError(
raise TypeError(
f"Failed to create InputEvent from {event = }"
) from exception
@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]),
if len(event_tuple) != 3:
raise TypeError(
f"failed to create InputEvent {event_tuple = }" f" must have length 3"
)
except ValueError as exception:
raise InputEventCreationError(
f"Failed to create InputEvent from {event_tuple = }"
) from exception
except TypeError as exception:
raise InputEventCreationError(
f"Failed to create InputEvent from {type(event_tuple) = }"
) from exception
@classmethod
def btn_left(cls):
return cls(0, 0, evdev.ecodes.EV_KEY, evdev.ecodes.BTN_LEFT, 1)
return cls(
0,
0,
int(event_tuple[0]),
int(event_tuple[1]),
int(event_tuple[2]),
)
@property
def type_and_code(self) -> Tuple[int, int]:
@ -194,14 +138,6 @@ class InputEvent:
def __str__(self):
return f"InputEvent{self.event_tuple}"
def description(self, exclude_threshold=False, exclude_direction=False) -> str:
"""Get a human-readable description of the event."""
return (
f"{self.get_name()} "
f"{self.get_direction() if not exclude_direction else ''} "
f"{self.get_threshold() if not exclude_threshold else ''}".strip()
)
def timestamp(self):
"""Return the unix timestamp of when the event was seen."""
return self.sec + self.usec / 1000000
@ -214,6 +150,7 @@ class InputEvent:
code: Optional[int] = None,
value: Optional[int] = None,
actions: Tuple[EventActions, ...] = None,
origin_hash: Optional[str] = None,
) -> InputEvent:
"""Return a new modified event."""
return InputEvent(
@ -223,105 +160,5 @@ class InputEvent:
code if code is not None else self.code,
value if value is not None else self.value,
actions if actions is not None else self.actions,
origin_hash=origin_hash if origin_hash is not None else self.origin_hash,
)
def json_key(self) -> str:
return ",".join([str(self.type), str(self.code), str(self.value)])
def get_name(self) -> Optional[str]:
"""Human-readable name."""
if self.type not in ecodes.bytype:
logger.warning("Unknown type for %s", self)
return f"unknown {self.type, self.code}"
if self.code not in ecodes.bytype[self.type]:
logger.warning("Unknown code for %s", self)
return f"unknown {self.type, self.code}"
key_name = None
# first try to find the name in xmodmap to not display wrong
# names due to the keyboard layout
if self.type == ecodes.EV_KEY:
key_name = system_mapping.get_name(self.code)
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[self.type][self.code]
if isinstance(key_name, list):
key_name = key_name[0]
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-X")
key_name = key_name.replace("ABS_HAT0Y", "DPad-Y")
key_name = key_name.replace("ABS_HAT1X", "DPad-2-X")
key_name = key_name.replace("ABS_HAT1Y", "DPad-2-Y")
key_name = key_name.replace("ABS_HAT2X", "DPad-3-X")
key_name = key_name.replace("ABS_HAT2Y", "DPad-3-Y")
key_name = key_name.replace("ABS_X", "Joystick-X")
key_name = key_name.replace("ABS_Y", "Joystick-Y")
key_name = key_name.replace("ABS_RX", "Joystick-RX")
key_name = key_name.replace("ABS_RY", "Joystick-RY")
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(" ", " ")
return key_name
def get_direction(self) -> str:
if self.type == ecodes.EV_KEY:
return ""
try:
event = self.modify(value=self.value // abs(self.value))
except ZeroDivisionError:
return ""
return {
# 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)) or ("+" if event.value > 0 else "-")
def get_threshold(self) -> str:
if self.value == 0:
return ""
return {
ecodes.EV_REL: f"{abs(self.value)}",
ecodes.EV_ABS: f"{abs(self.value)}%",
}.get(self.type) or ""

@ -24,7 +24,7 @@ import os
import sys
import time
from datetime import datetime
from typing import cast, Tuple
from typing import cast
import pkg_resources
@ -77,7 +77,7 @@ class Logger(logging.Logger):
msg = indent * line[1] + line[0]
self._log(logging.DEBUG, msg, args=None)
def debug_key(self, key: Tuple[int, int, int], msg, *args):
def debug_key(self, key, msg, *args):
"""Log a key-event message.
Example:

@ -21,15 +21,28 @@
"""Utility functions."""
import sys
from hashlib import md5
from typing import Optional
import evdev
DeviceHash = str
def is_service() -> bool:
return sys.argv[0].endswith("input-remapper-service")
def get_device_hash(device: evdev.InputDevice) -> DeviceHash:
"""get a unique hash for the given device"""
# the builtin hash() function can not be used because it is randomly
# seeded at python startup.
# a non-cryptographic hash would be faster but there is none in the standard lib
s = str(device.capabilities(absinfo=False)) + device.name
return md5(s.encode()).hexdigest().lower()
def get_evdev_constant_name(type_: int, code: int, *_) -> Optional[str]:
"""Handy function to get the evdev constant name."""
# this is more readable than

@ -1,26 +1,26 @@
# Usage
To open the UI to modify the mappings, look into your applications menu
and search for 'Input Remapper'. You should be prompted for your sudo password
as special permissions are needed to read events from `/dev/input/` files.
You can also start it via `input-remapper-gtk`.
Look into your applications menu and search for **Input Remapper** to open the UI.
You should be prompted for your sudo password as special permissions are needed to read
events from `/dev/input/` files. You can also start it via `input-remapper-gtk`.
First, select your device (like your keyboard) on the first page, then create a new
preset on the second page, and add a mapping. Then you can already edit your inputs,
as shown in the screenshots below.
<p align="center">
<img src="usage_1.png"/>
<img src="usage_2.png"/>
</p>
First, select your device (like your keyboard) on the first page, then create a new
preset on the second page, and add a mapping. Then you can already edit your inputs,
as shown in the screenshots.
In the text input field, type the key to which you would like to map this key.
More information about the possible mappings can be found in [examples.md](./examples.md) and [below](#key-names).
You can also write your macro into the text input field. If you hit enter, it will switch to a multiline-editor with
In the text input field, type the key to which you would like to map this input.
More information about the possible mappings can be found in
[examples.md](./examples.md) and [below](#key-names). You can also write your macro
into the text input field. If you hit enter, it will switch to a multiline-editor with
line-numbers.
Changes are saved automatically.
Press the "Apply" button to activate (inject) the mapping you created.
Changes are saved automatically. Press the "Apply" button to activate (inject) the
mapping you created.
If you later want to modify the Input of your mapping you need to use the
"Stop" button, so that the application can read your original input.
@ -44,8 +44,8 @@ the input (`Record` - Button) press multiple keys and/or move axis at once.
The mapping will be triggered as soon as all the recorded inputs are pressed.
If you use an axis an input you can modify the threshold at which the mapping is
activated in the advanced input configuration, which can be opened by clicking on the
`Advanced` button.
activated in the advanced input configuration, which can be opened by clicking
on the `Advanced` button.
A mapping with an input combination is only injected once all combination keys
are pressed. This means all the input keys you press before the combination is complete
@ -144,95 +144,133 @@ looks like, with an example autoload entry:
```json
{
"autoload": {
"Logitech USB Keyboard": "preset name"
},
"version": "1.6"
"autoload": {
"Logitech USB Keyboard": "preset name"
},
"version": "1.6"
}
```
`preset name` refers to `~/.config/input-remapper/presets/device name/preset name.json`.
The device name can be found with `sudo input-remapper-control --list-devices`.
#### Preset
### Preset
The preset files are a collection of mappings.
Here is an example configuration for preset "a" for the "gamepad" device:
`~/.config/input-remapper/presets/gamepad/a.json`
```json
{
"1,307,1": {
"target_uinput": "keyboard",
"output_symbol": "k(2).k(3)",
[
{
"input_combination": [
{"type": 1, "code": 307}
],
"target_uinput": "keyboard",
"output_symbol": "k(2).k(3)",
"macro_key_sleep_ms": 100
},
"1,315,1+1,16,1": {
"target_uinput": "keyboard",
},
{
"input_combination": [
{"type": 1, "code": 315, "origin_hash": "07f543a6d19f00769e7300c2b1033b7a"},
{"type": 3, "code": 1, "analog_threshold": 10}
],
"target_uinput": "keyboard",
"output_symbol": "1"
},
"3,1,0": {
"target_uinput": "mouse",
"output_type": 2,
"output_code": 1,
},
{
"input_combination": [
{"type": 3, "code": 1}
],
"target_uinput": "mouse",
"output_type": 2,
"output_code": 1,
"gain": 0.5
}
}
]
```
This preset consists of three mappings.
* The first maps the key event with code 307 to a macro and sets the time between
injected events of macros to 100 ms. The macro injects its events to the virtual keyboard.
* The second mapping is a key combination, chained using `+`.
* The third maps the y-Axis to the y-Axis on the virtual mouse.
* The second mapping is a combination of a key event with the code 315 and a
analog input of the axis 1 (y-Axis).
* The third maps the y-Axis of a joystick to the y-Axis on the virtual mouse.
#### Mapping
### Mapping
As shown above, the mapping is part of the preset. It consists of the input-combination
and the mapping parameters.
As shown above, the mapping is part of the preset. It consists of the input-combination,
which is a list of input-configurations and the mapping parameters.
```
<input-combination>: {
{
"input_combination": [
<InputConfig 1>,
<InputConfig 2>
]
<parameter 1>: <value1>,
<parameter 2>: <value2>
}
```
The input-combination is a string like `"EV_TYPE, EV_CODE, EV_VALUE + ..."`.
`EV_TYPE` and `EV_CODE` describe the input event. Use the program `evtest` to find
Available types and codes. See also the [evdev documentation](https://www.kernel.org/doc/html/latest/input/event-codes.html#input-event-codes)
The `EV_VALUE` describes the intention of the input.
A value of `0` means that the event will be mapped to an axis. A non-zero value means
that the event will be treated as a key input.
#### Input Combination and Configuration
The input-combination is a list of one or more input configurations. To trigger a
mapping, all input configurations must trigger.
If the event type is `3 (EV_ABS)` (as in: map a joystick axis to a key or macro) the
value can be between `-100 [%]` and `100 [%]`. The mapping will be triggered once the joystick
reaches the position described by the value.
A input configuration is a dictionary with some or all of the following parameters:
If the event type is `2 (EV_REL)` (as in: map a relative axis (e.g. mouse wheel) to a key or macro)
the value can be anything. The mapping will be triggered once the speed and direction of
the axis is higher than described by the value.
| Parameter | Default | Type | Description |
|------------------|---------|------------------------|---------------------------------------------------------------------|
| type | - | int | Input Event Type |
| code | - | int | Input Evnet Code |
| origin_hash | None | hex (string formatted) | A unique identifier for the device which emits the described event. |
| analog_threshold | None | int | The threshold above which a input axis triggers the mapping. |
##### type, code
The `type` and `code` parameters are always needed. Use the program `evtest` to find
Available types and codes. See also the [evdev documentation](https://www.kernel.org/doc/html/latest/input/event-codes.html#input-event-codes)
##### origin_hash
The origin_hash is an internally computed hash. It is used associate the input with a
specific `/dev/input/eventXX` device. This is useful when a single pyhsical device
creates multiple `/dev/input/eventXX` devices wihth similar capabilities.
See also: [Issue#435](https://github.com/sezanzeb/input-remapper/issues/435)
##### analog_threshold
Setting the `analog_threshold` to zero or omitting it means that the input will be
mapped to an axis. There can only be one axis input with a threshold of 0 in a mapping.
If the `type` is 1 (EV_KEY) the `analog_threshold` has no effect.
The `analog_threshold` is needend when the input is a analog axis which should be
treated as a key input. If the event type is `3 (EV_ABS)` (as in: map a joystick axis to
a key or macro) the threshold can be between `-100 [%]` and `100 [%]`. The mapping will
be triggered once the joystick reaches the position described by the value.
If the event type is `2 (EV_REL)` (as in: map a relative axis (e.g. mouse wheel) to a
key or macro) the threshold can be anything. The mapping will be triggered once the
speed and direction of the axis is higher than described by the threshold.
#### Mapping Parameters
The following table contains all possible parameters and their default values:
| Parameter | Default | Type | Description |
|----------------------------|---------|-----------------|---------------------------------------------------------------------------------------------------------------------------------|
| target_uinput | | string | The UInput to which the mapped event will be sent |
| output_symbol | | string | The symbol or macro string if applicable |
| output_type | | int | The event type of the mapped event |
| output_code | | int | The event code of the mapped event |
| release_combination_keys | true | bool | If release events will be sent to the forwarded device as soon as a combination triggers see also #229 |
| **Macro settings** |
| macro_key_sleep_ms | 0 | positive int | |
| **Axis settings** |
| deadzone | 0.1 | float ∈ (0, 1) | The deadzone of the input axis |
| gain | 1.0 | float | Scale factor when mapping an axis to an axis |
| expo | 0 | float ∈ (-1, 1) | Non liniarity factor see also [GeoGebra](https://www.geogebra.org/calculator/mkdqueky) |
| **EV_REL output** |
| rel_rate | 60 | positive int | The frequency `[Hz]` at which `EV_REL` events get generated (also effects mouse macro) |
| **EV_REL as input** |
| rel_to_abs_input_cutoff | 2 | positive float | The value relative to a predefined base-speed, at which `EV_REL` input (cursor and wheel) is considered at its maximum. |
| release_timeout | 0.05 | positive float | The time `[s]` until a relative axis is considered stationary if no new events arrive |
| Parameter | Default | Type | Description |
|--------------------------|---------|-----------------|-------------------------------------------------------------------------------------------------------------------------|
| input_combination | | list | see [above](#input-combination-and-configuration) |
| target_uinput | | string | The UInput to which the mapped event will be sent |
| output_symbol | | string | The symbol or macro string if applicable |
| output_type | | int | The event type of the mapped event |
| output_code | | int | The event code of the mapped event |
| release_combination_keys | true | bool | If release events will be sent to the forwarded device as soon as a combination triggers see also #229 |
| **Macro settings** | | | |
| macro_key_sleep_ms | 0 | positive int | |
| **Axis settings** | | | |
| deadzone | 0.1 | float ∈ (0, 1) | The deadzone of the input axis |
| gain | 1.0 | float | Scale factor when mapping an axis to an axis |
| expo | 0 | float ∈ (-1, 1) | Non liniarity factor see also [GeoGebra](https://www.geogebra.org/calculator/mkdqueky) |
| **EV_REL output** | | | |
| rel_rate | 60 | positive int | The frequency `[Hz]` at which `EV_REL` events get generated (also effects mouse macro) |
| **EV_REL as input** | | | |
| rel_to_abs_input_cutoff | 2 | positive float | The value relative to a predefined base-speed, at which `EV_REL` input (cursor and wheel) is considered at its maximum. |
| release_timeout | 0.05 | positive float | The time `[s]` until a relative axis is considered stationary if no new events arrive |
## CLI

@ -1,5 +1,5 @@
"""Tests that require a linux desktop environment to be running."""
import tests.test
import gi
gi.require_version("Gdk", "3.0")

@ -28,8 +28,6 @@ from evdev.ecodes import KEY_A, KEY_B, KEY_C
import gi
from inputremapper.configs.system_mapping import XKB_KEYCODE_OFFSET
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
gi.require_version("GLib", "2.0")
@ -40,7 +38,8 @@ from tests.lib.cleanup import quick_cleanup
from tests.lib.stuff import spy
from tests.lib.logger import logger
from inputremapper.input_event import InputEvent
from inputremapper.gui.controller import Controller
from inputremapper.configs.system_mapping import XKB_KEYCODE_OFFSET
from inputremapper.gui.utils import CTX_ERROR, CTX_WARNING, gtk_iteration
from inputremapper.gui.messages.message_broker import (
MessageBroker,
@ -65,7 +64,7 @@ from inputremapper.gui.components.editor import (
AutoloadSwitch,
ReleaseCombinationSwitch,
CombinationListbox,
EventEntry,
InputConfigEntry,
AnalogInputSwitch,
TriggerThresholdInput,
ReleaseTimeoutInput,
@ -86,7 +85,7 @@ from inputremapper.gui.components.device_groups import (
DeviceGroupSelection,
)
from inputremapper.configs.mapping import MappingData
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
class ComponentBaseTest(unittest.TestCase):
@ -94,7 +93,7 @@ class ComponentBaseTest(unittest.TestCase):
def setUp(self) -> None:
self.message_broker = MessageBroker()
self.controller_mock = MagicMock()
self.controller_mock: Controller = MagicMock()
def destroy_all_member_widgets(self):
# destroy all Gtk Widgets that are stored in self
@ -304,7 +303,8 @@ class TestPresetSelection(ComponentBaseTest):
"preset2",
(
MappingData(
name="m1", event_combination=EventCombination((1, 2, 3))
name="m1",
input_combination=InputCombination(InputConfig(type=1, code=2)),
),
),
)
@ -316,7 +316,8 @@ class TestPresetSelection(ComponentBaseTest):
"preset1",
(
MappingData(
name="m1", event_combination=EventCombination((1, 2, 3))
name="m1",
input_combination=InputCombination(InputConfig(type=1, code=2)),
),
),
)
@ -329,7 +330,8 @@ class TestPresetSelection(ComponentBaseTest):
"preset2",
(
MappingData(
name="m1", event_combination=EventCombination((1, 2, 3))
name="m1",
input_combination=InputCombination(InputConfig(type=1, code=2)),
),
),
)
@ -355,17 +357,24 @@ class TestMappingListbox(ComponentBaseTest):
(
MappingData(
name="mapping1",
event_combination=EventCombination((1, KEY_C, 1)),
input_combination=InputCombination(
InputConfig(type=1, code=KEY_C)
),
),
MappingData(
name="",
event_combination=EventCombination(
[(1, KEY_A, 1), (1, KEY_B, 1)]
input_combination=InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
),
MappingData(
name="mapping2",
event_combination=EventCombination((1, KEY_B, 1)),
input_combination=InputCombination(
InputConfig(type=1, code=KEY_B)
),
),
),
)
@ -378,7 +387,7 @@ class TestMappingListbox(ComponentBaseTest):
raise Exception("Expected one MappingSelectionLabel to be selected")
def select_row(self, combination: EventCombination):
def select_row(self, combination: InputCombination):
def select(label_: MappingSelectionLabel):
if label_.combination == combination:
self.gui.select_row(label_)
@ -397,23 +406,28 @@ class TestMappingListbox(ComponentBaseTest):
def test_activates_correct_row(self):
self.message_broker.publish(
MappingData(
name="mapping1", event_combination=EventCombination((1, KEY_C, 1))
name="mapping1",
input_combination=InputCombination(InputConfig(type=1, code=KEY_C)),
)
)
selected = self.get_selected_row()
self.assertEqual(selected.name, "mapping1")
self.assertEqual(selected.combination, EventCombination((1, KEY_C, 1)))
self.assertEqual(
selected.combination,
InputCombination(InputConfig(type=1, code=KEY_C)),
)
def test_loads_mapping(self):
self.select_row(EventCombination((1, KEY_B, 1)))
self.select_row(InputCombination(InputConfig(type=1, code=KEY_B)))
self.controller_mock.load_mapping.assert_called_once_with(
EventCombination((1, KEY_B, 1))
InputCombination(InputConfig(type=1, code=KEY_B))
)
def test_avoids_infinite_recursion(self):
self.message_broker.publish(
MappingData(
name="mapping1", event_combination=EventCombination((1, KEY_C, 1))
name="mapping1",
input_combination=InputCombination(InputConfig(type=1, code=KEY_C)),
)
)
self.controller_mock.load_mapping.assert_not_called()
@ -425,42 +439,50 @@ class TestMappingListbox(ComponentBaseTest):
(
MappingData(
name="qux",
event_combination=EventCombination((1, KEY_C, 1)),
input_combination=InputCombination(
InputConfig(type=1, code=KEY_C)
),
),
MappingData(
name="foo",
event_combination=EventCombination.empty_combination(),
input_combination=InputCombination.empty_combination(),
),
MappingData(
name="bar",
event_combination=EventCombination((1, KEY_B, 1)),
input_combination=InputCombination(
InputConfig(type=1, code=KEY_B)
),
),
),
)
)
bottom_row: MappingSelectionLabel = self.gui.get_row_at_index(2)
self.assertEqual(bottom_row.combination, EventCombination.empty_combination())
self.assertEqual(bottom_row.combination, InputCombination.empty_combination())
self.message_broker.publish(
PresetData(
"preset1",
(
MappingData(
name="foo",
event_combination=EventCombination.empty_combination(),
input_combination=InputCombination.empty_combination(),
),
MappingData(
name="qux",
event_combination=EventCombination((1, KEY_C, 1)),
input_combination=InputCombination(
InputConfig(type=1, code=KEY_C)
),
),
MappingData(
name="bar",
event_combination=EventCombination((1, KEY_B, 1)),
input_combination=InputCombination(
InputConfig(type=1, code=KEY_B)
),
),
),
)
)
bottom_row: MappingSelectionLabel = self.gui.get_row_at_index(2)
self.assertEqual(bottom_row.combination, EventCombination.empty_combination())
self.assertEqual(bottom_row.combination, InputCombination.empty_combination())
class TestMappingSelectionLabel(ComponentBaseTest):
@ -471,7 +493,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
self.message_broker,
self.controller_mock,
"",
EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
InputCombination(
[
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
]
),
)
self.gui.insert(self.mapping_selection_label, -1)
@ -498,7 +525,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
self.message_broker,
self.controller_mock,
"foo",
EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
)
self.assertEqual(self.gui.label.get_label(), "foo")
@ -506,38 +538,69 @@ class TestMappingSelectionLabel(ComponentBaseTest):
self.gui.select_row(self.mapping_selection_label)
self.assertEqual(
self.mapping_selection_label.combination,
EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
)
self.message_broker.publish(
CombinationUpdate(
EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
EventCombination((1, KEY_A, 1)),
InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
InputCombination(InputConfig(type=1, code=KEY_A)),
)
)
self.assertEqual(
self.mapping_selection_label.combination, EventCombination((1, KEY_A, 1))
self.mapping_selection_label.combination,
InputCombination(InputConfig(type=1, code=KEY_A)),
)
def test_doesnt_update_combination_when_not_selected(self):
self.assertEqual(
self.mapping_selection_label.combination,
EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
)
self.message_broker.publish(
CombinationUpdate(
EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
EventCombination((1, KEY_A, 1)),
InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
InputCombination(InputConfig(type=1, code=KEY_A)),
)
)
self.assertEqual(
self.mapping_selection_label.combination,
EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
)
def test_updates_name_when_mapping_changed_and_combination_matches(self):
self.message_broker.publish(
MappingData(
event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
input_combination=InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
name="foo",
)
)
@ -546,7 +609,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
def test_ignores_mapping_when_combination_does_not_match(self):
self.message_broker.publish(
MappingData(
event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_C, 1)]),
input_combination=InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_C),
)
),
name="foo",
)
)
@ -559,7 +627,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
# load the mapping associated with the ListBoxRow
self.message_broker.publish(
MappingData(
event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
input_combination=InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
)
)
self.assertTrue(self.mapping_selection_label.edit_btn.get_visible())
@ -567,7 +640,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
# load a different row
self.message_broker.publish(
MappingData(
event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_C, 1)]),
input_combination=InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_C),
)
),
)
)
self.assertFalse(self.mapping_selection_label.edit_btn.get_visible())
@ -575,7 +653,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
def test_enter_edit_mode_focuses_name_input(self):
self.message_broker.publish(
MappingData(
event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
input_combination=InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
)
)
self.mapping_selection_label.edit_btn.clicked()
@ -586,7 +669,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
def test_enter_edit_mode_updates_visibility(self):
self.message_broker.publish(
MappingData(
event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
input_combination=InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
)
)
self.assert_selected()
@ -598,7 +686,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
def test_leaves_edit_mode_on_esc(self):
self.message_broker.publish(
MappingData(
event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
input_combination=InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
)
)
self.mapping_selection_label.edit_btn.clicked()
@ -616,7 +709,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
def test_update_name(self):
self.message_broker.publish(
MappingData(
event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
input_combination=InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
)
)
self.mapping_selection_label.edit_btn.clicked()
@ -628,7 +726,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
def test_name_input_contains_combination_when_name_not_set(self):
self.message_broker.publish(
MappingData(
event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
input_combination=InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
)
)
self.mapping_selection_label.edit_btn.clicked()
@ -637,7 +740,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
def test_name_input_contains_name(self):
self.message_broker.publish(
MappingData(
event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
input_combination=InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
name="foo",
)
)
@ -647,7 +755,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
def test_removes_name_when_name_matches_combination(self):
self.message_broker.publish(
MappingData(
event_combination=EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]),
input_combination=InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
name="foo",
)
)
@ -958,18 +1071,20 @@ class TestReleaseCombinationSwitch(ComponentBaseTest):
class TestEventEntry(ComponentBaseTest):
def setUp(self) -> None:
super().setUp()
self.gui = EventEntry(InputEvent.from_string("3,0,1"), self.controller_mock)
self.gui = InputConfigEntry(
InputConfig(type=3, code=0, analog_threshold=1), self.controller_mock
)
def test_move_event(self):
self.gui._up_btn.clicked()
self.controller_mock.move_event_in_combination.assert_called_once_with(
InputEvent.from_string("3,0,1"), "up"
self.controller_mock.move_input_config_in_combination.assert_called_once_with(
InputConfig(type=3, code=0, analog_threshold=1), "up"
)
self.controller_mock.reset_mock()
self.gui._down_btn.clicked()
self.controller_mock.move_event_in_combination.assert_called_once_with(
InputEvent.from_string("3,0,1"), "down"
self.controller_mock.move_input_config_in_combination.assert_called_once_with(
InputConfig(type=3, code=0, analog_threshold=1), "down"
)
@ -981,41 +1096,57 @@ class TestCombinationListbox(ComponentBaseTest):
self.message_broker, self.controller_mock, self.gui
)
self.controller_mock.is_empty_mapping.return_value = False
combination = InputCombination(
(
InputConfig(type=1, code=1),
InputConfig(type=3, code=0, analog_threshold=1),
InputConfig(type=1, code=2),
)
)
self.message_broker.publish(
MappingData(event_combination="1,1,1+3,0,1+1,2,1", target_uinput="keyboard")
MappingData(
input_combination=combination.to_config(), target_uinput="keyboard"
)
)
def get_selected_row(self) -> EventEntry:
def get_selected_row(self) -> InputConfigEntry:
for entry in self.gui.get_children():
if entry.is_selected():
return entry
raise Exception("Expected one EventEntry to be selected")
raise Exception("Expected one InputConfigEntry to be selected")
def select_row(self, event: InputEvent):
def select_row(self, input_cfg: InputConfig):
for entry in self.gui.get_children():
if entry.input_event == event:
if entry.input_event == input_cfg:
self.gui.select_row(entry)
def test_loads_selected_row(self):
self.select_row(InputEvent.from_string("1,2,1"))
self.controller_mock.load_event.assert_called_once_with(
InputEvent.from_string("1,2,1")
self.select_row(InputConfig(type=1, code=2))
self.controller_mock.load_input_config.assert_called_once_with(
InputConfig(type=1, code=2)
)
def test_does_not_create_rows_when_mapping_is_empty(self):
self.controller_mock.is_empty_mapping.return_value = True
self.message_broker.publish(MappingData(event_combination="1,1,1+3,0,1"))
combination = InputCombination(
(
InputConfig(type=1, code=1),
InputConfig(type=3, code=0, analog_threshold=1),
)
)
self.message_broker.publish(MappingData(input_combination=combination))
self.assertEqual(len(self.gui.get_children()), 0)
def test_selects_row_when_selected_event_message_arrives(self):
self.message_broker.publish(InputEvent.from_string("3,0,1"))
self.message_broker.publish(InputConfig(type=3, code=0, analog_threshold=1))
self.assertEqual(
self.get_selected_row().input_event, InputEvent.from_string("3,0,1")
self.get_selected_row().input_event,
InputConfig(type=3, code=0, analog_threshold=1),
)
def test_avoids_infinite_recursion(self):
self.message_broker.publish(InputEvent.from_string("3,0,1"))
self.message_broker.publish(InputConfig(type=3, code=0, analog_threshold=1))
self.controller_mock.load_event.assert_not_called()
@ -1035,29 +1166,29 @@ class TestAnalogInputSwitch(ComponentBaseTest):
self.controller_mock.set_event_as_analog.assert_called_once_with(False)
def test_updates_state(self):
self.message_broker.publish(InputEvent.from_string("3,0,0"))
self.message_broker.publish(InputConfig(type=3, code=0))
self.assertTrue(self.gui.get_active())
self.message_broker.publish(InputEvent.from_string("3,0,10"))
self.message_broker.publish(InputConfig(type=3, code=0, analog_threshold=10))
self.assertFalse(self.gui.get_active())
def test_avoids_infinite_recursion(self):
self.message_broker.publish(InputEvent.from_string("3,0,0"))
self.message_broker.publish(InputEvent.from_string("3,0,-10"))
self.message_broker.publish(InputConfig(type=3, code=0))
self.message_broker.publish(InputConfig(type=3, code=0, analog_threshold=-10))
self.controller_mock.set_event_as_analog.assert_not_called()
def test_disables_switch_when_key_event(self):
self.message_broker.publish(InputEvent.from_string("1,1,1"))
self.message_broker.publish(InputConfig(type=1, code=1))
self.assertLess(self.gui.get_opacity(), 0.6)
self.assertFalse(self.gui.get_sensitive())
def test_enables_switch_when_axis_event(self):
self.message_broker.publish(InputEvent.from_string("1,1,1"))
self.message_broker.publish(InputEvent.from_string("3,0,10"))
self.message_broker.publish(InputConfig(type=1, code=1))
self.message_broker.publish(InputConfig(type=3, code=0, analog_threshold=10))
self.assertEqual(self.gui.get_opacity(), 1)
self.assertTrue(self.gui.get_sensitive())
self.message_broker.publish(InputEvent.from_string("1,1,1"))
self.message_broker.publish(InputEvent.from_string("2,0,10"))
self.message_broker.publish(InputConfig(type=1, code=1))
self.message_broker.publish(InputConfig(type=2, code=0, analog_threshold=10))
self.assertEqual(self.gui.get_opacity(), 1)
self.assertTrue(self.gui.get_sensitive())
@ -1069,7 +1200,7 @@ class TestTriggerThresholdInput(ComponentBaseTest):
self.input = TriggerThresholdInput(
self.message_broker, self.controller_mock, self.gui
)
self.message_broker.publish(InputEvent.from_string("3,0,-10"))
self.message_broker.publish(InputConfig(type=3, code=0, analog_threshold=-10))
def assert_abs_event_config(self):
self.assertEqual(self.gui.get_range(), (-99, 99))
@ -1087,23 +1218,23 @@ class TestTriggerThresholdInput(ComponentBaseTest):
def test_updates_event(self):
self.gui.set_value(15)
self.controller_mock.update_event.assert_called_once_with(
InputEvent.from_string("3,0,15")
self.controller_mock.update_input_config.assert_called_once_with(
InputConfig(type=3, code=0, analog_threshold=15)
)
def test_sets_value_on_selected_event_message(self):
self.message_broker.publish(InputEvent.from_string("3,0,10"))
self.message_broker.publish(InputConfig(type=3, code=0, analog_threshold=10))
self.assertEqual(self.gui.get_value(), 10)
def test_avoids_infinite_recursion(self):
self.message_broker.publish(InputEvent.from_string("3,0,10"))
self.controller_mock.update_event.assert_not_called()
self.message_broker.publish(InputConfig(type=3, code=0, analog_threshold=10))
self.controller_mock.update_input_config.assert_not_called()
def test_updates_configuration_according_to_selected_event(self):
self.assert_abs_event_config()
self.message_broker.publish(InputEvent.from_string("2,0,-10"))
self.message_broker.publish(InputConfig(type=2, code=0, analog_threshold=-10))
self.assert_rel_event_config()
self.message_broker.publish(InputEvent.from_string("1,1,1"))
self.message_broker.publish(InputConfig(type=1, code=1))
self.assert_key_event_config()
@ -1116,13 +1247,21 @@ class TestReleaseTimeoutInput(ComponentBaseTest):
)
self.message_broker.publish(
MappingData(
event_combination=EventCombination("2,0,1"), target_uinput="keyboard"
input_combination=InputCombination(
InputConfig(type=2, code=0, analog_threshold=1)
),
target_uinput="keyboard",
)
)
def test_updates_timeout_on_mapping_message(self):
self.message_broker.publish(
MappingData(event_combination=EventCombination("2,0,1"), release_timeout=1)
MappingData(
input_combination=InputCombination(
InputConfig(type=2, code=0, analog_threshold=1)
),
release_timeout=1,
)
)
self.assertEqual(self.gui.get_value(), 1)
@ -1132,28 +1271,64 @@ class TestReleaseTimeoutInput(ComponentBaseTest):
def test_avoids_infinite_recursion(self):
self.message_broker.publish(
MappingData(event_combination=EventCombination("2,0,1"), release_timeout=1)
MappingData(
input_combination=InputCombination(
InputConfig(type=2, code=0, analog_threshold=1)
),
release_timeout=1,
)
)
self.controller_mock.update_mapping.assert_not_called()
def test_disables_input_based_on_input_combination(self):
self.message_broker.publish(
MappingData(event_combination=EventCombination.from_string("2,0,1+1,1,1"))
MappingData(
input_combination=InputCombination(
(
InputConfig(type=2, code=0, analog_threshold=1),
InputConfig(type=1, code=1),
)
)
)
)
self.assertTrue(self.gui.get_sensitive())
self.assertEqual(self.gui.get_opacity(), 1)
self.message_broker.publish(
MappingData(event_combination=EventCombination.from_string("1,1,1+1,2,1"))
MappingData(
input_combination=InputCombination(
(
InputConfig(type=1, code=1),
InputConfig(type=1, code=2),
)
)
)
)
self.assertFalse(self.gui.get_sensitive())
self.assertLess(self.gui.get_opacity(), 0.6)
self.message_broker.publish(
MappingData(event_combination=EventCombination.from_string("2,0,1+1,1,1"))
MappingData(
input_combination=InputCombination(
(
InputConfig(type=2, code=0, analog_threshold=1),
InputConfig(type=1, code=1),
)
)
)
)
self.message_broker.publish(
MappingData(event_combination=EventCombination.from_string("3,0,1+1,2,1"))
MappingData(
input_combination=InputCombination(
(
InputConfig(type=3, code=0, analog_threshold=1),
InputConfig(
type=1,
code=2,
),
)
)
)
)
self.assertFalse(self.gui.get_sensitive())
self.assertLess(self.gui.get_opacity(), 0.6)
@ -1180,7 +1355,10 @@ class TestOutputAxisSelector(ComponentBaseTest):
)
)
self.message_broker.publish(
MappingData(target_uinput="mouse", event_combination="1,1,1")
MappingData(
target_uinput="mouse",
input_combination=InputCombination(InputConfig(type=1, code=1)),
)
)
def set_active_selection(self, selection: Tuple):
@ -1344,7 +1522,10 @@ class TestSliders(ComponentBaseTest):
self.expo,
)
self.message_broker.publish(
MappingData(event_combination="3,0,0", target_uinput="mouse")
MappingData(
input_combination=InputCombination(InputConfig(type=3, code=0)),
target_uinput="mouse",
)
)
@staticmethod
@ -1406,7 +1587,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
target_uinput="mouse",
event_combination="2,0,0",
input_combination=InputCombination(InputConfig(type=2, code=0)),
rel_to_abs_input_cutoff=1,
output_type=3,
output_code=0,
@ -1425,7 +1606,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
target_uinput="mouse",
event_combination="2,0,0",
input_combination=InputCombination(InputConfig(type=2, code=0)),
rel_to_abs_input_cutoff=3,
output_type=3,
output_code=0,
@ -1438,7 +1619,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
target_uinput="mouse",
event_combination="2,0,0",
input_combination=InputCombination(InputConfig(type=2, code=0)),
rel_to_abs_input_cutoff=rel_to_abs_input_cutoff,
output_type=3,
output_code=0,
@ -1455,7 +1636,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
target_uinput="mouse",
event_combination="3,0,0",
input_combination=InputCombination(InputConfig(type=3, code=0)),
output_type=3,
output_code=0,
)
@ -1467,7 +1648,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
target_uinput="mouse",
event_combination="2,0,0",
input_combination=InputCombination(InputConfig(type=2, code=0)),
rel_to_abs_input_cutoff=3,
output_type=2,
output_code=0,
@ -1479,7 +1660,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
target_uinput="mouse",
event_combination="3,0,0",
input_combination=InputCombination(InputConfig(type=3, code=0)),
output_type=3,
output_code=0,
)
@ -1488,7 +1669,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
target_uinput="mouse",
event_combination="2,0,0",
input_combination=InputCombination(InputConfig(type=2, code=0)),
rel_to_abs_input_cutoff=1,
output_type=3,
output_code=0,
@ -1505,7 +1686,7 @@ class TestRequireActiveMapping(ComponentBaseTest):
self.box,
require_recorded_input=False,
)
combination = EventCombination([(1, KEY_A, 1)])
combination = InputCombination(InputConfig(type=1, code=KEY_A))
self.message_broker.publish(MappingData())
self.assert_inactive(self.box)
@ -1518,7 +1699,7 @@ class TestRequireActiveMapping(ComponentBaseTest):
self.message_broker.publish(PresetData(name="preset", mappings=(combination,)))
self.assert_active(self.box)
self.message_broker.publish(MappingData(event_combination=combination))
self.message_broker.publish(MappingData(input_combination=combination))
self.assert_active(self.box)
self.message_broker.publish(MappingData())
@ -1531,7 +1712,7 @@ class TestRequireActiveMapping(ComponentBaseTest):
self.box,
require_recorded_input=True,
)
combination = EventCombination([(1, KEY_A, 1)])
combination = InputCombination(InputConfig(type=1, code=KEY_A))
self.message_broker.publish(MappingData())
self.assert_inactive(self.box)
@ -1543,7 +1724,7 @@ class TestRequireActiveMapping(ComponentBaseTest):
self.assert_inactive(self.box)
# the widget will be enabled once a mapping with recorded input is selected
self.message_broker.publish(MappingData(event_combination=combination))
self.message_broker.publish(MappingData(input_combination=combination))
self.assert_active(self.box)
# this mapping doesn't have input recorded, so the box is disabled
@ -1657,14 +1838,19 @@ class TestBreadcrumbs(ComponentBaseTest):
self.assertEqual(self.label_4.get_text(), "group / preset / mapping")
self.assertEqual(self.label_5.get_text(), "mapping")
combination = EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)])
self.message_broker.publish(MappingData(event_combination=combination))
combination = InputCombination(
(
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
)
self.message_broker.publish(MappingData(input_combination=combination))
self.assertEqual(self.label_4.get_text(), "group / preset / a + b")
self.assertEqual(self.label_5.get_text(), "a + b")
combination = EventCombination([(1, KEY_A, 1)])
combination = InputCombination(InputConfig(type=1, code=KEY_A))
self.message_broker.publish(
MappingData(name="qux", event_combination=combination)
MappingData(name="qux", input_combination=combination)
)
self.assertEqual(self.label_4.get_text(), "group / preset / qux")
self.assertEqual(self.label_5.get_text(), "qux")

@ -21,14 +21,14 @@
# the tests file needs to be imported first to make sure patches are loaded
from contextlib import contextmanager
from typing import Tuple, List, Optional
from typing import Tuple, List, Optional, Iterable
from tests.test import get_project_root
from tests.lib.fixtures import new_event
from tests.lib.cleanup import cleanup
from tests.lib.stuff import spy
from tests.lib.constants import EVENT_READ_TIMEOUT
from tests.lib.fixtures import prepare_presets
from tests.lib.fixtures import prepare_presets, get_combination_config
from tests.lib.logger import logger
from tests.lib.fixtures import fixtures
from tests.lib.pipes import push_event, push_events, uinput_write_history_pipe
@ -81,7 +81,7 @@ from inputremapper.gui.reader_service import ReaderService
from inputremapper.gui.utils import gtk_iteration, Colors, debounce, debounce_manager
from inputremapper.gui.user_interface import UserInterface
from inputremapper.injection.injector import InjectorState
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.daemon import Daemon, DaemonProxy
@ -337,10 +337,12 @@ class GuiTestBase(unittest.TestCase):
self.assertEqual(self.data_manager.active_mapping.target_uinput, "keyboard")
self.assertEqual(self.target_selection.get_active_id(), "keyboard")
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination((1, 5, 1)),
self.data_manager.active_mapping.input_combination,
InputCombination(InputConfig(type=1, code=5)),
)
self.assertEqual(
self.data_manager.active_input_config, InputConfig(type=1, code=5)
)
self.assertEqual(self.data_manager.active_event, InputEvent(0, 0, 1, 5, 1))
self.assertGreater(
len(self.user_interface.autocompletion._target_key_capabilities), 0
)
@ -416,7 +418,7 @@ class GuiTestBase(unittest.TestCase):
def add_mapping(self, mapping: Optional[Mapping] = None):
self.controller.create_mapping()
self.controller.load_mapping(EventCombination.empty_combination())
self.controller.load_mapping(InputCombination.empty_combination())
gtk_iteration()
if mapping:
self.controller.update_mapping(**mapping.dict(exclude_defaults=True))
@ -523,10 +525,14 @@ class TestGui(GuiTestBase):
self.assertFalse(self.data_manager.get_autoload())
self.assertFalse(self.autoload_toggle.get_active())
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination, ((1, 5, 1),)
self.selection_label_listbox.get_selected_row().combination,
InputCombination(InputConfig(type=1, code=5)),
)
self.assertEqual(
self.data_manager.active_mapping.event_combination, ((1, 5, 1),)
self.data_manager.active_mapping.input_combination,
InputCombination(
InputConfig(type=1, code=5),
),
)
self.assertEqual(self.selection_label_listbox.get_selected_row().name, "4")
self.assertIsNone(self.data_manager.active_mapping.name)
@ -659,14 +665,28 @@ class TestGui(GuiTestBase):
push_events(
fixtures.foo_device_2_keyboard,
[InputEvent.from_string("1,30,1"), InputEvent.from_string("1,31,1")],
[InputEvent(0, 0, 1, 30, 1), InputEvent(0, 0, 1, 31, 1)],
)
self.throttle(40)
origin = fixtures.foo_device_2_keyboard.get_device_hash()
mock1.assert_has_calls(
(
call(CombinationRecorded(EventCombination.from_string("1,30,1"))),
call(
CombinationRecorded(EventCombination.from_string("1,30,1+1,31,1"))
CombinationRecorded(
InputCombination(
InputConfig(type=1, code=30, origin_hash=origin)
)
)
),
call(
CombinationRecorded(
InputCombination(
(
InputConfig(type=1, code=30, origin_hash=origin),
InputConfig(type=1, code=31, origin_hash=origin),
)
)
)
),
),
any_order=False,
@ -674,12 +694,12 @@ class TestGui(GuiTestBase):
self.assertEqual(mock1.call_count, 2)
mock2.assert_not_called()
push_events(fixtures.foo_device_2_keyboard, [InputEvent.from_string("1,31,0")])
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 31, 0)])
self.throttle(40)
self.assertEqual(mock1.call_count, 2)
mock2.assert_not_called()
push_events(fixtures.foo_device_2_keyboard, [InputEvent.from_string("1,30,0")])
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 0)])
self.throttle(40)
self.assertEqual(mock1.call_count, 2)
mock2.assert_called_once()
@ -687,7 +707,7 @@ class TestGui(GuiTestBase):
self.assertFalse(self.recording_toggle.get_active())
mock3.assert_called_once()
def test_cannot_create_duplicate_event_combination(self):
def test_cannot_create_duplicate_input_combination(self):
# load a device with more capabilities
self.controller.load_group("Foo Device 2")
gtk_iteration()
@ -696,82 +716,100 @@ class TestGui(GuiTestBase):
self.controller.start_key_recording()
push_events(
fixtures.foo_device_2_keyboard,
[InputEvent.from_string("1,30,1"), InputEvent.from_string("1,30,0")],
[InputEvent(0, 0, 1, 30, 1), InputEvent(0, 0, 1, 30, 0)],
)
self.throttle(40)
# if this fails with <EventCombination (1, 5, 1)>: this is the initial
# if this fails with <InputCombination (1, 5, 1)>: this is the initial
# mapping or something, so it was never overwritten.
origin = fixtures.foo_device_2_keyboard.get_device_hash()
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.from_string("1,30,1"),
self.data_manager.active_mapping.input_combination,
InputCombination(InputConfig(type=1, code=30, origin_hash=origin)),
)
# create a new mapping
self.controller.create_mapping()
gtk_iteration()
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.empty_combination(),
self.data_manager.active_mapping.input_combination,
InputCombination.empty_combination(),
)
# try to record the same combination
self.controller.start_key_recording()
push_events(
fixtures.foo_device_2_keyboard,
[InputEvent.from_string("1,30,1"), InputEvent.from_string("1,30,0")],
[InputEvent(0, 0, 1, 30, 1), InputEvent(0, 0, 1, 30, 0)],
)
self.throttle(40)
# should still be the empty mapping
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.empty_combination(),
self.data_manager.active_mapping.input_combination,
InputCombination.empty_combination(),
)
# try to record a different combination
self.controller.start_key_recording()
push_events(fixtures.foo_device_2_keyboard, [InputEvent.from_string("1,30,1")])
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 1)])
self.throttle(40)
# nothing changed yet, as we got the duplicate combination
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.empty_combination(),
self.data_manager.active_mapping.input_combination,
InputCombination.empty_combination(),
)
push_events(fixtures.foo_device_2_keyboard, [InputEvent.from_string("1,31,1")])
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 31, 1)])
self.throttle(40)
# now the combination is different
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.from_string("1,30,1+1,31,1"),
self.data_manager.active_mapping.input_combination,
InputCombination(
(
InputConfig(type=1, code=30, origin_hash=origin),
InputConfig(type=1, code=31, origin_hash=origin),
)
),
)
# let's make the combination even longer
push_events(fixtures.foo_device_2_keyboard, [InputEvent.from_string("1,32,1")])
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 32, 1)])
self.throttle(40)
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.from_string("1,30,1+1,31,1+1,32,1"),
self.data_manager.active_mapping.input_combination,
InputCombination(
(
InputConfig(type=1, code=30, origin_hash=origin),
InputConfig(type=1, code=31, origin_hash=origin),
InputConfig(type=1, code=32, origin_hash=origin),
)
),
)
# make sure we stop recording by releasing all keys
push_events(
fixtures.foo_device_2_keyboard,
[
InputEvent.from_string("1,31,0"),
InputEvent.from_string("1,30,0"),
InputEvent.from_string("1,32,0"),
InputEvent(0, 0, 1, 31, 0),
InputEvent(0, 0, 1, 30, 0),
InputEvent(0, 0, 1, 32, 0),
],
)
self.throttle(40)
# sending a combination update now should not do anything
self.message_broker.publish(
CombinationRecorded(EventCombination.from_string("1,35,1"))
CombinationRecorded(InputCombination(InputConfig(type=1, code=35)))
)
gtk_iteration()
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.from_string("1,30,1+1,31,1+1,32,1"),
self.data_manager.active_mapping.input_combination,
InputCombination(
(
InputConfig(type=1, code=30, origin_hash=origin),
InputConfig(type=1, code=31, origin_hash=origin),
InputConfig(type=1, code=32, origin_hash=origin),
)
),
)
def test_create_simple_mapping(self):
@ -782,11 +820,11 @@ class TestGui(GuiTestBase):
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination,
EventCombination.empty_combination(),
InputCombination.empty_combination(),
)
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.empty_combination(),
self.data_manager.active_mapping.input_combination,
InputCombination.empty_combination(),
)
self.assertEqual(
self.selection_label_listbox.get_selected_row().name, "Empty Mapping"
@ -800,19 +838,20 @@ class TestGui(GuiTestBase):
# 2. record a combination for that mapping
self.recording_toggle.set_active(True)
gtk_iteration()
push_events(fixtures.foo_device_2_keyboard, [InputEvent.from_string("1,30,1")])
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 1)])
self.throttle(40)
push_events(fixtures.foo_device_2_keyboard, [InputEvent.from_string("1,30,0")])
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 0)])
self.throttle(40)
# check the event_combination
# check the input_combination
origin = fixtures.foo_device_2_keyboard.get_device_hash()
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination,
EventCombination.from_string("1,30,1"),
InputCombination(InputConfig(type=1, code=30, origin_hash=origin)),
)
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.from_string("1,30,1"),
self.data_manager.active_mapping.input_combination,
InputCombination(InputConfig(type=1, code=30, origin_hash=origin)),
)
self.assertEqual(self.selection_label_listbox.get_selected_row().name, "a")
self.assertIsNone(self.data_manager.active_mapping.name)
@ -828,7 +867,9 @@ class TestGui(GuiTestBase):
self.assertEqual(
self.data_manager.active_mapping,
Mapping(
event_combination="1,30,1",
input_combination=InputCombination(
InputConfig(type=1, code=30, origin_hash=origin)
),
output_symbol="Shift_L",
target_uinput="keyboard",
),
@ -841,7 +882,7 @@ class TestGui(GuiTestBase):
)
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination,
EventCombination.from_string("1,30,1"),
InputCombination(InputConfig(type=1, code=30, origin_hash=origin)),
)
# 4. update target to mouse
@ -850,7 +891,9 @@ class TestGui(GuiTestBase):
self.assertEqual(
self.data_manager.active_mapping,
Mapping(
event_combination="1,30,1",
input_combination=InputCombination(
InputConfig(type=1, code=30, origin_hash=origin)
),
output_symbol="Shift_L",
target_uinput="mouse",
),
@ -873,12 +916,14 @@ class TestGui(GuiTestBase):
gtk_iteration()
# it should be possible to add all of them
ev_1 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, -1))
ev_2 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, 1))
ev_3 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0Y, -1))
ev_4 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0Y, 1))
def add_mapping(event, symbol):
ev_1 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1)
ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, 1)
ev_3 = (EV_ABS, evdev.ecodes.ABS_HAT0Y, -1)
ev_4 = (EV_ABS, evdev.ecodes.ABS_HAT0Y, 1)
def add_mapping(event_tuple, symbol) -> InputCombination:
"""adds mapping and returns the expected input combination"""
event = InputEvent.from_tuple(event_tuple)
self.controller.create_mapping()
gtk_iteration()
self.controller.start_key_recording()
@ -887,33 +932,38 @@ class TestGui(GuiTestBase):
gtk_iteration()
self.code_editor.get_buffer().set_text(symbol)
gtk_iteration()
return InputCombination(
InputConfig.from_input_event(event).modify(
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash()
)
)
add_mapping(ev_1, "a")
add_mapping(ev_2, "b")
add_mapping(ev_3, "c")
add_mapping(ev_4, "d")
config_1 = add_mapping(ev_1, "a")
config_2 = add_mapping(ev_2, "b")
config_3 = add_mapping(ev_3, "c")
config_4 = add_mapping(ev_4, "d")
self.assertEqual(
self.data_manager.active_preset.get_mapping(
EventCombination(ev_1)
InputCombination(config_1)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
EventCombination(ev_2)
InputCombination(config_2)
).output_symbol,
"b",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
EventCombination(ev_3)
InputCombination(config_3)
).output_symbol,
"c",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(
EventCombination(ev_4)
InputCombination(config_4)
).output_symbol,
"d",
)
@ -927,27 +977,47 @@ class TestGui(GuiTestBase):
gtk_iteration()
# it should be possible to write a combination
ev_1 = InputEvent.from_tuple((EV_KEY, evdev.ecodes.KEY_A, 1))
ev_2 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, 1))
ev_3 = InputEvent.from_tuple((EV_KEY, evdev.ecodes.KEY_C, 1))
ev_4 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, -1))
combination_1 = EventCombination((ev_1, ev_2, ev_3))
combination_2 = EventCombination((ev_2, ev_1, ev_3))
ev_1 = (EV_KEY, evdev.ecodes.KEY_A, 1)
ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, 1)
ev_3 = (EV_KEY, evdev.ecodes.KEY_C, 1)
ev_4 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1)
combination_1 = (ev_1, ev_2, ev_3)
combination_2 = (ev_2, ev_1, ev_3)
# same as 1, but different D-Pad direction
combination_3 = EventCombination((ev_1, ev_4, ev_3))
combination_4 = EventCombination((ev_4, ev_1, ev_3))
combination_3 = (ev_1, ev_4, ev_3)
combination_4 = (ev_4, ev_1, ev_3)
# same as 1, but the last combination is different
combination_5 = EventCombination((ev_1, ev_3, ev_2))
combination_6 = EventCombination((ev_3, ev_1, ev_2))
def add_mapping(combi: EventCombination, symbol):
combination_5 = (ev_1, ev_3, ev_2)
combination_6 = (ev_3, ev_1, ev_2)
def get_combination(combi: Iterable[Tuple[int, int, int]]) -> InputCombination:
configs = []
for t in combi:
config = InputConfig.from_input_event(InputEvent.from_tuple(t))
if config.type == EV_KEY:
config = config.modify(
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash()
)
if config.type == EV_ABS:
config = config.modify(
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash()
)
if config.type == EV_REL:
config = config.modify(
origin_hash=fixtures.foo_device_2_mouse.get_device_hash()
)
configs.append(config)
return InputCombination(configs)
def add_mapping(combi: Iterable[Tuple[int, int, int]], symbol):
self.controller.create_mapping()
gtk_iteration()
self.controller.start_key_recording()
previous_event = InputEvent.from_string("1,1,1")
for event in combi:
previous_event = InputEvent(0, 0, 1, 1, 1)
for event_tuple in combi:
event = InputEvent.from_tuple(event_tuple)
if event.type != previous_event.type:
self.throttle(20) # avoid race condition if we switch fixture
if event.type == EV_KEY:
@ -957,7 +1027,8 @@ class TestGui(GuiTestBase):
if event.type == EV_REL:
push_event(fixtures.foo_device_2_mouse, event)
for event in combi:
for event_tuple in combi:
event = InputEvent.from_tuple(event_tuple)
if event.type == EV_KEY:
push_event(fixtures.foo_device_2_keyboard, event.modify(value=0))
if event.type == EV_ABS:
@ -972,100 +1043,160 @@ class TestGui(GuiTestBase):
add_mapping(combination_1, "a")
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_1).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_1)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_2).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_2)
).output_symbol,
"a",
)
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_3))
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_4))
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_5))
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_6))
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_3))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_4))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_5))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_6))
)
# it won't write the same combination again, even if the
# first two events are in a different order
add_mapping(combination_2, "b")
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_1).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_1)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_2).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_2)
).output_symbol,
"a",
)
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_3))
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_4))
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_5))
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_6))
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_3))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_4))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_5))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_6))
)
add_mapping(combination_3, "c")
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_1).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_1)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_2).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_2)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_3).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_3)
).output_symbol,
"c",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_4).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_4)
).output_symbol,
"c",
)
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_5))
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_6))
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_5))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_6))
)
# same as with combination_2, the existing combination_3 blocks
# combination_4 because they have the same keys and end in the
# same key.
add_mapping(combination_4, "d")
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_1).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_1)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_2).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_2)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_3).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_3)
).output_symbol,
"c",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_4).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_4)
).output_symbol,
"c",
)
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_5))
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_6))
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_5))
)
self.assertIsNone(
self.data_manager.active_preset.get_mapping(get_combination(combination_6))
)
add_mapping(combination_5, "e")
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_1).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_1)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_2).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_2)
).output_symbol,
"a",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_3).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_3)
).output_symbol,
"c",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_4).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_4)
).output_symbol,
"c",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_5).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_5)
).output_symbol,
"e",
)
self.assertEqual(
self.data_manager.active_preset.get_mapping(combination_6).output_symbol,
self.data_manager.active_preset.get_mapping(
get_combination(combination_6)
).output_symbol,
"e",
)
@ -1078,7 +1209,7 @@ class TestGui(GuiTestBase):
def test_only_one_empty_mapping_possible(self):
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination,
EventCombination.from_string("1,5,1"),
InputCombination(InputConfig(type=1, code=5)),
)
self.assertEqual(len(self.selection_label_listbox.get_children()), 1)
self.assertEqual(len(self.data_manager.active_preset), 1)
@ -1087,7 +1218,7 @@ class TestGui(GuiTestBase):
gtk_iteration()
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination,
EventCombination.empty_combination(),
InputCombination.empty_combination(),
)
self.assertEqual(len(self.selection_label_listbox.get_children()), 2)
self.assertEqual(len(self.data_manager.active_preset), 2)
@ -1111,7 +1242,7 @@ class TestGui(GuiTestBase):
self.recording_toggle.set_active(True)
gtk_iteration()
self.message_broker.publish(
CombinationRecorded(EventCombination((EV_KEY, KEY_Q, 1)))
CombinationRecorded(InputCombination(InputConfig(type=EV_KEY, code=KEY_Q)))
)
gtk_iteration()
self.message_broker.signal(MessageType.recording_finished)
@ -1131,7 +1262,7 @@ class TestGui(GuiTestBase):
self.recording_toggle.set_active(True)
gtk_iteration()
self.message_broker.publish(
CombinationRecorded(EventCombination((EV_KEY, KEY_Q, 1)))
CombinationRecorded(InputCombination(InputConfig(type=EV_KEY, code=KEY_Q)))
)
gtk_iteration()
self.message_broker.signal(MessageType.recording_finished)
@ -1140,7 +1271,7 @@ class TestGui(GuiTestBase):
self.controller.create_mapping()
gtk_iteration()
row: MappingSelectionLabel = self.selection_label_listbox.get_selected_row()
self.assertEqual(row.combination, EventCombination.empty_combination())
self.assertEqual(row.combination, InputCombination.empty_combination())
self.assertEqual(row.label.get_text(), "Empty Mapping")
self.assertIs(self.selection_label_listbox.get_row_at_index(2), row)
@ -1173,7 +1304,7 @@ class TestGui(GuiTestBase):
self.controller.create_mapping()
gtk_iteration()
row = self.selection_label_listbox.get_selected_row()
self.assertEqual(row.combination, EventCombination.empty_combination())
self.assertEqual(row.combination, InputCombination.empty_combination())
self.assertEqual(row.label.get_text(), "Empty Mapping")
self.assertIs(self.selection_label_listbox.get_row_at_index(2), row)
@ -1210,8 +1341,8 @@ class TestGui(GuiTestBase):
)
self.assertIsNone(self.data_manager.active_mapping.name)
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.empty_combination(),
self.data_manager.active_mapping.input_combination,
InputCombination.empty_combination(),
)
def test_remove_mapping(self):
@ -1232,11 +1363,12 @@ class TestGui(GuiTestBase):
self.controller.load_group("Foo Device 2")
gtk_iteration()
def add_mapping(combi: EventCombination, symbol):
def add_mapping(combi: Iterable[Tuple[int, int, int]], symbol):
combi = [InputEvent(0, 0, *t) for t in combi]
self.controller.create_mapping()
gtk_iteration()
self.controller.start_key_recording()
push_events(fixtures.foo_device_2_keyboard, [event for event in combi])
push_events(fixtures.foo_device_2_keyboard, combi)
push_events(
fixtures.foo_device_2_keyboard,
[event.modify(value=0) for event in combi],
@ -1246,7 +1378,8 @@ class TestGui(GuiTestBase):
self.code_editor.get_buffer().set_text(symbol)
gtk_iteration()
combination = EventCombination(((EV_KEY, KEY_LEFTSHIFT, 1), (EV_KEY, 82, 1)))
combination = [(EV_KEY, KEY_LEFTSHIFT, 1), (EV_KEY, 82, 1)]
add_mapping(combination, "b")
text = self.get_status_text()
self.assertIn("shift", text)
@ -1289,11 +1422,11 @@ class TestGui(GuiTestBase):
self.controller.load_preset("preset1")
self.throttle(20)
self.controller.load_mapping(EventCombination.from_string("1,1,1"))
self.controller.load_mapping(InputCombination(InputConfig(type=1, code=1)))
gtk_iteration()
self.controller.update_mapping(output_symbol="foo")
gtk_iteration()
self.controller.load_mapping(EventCombination.from_string("1,2,1"))
self.controller.load_mapping(InputCombination(InputConfig(type=1, code=2)))
gtk_iteration()
self.controller.update_mapping(output_symbol="qux")
gtk_iteration()
@ -1316,7 +1449,7 @@ class TestGui(GuiTestBase):
self.assertTrue(error_icon.get_visible())
self.assertFalse(warning_icon.get_visible())
self.controller.load_mapping(EventCombination.from_string("1,1,1"))
self.controller.load_mapping(InputCombination(InputConfig(type=1, code=1)))
gtk_iteration()
self.controller.update_mapping(output_symbol="b")
gtk_iteration()
@ -1392,7 +1525,7 @@ class TestGui(GuiTestBase):
)
self.assertIsNotNone(self.data_manager.active_mapping)
self.assertEqual(
self.data_manager.active_mapping.event_combination,
self.data_manager.active_mapping.input_combination,
self.selection_label_listbox.get_selected_row().combination,
)
@ -1410,8 +1543,8 @@ class TestGui(GuiTestBase):
self.assertEqual(
mappings,
{
EventCombination.from_string("1,1,1"),
EventCombination.from_string("1,2,1"),
InputCombination(InputConfig(type=1, code=1)),
InputCombination(InputConfig(type=1, code=2)),
},
)
self.assertFalse(self.autoload_toggle.get_active())
@ -1425,8 +1558,8 @@ class TestGui(GuiTestBase):
self.assertEqual(
mappings,
{
EventCombination.from_string("1,3,1"),
EventCombination.from_string("1,4,1"),
InputCombination(InputConfig(type=1, code=3)),
InputCombination(InputConfig(type=1, code=4)),
},
)
self.assertTrue(self.autoload_toggle.get_active())
@ -1525,7 +1658,7 @@ class TestGui(GuiTestBase):
self.controller.create_mapping()
gtk_iteration()
self.controller.update_mapping(
event_combination=EventCombination(InputEvent.btn_left()),
input_combination=InputCombination(InputConfig.btn_left()),
output_symbol="a",
)
gtk_iteration()
@ -1828,8 +1961,8 @@ class TestGui(GuiTestBase):
push_events(
fixtures.bar_device,
[
InputEvent.from_string("1,30,1"),
InputEvent.from_string("1,30,0"),
InputEvent(0, 0, 1, 30, 1),
InputEvent(0, 0, 1, 30, 0),
],
)
self.throttle(100) # give time for the input to arrive

@ -10,12 +10,12 @@ gi.require_version("GLib", "2.0")
gi.require_version("GtkSource", "4")
from gi.repository import Gtk, Gdk, GLib
from inputremapper.gui.utils import gtk_iteration
from tests.lib.cleanup import quick_cleanup
from inputremapper.gui.utils import gtk_iteration
from inputremapper.gui.messages.message_broker import MessageBroker, MessageType
from inputremapper.gui.user_interface import UserInterface
from inputremapper.configs.mapping import MappingData
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
class TestUserInterface(unittest.TestCase):
@ -92,7 +92,10 @@ class TestUserInterface(unittest.TestCase):
def test_combination_label_shows_combination(self):
self.message_broker.publish(
MappingData(
event_combination=EventCombination((EV_KEY, KEY_A, 1)), name="foo"
input_combination=InputCombination(
InputConfig(type=EV_KEY, code=KEY_A)
),
name="foo",
)
)
gtk_iteration()

@ -22,13 +22,13 @@ from __future__ import annotations
import dataclasses
import json
from typing import Dict, Optional
from hashlib import md5
from typing import Dict, Optional, Tuple, Iterable
import time
import evdev
# input-remapper is only interested in devices that have EV_KEY, add some
# random other stuff to test that they are ignored.
phys_foo = "usb-0000:03:00.0-1/input2"
@ -49,6 +49,10 @@ class Fixture:
def __hash__(self):
return hash(self.path)
def get_device_hash(self):
s = str(self.capabilities) + self.name
return md5(s.encode()).hexdigest()
class _Fixtures:
"""contains all predefined Fixtures.
@ -304,25 +308,43 @@ class _Fixtures:
fixtures = _Fixtures()
def get_ui_mapping(combination="99,99,99", target_uinput="keyboard", output_symbol="a"):
def get_combination_config(
*event_tuples: Tuple[int, int] | Tuple[int, int, int]
) -> Iterable[Dict[str, int]]:
"""convenient function to get a iterable of dicts, InputEvent.event_tuple's"""
for event in event_tuples:
if len(event) == 3:
yield {k: v for k, v in zip(("type", "code", "analog_threshold"), event)}
elif len(event) == 2:
yield {k: v for k, v in zip(("type", "code"), event)}
else:
raise TypeError
def get_ui_mapping(combination=None, target_uinput="keyboard", output_symbol="a"):
"""Convenient function to get a valid mapping."""
from inputremapper.configs.mapping import UIMapping
if not combination:
combination = get_combination_config((99, 99))
return UIMapping(
event_combination=combination,
input_combination=combination,
target_uinput=target_uinput,
output_symbol=output_symbol,
)
def get_key_mapping(
combination="99,99,99", target_uinput="keyboard", output_symbol="a"
):
def get_key_mapping(combination=None, target_uinput="keyboard", output_symbol="a"):
"""Convenient function to get a valid mapping."""
from inputremapper.configs.mapping import Mapping
if not combination:
combination = [{"type": 99, "code": 99, "analog_threshold": 99}]
return Mapping(
event_combination=combination,
input_combination=combination,
target_uinput=target_uinput,
output_symbol=output_symbol,
)
@ -350,21 +372,23 @@ def prepare_presets():
from inputremapper.configs.global_config import global_config
preset1 = Preset(get_preset_path("Foo Device", "preset1"))
preset1.add(get_key_mapping(combination="1,1,1", output_symbol="b"))
preset1.add(get_key_mapping(combination="1,2,1"))
preset1.add(
get_key_mapping(combination=get_combination_config((1, 1)), output_symbol="b")
)
preset1.add(get_key_mapping(combination=get_combination_config((1, 2))))
preset1.save()
time.sleep(0.1)
preset2 = Preset(get_preset_path("Foo Device", "preset2"))
preset2.add(get_key_mapping(combination="1,3,1"))
preset2.add(get_key_mapping(combination="1,4,1"))
preset2.add(get_key_mapping(combination=get_combination_config((1, 3))))
preset2.add(get_key_mapping(combination=get_combination_config((1, 4))))
preset2.save()
# make sure the timestamp of preset 3 is the newest,
# so that it will be automatically loaded by the GUI
time.sleep(0.1)
preset3 = Preset(get_preset_path("Foo Device", "preset3"))
preset3.add(get_key_mapping(combination="1,5,1"))
preset3.add(get_key_mapping(combination=get_combination_config((1, 5))))
preset3.save()
with open(get_config_path("config.json"), "w") as file:

@ -66,6 +66,7 @@ def push_event(fixture: Fixture, event, force: bool = False):
fixture
For example 'Foo Device'
event
The InputEvent to send
force
don't check if the event is in fixture.capabilities
"""

@ -17,9 +17,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/>.
from inputremapper.configs.input_config import InputConfig
from tests.lib.cleanup import quick_cleanup
from tests.lib.fixtures import get_key_mapping
from tests.lib.fixtures import get_key_mapping, get_combination_config
from evdev.ecodes import (
EV_REL,
EV_ABS,
@ -32,7 +33,6 @@ import unittest
from inputremapper.injection.context import Context
from inputremapper.configs.preset import Preset
from inputremapper.event_combination import EventCombination
from inputremapper.configs.mapping import Mapping
@ -44,23 +44,23 @@ class TestContext(unittest.TestCase):
def test_callbacks(self):
preset = Preset()
cfg = {
"event_combination": ",".join((str(EV_ABS), str(ABS_X), "0")),
"input_combination": get_combination_config((EV_ABS, ABS_X)),
"target_uinput": "mouse",
"output_type": EV_REL,
"output_code": REL_HWHEEL_HI_RES,
}
preset.add(Mapping(**cfg)) # abs x -> wheel
cfg["event_combination"] = ",".join((str(EV_ABS), str(ABS_Y), "0"))
cfg["input_combination"] = get_combination_config((EV_ABS, ABS_Y))
cfg["output_code"] = REL_WHEEL_HI_RES
preset.add(Mapping(**cfg)) # abs y -> wheel
preset.add(get_key_mapping(EventCombination((1, 31, 1)), "keyboard", "k(a)"))
preset.add(get_key_mapping(EventCombination((1, 32, 1)), "keyboard", "b"))
preset.add(get_key_mapping(get_combination_config((1, 31)), "keyboard", "k(a)"))
preset.add(get_key_mapping(get_combination_config((1, 32)), "keyboard", "b"))
# overlapping combination for (1, 32, 1)
preset.add(
get_key_mapping(
EventCombination(((1, 32, 1), (1, 33, 1), (1, 34, 1))),
get_combination_config((1, 32), (1, 33), (1, 34)),
"keyboard",
"c",
)
@ -68,27 +68,29 @@ class TestContext(unittest.TestCase):
# map abs x to key "b"
preset.add(
get_key_mapping(EventCombination([EV_ABS, ABS_X, 20]), "keyboard", "d"),
get_key_mapping(
get_combination_config((EV_ABS, ABS_X, 20)),
"keyboard",
"d",
),
)
context = Context(preset)
# expected callbacks and their lengths:
callbacks = {
(
EV_ABS,
ABS_X,
): 2, # ABS_X -> "d" and ABS_X -> wheel have the same type and code
(EV_ABS, ABS_Y): 1,
(1, 31): 1,
# ABS_X -> "d" and ABS_X -> wheel have the same type and code
InputConfig(type=EV_ABS, code=ABS_X).input_match_hash: 2,
InputConfig(type=EV_ABS, code=ABS_Y).input_match_hash: 1,
InputConfig(type=1, code=31).input_match_hash: 1,
# even though we have 2 mappings with this type and code, we only expect one callback
# because they both map to keys. We don't want to trigger two mappings with the same key press
(1, 32): 1,
(1, 33): 1,
(1, 34): 1,
InputConfig(type=1, code=32).input_match_hash: 1,
InputConfig(type=1, code=33).input_match_hash: 1,
InputConfig(type=1, code=34).input_match_hash: 1,
}
self.assertEqual(set(callbacks.keys()), set(context.notify_callbacks.keys()))
self.assertEqual(set(callbacks.keys()), set(context._notify_callbacks.keys()))
for key, val in callbacks.items():
self.assertEqual(val, len(context.notify_callbacks[key]))
self.assertEqual(val, len(context._notify_callbacks[key]))
# 7 unique input events in the preset
self.assertEqual(7, len(context._handlers))

@ -43,7 +43,7 @@ from inputremapper.groups import groups
def import_control():
"""Import the core function of the input-remapper-control command."""
bin_path = os.path.join(
os.getcwd().replace("/tests/integration", ""),
os.getcwd().replace("/tests", ""),
"bin",
"input-remapper-control",
)

@ -26,12 +26,11 @@ import gi
from inputremapper.configs.system_mapping import system_mapping
from inputremapper.injection.injector import InjectorState
from inputremapper.input_event import InputEvent
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.groups import _Groups
from inputremapper.gui.messages.message_broker import (
MessageBroker,
@ -54,7 +53,7 @@ from inputremapper.configs.mapping import UIMapping, MappingData
from tests.lib.cleanup import quick_cleanup
from tests.lib.stuff import spy
from tests.lib.patches import FakeDaemonProxy
from tests.lib.fixtures import fixtures, prepare_presets
from tests.lib.fixtures import fixtures, prepare_presets, get_combination_config
from inputremapper.configs.global_config import GlobalConfig
from inputremapper.gui.controller import Controller, MAPPING_DEFAULTS
from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME
@ -457,7 +456,9 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(combination=EventCombination("1,4,1"))
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=4))
)
with patch.object(self.data_manager, "update_mapping") as mock:
self.controller.update_mapping(
@ -513,7 +514,7 @@ class TestController(unittest.TestCase):
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
self.message_broker.subscribe(
MessageType.user_confirm_request, lambda msg: msg.respond(True)
)
@ -522,7 +523,9 @@ class TestController(unittest.TestCase):
preset = Preset(get_preset_path("Foo Device", "preset2"))
preset.load()
self.assertIsNone(preset.get_mapping(EventCombination("1,3,1")))
self.assertIsNone(
preset.get_mapping(InputCombination(InputConfig(type=1, code=3)))
)
def test_does_not_delete_mapping_when_not_confirmed(self):
prepare_presets()
@ -530,7 +533,7 @@ class TestController(unittest.TestCase):
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
self.user_interface.confirm_delete.configure_mock(
return_value=Gtk.ResponseType.CANCEL
)
@ -540,14 +543,16 @@ class TestController(unittest.TestCase):
preset = Preset(get_preset_path("Foo Device", "preset2"))
preset.load()
self.assertIsNotNone(preset.get_mapping(EventCombination("1,3,1")))
self.assertIsNotNone(
preset.get_mapping(InputCombination(InputConfig(type=1, code=3)))
)
def test_should_update_combination(self):
"""When combination is free."""
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination.from_string("1,3,1"))
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
calls: List[CombinationUpdate] = []
@ -555,12 +560,14 @@ class TestController(unittest.TestCase):
calls.append(data)
self.message_broker.subscribe(MessageType.combination_update, f)
self.controller.update_combination(EventCombination.from_string("1,10,1"))
self.controller.update_combination(
InputCombination(InputConfig(type=1, code=10))
)
self.assertEqual(
calls[0],
CombinationUpdate(
EventCombination.from_string("1,3,1"),
EventCombination.from_string("1,10,1"),
InputCombination(InputConfig(type=1, code=3)),
InputCombination(InputConfig(type=1, code=10)),
),
)
@ -569,7 +576,7 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination.from_string("1,3,1"))
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
calls: List[CombinationUpdate] = []
@ -577,7 +584,9 @@ class TestController(unittest.TestCase):
calls.append(data)
self.message_broker.subscribe(MessageType.combination_update, f)
self.controller.update_combination(EventCombination.from_string("1,4,1"))
self.controller.update_combination(
InputCombination(InputConfig(type=1, code=4))
)
self.assertEqual(len(calls), 0)
def test_key_recording_disables_gui_shortcuts(self):
@ -623,7 +632,7 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination.from_string("1,3,1"))
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
calls: List[CombinationUpdate] = []
@ -634,23 +643,25 @@ class TestController(unittest.TestCase):
self.controller.start_key_recording()
self.message_broker.publish(
CombinationRecorded(EventCombination.from_string("1,10,1"))
CombinationRecorded(InputCombination(InputConfig(type=1, code=10)))
)
self.assertEqual(
calls[0],
CombinationUpdate(
EventCombination.from_string("1,3,1"),
EventCombination.from_string("1,10,1"),
InputCombination(InputConfig(type=1, code=3)),
InputCombination(InputConfig(type=1, code=10)),
),
)
self.message_broker.publish(
CombinationRecorded(EventCombination.from_string("1,10,1+1,3,1"))
CombinationRecorded(
InputCombination(get_combination_config((1, 10), (1, 3)))
)
)
self.assertEqual(
calls[1],
CombinationUpdate(
EventCombination.from_string("1,10,1"),
EventCombination.from_string("1,10,1+1,3,1"),
InputCombination(InputConfig(type=1, code=10)),
InputCombination(get_combination_config((1, 10), (1, 3))),
),
)
@ -658,7 +669,7 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination.from_string("1,3,1"))
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
calls: List[CombinationUpdate] = []
@ -668,7 +679,7 @@ class TestController(unittest.TestCase):
self.message_broker.subscribe(MessageType.combination_update, f)
self.message_broker.publish(
CombinationRecorded(EventCombination.from_string("1,10,1"))
CombinationRecorded(InputCombination(InputConfig(type=1, code=10)))
)
self.assertEqual(len(calls), 0)
@ -676,7 +687,7 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination.from_string("1,3,1"))
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
calls: List[CombinationUpdate] = []
@ -687,11 +698,13 @@ class TestController(unittest.TestCase):
self.controller.start_key_recording()
self.message_broker.publish(
CombinationRecorded(EventCombination.from_string("1,10,1"))
CombinationRecorded(InputCombination(InputConfig(type=1, code=10)))
)
self.message_broker.signal(MessageType.recording_finished)
self.message_broker.publish(
CombinationRecorded(EventCombination.from_string("1,10,1+1,3,1"))
CombinationRecorded(
InputCombination(get_combination_config((1, 10), (1, 3)))
)
)
self.assertEqual(len(calls), 1) # only the first was processed
@ -700,7 +713,7 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination.from_string("1,3,1"))
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
calls: List[CombinationUpdate] = []
@ -711,11 +724,13 @@ class TestController(unittest.TestCase):
self.controller.start_key_recording()
self.message_broker.publish(
CombinationRecorded(EventCombination.from_string("1,10,1"))
CombinationRecorded(InputCombination(InputConfig(type=1, code=10)))
)
self.controller.stop_key_recording()
self.message_broker.publish(
CombinationRecorded(EventCombination.from_string("1,10,1+1,3,1"))
CombinationRecorded(
InputCombination(get_combination_config((1, 10), (1, 3)))
)
)
self.assertEqual(len(calls), 1) # only the first was processed
@ -747,7 +762,7 @@ class TestController(unittest.TestCase):
self.data_manager.load_preset("foo")
self.data_manager.create_mapping()
self.data_manager.update_mapping(
event_combination=EventCombination(InputEvent.btn_left()),
input_combination=InputCombination(InputConfig.btn_left()),
target_uinput="keyboard",
output_symbol="a",
)
@ -773,7 +788,7 @@ class TestController(unittest.TestCase):
self.data_manager.load_preset("foo")
self.data_manager.create_mapping()
self.data_manager.update_mapping(
event_combination=EventCombination(InputEvent.btn_left()),
input_combination=InputCombination(InputConfig.btn_left()),
target_uinput="keyboard",
output_symbol="a",
)
@ -790,14 +805,14 @@ class TestController(unittest.TestCase):
self.data_manager.load_preset("foo")
self.data_manager.create_mapping()
self.data_manager.update_mapping(
event_combination=EventCombination(InputEvent.btn_left()),
input_combination=InputCombination(InputConfig.btn_left()),
target_uinput="keyboard",
output_symbol="a",
)
self.data_manager.create_mapping()
self.data_manager.load_mapping(EventCombination.empty_combination())
self.data_manager.load_mapping(InputCombination.empty_combination())
self.data_manager.update_mapping(
event_combination=EventCombination.from_string("1,5,1"),
input_combination=InputCombination(InputConfig(type=1, code=5)),
target_uinput="mouse",
output_symbol="BTN_LEFT",
)
@ -932,101 +947,113 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
event_combination=EventCombination.from_string("1,1,1+1,2,1+1,3,1")
input_combination=InputCombination(
get_combination_config((1, 1), (1, 2), (1, 3))
)
)
self.controller.move_event_in_combination(InputEvent.from_string("1,2,1"), "up")
self.controller.move_input_config_in_combination(
InputConfig(type=1, code=2), "up"
)
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.from_string("1,2,1+1,1,1+1,3,1"),
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 2), (1, 1), (1, 3))),
)
# now nothing changes
self.controller.move_event_in_combination(InputEvent.from_string("1,2,1"), "up")
self.controller.move_input_config_in_combination(
InputConfig(type=1, code=2), "up"
)
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.from_string("1,2,1+1,1,1+1,3,1"),
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 2), (1, 1), (1, 3))),
)
def test_move_event_down(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
event_combination=EventCombination.from_string("1,1,1+1,2,1+1,3,1")
input_combination=InputCombination(
get_combination_config((1, 1), (1, 2), (1, 3))
)
)
self.controller.move_event_in_combination(
InputEvent.from_string("1,2,1"), "down"
self.controller.move_input_config_in_combination(
InputConfig(type=1, code=2), "down"
)
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.from_string("1,1,1+1,3,1+1,2,1"),
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 1), (1, 3), (1, 2))),
)
# now nothing changes
self.controller.move_event_in_combination(
InputEvent.from_string("1,2,1"), "down"
self.controller.move_input_config_in_combination(
InputConfig(type=1, code=2), "down"
)
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.from_string("1,1,1+1,3,1+1,2,1"),
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 1), (1, 3), (1, 2))),
)
def test_move_event_in_combination_of_len_1(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.controller.move_event_in_combination(
InputEvent.from_string("1,3,1"), "down"
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.controller.move_input_config_in_combination(
InputConfig(type=1, code=3), "down"
)
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.from_string("1,3,1"),
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 3))),
)
def test_move_event_loads_it_again(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
event_combination=EventCombination.from_string("1,1,1+1,2,1+1,3,1")
input_combination=InputCombination(
get_combination_config((1, 1), (1, 2), (1, 3))
)
)
mock = MagicMock()
self.message_broker.subscribe(MessageType.selected_event, mock)
self.controller.move_event_in_combination(
InputEvent.from_string("1,2,1"), "down"
self.controller.move_input_config_in_combination(
InputConfig(type=1, code=2), "down"
)
mock.assert_called_once_with(InputEvent.from_string("1,2,1"))
mock.assert_called_once_with(InputConfig(type=1, code=2))
def test_update_event(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_event(InputEvent.from_string("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_input_config(InputConfig(type=1, code=3))
mock = MagicMock()
self.message_broker.subscribe(MessageType.selected_event, mock)
self.controller.update_event(InputEvent.from_string("1,10,1"))
mock.assert_called_once_with(InputEvent.from_string("1,10,1"))
self.controller.update_input_config(InputConfig(type=1, code=10))
mock.assert_called_once_with(InputConfig(type=1, code=10))
def test_update_event_reloads_mapping_and_event_when_update_fails(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_event(InputEvent.from_string("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_input_config(InputConfig(type=1, code=3))
mock = MagicMock()
self.message_broker.subscribe(MessageType.selected_event, mock)
self.message_broker.subscribe(MessageType.mapping, mock)
calls = [
call(self.data_manager.active_mapping.get_bus_message()),
call(InputEvent.from_string("1,3,1")),
call(InputConfig(type=1, code=3)),
]
self.controller.update_event(InputEvent.from_string("1,4,1")) # already exists
self.controller.update_input_config(
InputConfig(type=1, code=4)
) # already exists
mock.assert_has_calls(calls, any_order=False)
def test_remove_event_does_nothing_when_mapping_not_loaded(self):
@ -1038,44 +1065,50 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.update_mapping(event_combination="1,3,1+1,4,1")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
input_combination=get_combination_config((1, 3), (1, 4))
)
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.from_string("1,3,1+1,4,1"),
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 3), (1, 4))),
)
self.data_manager.load_event(InputEvent.from_string("1,4,1"))
self.data_manager.load_input_config(InputConfig(type=1, code=4))
self.controller.remove_event()
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.from_string("1,3,1"),
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 3))),
)
def test_remove_event_loads_a_event(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.update_mapping(event_combination="1,3,1+1,4,1")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
input_combination=get_combination_config((1, 3), (1, 4))
)
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.from_string("1,3,1+1,4,1"),
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 3), (1, 4))),
)
self.data_manager.load_event(InputEvent.from_string("1,4,1"))
self.data_manager.load_input_config(InputConfig(type=1, code=4))
mock = MagicMock()
self.message_broker.subscribe(MessageType.selected_event, mock)
self.controller.remove_event()
mock.assert_called_once_with(InputEvent.from_string("1,3,1"))
mock.assert_called_once_with(InputConfig(type=1, code=3))
def test_remove_event_reloads_mapping_and_event_when_update_fails(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.update_mapping(event_combination="1,3,1+1,4,1")
self.data_manager.load_event(InputEvent.from_string("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
input_combination=get_combination_config((1, 3), (1, 4))
)
self.data_manager.load_input_config(InputConfig(type=1, code=3))
# removing "1,3,1" will throw a key error because a mapping with combination
# "1,4,1" already exists in preset
@ -1084,7 +1117,7 @@ class TestController(unittest.TestCase):
self.message_broker.subscribe(MessageType.mapping, mock)
calls = [
call(self.data_manager.active_mapping.get_bus_message()),
call(InputEvent.from_string("1,3,1")),
call(InputConfig(type=1, code=3)),
]
self.controller.remove_event()
mock.assert_has_calls(calls, any_order=False)
@ -1093,9 +1126,15 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.update_mapping(event_combination="3,0,10")
self.data_manager.load_mapping(EventCombination("3,0,10"))
self.data_manager.load_event(InputEvent.from_string("3,0,10"))
self.data_manager.update_mapping(
input_combination=get_combination_config((3, 0, 10))
)
self.data_manager.load_mapping(
InputCombination(get_combination_config((3, 0, 10)))
)
self.data_manager.load_input_config(
InputConfig(type=3, code=0, analog_threshold=10)
)
with patch.object(self.data_manager, "save") as mock:
self.controller.set_event_as_analog(False)
@ -1109,53 +1148,67 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.update_mapping(event_combination="3,0,10")
self.data_manager.load_event(InputEvent.from_string("3,0,10"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
input_combination=get_combination_config((3, 0, 10))
)
self.data_manager.load_input_config(
InputConfig(type=3, code=0, analog_threshold=10)
)
self.controller.set_event_as_analog(True)
self.assertEqual(
self.data_manager.active_mapping.event_combination,
EventCombination.from_string("3,0,0"),
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((3, 0))),
)
def test_set_event_as_analog_adds_rel_threshold(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.update_mapping(event_combination="2,0,0")
self.data_manager.load_event(InputEvent.from_string("2,0,0"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
input_combination=get_combination_config((2, 0))
)
self.data_manager.load_input_config(InputConfig(type=2, code=0))
self.controller.set_event_as_analog(False)
combinations = [EventCombination("2,0,1"), EventCombination("2,0,-1")]
self.assertIn(self.data_manager.active_mapping.event_combination, combinations)
combinations = [
InputCombination(get_combination_config((2, 0, 1))),
InputCombination(get_combination_config((2, 0, -1))),
]
self.assertIn(self.data_manager.active_mapping.input_combination, combinations)
def test_set_event_as_analog_adds_abs_threshold(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.update_mapping(event_combination="3,0,0")
self.data_manager.load_event(InputEvent.from_string("3,0,0"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
input_combination=get_combination_config((3, 0))
)
self.data_manager.load_input_config(InputConfig(type=3, code=0))
self.controller.set_event_as_analog(False)
combinations = [EventCombination("3,0,10"), EventCombination("3,0,-10")]
self.assertIn(self.data_manager.active_mapping.event_combination, combinations)
combinations = [
InputCombination(get_combination_config((3, 0, 10))),
InputCombination(get_combination_config((3, 0, -10))),
]
self.assertIn(self.data_manager.active_mapping.input_combination, combinations)
def test_set_event_as_analog_reloads_mapping_and_event_when_key_event(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_event(InputEvent.from_string("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_input_config(InputConfig(type=1, code=3))
mock = MagicMock()
self.message_broker.subscribe(MessageType.selected_event, mock)
self.message_broker.subscribe(MessageType.mapping, mock)
calls = [
call(self.data_manager.active_mapping.get_bus_message()),
call(InputEvent.from_string("1,3,1")),
call(InputConfig(type=1, code=3)),
]
self.controller.set_event_as_analog(True)
mock.assert_has_calls(calls, any_order=False)
@ -1164,16 +1217,20 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.update_mapping(event_combination="3,0,10")
self.data_manager.load_event(InputEvent.from_string("3,0,10"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
input_combination=get_combination_config((3, 0, 10))
)
self.data_manager.load_input_config(
InputConfig(type=3, code=0, analog_threshold=10)
)
mock = MagicMock()
self.message_broker.subscribe(MessageType.selected_event, mock)
self.message_broker.subscribe(MessageType.mapping, mock)
calls = [
call(self.data_manager.active_mapping.get_bus_message()),
call(InputEvent.from_string("3,0,10")),
call(InputConfig(type=3, code=0, analog_threshold=10)),
]
with patch.object(self.data_manager, "update_mapping", side_effect=KeyError):
self.controller.set_event_as_analog(True)
@ -1183,16 +1240,18 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.update_mapping(event_combination="3,0,0")
self.data_manager.load_event(InputEvent.from_string("3,0,0"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
input_combination=get_combination_config((3, 0))
)
self.data_manager.load_input_config(InputConfig(type=3, code=0))
mock = MagicMock()
self.message_broker.subscribe(MessageType.selected_event, mock)
self.message_broker.subscribe(MessageType.mapping, mock)
calls = [
call(self.data_manager.active_mapping.get_bus_message()),
call(InputEvent.from_string("3,0,0")),
call(InputConfig(type=3, code=0)),
]
with patch.object(self.data_manager, "update_mapping", side_effect=KeyError):
self.controller.set_event_as_analog(False)
@ -1202,7 +1261,7 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
request: UserConfirmRequest = None
def f(r: UserConfirmRequest):
@ -1217,7 +1276,7 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(output_symbol=None)
request: UserConfirmRequest = None
@ -1233,9 +1292,10 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
event_combination="1,3,1+2,1,1", output_symbol=None
input_combination=get_combination_config((1, 3), (2, 1, 1)),
output_symbol=None,
)
request: UserConfirmRequest = None
@ -1251,9 +1311,10 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
event_combination="1,3,1+2,1,1", output_symbol=None
input_combination=get_combination_config((1, 3), (2, 1, 1)),
output_symbol=None,
)
self.message_broker.subscribe(
@ -1264,14 +1325,16 @@ class TestController(unittest.TestCase):
mock.assert_called_once_with(
mapping_type="analog",
output_symbol=None,
event_combination=EventCombination.from_string("1,3,1+2,1,0"),
input_combination=InputCombination(
get_combination_config((1, 3), (2, 1))
),
)
def test_update_mapping_type_will_abort_when_user_denys(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.message_broker.subscribe(
MessageType.user_confirm_request, lambda r: r.respond(False)
@ -1281,7 +1344,9 @@ class TestController(unittest.TestCase):
mock.assert_not_called()
self.data_manager.update_mapping(
event_combination="1,3,1+2,1,0", output_symbol=None, mapping_type="analog"
input_combination=get_combination_config((1, 3), (2, 1)),
output_symbol=None,
mapping_type="analog",
)
with patch.object(self.data_manager, "update_mapping") as mock:
self.controller.update_mapping(mapping_type="key_macro")
@ -1291,7 +1356,7 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.message_broker.subscribe(
MessageType.user_confirm_request, lambda r: r.respond(True)
@ -1304,9 +1369,11 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
event_combination="1,3,1+2,1,0", output_symbol=None, mapping_type="analog"
input_combination=get_combination_config((1, 3), (2, 1)),
output_symbol=None,
mapping_type="analog",
)
request: UserConfirmRequest = None
@ -1322,9 +1389,9 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
event_combination="1,3,1+2,1,0",
input_combination=get_combination_config((1, 3), (2, 1)),
output_symbol=None,
)
mock = MagicMock()
@ -1336,9 +1403,9 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
event_combination="1,3,1+2,1,1",
input_combination=get_combination_config((1, 3), (2, 1, 1)),
mapping_type="analog",
output_symbol=None,
)
@ -1351,9 +1418,11 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(EventCombination("1,3,1"))
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.update_mapping(
event_combination="1,3,1+2,1,0", output_symbol=None, mapping_type="analog"
input_combination=get_combination_config((1, 3), (2, 1)),
output_symbol=None,
mapping_type="analog",
)
self.message_broker.subscribe(
MessageType.user_confirm_request, lambda r: r.respond(True)

@ -20,8 +20,9 @@
from tests.test import is_service_running
from tests.lib.logger import logger
from tests.lib.cleanup import cleanup
from tests.lib.fixtures import new_event
from tests.lib.fixtures import new_event, get_combination_config
from tests.lib.pipes import push_events, uinput_write_history_pipe
from tests.lib.tmp import tmp
from tests.lib.fixtures import fixtures, get_key_mapping
@ -40,7 +41,7 @@ from inputremapper.configs.system_mapping import system_mapping
from inputremapper.configs.global_config import global_config
from inputremapper.groups import groups
from inputremapper.configs.paths import get_config_path, mkdir, get_preset_path
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.preset import Preset
from inputremapper.injection.injector import InjectorState
from inputremapper.daemon import Daemon
@ -114,8 +115,7 @@ class TestDaemon(unittest.TestCase):
preset_name = "foo"
ev_1 = (EV_KEY, BTN_A)
ev_2 = (EV_ABS, ABS_X)
ev = (EV_ABS, ABS_X)
group = groups.find(name="gamepad")
@ -123,8 +123,18 @@ class TestDaemon(unittest.TestCase):
group2 = groups.find(name="Bar Device")
preset = Preset(group.get_preset_path(preset_name))
preset.add(get_key_mapping(EventCombination([*ev_1, 1]), "keyboard", "a"))
preset.add(get_key_mapping(EventCombination([*ev_2, -1]), "keyboard", "b"))
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=BTN_A)),
"keyboard",
"a",
)
)
preset.add(
get_key_mapping(
InputCombination(get_combination_config((*ev, -1))), "keyboard", "b"
)
)
preset.save()
global_config.set_autoload_preset(group.key, preset_name)
@ -140,6 +150,7 @@ class TestDaemon(unittest.TestCase):
# has been cleanedUp in setUp
self.assertNotIn("keyboard", global_uinputs.devices)
logger.info(f"start injector for {group.key}")
self.daemon.start_injecting(group.key, preset_name)
# created on demand
@ -155,6 +166,7 @@ class TestDaemon(unittest.TestCase):
self.assertEqual(event.code, BTN_B)
self.assertEqual(event.value, 1)
logger.info(f"stopping injector for {group.key}")
self.daemon.stop_injecting(group.key)
time.sleep(0.2)
self.assertEqual(self.daemon.get_state(group.key), InjectorState.STOPPED)
@ -167,11 +179,12 @@ class TestDaemon(unittest.TestCase):
raise
"""Injection 2"""
logger.info(f"start injector for {group.key}")
self.daemon.start_injecting(group.key, preset_name)
time.sleep(0.1)
# -1234 will be classified as -1 by the injector
push_events(fixtures.gamepad, [new_event(*ev_2, -1234)])
push_events(fixtures.gamepad, [new_event(*ev, -1234)])
time.sleep(0.1)
self.assertTrue(uinput_write_history_pipe[0].poll())
@ -214,7 +227,11 @@ class TestDaemon(unittest.TestCase):
system_mapping._set("a", KEY_A)
preset = Preset(get_preset_path(group_name, preset_name))
preset.add(get_key_mapping(EventCombination([*ev, 1]), "keyboard", "a"))
preset.add(
get_key_mapping(
InputCombination(get_combination_config(ev)), "keyboard", "a"
)
)
# make the daemon load the file instead
with open(get_config_path("xmodmap.json"), "w") as file:
@ -294,7 +311,11 @@ class TestDaemon(unittest.TestCase):
path = os.path.join(config_dir, "presets", name, f"{preset_name}.json")
preset = Preset(path)
preset.add(get_key_mapping(EventCombination(event), target, to_name))
preset.add(
get_key_mapping(
InputCombination(get_combination_config(event)), target, to_name
)
)
preset.save()
system_mapping.clear()
@ -334,7 +355,11 @@ class TestDaemon(unittest.TestCase):
pereset = Preset(group.get_preset_path(preset_name))
pereset.add(
get_key_mapping(EventCombination((EV_KEY, KEY_A, 1)), "keyboard", "a")
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=KEY_A)),
"keyboard",
"a",
)
)
pereset.save()
@ -398,7 +423,11 @@ class TestDaemon(unittest.TestCase):
preset = Preset(group.get_preset_path(preset_name))
preset.add(
get_key_mapping(EventCombination((EV_KEY, KEY_A, 1)), "keyboard", "a")
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=KEY_A)),
"keyboard",
"a",
)
)
preset.save()
@ -451,7 +480,13 @@ class TestDaemon(unittest.TestCase):
preset_name = "preset7"
group = groups.find(key="Foo Device 2")
preset = Preset(group.get_preset_path(preset_name))
preset.add(get_key_mapping(EventCombination([3, 2, 1]), "keyboard", "a"))
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=3, code=2, analog_threshold=1)),
"keyboard",
"a",
)
)
preset.save()
global_config.set_autoload_preset(group.key, preset_name)
@ -468,7 +503,13 @@ class TestDaemon(unittest.TestCase):
group = groups.find(key="Foo Device 2")
preset = Preset(group.get_preset_path(preset_name))
preset.add(get_key_mapping(EventCombination([3, 2, 1]), "keyboard", "a"))
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=3, code=2, analog_threshold=1)),
"keyboard",
"a",
)
)
preset.save()
global_config.set_autoload_preset(group.key, preset_name)

@ -27,7 +27,7 @@ from unittest.mock import MagicMock, call
from inputremapper.configs.global_config import global_config
from inputremapper.configs.mapping import UIMapping, MappingData
from inputremapper.configs.system_mapping import system_mapping
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.exceptions import DataManagementError
from inputremapper.groups import _Groups
from inputremapper.gui.messages.message_broker import (
@ -40,10 +40,9 @@ from inputremapper.gui.messages.message_data import (
)
from inputremapper.gui.reader_client import ReaderClient
from inputremapper.injection.global_uinputs import GlobalUInputs
from inputremapper.input_event import InputEvent
from tests.lib.cleanup import quick_cleanup
from tests.lib.patches import FakeDaemonProxy
from tests.lib.fixtures import prepare_presets
from tests.lib.fixtures import prepare_presets, get_combination_config
from inputremapper.configs.paths import get_preset_path
from inputremapper.configs.preset import Preset
@ -163,13 +162,17 @@ class TestDataManager(unittest.TestCase):
self.data_manager.load_preset(name="preset1")
listener = Listener()
self.message_broker.subscribe(MessageType.mapping, listener)
self.data_manager.load_mapping(combination=EventCombination("1,1,1"))
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=1))
)
mapping: MappingData = listener.calls[0]
control_preset = Preset(get_preset_path("Foo Device", "preset1"))
control_preset.load()
self.assertEqual(
control_preset.get_mapping(EventCombination("1,1,1")).output_symbol,
control_preset.get_mapping(
InputCombination(InputConfig(type=1, code=1))
).output_symbol,
mapping.output_symbol,
)
@ -181,7 +184,9 @@ class TestDataManager(unittest.TestCase):
control_preset.empty()
control_preset.load()
self.assertEqual(
control_preset.get_mapping(EventCombination("1,1,1")).output_symbol,
control_preset.get_mapping(
InputCombination(InputConfig(type=1, code=1))
).output_symbol,
"key(a)",
)
@ -373,13 +378,17 @@ class TestDataManager(unittest.TestCase):
def test_load_mapping(self):
"""should be able to load a mapping"""
preset, _, _ = prepare_presets()
expected_mapping = preset.get_mapping(EventCombination("1,1,1"))
expected_mapping = preset.get_mapping(
InputCombination(InputConfig(type=1, code=1))
)
self.data_manager.load_group(group_key="Foo Device")
self.data_manager.load_preset(name="preset1")
listener = Listener()
self.message_broker.subscribe(MessageType.mapping, listener)
self.data_manager.load_mapping(combination=EventCombination("1,1,1"))
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=1))
)
mapping = listener.calls[0]
self.assertEqual(mapping, expected_mapping)
@ -393,7 +402,7 @@ class TestDataManager(unittest.TestCase):
self.assertRaises(
KeyError,
self.data_manager.load_mapping,
combination=EventCombination("1,1,1"),
combination=InputCombination(InputConfig(type=1, code=1)),
)
def test_cannot_load_mapping_without_preset(self):
@ -404,13 +413,13 @@ class TestDataManager(unittest.TestCase):
self.assertRaises(
DataManagementError,
self.data_manager.load_mapping,
combination=EventCombination("1,1,1"),
combination=InputCombination(InputConfig(type=1, code=1)),
)
self.data_manager.load_group("Foo Device")
self.assertRaises(
DataManagementError,
self.data_manager.load_mapping,
combination=EventCombination("1,1,1"),
combination=InputCombination(InputConfig(type=1, code=1)),
)
def test_load_event(self):
@ -419,11 +428,11 @@ class TestDataManager(unittest.TestCase):
self.message_broker.subscribe(MessageType.selected_event, mock)
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(EventCombination("1,1,1"))
self.data_manager.load_event(InputEvent.from_string("1,1,1"))
mock.assert_called_once_with(InputEvent.from_string("1,1,1"))
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1)))
self.data_manager.load_input_config(InputConfig(type=1, code=1))
mock.assert_called_once_with(InputConfig(type=1, code=1))
self.assertEqual(
self.data_manager.active_event, InputEvent.from_string("1,1,1")
self.data_manager.active_input_config, InputConfig(type=1, code=1)
)
def test_cannot_load_event_when_mapping_not_set(self):
@ -431,45 +440,48 @@ class TestDataManager(unittest.TestCase):
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
with self.assertRaises(DataManagementError):
self.data_manager.load_event(InputEvent.from_string("1,1,1"))
self.data_manager.load_input_config(InputConfig(type=1, code=1))
def test_cannot_load_event_when_not_in_mapping_combination(self):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(EventCombination("1,1,1"))
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1)))
with self.assertRaises(ValueError):
self.data_manager.load_event(InputEvent.from_string("1,5,1"))
self.data_manager.load_input_config(InputConfig(type=1, code=5))
def test_update_event(self):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(EventCombination("1,1,1"))
self.data_manager.load_event(InputEvent.from_string("1,1,1"))
self.data_manager.update_event(InputEvent.from_string("1,5,1"))
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1)))
self.data_manager.load_input_config(InputConfig(type=1, code=1))
self.data_manager.update_input_config(InputConfig(type=1, code=5))
self.assertEqual(
self.data_manager.active_event, InputEvent.from_string("1,5,1")
self.data_manager.active_input_config, InputConfig(type=1, code=5)
)
def test_update_event_sends_messages(self):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(EventCombination("1,1,1"))
self.data_manager.load_event(InputEvent.from_string("1,1,1"))
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1)))
self.data_manager.load_input_config(InputConfig(type=1, code=1))
mock = MagicMock()
self.message_broker.subscribe(MessageType.selected_event, mock)
self.message_broker.subscribe(MessageType.combination_update, mock)
self.message_broker.subscribe(MessageType.mapping, mock)
self.data_manager.update_event(InputEvent.from_string("1,5,1"))
self.data_manager.update_input_config(InputConfig(type=1, code=5))
expected = [
call(
CombinationUpdate(EventCombination("1,1,1"), EventCombination("1,5,1"))
CombinationUpdate(
InputCombination(InputConfig(type=1, code=1)),
InputCombination(InputConfig(type=1, code=5)),
)
),
call(self.data_manager.active_mapping.get_bus_message()),
call(InputEvent.from_string("1,5,1")),
call(InputConfig(type=1, code=5)),
]
mock.assert_has_calls(expected, any_order=False)
@ -477,25 +489,27 @@ class TestDataManager(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(EventCombination("1,1,1"))
self.data_manager.load_event(InputEvent.from_string("1,1,1"))
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1)))
self.data_manager.load_input_config(InputConfig(type=1, code=1))
with self.assertRaises(KeyError):
self.data_manager.update_event(InputEvent.from_string("1,2,1"))
self.data_manager.update_input_config(InputConfig(type=1, code=2))
def test_cannot_update_event_when_not_loaded(self):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(EventCombination("1,1,1"))
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1)))
with self.assertRaises(DataManagementError):
self.data_manager.update_event(InputEvent.from_string("1,2,1"))
self.data_manager.update_input_config(InputConfig(type=1, code=2))
def test_update_mapping_emits_mapping_changed(self):
"""update mapping should emit a mapping_changed event"""
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(combination=EventCombination("1,4,1"))
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=4))
)
listener = Listener()
self.message_broker.subscribe(MessageType.mapping, listener)
@ -515,7 +529,9 @@ class TestDataManager(unittest.TestCase):
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(combination=EventCombination("1,4,1"))
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=4))
)
self.data_manager.update_mapping(
name="foo",
@ -526,7 +542,7 @@ class TestDataManager(unittest.TestCase):
preset = Preset(get_preset_path("Foo Device", "preset2"), UIMapping)
preset.load()
mapping = preset.get_mapping(EventCombination("1,4,1"))
mapping = preset.get_mapping(InputCombination(InputConfig(type=1, code=4)))
self.assertEqual(mapping.format_name(), "foo")
self.assertEqual(mapping.output_symbol, "f")
self.assertEqual(mapping.release_timeout, 0.3)
@ -536,7 +552,9 @@ class TestDataManager(unittest.TestCase):
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(combination=EventCombination("1,4,1"))
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=4))
)
self.data_manager.update_mapping(
output_symbol="bar", # not a macro and not a valid symbol
@ -545,7 +563,7 @@ class TestDataManager(unittest.TestCase):
preset = Preset(get_preset_path("Foo Device", "preset2"), UIMapping)
preset.load()
mapping = preset.get_mapping(EventCombination("1,4,1"))
mapping = preset.get_mapping(InputCombination(InputConfig(type=1, code=4)))
self.assertIsNotNone(mapping.get_error())
self.assertEqual(mapping.output_symbol, "bar")
@ -554,28 +572,30 @@ class TestDataManager(unittest.TestCase):
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(combination=EventCombination("1,4,1"))
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=4))
)
listener = Listener()
self.message_broker.subscribe(MessageType.mapping, listener)
self.message_broker.subscribe(MessageType.combination_update, listener)
# we expect a message for combination update first, and then for mapping
self.data_manager.update_mapping(
event_combination=EventCombination.from_string("1,5,1+1,6,1")
input_combination=InputCombination(get_combination_config((1, 5), (1, 6)))
)
self.assertEqual(listener.calls[0].message_type, MessageType.combination_update)
self.assertEqual(
listener.calls[0].old_combination,
EventCombination.from_string("1,4,1"),
InputCombination(InputConfig(type=1, code=4)),
)
self.assertEqual(
listener.calls[0].new_combination,
EventCombination.from_string("1,5,1+1,6,1"),
InputCombination(get_combination_config((1, 5), (1, 6))),
)
self.assertEqual(listener.calls[1].message_type, MessageType.mapping)
self.assertEqual(
listener.calls[1].event_combination,
EventCombination.from_string("1,5,1+1,6,1"),
listener.calls[1].input_combination,
InputCombination(get_combination_config((1, 5), (1, 6))),
)
def test_cannot_update_mapping_combination(self):
@ -584,12 +604,14 @@ class TestDataManager(unittest.TestCase):
prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(combination=EventCombination("1,4,1"))
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=4))
)
self.assertRaises(
KeyError,
self.data_manager.update_mapping,
event_combination=EventCombination("1,3,1"),
input_combination=InputCombination(InputConfig(type=1, code=3)),
)
def test_cannot_update_mapping(self):
@ -624,7 +646,7 @@ class TestDataManager(unittest.TestCase):
self.message_broker.subscribe(MessageType.preset, listener)
self.data_manager.create_mapping() # emits preset_changed
self.data_manager.load_mapping(combination=EventCombination.empty_combination())
self.data_manager.load_mapping(combination=InputCombination.empty_combination())
self.assertEqual(listener.calls[0].name, "preset2")
self.assertEqual(len(listener.calls[0].mappings), 3)
@ -648,7 +670,9 @@ class TestDataManager(unittest.TestCase):
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(combination=EventCombination("1,3,1"))
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=3))
)
listener = Listener()
self.message_broker.subscribe(MessageType.preset, listener)
@ -657,7 +681,9 @@ class TestDataManager(unittest.TestCase):
self.data_manager.delete_mapping() # emits preset
self.data_manager.save()
deleted_mapping = old_preset.get_mapping(EventCombination("1,3,1"))
deleted_mapping = old_preset.get_mapping(
InputCombination(InputConfig(type=1, code=3))
)
mappings = listener.calls[0].mappings
preset_name = listener.calls[0].name
expected_preset = Preset(get_preset_path("Foo Device", "preset2"))

@ -1,204 +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 unittest
from evdev.ecodes import (
EV_KEY,
EV_ABS,
EV_REL,
BTN_C,
BTN_B,
BTN_A,
REL_WHEEL,
REL_HWHEEL,
ABS_RY,
ABS_X,
ABS_HAT0Y,
ABS_HAT0X,
KEY_A,
KEY_LEFTSHIFT,
KEY_RIGHTALT,
KEY_LEFTCTRL,
)
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 = 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 = 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 = EventCombination((1, 3, 1))
self.assertEqual(len(key_3), 1)
self.assertEqual(key_3, key_2)
self.assertNotEqual(key_3, (1, 3, 1))
self.assertEqual(hash(key_3), hash(key_2))
self.assertEqual(hash(key_3), hash(((1, 3, 1),)))
key_4 = EventCombination(*key_3)
self.assertEqual(len(key_4), 1)
self.assertEqual(key_4, key_3)
self.assertEqual(hash(key_4), hash(key_3))
key_5 = EventCombination((*key_4, *key_4, (1, 7, 1)))
self.assertEqual(len(key_5), 3)
self.assertNotEqual(key_5, key_4)
self.assertNotEqual(hash(key_5), hash(key_4))
self.assertEqual(key_5, ((1, 3, 1), (1, 3, 1), (1, 7, 1)))
self.assertEqual(hash(key_5), hash(((1, 3, 1), (1, 3, 1), (1, 7, 1))))
def test_get_permutations(self):
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 = 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 = 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],
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 = EventCombination(((1, KEY_LEFTSHIFT, 1), (1, 5, 1)))
self.assertTrue(key_1.is_problematic())
key_2 = EventCombination(((1, KEY_RIGHTALT, 1), (1, 5, 1)))
self.assertTrue(key_2.is_problematic())
key_3 = EventCombination(((1, 3, 1), (1, KEY_LEFTCTRL, 1)))
self.assertTrue(key_3.is_problematic())
key_4 = EventCombination((1, 3, 1))
self.assertFalse(key_4.is_problematic())
key_5 = EventCombination(((1, 3, 1), (1, 5, 1)))
self.assertFalse(key_5.is_problematic())
def test_init(self):
self.assertRaises(TypeError, lambda: EventCombination(1))
self.assertRaises(TypeError, 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
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_key(self):
c1 = EventCombination((1, 2, 3))
c2 = EventCombination(((1, 2, 3), (4, 5, 6)))
self.assertEqual(c1.json_key(), "1,2,3")
self.assertEqual(c2.json_key(), "1,2,3+4,5,6")
def test_beautify(self):
# not an integration test, but I have all the selection_label tests here already
self.assertEqual(
EventCombination((EV_KEY, KEY_A, 1)).beautify(),
"a",
)
self.assertEqual(
EventCombination([EV_KEY, KEY_A, 1]).beautify(),
"a",
)
self.assertEqual(
EventCombination((EV_ABS, ABS_HAT0Y, -1)).beautify(),
"DPad-Y Up",
)
self.assertEqual(
EventCombination((EV_KEY, BTN_A, 1)).beautify(),
"Button A",
)
self.assertEqual(
EventCombination((EV_KEY, 1234, 1)).beautify(), "unknown (1, 1234)"
)
self.assertEqual(
EventCombination([EV_ABS, ABS_HAT0X, -1]).beautify(),
"DPad-X Left",
)
self.assertEqual(
EventCombination([EV_ABS, ABS_HAT0Y, -1]).beautify(),
"DPad-Y Up",
)
self.assertEqual(
EventCombination([EV_KEY, BTN_A, 1]).beautify(),
"Button A",
)
self.assertEqual(
EventCombination([EV_ABS, ABS_X, 1]).beautify(),
"Joystick-X Right",
)
self.assertEqual(
EventCombination([EV_ABS, ABS_RY, 1]).beautify(),
"Joystick-RY Down",
)
self.assertEqual(
EventCombination([EV_REL, REL_HWHEEL, 1]).beautify(),
"Wheel Right",
)
self.assertEqual(
EventCombination([EV_REL, REL_WHEEL, -1]).beautify(),
"Wheel Down",
)
# combinations
self.assertEqual(
EventCombination(
(
(EV_KEY, BTN_A, 1),
(EV_KEY, BTN_B, 1),
(EV_KEY, BTN_C, 1),
),
).beautify(),
"Button A + Button B + Button C",
)
if __name__ == "__main__":
unittest.main()

@ -99,7 +99,7 @@ class TestAxisTransformation(unittest.TestCase):
self.assertAlmostEqual(x, y2, msg=f"test expo symmetry for {init_args}")
def test_origin_symmetry(self):
"""Test that the transformation is symmetric to the origin
"""Test that the transformation is symmetric to the origin_hash
f(x) = - f(-x)
within the constraints: min = -max
"""
@ -111,7 +111,7 @@ class TestAxisTransformation(unittest.TestCase):
self.assertAlmostEqual(
f(x),
-f(-x),
msg=f"test origin symmetry at {x=} for {init_args}",
msg=f"test origin_hash symmetry at {x=} for {init_args}",
)
def test_gain(self):

@ -54,11 +54,11 @@ from inputremapper.configs.mapping import (
)
from inputremapper.configs.preset import Preset
from inputremapper.configs.system_mapping import system_mapping
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.injection.context import Context
from inputremapper.injection.event_reader import EventReader
from inputremapper.injection.global_uinputs import global_uinputs
from inputremapper.input_event import InputEvent, USE_AS_ANALOG_VALUE
from inputremapper.input_event import InputEvent
from tests.lib.cleanup import cleanup
from tests.lib.logger import logger
from tests.lib.constants import MAX_ABS, MIN_ABS
@ -67,6 +67,7 @@ from tests.lib.fixtures import (
Fixture,
fixtures,
get_key_mapping,
get_combination_config,
)
@ -148,32 +149,40 @@ class TestIdk(EventPipelineTestBase):
system_mapping._set("c", code_c)
preset = Preset()
preset.add(get_key_mapping(EventCombination(b_down), "keyboard", "b"))
preset.add(get_key_mapping(EventCombination(c_down), "keyboard", "c"))
preset.add(
get_key_mapping(
EventCombination([*w_down[:2], -10]),
InputCombination(get_combination_config(b_down)), "keyboard", "b"
)
)
preset.add(
get_key_mapping(
InputCombination(get_combination_config(c_down)), "keyboard", "c"
)
)
preset.add(
get_key_mapping(
InputCombination(get_combination_config((*w_down[:2], -10))),
"keyboard",
"w",
)
)
preset.add(
get_key_mapping(
EventCombination([*d_down[:2], 10]),
InputCombination(get_combination_config((*d_down[:2], 10))),
"keyboard",
"k(d)",
)
)
preset.add(
get_key_mapping(
EventCombination([*s_down[:2], 10]),
InputCombination(get_combination_config((*s_down[:2], 10))),
"keyboard",
"s",
)
)
preset.add(
get_key_mapping(
EventCombination([*a_down[:2], -10]),
InputCombination(get_combination_config((*a_down[:2], -10))),
"keyboard",
"a",
)
@ -222,10 +231,15 @@ class TestIdk(EventPipelineTestBase):
async def test_reset_releases_keys(self):
"""Make sure that macros and keys are releases when the stop event is set."""
preset = Preset()
preset.add(get_key_mapping(combination="1,1,1", output_symbol="hold(a)"))
preset.add(get_key_mapping(combination="1,2,1", output_symbol="b"))
input_cfg = InputCombination(InputConfig(type=1, code=1)).to_config()
preset.add(get_key_mapping(combination=input_cfg, output_symbol="hold(a)"))
input_cfg = InputCombination(InputConfig(type=1, code=2)).to_config()
preset.add(get_key_mapping(combination=input_cfg, output_symbol="b"))
input_cfg = InputCombination(InputConfig(type=1, code=3)).to_config()
preset.add(
get_key_mapping(combination="1,3,1", output_symbol="modify(c,hold(d))"),
get_key_mapping(combination=input_cfg, output_symbol="modify(c,hold(d))"),
)
event_reader = self.get_event_reader(preset, fixtures.foo_device_2_keyboard)
@ -279,7 +293,13 @@ class TestIdk(EventPipelineTestBase):
preset = Preset()
# BTN_A -> 77
system_mapping._set("b", 77)
preset.add(get_key_mapping(EventCombination([1, BTN_A, 1]), "keyboard", "b"))
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=1, code=BTN_A)),
"keyboard",
"b",
)
)
event_reader = self.get_event_reader(preset, fixtures.gamepad)
# should forward them unmodified
@ -314,7 +334,13 @@ class TestIdk(EventPipelineTestBase):
preset = Preset()
# BTN_A -> 77
system_mapping._set("b", 77)
preset.add(get_key_mapping(EventCombination([1, BTN_LEFT, 1]), "keyboard", "b"))
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=1, code=BTN_LEFT)),
"keyboard",
"b",
)
)
event_reader = self.get_event_reader(preset, fixtures.gamepad)
# should forward them unmodified
@ -353,15 +379,20 @@ class TestIdk(EventPipelineTestBase):
c = system_mapping.get("c")
mapping_1 = get_key_mapping(
EventCombination((EV_ABS, ABS_X, 1)), output_symbol="a"
InputCombination(InputConfig(type=EV_ABS, code=ABS_X, analog_threshold=1)),
output_symbol="a",
)
mapping_2 = get_key_mapping(
EventCombination(((EV_ABS, ABS_X, 1), (EV_KEY, BTN_A, 1))),
InputCombination(
get_combination_config((EV_ABS, ABS_X, 1), (EV_KEY, BTN_A, 1))
),
output_symbol="b",
)
m3 = get_key_mapping(
EventCombination(
((EV_ABS, ABS_X, 1), (EV_KEY, BTN_A, 1), (EV_KEY, BTN_B, 1)),
InputCombination(
get_combination_config(
(EV_ABS, ABS_X, 1), (EV_KEY, BTN_A, 1), (EV_KEY, BTN_B, 1)
),
),
output_symbol="c",
)
@ -423,7 +454,11 @@ class TestIdk(EventPipelineTestBase):
ev_3 = (*key, 0)
preset = Preset()
preset.add(get_key_mapping(EventCombination(ev_1), output_symbol="a"))
preset.add(
get_key_mapping(
InputCombination(get_combination_config(ev_1)), output_symbol="a"
)
)
a = system_mapping.get("a")
event_reader = self.get_event_reader(preset, fixtures.gamepad)
@ -456,14 +491,30 @@ class TestIdk(EventPipelineTestBase):
ev_5 = (EV_KEY, KEY_A, 1)
ev_6 = (EV_KEY, KEY_A, 0)
combi_1 = EventCombination((ev_5, ev_3))
combi_2 = EventCombination((ev_3, ev_5))
combi_1 = (ev_5, ev_3)
combi_2 = (ev_3, ev_5)
preset = Preset()
preset.add(get_key_mapping(EventCombination(ev_1), output_symbol="a"))
preset.add(get_key_mapping(EventCombination(ev_3), output_symbol="disable"))
preset.add(get_key_mapping(combi_1, output_symbol="b"))
preset.add(get_key_mapping(combi_2, output_symbol="c"))
preset.add(
get_key_mapping(
InputCombination(get_combination_config(ev_1)), output_symbol="a"
)
)
preset.add(
get_key_mapping(
InputCombination(get_combination_config(ev_3)), output_symbol="disable"
)
)
preset.add(
get_key_mapping(
InputCombination(get_combination_config(*combi_1)), output_symbol="b"
)
)
preset.add(
get_key_mapping(
InputCombination(get_combination_config(*combi_2)), output_symbol="c"
)
)
a = system_mapping.get("a")
b = system_mapping.get("b")
@ -494,7 +545,7 @@ class TestIdk(EventPipelineTestBase):
"""A combination that ends in a disabled key"""
# ev_5 should be forwarded and the combination triggered
await self.send_events(combi_1, event_reader)
await self.send_events(map(InputEvent.from_tuple, combi_1), event_reader)
keyboard_history = convert_to_internal_events(
global_uinputs.get_uinput("keyboard").write_history
)
@ -528,7 +579,7 @@ class TestIdk(EventPipelineTestBase):
"""A combination that starts with a disabled key"""
# only the combination should get triggered
await self.send_events(combi_2, event_reader)
await self.send_events(map(InputEvent.from_tuple, combi_2), event_reader)
keyboard_history = convert_to_internal_events(
global_uinputs.get_uinput("keyboard").write_history
)
@ -575,9 +626,17 @@ class TestIdk(EventPipelineTestBase):
b = system_mapping.get("b")
preset = Preset()
preset.add(get_key_mapping(EventCombination(down_1), output_symbol="h(k(a))"))
preset.add(
get_key_mapping(EventCombination((down_1, down_2)), output_symbol="b")
get_key_mapping(
InputCombination(get_combination_config(down_1)),
output_symbol="h(k(a))",
)
)
preset.add(
get_key_mapping(
InputCombination(get_combination_config(down_1, down_2)),
output_symbol="b",
)
)
event_reader = self.get_event_reader(preset, fixtures.gamepad)
@ -650,7 +709,7 @@ class TestIdk(EventPipelineTestBase):
scroll_release = InputEvent.from_tuple((2, 8, 0))
btn_down = InputEvent.from_tuple((1, 276, 1))
btn_up = InputEvent.from_tuple((1, 276, 0))
combination = EventCombination(((1, 276, 1), (2, 8, -1)))
combination = InputCombination(get_combination_config((1, 276, 1), (2, 8, -1)))
system_mapping.clear()
system_mapping._set("a", 30)
@ -717,13 +776,13 @@ class TestIdk(EventPipelineTestBase):
ev_6 = (EV_KEY, KEY_C, 0)
mapping_1 = Mapping(
event_combination=EventCombination(ev_2),
input_combination=InputCombination(get_combination_config(ev_2)),
target_uinput="keyboard",
output_type=EV_KEY,
output_code=BTN_TL,
)
mapping_2 = Mapping(
event_combination=EventCombination(ev_3),
input_combination=InputCombination(get_combination_config(ev_3)),
target_uinput="keyboard",
output_type=EV_KEY,
output_code=KEY_A,
@ -777,9 +836,11 @@ class TestIdk(EventPipelineTestBase):
mouse_history = mouse.write_history
# ABS_X to REL_Y if ABS_Y is above 10%
combination = EventCombination(((EV_ABS, ABS_X, 0), (EV_ABS, ABS_Y, 10)))
combination = InputCombination(
get_combination_config((EV_ABS, ABS_X, 0), (EV_ABS, ABS_Y, 10))
)
cfg = {
"event_combination": combination.json_key(),
"input_combination": combination.to_config(),
"target_uinput": "mouse",
"output_type": EV_REL,
"output_code": REL_X,
@ -854,8 +915,9 @@ class TestAbsToAbs(EventPipelineTestBase):
async def test_abs_to_abs(self):
gain = 0.5
# left x to mouse x
input_config = InputConfig(type=EV_ABS, code=ABS_X)
mapping_config = {
"event_combination": ",".join((str(EV_ABS), str(ABS_X), "0")),
"input_combination": InputCombination(input_config).to_config(),
"target_uinput": "gamepad",
"output_type": EV_ABS,
"output_code": ABS_X,
@ -865,7 +927,8 @@ class TestAbsToAbs(EventPipelineTestBase):
mapping_1 = Mapping(**mapping_config)
preset = Preset()
preset.add(mapping_1)
mapping_config["event_combination"] = ",".join((str(EV_ABS), str(ABS_Y), "0"))
input_config = InputConfig(type=EV_ABS, code=ABS_Y)
mapping_config["input_combination"] = InputCombination(input_config).to_config()
mapping_config["output_code"] = ABS_Y
mapping_2 = Mapping(**mapping_config)
preset.add(mapping_2)
@ -898,9 +961,15 @@ class TestAbsToAbs(EventPipelineTestBase):
async def test_abs_to_abs_with_input_switch(self):
gain = 0.5
input_combination = InputCombination(
(
InputConfig(type=EV_ABS, code=0),
InputConfig(type=EV_ABS, code=1, analog_threshold=10),
)
)
# left x to mouse x
mapping_config = {
"event_combination": f"{EV_ABS},0,{USE_AS_ANALOG_VALUE}+{EV_ABS},1,10",
"input_combination": input_combination.to_config(),
"target_uinput": "gamepad",
"output_type": EV_ABS,
"output_code": ABS_X,
@ -956,8 +1025,9 @@ class TestRelToAbs(EventPipelineTestBase):
gain = 0.5
# left mouse x to abs x
cutoff = 2
input_combination = InputCombination(InputConfig(type=EV_REL, code=REL_X))
mapping_config = {
"event_combination": f"{EV_REL},{REL_X},{USE_AS_ANALOG_VALUE}",
"input_combination": input_combination.to_config(),
"target_uinput": "gamepad",
"output_type": EV_ABS,
"output_code": ABS_X,
@ -969,7 +1039,8 @@ class TestRelToAbs(EventPipelineTestBase):
mapping_1 = Mapping(**mapping_config)
preset = Preset()
preset.add(mapping_1)
mapping_config["event_combination"] = f"{EV_REL},{REL_Y},{USE_AS_ANALOG_VALUE}"
input_combination = InputCombination(InputConfig(type=EV_REL, code=REL_Y))
mapping_config["input_combination"] = input_combination.to_config()
mapping_config["output_code"] = ABS_Y
mapping_2 = Mapping(**mapping_config)
preset.add(mapping_2)
@ -1030,11 +1101,15 @@ class TestRelToAbs(EventPipelineTestBase):
gain = 0.5
cutoff = 1
input_combination = InputCombination(
(
InputConfig(type=EV_REL, code=REL_X),
InputConfig(type=EV_REL, code=REL_Y, analog_threshold=10),
)
)
# left mouse x to x
mapping_config = {
"event_combination": (
f"{EV_REL},{REL_X},{USE_AS_ANALOG_VALUE}+{EV_REL},{REL_Y},10"
),
"input_combination": input_combination.to_config(),
"target_uinput": "gamepad",
"output_type": EV_ABS,
"output_code": ABS_X,
@ -1086,8 +1161,9 @@ class TestAbsToRel(EventPipelineTestBase):
rel_rate = 60 # rate [Hz] at which events are produced
gain = 0.5 # halve the speed of the rel axis
# left x to mouse x
input_config = InputConfig(type=EV_ABS, code=ABS_X)
mapping_config = {
"event_combination": ",".join((str(EV_ABS), str(ABS_X), "0")),
"input_combination": InputCombination(input_config).to_config(),
"target_uinput": "mouse",
"output_type": EV_REL,
"output_code": REL_X,
@ -1099,7 +1175,8 @@ class TestAbsToRel(EventPipelineTestBase):
preset = Preset()
preset.add(mapping_1)
# left y to mouse y
mapping_config["event_combination"] = ",".join((str(EV_ABS), str(ABS_Y), "0"))
input_config = InputConfig(type=EV_ABS, code=ABS_Y)
mapping_config["input_combination"] = InputCombination(input_config).to_config()
mapping_config["output_code"] = REL_Y
mapping_2 = Mapping(**mapping_config)
preset.add(mapping_2)
@ -1159,8 +1236,9 @@ class TestAbsToRel(EventPipelineTestBase):
rel_rate = 60 # rate [Hz] at which events are produced
gain = 1
# left x to mouse x
input_config = InputConfig(type=EV_ABS, code=ABS_X)
mapping_config = {
"event_combination": ",".join((str(EV_ABS), str(ABS_X), "0")),
"input_combination": InputCombination(input_config).to_config(),
"target_uinput": "mouse",
"output_type": EV_REL,
"output_code": REL_WHEEL,
@ -1173,7 +1251,8 @@ class TestAbsToRel(EventPipelineTestBase):
preset = Preset()
preset.add(mapping_1)
# left y to mouse y
mapping_config["event_combination"] = ",".join((str(EV_ABS), str(ABS_Y), "0"))
input_config = InputConfig(type=EV_ABS, code=ABS_Y)
mapping_config["input_combination"] = InputCombination(input_config).to_config()
mapping_config["output_code"] = REL_HWHEEL_HI_RES
mapping_2 = Mapping(**mapping_config)
preset.add(mapping_2)
@ -1241,8 +1320,12 @@ class TestRelToBtn(EventPipelineTestBase):
# set a high release timeout to make sure the tests pass
release_timeout = 0.2
mapping_1 = get_key_mapping(EventCombination(hw_right), "keyboard", "k(b)")
mapping_2 = get_key_mapping(EventCombination(w_up), "keyboard", "c")
mapping_1 = get_key_mapping(
InputCombination(get_combination_config(hw_right)), "keyboard", "k(b)"
)
mapping_2 = get_key_mapping(
InputCombination(get_combination_config(w_up)), "keyboard", "c"
)
mapping_1.release_timeout = release_timeout
mapping_2.release_timeout = release_timeout
@ -1287,14 +1370,14 @@ class TestRelToBtn(EventPipelineTestBase):
async def test_rel_trigger_threshold(self):
"""Test that different activation points for rel_to_btn work correctly."""
# at 30% map to a
# at 5 map to a
mapping_1 = get_key_mapping(
EventCombination((EV_REL, REL_X, 5)),
InputCombination(InputConfig(type=EV_REL, code=REL_X, analog_threshold=5)),
output_symbol="a",
)
# at 70% map to b
# at 15 map to b
mapping_2 = get_key_mapping(
EventCombination((EV_REL, REL_X, 15)),
InputCombination(InputConfig(type=EV_REL, code=REL_X, analog_threshold=15)),
output_symbol="b",
)
release_timeout = 0.2 # give some time to do assertions before the release
@ -1323,6 +1406,7 @@ class TestRelToBtn(EventPipelineTestBase):
global_uinputs.get_uinput("keyboard").write_history
)
self.assertEqual(keyboard_history, [(EV_KEY, a, 1), (EV_KEY, a, 0)])
self.assertEqual(keyboard_history.count((EV_KEY, a, 1)), 1)
self.assertEqual(keyboard_history.count((EV_KEY, a, 0)), 1)
self.assertNotIn((EV_KEY, b, 1), keyboard_history)
@ -1363,12 +1447,12 @@ class TestAbsToBtn(EventPipelineTestBase):
# at 30% map to a
mapping_1 = get_key_mapping(
EventCombination((EV_ABS, ABS_X, 30)),
InputCombination(InputConfig(type=EV_ABS, code=ABS_X, analog_threshold=30)),
output_symbol="a",
)
# at 70% map to b
mapping_2 = get_key_mapping(
EventCombination((EV_ABS, ABS_X, 70)),
InputCombination(InputConfig(type=EV_ABS, code=ABS_X, analog_threshold=70)),
output_symbol="b",
)
preset = Preset()
@ -1433,9 +1517,9 @@ class TestRelToRel(EventPipelineTestBase):
async def _test(self, input_code, input_value, output_code, output_value, gain=1):
preset = Preset()
input_event = InputEvent(0, 0, EV_REL, input_code, USE_AS_ANALOG_VALUE)
input_config = InputConfig(type=EV_REL, code=input_code)
mapping = Mapping(
event_combination=EventCombination(input_event),
input_combination=InputCombination(input_config).to_config(),
target_uinput="mouse",
output_type=EV_REL,
output_code=output_code,
@ -1493,9 +1577,9 @@ class TestRelToRel(EventPipelineTestBase):
preset = Preset()
input_event = InputEvent(0, 0, EV_REL, input_code, USE_AS_ANALOG_VALUE)
input_config = InputConfig(type=EV_REL, code=input_code)
mapping = Mapping(
event_combination=EventCombination(input_event),
input_combination=InputCombination(input_config).to_config(),
target_uinput="mouse",
output_type=EV_REL,
output_code=output_code,
@ -1541,10 +1625,10 @@ class TestRelToRel(EventPipelineTestBase):
history = global_uinputs.get_uinput("mouse").write_history
# REL_WHEEL_HI_RES to REL_Y
input_event = InputEvent(0, 0, EV_REL, REL_WHEEL_HI_RES, USE_AS_ANALOG_VALUE)
input_config = InputConfig(type=EV_REL, code=REL_WHEEL_HI_RES)
gain = 0.01
mapping = Mapping(
event_combination=EventCombination(input_event),
input_combination=InputCombination(input_config).to_config(),
target_uinput="mouse",
output_type=EV_REL,
output_code=REL_Y,

@ -39,9 +39,14 @@ from evdev.ecodes import (
REL_Y,
REL_WHEEL,
)
from inputremapper.injection.mapping_handlers.combination_handler import (
CombinationHandler,
)
from inputremapper.injection.mapping_handlers.rel_to_btn_handler import RelToBtnHandler
from inputremapper.configs.mapping import Mapping, DEFAULT_REL_RATE
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.injection.global_uinputs import global_uinputs
from inputremapper.injection.mapping_handlers.abs_to_abs_handler import AbsToAbsHandler
from inputremapper.injection.mapping_handlers.abs_to_btn_handler import AbsToBtnHandler
@ -55,7 +60,7 @@ from inputremapper.injection.mapping_handlers.key_handler import KeyHandler
from inputremapper.injection.mapping_handlers.macro_handler import MacroHandler
from inputremapper.injection.mapping_handlers.mapping_handler import MappingHandler
from inputremapper.injection.mapping_handlers.rel_to_abs_handler import RelToAbsHandler
from inputremapper.input_event import InputEvent, EventActions, USE_AS_ANALOG_VALUE
from inputremapper.input_event import InputEvent, EventActions
from tests.lib.cleanup import cleanup
from tests.lib.patches import InputDevice
from tests.lib.constants import MAX_ABS
@ -84,10 +89,16 @@ class BaseTests:
class TestAxisSwitchHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(
(
InputConfig(type=2, code=5),
InputConfig(type=1, code=3),
)
)
self.handler = AxisSwitchHandler(
EventCombination.from_string("2,5,0+1,3,1"),
input_combination,
Mapping(
event_combination="2,5,0+1,3,1",
input_combination=input_combination.to_config(),
target_uinput="mouse",
output_type=2,
output_code=1,
@ -97,10 +108,13 @@ class TestAxisSwitchHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
class TestAbsToBtnHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(
InputConfig(type=3, code=5, analog_threshold=10)
)
self.handler = AbsToBtnHandler(
EventCombination.from_string("3,5,10"),
input_combination,
Mapping(
event_combination="3,5,10",
input_combination=input_combination.to_config(),
target_uinput="mouse",
output_symbol="BTN_LEFT",
),
@ -109,10 +123,11 @@ class TestAbsToBtnHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
class TestAbsToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(InputConfig(type=EV_ABS, code=ABS_X))
self.handler = AbsToAbsHandler(
EventCombination((EV_ABS, ABS_X, 0)),
input_combination,
Mapping(
event_combination=f"{EV_ABS},{ABS_X},0",
input_combination=input_combination.to_config(),
target_uinput="gamepad",
output_type=EV_ABS,
output_code=ABS_X,
@ -135,10 +150,11 @@ class TestAbsToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
class TestRelToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(InputConfig(type=EV_REL, code=REL_X))
self.handler = RelToAbsHandler(
EventCombination((EV_REL, REL_X, 0)),
input_combination,
Mapping(
event_combination=f"{EV_REL},{REL_X},0",
input_combination=input_combination.to_config(),
target_uinput="gamepad",
output_type=EV_ABS,
output_code=ABS_X,
@ -202,10 +218,11 @@ class TestRelToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
class TestAbsToRelHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(InputConfig(type=EV_ABS, code=ABS_X))
self.handler = AbsToRelHandler(
EventCombination((EV_ABS, ABS_X, 0)),
input_combination,
Mapping(
event_combination=f"{EV_ABS},{ABS_X},0",
input_combination=input_combination.to_config(),
target_uinput="mouse",
output_type=EV_REL,
output_code=REL_X,
@ -229,11 +246,19 @@ class TestAbsToRelHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
class TestCombinationHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
handler: CombinationHandler
def setUp(self):
self.handler = AxisSwitchHandler(
EventCombination.from_string("2,0,10+1,3,1"),
input_combination = InputCombination(
(
InputConfig(type=2, code=0, analog_threshold=10),
InputConfig(type=1, code=3),
)
)
self.handler = CombinationHandler(
input_combination,
Mapping(
event_combination="2,0,10+1,3,1",
input_combination=input_combination.to_config(),
target_uinput="mouse",
output_symbol="BTN_LEFT",
),
@ -247,7 +272,7 @@ class TestHierarchyHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
self.mock3 = MagicMock()
self.handler = HierarchyHandler(
[self.mock1, self.mock2, self.mock3],
InputEvent.from_tuple((EV_KEY, KEY_A, 1)),
InputConfig(type=EV_KEY, code=KEY_A),
)
def test_reset(self):
@ -259,10 +284,16 @@ class TestHierarchyHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
class TestKeyHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(
(
InputConfig(type=2, code=0, analog_threshold=10),
InputConfig(type=1, code=3),
)
)
self.handler = KeyHandler(
EventCombination.from_string("2,0,10+1,3,1"),
input_combination,
Mapping(
event_combination="2,0,10+1,3,1",
input_combination=input_combination.to_config(),
target_uinput="mouse",
output_symbol="BTN_LEFT",
),
@ -290,11 +321,17 @@ class TestKeyHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
class TestMacroHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(
(
InputConfig(type=2, code=0, analog_threshold=10),
InputConfig(type=1, code=3),
)
)
self.context_mock = MagicMock()
self.handler = MacroHandler(
EventCombination.from_string("2,0,10+1,3,1"),
input_combination,
Mapping(
event_combination="2,0,10+1,3,1",
input_combination=input_combination.to_config(),
target_uinput="mouse",
output_symbol="hold_keys(BTN_LEFT, BTN_RIGHT)",
),
@ -326,12 +363,15 @@ class TestMacroHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
self.assertEqual(len(history), 4)
class TestRelToBtnHanlder(BaseTests, unittest.IsolatedAsyncioTestCase):
class TestRelToBtnHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.handler = AxisSwitchHandler(
EventCombination.from_string("2,0,10+1,3,1"),
input_combination = InputCombination(
InputConfig(type=2, code=0, analog_threshold=10)
)
self.handler = RelToBtnHandler(
input_combination,
Mapping(
event_combination="2,0,10+1,3,1",
input_combination=input_combination.to_config(),
target_uinput="mouse",
output_symbol="BTN_LEFT",
),
@ -339,12 +379,14 @@ class TestRelToBtnHanlder(BaseTests, unittest.IsolatedAsyncioTestCase):
class TestRelToRelHanlder(BaseTests, unittest.IsolatedAsyncioTestCase):
handler: RelToRelHandler
def setUp(self):
input_ = InputEvent(0, 0, EV_REL, REL_X, USE_AS_ANALOG_VALUE)
input_combination = InputCombination(InputConfig(type=EV_REL, code=REL_X))
self.handler = RelToRelHandler(
EventCombination(input_),
input_combination,
Mapping(
event_combination=EventCombination(input_),
input_combination=input_combination.to_config(),
output_type=EV_REL,
output_code=REL_Y,
output_value=20,
@ -360,7 +402,7 @@ class TestRelToRelHanlder(BaseTests, unittest.IsolatedAsyncioTestCase):
0,
EV_REL,
REL_X,
USE_AS_ANALOG_VALUE,
0,
)
)
)

@ -39,18 +39,22 @@ from evdev.ecodes import (
from inputremapper.configs.mapping import Mapping
from inputremapper.configs.preset import Preset
from inputremapper.configs.system_mapping import system_mapping
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.injection.context import Context
from inputremapper.injection.event_reader import EventReader
from inputremapper.injection.global_uinputs import global_uinputs
from tests.lib.fixtures import new_event
from tests.lib.fixtures import (
new_event,
get_combination_config,
get_key_mapping,
fixtures,
)
from tests.lib.cleanup import quick_cleanup
from tests.lib.fixtures import get_key_mapping
class TestEventReader(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.gamepad_source = evdev.InputDevice("/dev/input/event30")
self.gamepad_source = evdev.InputDevice(fixtures.gamepad.path)
self.stop_event = asyncio.Event()
self.preset = Preset()
@ -77,18 +81,37 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase):
self.preset.add(
get_key_mapping(
EventCombination([EV_KEY, trigger, 1]),
InputCombination(
InputConfig(
type=EV_KEY,
code=trigger,
origin_hash=fixtures.gamepad.get_device_hash(),
)
),
"keyboard",
"if_single(key(a), key(KEY_LEFTSHIFT))",
)
)
self.preset.add(
get_key_mapping(EventCombination([EV_ABS, ABS_Y, 1]), "keyboard", "b"),
get_key_mapping(
InputCombination(
InputConfig(
type=EV_ABS,
code=ABS_Y,
analog_threshold=1,
origin_hash=fixtures.gamepad.get_device_hash(),
)
),
"keyboard",
"b",
),
)
# left x to mouse x
cfg = {
"event_combination": ",".join((str(EV_ABS), str(ABS_X), "0")),
"input_combination": InputConfig(
type=EV_ABS, code=ABS_X, origin_hash=fixtures.gamepad.get_device_hash()
),
"target_uinput": "mouse",
"output_type": EV_REL,
"output_code": REL_X,
@ -96,17 +119,23 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase):
self.preset.add(Mapping(**cfg))
# left y to mouse y
cfg["event_combination"] = ",".join((str(EV_ABS), str(ABS_Y), "0"))
cfg["input_combination"] = InputConfig(
type=EV_ABS, code=ABS_Y, origin_hash=fixtures.gamepad.get_device_hash()
)
cfg["output_code"] = REL_Y
self.preset.add(Mapping(**cfg))
# right x to wheel x
cfg["event_combination"] = ",".join((str(EV_ABS), str(ABS_RX), "0"))
cfg["input_combination"] = InputConfig(
type=EV_ABS, code=ABS_RX, origin_hash=fixtures.gamepad.get_device_hash()
)
cfg["output_code"] = REL_HWHEEL_HI_RES
self.preset.add(Mapping(**cfg))
# right y to wheel y
cfg["event_combination"] = ",".join((str(EV_ABS), str(ABS_RY), "0"))
cfg["input_combination"] = InputConfig(
type=EV_ABS, code=ABS_RY, origin_hash=fixtures.gamepad.get_device_hash()
)
cfg["output_code"] = REL_WHEEL_HI_RES
self.preset.add(Mapping(**cfg))
@ -139,13 +168,30 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase):
trigger = evdev.ecodes.BTN_A
self.preset.add(
get_key_mapping(
EventCombination([EV_KEY, trigger, 1]),
InputCombination(
InputConfig(
type=EV_KEY,
code=trigger,
origin_hash=fixtures.gamepad.get_device_hash(),
)
),
"keyboard",
"if_single(k(a), k(KEY_LEFTSHIFT))",
)
)
self.preset.add(
get_key_mapping(EventCombination([EV_ABS, ABS_Y, 1]), "keyboard", "b"),
get_key_mapping(
InputCombination(
InputConfig(
type=EV_ABS,
code=ABS_Y,
analog_threshold=1,
origin_hash=fixtures.gamepad.get_device_hash(),
)
),
"keyboard",
"b",
),
)
# self.preset.set("gamepad.joystick.left_purpose", BUTTONS)

@ -23,7 +23,7 @@ from tests.lib.fixtures import new_event
from tests.lib.patches import uinputs
from tests.lib.cleanup import quick_cleanup
from tests.lib.constants import EVENT_READ_TIMEOUT
from tests.lib.fixtures import fixtures
from tests.lib.fixtures import fixtures, get_combination_config
from tests.lib.pipes import uinput_write_history_pipe
from tests.lib.pipes import read_write_history_pipe, push_events
from tests.lib.fixtures import (
@ -61,7 +61,7 @@ from inputremapper.configs.system_mapping import (
DISABLE_NAME,
)
from inputremapper.configs.preset import Preset
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.injection.macros.parse import parse
from inputremapper.injection.context import Context
from inputremapper.groups import groups, classify, DeviceType
@ -107,11 +107,22 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
quick_cleanup()
def initialize_injector(self, group, preset: Preset):
self.injector = Injector(group, preset)
self.injector._devices = self.injector.group.get_devices()
self.injector._update_preset()
def test_grab(self):
# path is from the fixtures
path = "/dev/input/event10"
preset = Preset()
preset.add(get_key_mapping(EventCombination([EV_KEY, 10, 1]), "keyboard", "a"))
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=10)),
"keyboard",
"a",
)
)
self.injector = Injector(groups.find(key="Foo Device 2"), preset)
# this test needs to pass around all other constraints of
@ -127,7 +138,13 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
def test_fail_grab(self):
self.make_it_fail = 999
preset = Preset()
preset.add(get_key_mapping(EventCombination([EV_KEY, 10, 1]), "keyboard", "a"))
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=10)),
"keyboard",
"a",
)
)
self.injector = Injector(groups.find(key="Foo Device 2"), preset)
path = "/dev/input/event10"
@ -148,9 +165,15 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
def test_grab_device_1(self):
preset = Preset()
preset.add(
get_key_mapping(EventCombination([EV_ABS, ABS_HAT0X, 1]), "keyboard", "a"),
get_key_mapping(
InputCombination(
InputConfig(type=EV_ABS, code=ABS_HAT0X, analog_threshold=1)
),
"keyboard",
"a",
),
)
self.injector = Injector(groups.find(name="gamepad"), preset)
self.initialize_injector(groups.find(name="gamepad"), preset)
self.injector.context = Context(preset)
self.injector.group.paths = [
"/dev/input/event10",
@ -165,16 +188,18 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
def test_forward_gamepad_events(self):
# forward abs joystick events
preset = Preset()
self.injector = Injector(groups.find(name="gamepad"), preset)
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=BTN_A)),
"keyboard",
"a",
),
)
self.initialize_injector(groups.find(name="gamepad"), preset)
self.injector.context = Context(preset)
path = "/dev/input/event30"
device = self.injector._grab_devices()
self.assertEqual(device, []) # no capability is used, so it won't grab
preset.add(
get_key_mapping(EventCombination([EV_KEY, BTN_A, 1]), "keyboard", "a"),
)
devices = self.injector._grab_devices()
self.assertEqual(len(devices), 1)
self.assertEqual(devices[0].path, path)
@ -184,25 +209,34 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
def test_skip_unused_device(self):
# skips a device because its capabilities are not used in the preset
preset = Preset()
preset.add(get_key_mapping(EventCombination([EV_KEY, 10, 1]), "keyboard", "a"))
self.injector = Injector(groups.find(key="Foo Device 2"), preset)
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=10)),
"keyboard",
"a",
)
)
self.initialize_injector(groups.find(key="Foo Device 2"), preset)
self.injector.context = Context(preset)
path = "/dev/input/event11"
self.injector.group.paths = [path]
# grabs only one device even though the group has 4 devices
devices = self.injector._grab_devices()
self.assertEqual(devices, [])
self.assertEqual(self.failed, 0)
self.assertEqual(len(devices), 1)
self.assertEqual(self.failed, 2)
def test_skip_unknown_device(self):
preset = Preset()
preset.add(get_key_mapping(EventCombination([EV_KEY, 10, 1]), "keyboard", "a"))
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=1234)),
"keyboard",
"a",
)
)
# skips a device because its capabilities are not used in the preset
self.injector = Injector(groups.find(key="Foo Device 2"), preset)
self.initialize_injector(groups.find(key="Foo Device 2"), preset)
self.injector.context = Context(preset)
path = "/dev/input/event11"
self.injector.group.paths = [path]
devices = self.injector._grab_devices()
# skips the device alltogether, so no grab attempts fail
@ -226,9 +260,26 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
@mock.patch("evdev.InputDevice.ungrab")
def test_capabilities_and_uinput_presence(self, ungrab_patch):
preset = Preset()
m1 = get_key_mapping(EventCombination([EV_KEY, KEY_A, 1]), "keyboard", "c")
m1 = get_key_mapping(
InputCombination(
InputConfig(
type=EV_KEY,
code=KEY_A,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
),
"keyboard",
"c",
)
m2 = get_key_mapping(
EventCombination([EV_REL, REL_HWHEEL, 1]),
InputCombination(
InputConfig(
type=EV_REL,
code=REL_HWHEEL,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
)
),
"keyboard",
"key(b)",
)
@ -239,11 +290,28 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
self.injector.run()
self.assertEqual(
self.injector.preset.get_mapping(EventCombination([EV_KEY, KEY_A, 1])),
self.injector.preset.get_mapping(
InputCombination(
InputConfig(
type=EV_KEY,
code=KEY_A,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
)
),
m1,
)
self.assertEqual(
self.injector.preset.get_mapping(EventCombination([EV_REL, REL_HWHEEL, 1])),
self.injector.preset.get_mapping(
InputCombination(
InputConfig(
type=EV_REL,
code=REL_HWHEEL,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
)
)
),
m2,
)
@ -286,14 +354,34 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
preset = Preset()
preset.add(
get_key_mapping(
EventCombination(((EV_KEY, 8, 1), (EV_KEY, 9, 1))),
InputCombination(
(
InputConfig(
type=EV_KEY,
code=8,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
),
InputConfig(
type=EV_KEY,
code=9,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
),
)
),
"keyboard",
"k(KEY_Q).k(w)",
)
)
preset.add(
get_key_mapping(
EventCombination([EV_ABS, ABS_HAT0X, -1]),
InputCombination(
InputConfig(
type=EV_ABS,
code=ABS_HAT0X,
analog_threshold=-1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
),
"keyboard",
"a",
)
@ -303,23 +391,52 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
with self.assertRaises(ValidationError):
preset.add(
get_key_mapping(
EventCombination([EV_KEY, input_b, 1]),
InputCombination(
InputConfig(
type=EV_KEY,
code=input_b,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
),
"keyboard",
"b",
)
)
self.injector = Injector(groups.find(key="Foo Device 2"), preset)
self.assertEqual(self.injector.get_state(), InjectorState.UNKNOWN)
self.injector.start()
self.assertEqual(self.injector.get_state(), InjectorState.STARTING)
uinput_write_history_pipe[0].poll(timeout=1)
self.assertEqual(self.injector.get_state(), InjectorState.RUNNING)
time.sleep(EVENT_READ_TIMEOUT * 10)
push_events(
fixtures.gamepad,
fixtures.foo_device_2_keyboard,
[
# should execute a macro...
new_event(EV_KEY, 8, 1), # forwarded
new_event(EV_KEY, 9, 1), # triggers macro
new_event(EV_KEY, 8, 0), # releases macro
new_event(EV_KEY, 9, 0), # forwarded
],
)
time.sleep(0.1) # give a chance that everything arrives in order
push_events(
fixtures.foo_device_2_gamepad,
[
# gamepad stuff. trigger a combination
new_event(EV_ABS, ABS_HAT0X, -1),
new_event(EV_ABS, ABS_HAT0X, 0),
],
)
time.sleep(0.1)
push_events(
fixtures.foo_device_2_keyboard,
[
# just pass those over without modifying
new_event(EV_KEY, 10, 1),
new_event(EV_KEY, 10, 0),
@ -328,14 +445,8 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
force=True,
)
self.injector = Injector(groups.find(name="gamepad"), preset)
self.assertEqual(self.injector.get_state(), InjectorState.UNKNOWN)
self.injector.start()
self.assertEqual(self.injector.get_state(), InjectorState.STARTING)
uinput_write_history_pipe[0].poll(timeout=1)
self.assertEqual(self.injector.get_state(), InjectorState.RUNNING)
time.sleep(EVENT_READ_TIMEOUT * 10)
# the injector needs time to process this
time.sleep(0.1)
# sending anything arbitrary does not stop the process
# (is_alive checked later after some time)
@ -400,18 +511,18 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
self.assertEqual(self.injector.get_state(), InjectorState.RUNNING)
def test_is_in_capabilities(self):
key = EventCombination((1, 2, 1))
key = InputCombination(get_combination_config((1, 2, 1)))
capabilities = {1: [9, 2, 5]}
self.assertTrue(is_in_capabilities(key, capabilities))
key = EventCombination(((1, 2, 1), (1, 3, 1)))
key = InputCombination(get_combination_config((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,
# that make up one hardware device
self.assertTrue(is_in_capabilities(key, capabilities))
key = EventCombination(((1, 2, 1), (1, 5, 1)))
key = InputCombination(get_combination_config((1, 2, 1), (1, 5, 1)))
capabilities = {1: [9, 2, 5]}
self.assertTrue(is_in_capabilities(key, capabilities))
@ -459,10 +570,16 @@ class TestModifyCapabilities(unittest.TestCase):
return self._capabilities
preset = Preset()
preset.add(get_key_mapping(EventCombination([EV_KEY, 80, 1]), "keyboard", "a"))
preset.add(
get_key_mapping(
EventCombination([EV_KEY, 81, 1]),
InputCombination(InputConfig(type=EV_KEY, code=80)),
"keyboard",
"a",
)
)
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=81)),
"keyboard",
DISABLE_NAME,
),
@ -473,14 +590,22 @@ class TestModifyCapabilities(unittest.TestCase):
preset.add(
get_key_mapping(
EventCombination([EV_KEY, 60, 111]), "keyboard", macro_code
InputCombination(InputConfig(type=EV_KEY, code=60)),
"keyboard",
macro_code,
),
)
# going to be ignored, because EV_REL cannot be mapped, that's
# mouse movements.
preset.add(
get_key_mapping(EventCombination([EV_REL, 1234, 3]), "keyboard", "b"),
get_key_mapping(
InputCombination(
InputConfig(type=EV_REL, code=1234, analog_threshold=3)
),
"keyboard",
"b",
),
)
self.a = system_mapping.get("a")
@ -506,14 +631,6 @@ class TestModifyCapabilities(unittest.TestCase):
quick_cleanup()
def test_copy_capabilities(self):
self.preset.add(
get_key_mapping(
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
self.injector = Injector(mock.Mock(), self.preset)

@ -0,0 +1,511 @@
#!/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
from evdev.ecodes import (
EV_KEY,
EV_ABS,
EV_REL,
BTN_C,
BTN_B,
BTN_A,
BTN_MIDDLE,
REL_X,
REL_Y,
REL_WHEEL,
REL_HWHEEL,
ABS_RY,
ABS_X,
ABS_HAT0Y,
ABS_HAT0X,
KEY_A,
KEY_LEFTSHIFT,
KEY_RIGHTALT,
KEY_LEFTCTRL,
)
from inputremapper.configs.input_config import InputCombination, InputConfig
from tests.lib.fixtures import get_combination_config
class TestInputConfig(unittest.TestCase):
def test_input_config(self):
test_cases = [
# basic test, nothing fancy here
{
"input": {
"type": EV_KEY,
"code": KEY_A,
"origin_hash": "foo",
},
"properties": {
"type": EV_KEY,
"code": KEY_A,
"origin_hash": "foo",
"input_match_hash": (EV_KEY, KEY_A, "foo"),
"defines_analog_input": False,
"type_and_code": (EV_KEY, KEY_A),
},
"methods": [
{
"name": "description",
"args": (),
"kwargs": {},
"return": "a",
},
{
"name": "__hash__",
"args": (),
"kwargs": {},
"return": hash((EV_KEY, KEY_A, "foo", None)),
},
],
},
# removes analog_threshold
{
"input": {
"type": EV_KEY,
"code": KEY_A,
"origin_hash": "foo",
"analog_threshold": 10,
},
"properties": {
"type": EV_KEY,
"code": KEY_A,
"origin_hash": "foo",
"analog_threshold": None,
"input_match_hash": (EV_KEY, KEY_A, "foo"),
"defines_analog_input": False,
"type_and_code": (EV_KEY, KEY_A),
},
"methods": [
{
"name": "description",
"args": (),
"kwargs": {},
"return": "a",
},
{
"name": "__hash__",
"args": (),
"kwargs": {},
"return": hash((EV_KEY, KEY_A, "foo", None)),
},
],
},
# abs to btn
{
"input": {
"type": EV_ABS,
"code": ABS_X,
"origin_hash": "foo",
"analog_threshold": 10,
},
"properties": {
"type": EV_ABS,
"code": ABS_X,
"origin_hash": "foo",
"analog_threshold": 10,
"input_match_hash": (EV_ABS, ABS_X, "foo"),
"defines_analog_input": False,
"type_and_code": (EV_ABS, ABS_X),
},
"methods": [
{
"name": "description",
"args": (),
"kwargs": {},
"return": "Joystick-X Right 10%",
},
{
"name": "description",
"args": (),
"kwargs": {"exclude_threshold": True},
"return": "Joystick-X Right",
},
{
"name": "description",
"args": (),
"kwargs": {
"exclude_threshold": True,
"exclude_direction": True,
},
"return": "Joystick-X",
},
{
"name": "__hash__",
"args": (),
"kwargs": {},
"return": hash((EV_ABS, ABS_X, "foo", 10)),
},
],
},
# abs to btn with d-pad
{
"input": {
"type": EV_ABS,
"code": ABS_HAT0Y,
"origin_hash": "foo",
"analog_threshold": 10,
},
"properties": {
"type": EV_ABS,
"code": ABS_HAT0Y,
"origin_hash": "foo",
"analog_threshold": 10,
"input_match_hash": (EV_ABS, ABS_HAT0Y, "foo"),
"defines_analog_input": False,
"type_and_code": (EV_ABS, ABS_HAT0Y),
},
"methods": [
{
"name": "description",
"args": (),
"kwargs": {},
"return": "DPad-Y Down 10%",
},
{
"name": "__hash__",
"args": (),
"kwargs": {},
"return": hash((EV_ABS, ABS_HAT0Y, "foo", 10)),
},
],
},
# rel to btn
{
"input": {
"type": EV_REL,
"code": REL_Y,
"origin_hash": "foo",
"analog_threshold": 10,
},
"properties": {
"type": EV_REL,
"code": REL_Y,
"origin_hash": "foo",
"analog_threshold": 10,
"input_match_hash": (EV_REL, REL_Y, "foo"),
"defines_analog_input": False,
"type_and_code": (EV_REL, REL_Y),
},
"methods": [
{
"name": "description",
"args": (),
"kwargs": {},
"return": "Y Down 10",
},
{
"name": "__hash__",
"args": (),
"kwargs": {},
"return": hash((EV_REL, REL_Y, "foo", 10)),
},
],
},
# abs as axis
{
"input": {
"type": EV_ABS,
"code": ABS_X,
"origin_hash": "foo",
"analog_threshold": 0,
},
"properties": {
"type": EV_ABS,
"code": ABS_X,
"origin_hash": "foo",
"analog_threshold": None,
"input_match_hash": (EV_ABS, ABS_X, "foo"),
"defines_analog_input": True,
"type_and_code": (EV_ABS, ABS_X),
},
"methods": [
{
"name": "description",
"args": (),
"kwargs": {},
"return": "Joystick-X",
},
{
"name": "description",
"args": (),
"kwargs": {
"exclude_threshold": True,
"exclude_direction": True,
},
"return": "Joystick-X",
},
{
"name": "__hash__",
"args": (),
"kwargs": {},
"return": hash((EV_ABS, ABS_X, "foo", None)),
},
],
},
# rel as axis
{
"input": {
"type": EV_REL,
"code": REL_WHEEL,
"origin_hash": "foo",
},
"properties": {
"type": EV_REL,
"code": REL_WHEEL,
"origin_hash": "foo",
"analog_threshold": None,
"input_match_hash": (EV_REL, REL_WHEEL, "foo"),
"defines_analog_input": True,
"type_and_code": (EV_REL, REL_WHEEL),
},
"methods": [
{
"name": "description",
"args": (),
"kwargs": {},
"return": "Wheel",
},
{
"name": "__hash__",
"args": (),
"kwargs": {},
"return": hash((EV_REL, REL_WHEEL, "foo", None)),
},
],
},
]
for test_case in test_cases:
input_config = InputConfig(**test_case["input"])
for property_, value in test_case["properties"].items():
self.assertEqual(
value,
getattr(input_config, property_),
f"property mismatch for input: {test_case['input']} "
f"property: {property_} expected value: {value}",
)
for method in test_case["methods"]:
self.assertEqual(
method["return"],
getattr(input_config, method["name"])(
*method["args"], **method["kwargs"]
),
f"wrong method return for input: {test_case['input']} "
f"method: {method}",
)
def test_is_immutable(self):
input_config = InputConfig(type=1, code=2)
with self.assertRaises(TypeError):
input_config.origin_hash = "foo"
class TestInputCombination(unittest.TestCase):
def test_get_permutations(self):
key_1 = InputCombination(get_combination_config((1, 3, 1)))
self.assertEqual(len(key_1.get_permutations()), 1)
self.assertEqual(key_1.get_permutations()[0], key_1)
key_2 = InputCombination(get_combination_config((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 = InputCombination(
get_combination_config((1, 3, 1), (1, 5, 1), (1, 7, 1))
)
self.assertEqual(len(key_3.get_permutations()), 2)
self.assertEqual(
key_3.get_permutations()[0],
InputCombination(get_combination_config((1, 3, 1), (1, 5, 1), (1, 7, 1))),
)
self.assertEqual(
key_3.get_permutations()[1],
InputCombination(get_combination_config((1, 5, 1), (1, 3, 1), (1, 7, 1))),
)
def test_is_problematic(self):
key_1 = InputCombination(
get_combination_config((1, KEY_LEFTSHIFT, 1), (1, 5, 1))
)
self.assertTrue(key_1.is_problematic())
key_2 = InputCombination(
get_combination_config((1, KEY_RIGHTALT, 1), (1, 5, 1))
)
self.assertTrue(key_2.is_problematic())
key_3 = InputCombination(
get_combination_config((1, 3, 1), (1, KEY_LEFTCTRL, 1))
)
self.assertTrue(key_3.is_problematic())
key_4 = InputCombination(get_combination_config((1, 3, 1)))
self.assertFalse(key_4.is_problematic())
key_5 = InputCombination(get_combination_config((1, 3, 1), (1, 5, 1)))
self.assertFalse(key_5.is_problematic())
def test_init(self):
self.assertRaises(TypeError, lambda: InputCombination(1))
self.assertRaises(TypeError, lambda: InputCombination(None))
self.assertRaises(TypeError, lambda: InputCombination([1]))
self.assertRaises(TypeError, lambda: InputCombination((1,)))
self.assertRaises(TypeError, lambda: InputCombination((1, 2)))
self.assertRaises(TypeError, lambda: InputCombination("1"))
self.assertRaises(TypeError, lambda: InputCombination("(1,2,3)"))
self.assertRaises(
TypeError,
lambda: InputCombination(((1, 2, 3), (1, 2, 3), None)),
)
# those don't raise errors
InputCombination(({"type": 1, "code": 2}, {"type": 1, "code": 1}))
InputCombination(({"type": 1, "code": 2},))
InputCombination(({"type": "1", "code": "2"},))
InputCombination(InputConfig(type=1, code=2, analog_threshold=3))
InputCombination(
(
{"type": 1, "code": 2},
{"type": "1", "code": "2"},
InputConfig(type=1, code=2),
)
)
def test_to_config(self):
c1 = InputCombination(InputConfig(type=1, code=2, analog_threshold=3))
c2 = InputCombination(
(
InputConfig(type=1, code=2, analog_threshold=3),
InputConfig(type=4, code=5, analog_threshold=6),
)
)
# analog_threshold is removed for key events
self.assertEqual(c1.to_config(), ({"type": 1, "code": 2},))
self.assertEqual(
c2.to_config(),
({"type": 1, "code": 2}, {"type": 4, "code": 5, "analog_threshold": 6}),
)
def test_beautify(self):
# not an integration test, but I have all the selection_label tests here already
self.assertEqual(
InputCombination(get_combination_config((EV_KEY, KEY_A, 1))).beautify(),
"a",
)
self.assertEqual(
InputCombination(get_combination_config((EV_KEY, KEY_A, 1))).beautify(),
"a",
)
self.assertEqual(
InputCombination(
get_combination_config((EV_ABS, ABS_HAT0Y, -1))
).beautify(),
"DPad-Y Up",
)
self.assertEqual(
InputCombination(get_combination_config((EV_KEY, BTN_A, 1))).beautify(),
"Button A",
)
self.assertEqual(
InputCombination(get_combination_config((EV_KEY, 1234, 1))).beautify(),
"unknown (1, 1234)",
)
self.assertEqual(
InputCombination(
get_combination_config((EV_ABS, ABS_HAT0X, -1))
).beautify(),
"DPad-X Left",
)
self.assertEqual(
InputCombination(
get_combination_config((EV_ABS, ABS_HAT0Y, -1))
).beautify(),
"DPad-Y Up",
)
self.assertEqual(
InputCombination(get_combination_config((EV_KEY, BTN_A, 1))).beautify(),
"Button A",
)
self.assertEqual(
InputCombination(get_combination_config((EV_ABS, ABS_X, 1))).beautify(),
"Joystick-X Right",
)
self.assertEqual(
InputCombination(get_combination_config((EV_ABS, ABS_RY, 1))).beautify(),
"Joystick-RY Down",
)
self.assertEqual(
InputCombination(
get_combination_config((EV_REL, REL_HWHEEL, 1))
).beautify(),
"Wheel Right",
)
self.assertEqual(
InputCombination(
get_combination_config((EV_REL, REL_WHEEL, -1))
).beautify(),
"Wheel Down",
)
# combinations
self.assertEqual(
InputCombination(
get_combination_config(
(EV_KEY, BTN_A, 1),
(EV_KEY, BTN_B, 1),
(EV_KEY, BTN_C, 1),
),
).beautify(),
"Button A + Button B + Button C",
)
def test_find_analog_input_config(self):
analog_input = InputConfig(type=EV_REL, code=REL_X)
combination = InputCombination(
(
InputConfig(type=EV_KEY, code=BTN_MIDDLE),
InputConfig(type=EV_REL, code=REL_Y, analog_threshold=1),
analog_input,
)
)
self.assertIsNone(combination.find_analog_input_config(type_=EV_ABS))
self.assertEqual(
combination.find_analog_input_config(type_=EV_REL), analog_input
)
self.assertEqual(combination.find_analog_input_config(), analog_input)
combination = InputCombination(
(
InputConfig(type=EV_REL, code=REL_X, analog_threshold=1),
InputConfig(type=EV_KEY, code=BTN_MIDDLE),
)
)
self.assertIsNone(combination.find_analog_input_config(type_=EV_ABS))
self.assertIsNone(combination.find_analog_input_config(type_=EV_REL))
self.assertIsNone(combination.find_analog_input_config())
if __name__ == "__main__":
unittest.main()

@ -23,7 +23,6 @@ import unittest
import evdev
from dataclasses import FrozenInstanceError
from inputremapper.input_event import InputEvent
from inputremapper.exceptions import InputEventCreationError
class TestInputEvent(unittest.TestCase):
@ -44,27 +43,7 @@ class TestInputEvent(unittest.TestCase):
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)
self.assertRaises(TypeError, InputEvent.from_event, "1,2,3")
def test_from_event_tuple(self):
t1 = (1, 2, 3)
@ -81,11 +60,8 @@ class TestInputEvent(unittest.TestCase):
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()
e1 = InputEvent.from_tuple((evdev.ecodes.EV_KEY, evdev.ecodes.BTN_LEFT, 1))
self.assertEqual(
e1.event_tuple,
(evdev.ecodes.EV_KEY, evdev.ecodes.BTN_LEFT, 1),

@ -22,13 +22,9 @@ import unittest
from functools import partial
from evdev.ecodes import (
EV_ABS,
EV_REL,
REL_X,
BTN_MIDDLE,
EV_KEY,
KEY_A,
ABS_X,
REL_Y,
REL_WHEEL,
REL_WHEEL_HI_RES,
@ -37,21 +33,22 @@ from pydantic import ValidationError
from inputremapper.configs.mapping import Mapping, UIMapping
from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.gui.messages.message_broker import MessageType
from inputremapper.input_event import EventActions, InputEvent, USE_AS_ANALOG_VALUE
class TestMapping(unittest.IsolatedAsyncioTestCase):
def test_init(self):
"""Test init and that defaults are set."""
cfg = {
"event_combination": "1,2,1",
"input_combination": [{"type": 1, "code": 2}],
"target_uinput": "keyboard",
"output_symbol": "a",
}
m = Mapping(**cfg)
self.assertEqual(m.event_combination, EventCombination.validate("1,2,1"))
self.assertEqual(
m.input_combination, InputCombination(InputConfig(type=1, code=2))
)
self.assertEqual(m.target_uinput, "keyboard")
self.assertEqual(m.output_symbol, "a")
@ -68,9 +65,7 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
def test_is_wheel_output(self):
mapping = Mapping(
event_combination=EventCombination(
events=(InputEvent(0, 0, EV_REL, REL_X, USE_AS_ANALOG_VALUE),)
),
input_combination=InputCombination(InputConfig(type=EV_REL, code=REL_X)),
target_uinput="keyboard",
output_type=EV_REL,
output_code=REL_Y,
@ -79,9 +74,7 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
self.assertFalse(mapping.is_high_res_wheel_output())
mapping = Mapping(
event_combination=EventCombination(
events=(InputEvent(0, 0, EV_REL, REL_X, USE_AS_ANALOG_VALUE),)
),
input_combination=InputCombination(InputConfig(type=EV_REL, code=REL_X)),
target_uinput="keyboard",
output_type=EV_REL,
output_code=REL_WHEEL,
@ -90,9 +83,7 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
self.assertFalse(mapping.is_high_res_wheel_output())
mapping = Mapping(
event_combination=EventCombination(
events=(InputEvent(0, 0, EV_REL, REL_X, USE_AS_ANALOG_VALUE),)
),
input_combination=InputCombination(InputConfig(type=EV_REL, code=REL_X)),
target_uinput="keyboard",
output_type=EV_REL,
output_code=REL_WHEEL_HI_RES,
@ -100,43 +91,9 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
self.assertFalse(mapping.is_wheel_output())
self.assertTrue(mapping.is_high_res_wheel_output())
def test_find_analog_input_event(self):
analog_input = InputEvent(0, 0, EV_REL, REL_X, USE_AS_ANALOG_VALUE)
mapping = Mapping(
event_combination=EventCombination(
events=(
InputEvent(0, 0, EV_KEY, BTN_MIDDLE, 1),
InputEvent(0, 0, EV_REL, REL_Y, 1),
analog_input,
)
),
target_uinput="keyboard",
output_type=EV_ABS,
output_code=ABS_X,
)
self.assertIsNone(mapping.find_analog_input_event(type_=EV_ABS))
self.assertEqual(mapping.find_analog_input_event(type_=EV_REL), analog_input)
self.assertEqual(mapping.find_analog_input_event(), analog_input)
mapping = Mapping(
event_combination=EventCombination(
events=(
InputEvent(0, 0, EV_REL, REL_X, 1),
InputEvent(0, 0, EV_KEY, BTN_MIDDLE, 1),
)
),
target_uinput="keyboard",
output_type=EV_KEY,
output_code=KEY_A,
)
self.assertIsNone(mapping.find_analog_input_event(type_=EV_ABS))
self.assertIsNone(mapping.find_analog_input_event(type_=EV_REL))
self.assertIsNone(mapping.find_analog_input_event())
def test_get_output_type_code(self):
cfg = {
"event_combination": "1,2,1",
"input_combination": [{"type": 1, "code": 2}],
"target_uinput": "keyboard",
"output_symbol": "a",
}
@ -148,7 +105,7 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
self.assertIsNone(m.get_output_type_code())
cfg = {
"event_combination": "1,2,1+3,1,0",
"input_combination": [{"type": 1, "code": 2}, {"type": 3, "code": 1}],
"target_uinput": "keyboard",
"output_type": 2,
"output_code": 3,
@ -158,7 +115,7 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
def test_strips_output_symbol(self):
cfg = {
"event_combination": "1,2,1",
"input_combination": [{"type": 1, "code": 2}],
"target_uinput": "keyboard",
"output_symbol": "\t a \n",
}
@ -166,34 +123,9 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
a = system_mapping.get("a")
self.assertEqual(m.get_output_type_code(), (EV_KEY, a))
def test_init_sets_event_actions(self):
"""Test that InputEvent.actions are set properly."""
cfg = {
"event_combination": "1,2,1+2,1,1+3,1,0",
"target_uinput": "keyboard",
"output_type": 2,
"output_code": 3,
}
m = Mapping(**cfg)
expected_actions = [(EventActions.as_key,), (EventActions.as_key,), ()]
actions = [event.actions for event in m.event_combination]
self.assertEqual(expected_actions, actions)
# copy keeps the event actions
m2 = m.copy()
actions = [event.actions for event in m2.event_combination]
self.assertEqual(expected_actions, actions)
# changing the combination sets the actions
m3 = m.copy()
m3.event_combination = "1,2,1+2,1,0+3,1,10"
expected_actions = [(EventActions.as_key,), (), (EventActions.as_key,)]
actions = [event.actions for event in m3.event_combination]
self.assertEqual(expected_actions, actions)
def test_combination_changed_callback(self):
cfg = {
"event_combination": "1,1,1",
"input_combination": [{"type": 1, "code": 1}],
"target_uinput": "keyboard",
"output_symbol": "a",
}
@ -204,30 +136,30 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
arguments.append(tuple(args))
m.set_combination_changed_callback(callback)
m.event_combination = "1,1,2"
m.event_combination = "1,1,3"
m.input_combination = [{"type": 1, "code": 2}]
m.input_combination = [{"type": 1, "code": 3}]
# make sure a copy works as expected and keeps the callback
m2 = m.copy()
m2.event_combination = "1,1,4"
m2.input_combination = [{"type": 1, "code": 4}]
m2.remove_combination_changed_callback()
m.remove_combination_changed_callback()
m.event_combination = "1,1,5"
m2.event_combination = "1,1,6"
m.input_combination = [{"type": 1, "code": 5}]
m2.input_combination = [{"type": 1, "code": 6}]
self.assertEqual(
arguments,
[
(
EventCombination.from_string("1,1,2"),
EventCombination.from_string("1,1,1"),
InputCombination([{"type": 1, "code": 2}]),
InputCombination([{"type": 1, "code": 1}]),
),
(
EventCombination.from_string("1,1,3"),
EventCombination.from_string("1,1,2"),
InputCombination([{"type": 1, "code": 3}]),
InputCombination([{"type": 1, "code": 2}]),
),
(
EventCombination.from_string("1,1,4"),
EventCombination.from_string("1,1,3"),
InputCombination([{"type": 1, "code": 4}]),
InputCombination([{"type": 1, "code": 3}]),
),
],
)
@ -237,7 +169,7 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
"""Test that the init fails with invalid data."""
test = partial(self.assertRaises, ValidationError, Mapping)
cfg = {
"event_combination": "1,2,3",
"input_combination": [{"type": 1, "code": 2}],
"target_uinput": "keyboard",
"output_symbol": "a",
}
@ -286,10 +218,10 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
cfg["target_uinput"] = "keyboard"
Mapping(**cfg)
# missing event_combination
del cfg["event_combination"]
# missing input_combination
del cfg["input_combination"]
test(**cfg)
cfg["event_combination"] = "1,2,3"
cfg["input_combination"] = [{"type": 1, "code": 2}]
Mapping(**cfg)
# no macro and not a known symbol
@ -305,7 +237,7 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
Mapping(**cfg)
# map axis but no output type and code given
cfg["event_combination"] = "3,0,0"
cfg["input_combination"] = [{"type": 3, "code": 0}]
test(**cfg)
# output symbol=disable is allowed
cfg["output_symbol"] = DISABLE_NAME
@ -321,28 +253,28 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
del cfg["output_symbol"]
# multiple axis as axis in event combination
cfg["event_combination"] = "3,0,0+3,1,0"
cfg["input_combination"] = [{"type": 3, "code": 0}, {"type": 3, "code": 1}]
test(**cfg)
cfg["event_combination"] = "3,0,0"
cfg["input_combination"] = [{"type": 3, "code": 0}]
Mapping(**cfg)
del cfg["output_type"]
del cfg["output_code"]
cfg["event_combination"] = "1,2,3"
cfg["input_combination"] = [{"type": 1, "code": 2}]
cfg["output_symbol"] = "a"
Mapping(**cfg)
# map EV_ABS as key with trigger point out of range
cfg["event_combination"] = "3,0,100"
cfg["input_combination"] = [{"type": 3, "code": 0, "analog_threshold": 100}]
test(**cfg)
cfg["event_combination"] = "3,0,99"
cfg["input_combination"] = [{"type": 3, "code": 0, "analog_threshold": 99}]
Mapping(**cfg)
cfg["event_combination"] = "3,0,-100"
cfg["input_combination"] = [{"type": 3, "code": 0, "analog_threshold": -100}]
test(**cfg)
cfg["event_combination"] = "3,0,-99"
cfg["input_combination"] = [{"type": 3, "code": 0, "analog_threshold": -99}]
Mapping(**cfg)
cfg["event_combination"] = "1,2,3"
cfg["input_combination"] = [{"type": 1, "code": 2}]
Mapping(**cfg)
# deadzone out of range
@ -378,22 +310,22 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
# analog output but no analog input
cfg = {
"event_combination": "3,1,-1",
"input_combination": [{"type": 3, "code": 1, "analog_threshold": -1}],
"target_uinput": "gamepad",
"output_type": 3,
"output_code": 1,
}
test(**cfg)
cfg["event_combination"] = "2,1,-1"
cfg["input_combination"] = [{"type": 2, "code": 1, "analog_threshold": -1}]
test(**cfg)
cfg["output_type"] = 2
test(**cfg)
cfg["event_combination"] = "3,1,-1"
cfg["input_combination"] = [{"type": 3, "code": 1, "analog_threshold": -1}]
test(**cfg)
def test_revalidate_at_assignment(self):
cfg = {
"event_combination": "1,1,1",
"input_combination": [{"type": 1, "code": 1}],
"target_uinput": "keyboard",
"output_symbol": "a",
}
@ -401,7 +333,7 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
test = partial(self.assertRaises, ValidationError, m.__setattr__)
# invalid input event
test("event_combination", "1,2,3,4")
test("input_combination", "1,2,3,4")
# unknown target
test("target_uinput", "foo")
@ -415,19 +347,19 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
def test_set_invalid_combination_with_callback(self):
cfg = {
"event_combination": "1,1,1",
"input_combination": [{"type": 1, "code": 1}],
"target_uinput": "keyboard",
"output_symbol": "a",
}
m = Mapping(**cfg)
m.set_combination_changed_callback(lambda *args: None)
self.assertRaises(ValidationError, m.__setattr__, "event_combination", "1,2")
m.event_combination = "1,2,3"
m.event_combination = "1,2,3"
self.assertRaises(ValidationError, m.__setattr__, "input_combination", "1,2")
m.input_combination = [{"type": 1, "code": 2}]
m.input_combination = [{"type": 1, "code": 2}]
def test_is_valid(self):
cfg = {
"event_combination": "1,1,1",
"input_combination": [{"type": 1, "code": 1}],
"target_uinput": "keyboard",
"output_symbol": "a",
}
@ -446,7 +378,7 @@ class TestUIMapping(unittest.IsolatedAsyncioTestCase):
m = UIMapping()
self.assertFalse(m.is_valid())
m.event_combination = "1,2,3"
m.input_combination = [{"type": 1, "code": 2}]
m.output_symbol = "a"
self.assertFalse(m.is_valid())
m.target_uinput = "keyboard"
@ -455,7 +387,7 @@ class TestUIMapping(unittest.IsolatedAsyncioTestCase):
def test_updates_validation_error(self):
m = UIMapping()
self.assertGreaterEqual(len(m.get_error().errors()), 2)
m.event_combination = "1,2,3"
m.input_combination = [{"type": 1, "code": 2}]
m.output_symbol = "a"
self.assertIn(
"1 validation error for Mapping\ntarget_uinput", str(m.get_error())
@ -469,7 +401,7 @@ class TestUIMapping(unittest.IsolatedAsyncioTestCase):
m = UIMapping()
m2 = m.copy()
self.assertIsInstance(m2, UIMapping)
self.assertEqual(m2.event_combination, EventCombination.empty_combination())
self.assertEqual(m2.input_combination, InputCombination.empty_combination())
self.assertIsNone(m2.output_symbol)
def test_get_bus_massage(self):
@ -489,7 +421,7 @@ class TestUIMapping(unittest.IsolatedAsyncioTestCase):
def test_has_input_defined(self):
m = UIMapping()
self.assertFalse(m.has_input_defined())
m.event_combination = EventCombination((EV_KEY, 1, 1))
m.input_combination = InputCombination(InputConfig(type=EV_KEY, code=1))
self.assertTrue(m.has_input_defined())

@ -12,9 +12,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/>.
from inputremapper.configs.mapping import UIMapping
from tests.lib.cleanup import quick_cleanup
from tests.lib.tmp import tmp
from tests.lib.fixtures import get_combination_config
import os
import unittest
@ -37,12 +38,13 @@ from evdev.ecodes import (
REL_HWHEEL_HI_RES,
)
from inputremapper.configs.mapping import UIMapping
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.logger import IS_BETA
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.user import HOME
from inputremapper.logger import VERSION
@ -178,7 +180,7 @@ class TestMigrations(unittest.TestCase):
mappings like
{(type, code): symbol} or {(type, code, value): symbol} should migrate
to {EventCombination: {target: target, symbol: symbol, ...}}
to {InputCombination: {target: target, symbol: symbol, ...}}
"""
path = os.path.join(CONFIG_PATH, "presets", "Foo Device", "test.json")
os.makedirs(os.path.dirname(path), exist_ok=True)
@ -209,69 +211,81 @@ class TestMigrations(unittest.TestCase):
preset.load()
self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 1, 1])),
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=1))),
UIMapping(
event_combination=EventCombination([EV_KEY, 1, 1]),
input_combination=InputCombination(InputConfig(type=EV_KEY, code=1)),
target_uinput="keyboard",
output_symbol="a",
),
)
self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 2, 1])),
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=2))),
UIMapping(
event_combination=EventCombination([EV_KEY, 2, 1]),
input_combination=InputCombination(InputConfig(type=EV_KEY, code=2)),
target_uinput="gamepad",
output_symbol="BTN_B",
),
)
self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 3, 1])),
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=3))),
UIMapping(
event_combination=EventCombination([EV_KEY, 3, 1]),
input_combination=InputCombination(InputConfig(type=EV_KEY, code=3)),
target_uinput="keyboard",
output_symbol="BTN_1\n# Broken mapping:\n# No target can handle all specified keycodes",
),
)
self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 4, 1])),
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=4))),
UIMapping(
event_combination=EventCombination([EV_KEY, 4, 1]),
input_combination=InputCombination(InputConfig(type=EV_KEY, code=4)),
target_uinput="keyboard",
output_symbol="d",
),
)
self.assertEqual(
preset.get_mapping(EventCombination([EV_ABS, ABS_HAT0X, -1])),
preset.get_mapping(
InputCombination(
InputConfig(type=EV_ABS, code=ABS_HAT0X, analog_threshold=-1)
)
),
UIMapping(
event_combination=EventCombination([EV_ABS, ABS_HAT0X, -1]),
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_HAT0X, analog_threshold=-1)
),
target_uinput="keyboard",
output_symbol="b",
),
)
self.assertEqual(
preset.get_mapping(
EventCombination(((EV_ABS, 1, 1), (EV_ABS, 2, -1), (EV_ABS, 3, 1))),
InputCombination(
get_combination_config(
(EV_ABS, 1, 1), (EV_ABS, 2, -1), (EV_ABS, 3, 1)
)
),
),
UIMapping(
event_combination=EventCombination(
((EV_ABS, 1, 1), (EV_ABS, 2, -1), (EV_ABS, 3, 1)),
input_combination=InputCombination(
get_combination_config(
(EV_ABS, 1, 1), (EV_ABS, 2, -1), (EV_ABS, 3, 1)
),
),
target_uinput="keyboard",
output_symbol="c",
),
)
self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 5, 1])),
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=5))),
UIMapping(
event_combination=EventCombination([EV_KEY, 5, 1]),
input_combination=InputCombination(InputConfig(type=EV_KEY, code=5)),
target_uinput="foo",
output_symbol="e",
),
)
self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 6, 1])),
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=6))),
UIMapping(
event_combination=EventCombination([EV_KEY, 6, 1]),
input_combination=InputCombination(InputConfig(type=EV_KEY, code=6)),
target_uinput="keyboard",
output_symbol="key(a, b)",
),
@ -302,41 +316,41 @@ class TestMigrations(unittest.TestCase):
preset.load()
self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 1, 1])),
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=1))),
UIMapping(
event_combination=EventCombination([EV_KEY, 1, 1]),
input_combination=InputCombination(InputConfig(type=EV_KEY, code=1)),
target_uinput="keyboard",
output_symbol="otherwise + otherwise",
),
)
self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 2, 1])),
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=2))),
UIMapping(
event_combination=EventCombination([EV_KEY, 2, 1]),
input_combination=InputCombination(InputConfig(type=EV_KEY, code=2)),
target_uinput="keyboard",
output_symbol="bar($otherwise)",
),
)
self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 3, 1])),
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=3))),
UIMapping(
event_combination=EventCombination([EV_KEY, 3, 1]),
input_combination=InputCombination(InputConfig(type=EV_KEY, code=3)),
target_uinput="keyboard",
output_symbol="foo(else=qux)",
),
)
self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 4, 1])),
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=4))),
UIMapping(
event_combination=EventCombination([EV_KEY, 4, 1]),
input_combination=InputCombination(InputConfig(type=EV_KEY, code=4)),
target_uinput="foo",
output_symbol="qux(otherwise).bar(else=1)",
),
)
self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 5, 1])),
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=5))),
UIMapping(
event_combination=EventCombination([EV_KEY, 5, 1]),
input_combination=InputCombination(InputConfig(type=EV_KEY, code=5)),
target_uinput="keyboard",
output_symbol="foo(otherwise1=2qux)",
),
@ -400,9 +414,11 @@ class TestMigrations(unittest.TestCase):
# 2 mappings for wheel
self.assertEqual(len(preset), 4)
self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_X, 0))),
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_X))),
UIMapping(
event_combination=EventCombination((EV_ABS, ABS_X, 0)),
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_X)
),
target_uinput="mouse",
output_type=EV_REL,
output_code=REL_X,
@ -410,9 +426,11 @@ class TestMigrations(unittest.TestCase):
),
)
self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_Y, 0))),
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_Y))),
UIMapping(
event_combination=EventCombination((EV_ABS, ABS_Y, 0)),
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_Y)
),
target_uinput="mouse",
output_type=EV_REL,
output_code=REL_Y,
@ -420,9 +438,11 @@ class TestMigrations(unittest.TestCase):
),
)
self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_RX, 0))),
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RX))),
UIMapping(
event_combination=EventCombination((EV_ABS, ABS_RX, 0)),
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_RX)
),
target_uinput="mouse",
output_type=EV_REL,
output_code=REL_HWHEEL_HI_RES,
@ -430,9 +450,11 @@ class TestMigrations(unittest.TestCase):
),
)
self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_RY, 0))),
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RY))),
UIMapping(
event_combination=EventCombination((EV_ABS, ABS_RY, 0)),
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_RY)
),
target_uinput="mouse",
output_type=EV_REL,
output_code=REL_WHEEL_HI_RES,
@ -468,9 +490,11 @@ class TestMigrations(unittest.TestCase):
# 2 mappings for wheel
self.assertEqual(len(preset), 4)
self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_RX, 0))),
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RX))),
UIMapping(
event_combination=EventCombination((EV_ABS, ABS_RX, 0)),
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_RX)
),
target_uinput="mouse",
output_type=EV_REL,
output_code=REL_X,
@ -478,9 +502,11 @@ class TestMigrations(unittest.TestCase):
),
)
self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_RY, 0))),
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RY))),
UIMapping(
event_combination=EventCombination((EV_ABS, ABS_RY, 0)),
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_RY)
),
target_uinput="mouse",
output_type=EV_REL,
output_code=REL_Y,
@ -488,9 +514,11 @@ class TestMigrations(unittest.TestCase):
),
)
self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_X, 0))),
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_X))),
UIMapping(
event_combination=EventCombination((EV_ABS, ABS_X, 0)),
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_X)
),
target_uinput="mouse",
output_type=EV_REL,
output_code=REL_HWHEEL_HI_RES,
@ -498,9 +526,11 @@ class TestMigrations(unittest.TestCase):
),
)
self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_Y, 0))),
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_Y))),
UIMapping(
event_combination=EventCombination((EV_ABS, ABS_Y, 0)),
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_Y)
),
target_uinput="mouse",
output_type=EV_REL,
output_code=REL_WHEEL_HI_RES,

@ -28,10 +28,9 @@ from inputremapper.configs.mapping import Mapping
from inputremapper.configs.mapping import UIMapping
from inputremapper.configs.paths import get_preset_path, get_config_path, CONFIG_PATH
from inputremapper.configs.preset import Preset
from inputremapper.event_combination import EventCombination
from inputremapper.input_event import InputEvent
from inputremapper.configs.input_config import InputCombination, InputConfig
from tests.lib.cleanup import quick_cleanup
from tests.lib.fixtures import get_key_mapping
from tests.lib.fixtures import get_key_mapping, get_combination_config
class TestPreset(unittest.TestCase):
@ -43,19 +42,21 @@ class TestPreset(unittest.TestCase):
quick_cleanup()
def test_is_mapped_multiple_times(self):
combination = EventCombination.from_string("1,1,1+2,2,2+3,3,3+4,4,4")
combination = InputCombination(
get_combination_config((1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4))
)
permutations = combination.get_permutations()
self.assertEqual(len(permutations), 6)
self.preset._mappings[permutations[0]] = Mapping(
event_combination=permutations[0],
input_combination=permutations[0],
target_uinput="keyboard",
output_symbol="a",
)
self.assertFalse(self.preset._is_mapped_multiple_times(permutations[2]))
self.preset._mappings[permutations[1]] = Mapping(
event_combination=permutations[1],
input_combination=permutations[1],
target_uinput="keyboard",
output_symbol="a",
)
@ -76,7 +77,7 @@ class TestPreset(unittest.TestCase):
# load again from the disc
self.preset.load()
self.assertEqual(
self.preset.get_mapping(EventCombination([99, 99, 99])),
self.preset.get_mapping(InputCombination.empty_combination()),
get_key_mapping(),
)
self.assertFalse(self.preset.has_unsaved_changes())
@ -92,13 +93,13 @@ class TestPreset(unittest.TestCase):
self.assertFalse(self.preset.has_unsaved_changes())
# modify the mapping
mapping = self.preset.get_mapping(EventCombination([99, 99, 99]))
mapping = self.preset.get_mapping(InputCombination.empty_combination())
mapping.gain = 0.5
self.assertTrue(self.preset.has_unsaved_changes())
self.preset.load()
self.preset.path = get_preset_path("bar", "foo")
self.preset.remove(get_key_mapping().event_combination)
self.preset.remove(get_key_mapping().input_combination)
# empty preset and empty file
self.assertFalse(self.preset.has_unsaved_changes())
@ -117,14 +118,14 @@ class TestPreset(unittest.TestCase):
self.assertEqual(len(self.preset), 0)
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))
one = InputConfig(type=EV_KEY, code=10)
two = InputConfig(type=EV_KEY, code=11)
three = InputConfig(type=EV_KEY, code=12)
self.preset.add(get_key_mapping(EventCombination(one), "keyboard", "1"))
self.preset.add(get_key_mapping(EventCombination(two), "keyboard", "2"))
self.preset.add(get_key_mapping(InputCombination(one), "keyboard", "1"))
self.preset.add(get_key_mapping(InputCombination(two), "keyboard", "2"))
self.preset.add(
get_key_mapping(EventCombination((two, three)), "keyboard", "3"),
get_key_mapping(InputCombination((two, three)), "keyboard", "3"),
)
self.preset.path = get_preset_path("Foo Device", "test")
self.preset.save()
@ -139,16 +140,16 @@ class TestPreset(unittest.TestCase):
self.assertEqual(len(loaded), 3)
self.assertRaises(TypeError, loaded.get_mapping, one)
self.assertEqual(
loaded.get_mapping(EventCombination(one)),
get_key_mapping(EventCombination(one), "keyboard", "1"),
loaded.get_mapping(InputCombination(one)),
get_key_mapping(InputCombination(one), "keyboard", "1"),
)
self.assertEqual(
loaded.get_mapping(EventCombination(two)),
get_key_mapping(EventCombination(two), "keyboard", "2"),
loaded.get_mapping(InputCombination(two)),
get_key_mapping(InputCombination(two), "keyboard", "2"),
)
self.assertEqual(
loaded.get_mapping(EventCombination((two, three))),
get_key_mapping(EventCombination((two, three)), "keyboard", "3"),
loaded.get_mapping(InputCombination((two, three))),
get_key_mapping(InputCombination((two, three)), "keyboard", "3"),
)
# load missing file
@ -156,13 +157,10 @@ class TestPreset(unittest.TestCase):
self.assertRaises(FileNotFoundError, preset.load)
def test_modify_mapping(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_1 = InputCombination(InputConfig(type=EV_KEY, code=1))
ev_3 = InputCombination(InputConfig(type=EV_KEY, code=2))
# only values between -99 and 99 are allowed as mapping for EV_ABS or EV_REL
ev_4 = EventCombination((EV_ABS, 1, 99))
ev_4 = InputCombination(InputConfig(type=EV_ABS, code=1, analog_threshold=99))
# add the first mapping
self.preset.add(get_key_mapping(ev_1, "keyboard", "a"))
@ -171,7 +169,7 @@ class TestPreset(unittest.TestCase):
# change ev_1 to ev_3 and change a to b
mapping = self.preset.get_mapping(ev_1)
mapping.event_combination = ev_3
mapping.input_combination = ev_3
mapping.output_symbol = "b"
self.assertIsNone(self.preset.get_mapping(ev_1))
self.assertEqual(
@ -204,7 +202,7 @@ class TestPreset(unittest.TestCase):
# try to change combination of 4 to 3
mapping = self.preset.get_mapping(ev_4)
with self.assertRaises(KeyError):
mapping.event_combination = ev_3
mapping.input_combination = ev_3
self.assertEqual(
self.preset.get_mapping(ev_3),
@ -228,13 +226,13 @@ class TestPreset(unittest.TestCase):
self.assertFalse(content)
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, 99))
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))
ev_1 = InputConfig(type=EV_KEY, code=1, analog_threshold=111)
ev_2 = InputConfig(type=EV_KEY, code=1, analog_threshold=222)
ev_3 = InputConfig(type=EV_KEY, code=2, analog_threshold=111)
ev_4 = InputConfig(type=EV_ABS, code=1, analog_threshold=99)
combi_1 = InputCombination((ev_1, ev_2, ev_3))
combi_2 = InputCombination((ev_2, ev_1, ev_3))
combi_3 = InputCombination((ev_1, ev_2, ev_4))
self.preset.add(get_key_mapping(combi_1, "keyboard", "a"))
self.assertEqual(
@ -277,7 +275,7 @@ class TestPreset(unittest.TestCase):
mapping = self.preset.get_mapping(combi_1)
mapping.output_symbol = "c"
with self.assertRaises(KeyError):
mapping.event_combination = combi_3
mapping.input_combination = combi_3
self.assertEqual(
self.preset.get_mapping(combi_1),
@ -294,10 +292,10 @@ class TestPreset(unittest.TestCase):
def test_remove(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))
ev_1 = InputCombination(InputConfig(type=EV_KEY, code=40))
ev_2 = InputCombination(InputConfig(type=EV_KEY, code=30))
ev_3 = InputCombination(InputConfig(type=EV_KEY, code=20))
ev_4 = InputCombination(InputConfig(type=EV_KEY, code=10))
self.assertRaises(TypeError, self.preset.remove, (EV_KEY, 10, 1))
self.preset.remove(ev_1)
@ -328,13 +326,25 @@ class TestPreset(unittest.TestCase):
def test_empty(self):
self.preset.add(
get_key_mapping(EventCombination([EV_KEY, 10, 1]), "keyboard", "1"),
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=10)),
"keyboard",
"1",
),
)
self.preset.add(
get_key_mapping(EventCombination([EV_KEY, 11, 1]), "keyboard", "2"),
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=11)),
"keyboard",
"2",
),
)
self.preset.add(
get_key_mapping(EventCombination([EV_KEY, 12, 1]), "keyboard", "3"),
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=12)),
"keyboard",
"3",
),
)
self.assertEqual(len(self.preset), 3)
self.preset.path = get_config_path("test.json")
@ -348,13 +358,25 @@ class TestPreset(unittest.TestCase):
def test_clear(self):
self.preset.add(
get_key_mapping(EventCombination([EV_KEY, 10, 1]), "keyboard", "1"),
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=10)),
"keyboard",
"1",
),
)
self.preset.add(
get_key_mapping(EventCombination([EV_KEY, 11, 1]), "keyboard", "2"),
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=11)),
"keyboard",
"2",
),
)
self.preset.add(
get_key_mapping(EventCombination([EV_KEY, 12, 1]), "keyboard", "3"),
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=12)),
"keyboard",
"3",
),
)
self.assertEqual(len(self.preset), 3)
self.preset.path = get_config_path("test.json")
@ -370,7 +392,7 @@ class TestPreset(unittest.TestCase):
# btn left is mapped
self.preset.add(
get_key_mapping(
EventCombination(InputEvent.btn_left()),
InputCombination(InputConfig.btn_left()),
"keyboard",
"1",
)
@ -378,7 +400,7 @@ class TestPreset(unittest.TestCase):
self.assertTrue(self.preset.dangerously_mapped_btn_left())
self.preset.add(
get_key_mapping(
EventCombination([EV_KEY, 41, 1]),
InputCombination(InputConfig(type=EV_KEY, code=41)),
"keyboard",
"2",
)
@ -388,14 +410,16 @@ class TestPreset(unittest.TestCase):
# another mapping maps to btn_left
self.preset.add(
get_key_mapping(
EventCombination([EV_KEY, 42, 1]),
InputCombination(InputConfig(type=EV_KEY, code=42)),
"mouse",
"btn_left",
)
)
self.assertFalse(self.preset.dangerously_mapped_btn_left())
mapping = self.preset.get_mapping(EventCombination([EV_KEY, 42, 1]))
mapping = self.preset.get_mapping(
InputCombination(InputConfig(type=EV_KEY, code=42))
)
mapping.output_symbol = "BTN_Left"
self.assertFalse(self.preset.dangerously_mapped_btn_left())
@ -404,7 +428,7 @@ class TestPreset(unittest.TestCase):
self.assertTrue(self.preset.dangerously_mapped_btn_left())
# btn_left is not mapped
self.preset.remove(EventCombination(InputEvent.btn_left()))
self.preset.remove(InputCombination(InputConfig.btn_left()))
self.assertFalse(self.preset.dangerously_mapped_btn_left())
def test_save_load_with_invalid_mappings(self):
@ -414,12 +438,12 @@ class TestPreset(unittest.TestCase):
self.assertFalse(ui_preset.is_valid())
# make the mapping valid
m = ui_preset.get_mapping(EventCombination.empty_combination())
m = ui_preset.get_mapping(InputCombination.empty_combination())
m.output_symbol = "a"
m.target_uinput = "keyboard"
self.assertTrue(ui_preset.is_valid())
m2 = UIMapping(event_combination="1,2,1")
m2 = UIMapping(input_combination=InputCombination(InputConfig(type=1, code=2)))
ui_preset.add(m2)
self.assertFalse(ui_preset.is_valid())
ui_preset.save()
@ -429,12 +453,12 @@ class TestPreset(unittest.TestCase):
preset.load()
self.assertEqual(len(preset), 1)
a = preset.get_mapping(m.event_combination).dict()
a = preset.get_mapping(m.input_combination).dict()
b = m.dict()
a.pop("mapping_type")
b.pop("mapping_type")
self.assertEqual(a, b)
# self.assertEqual(preset.get_mapping(m.event_combination), m)
# self.assertEqual(preset.get_mapping(m.input_combination), m)
# both presets load
ui_preset.clear()
@ -442,13 +466,13 @@ class TestPreset(unittest.TestCase):
ui_preset.load()
self.assertEqual(len(ui_preset), 2)
a = ui_preset.get_mapping(m.event_combination).dict()
a = ui_preset.get_mapping(m.input_combination).dict()
b = m.dict()
a.pop("mapping_type")
b.pop("mapping_type")
self.assertEqual(a, b)
# self.assertEqual(ui_preset.get_mapping(m.event_combination), m)
self.assertEqual(ui_preset.get_mapping(m2.event_combination), m2)
# self.assertEqual(ui_preset.get_mapping(m.input_combination), m)
self.assertEqual(ui_preset.get_mapping(m2.input_combination), m2)
if __name__ == "__main__":

@ -38,9 +38,10 @@ from evdev.ecodes import (
REL_X,
ABS_X,
REL_HWHEEL,
BTN_LEFT,
)
from inputremapper.event_combination import EventCombination
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.groups import _Groups, DeviceType
from inputremapper.gui.messages.message_broker import (
MessageBroker,
@ -59,7 +60,7 @@ from tests.lib.constants import (
MIN_ABS,
)
from tests.lib.pipes import push_event, push_events
from tests.lib.fixtures import fixtures
from tests.lib.fixtures import fixtures, get_combination_config
CODE_1 = 100
CODE_2 = 101
@ -138,8 +139,34 @@ class TestReader(unittest.TestCase):
self.reader_client._read()
self.assertEqual(
[
CombinationRecorded(EventCombination.from_string("3,16,1")),
CombinationRecorded(EventCombination.from_string("3,16,1+2,0,1")),
CombinationRecorded(
InputCombination(
InputConfig(
type=3,
code=16,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
)
),
CombinationRecorded(
InputCombination(
(
InputConfig(
type=3,
code=16,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
),
InputConfig(
type=2,
code=0,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
),
)
)
),
],
l1.calls,
)
@ -166,7 +193,18 @@ class TestReader(unittest.TestCase):
self.reader_client._read()
self.assertEqual(
[CombinationRecorded(EventCombination.from_string("2,0,-1"))],
[
CombinationRecorded(
InputCombination(
InputConfig(
type=2,
code=0,
analog_threshold=-1,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
)
)
)
],
l1.calls,
)
self.assertEqual([], l2.calls) # no stop recording yet
@ -203,8 +241,34 @@ class TestReader(unittest.TestCase):
self.assertEqual(
[
CombinationRecorded(EventCombination.from_string("2,8,-1")),
CombinationRecorded(EventCombination.from_string("2,8,-1+2,6,1")),
CombinationRecorded(
InputCombination(
InputConfig(
type=2,
code=8,
analog_threshold=-1,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
)
)
),
CombinationRecorded(
InputCombination(
(
InputConfig(
type=2,
code=8,
analog_threshold=-1,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
),
InputConfig(
type=2,
code=6,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
),
)
)
),
],
l1.calls,
)
@ -225,7 +289,18 @@ class TestReader(unittest.TestCase):
self.reader_client._read()
self.assertEqual(
[CombinationRecorded(EventCombination.from_string("1,30,1"))],
[
CombinationRecorded(
InputCombination(
InputConfig(
type=1,
code=30,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
)
)
],
l1.calls,
)
@ -246,7 +321,18 @@ class TestReader(unittest.TestCase):
time.sleep(0.1)
self.reader_client._read()
self.assertEqual(
[CombinationRecorded(EventCombination.from_string("3,0,1"))],
[
CombinationRecorded(
InputCombination(
InputConfig(
type=3,
code=0,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
)
)
],
l1.calls,
)
self.assertEqual([], l2.calls) # no stop recording yet
@ -259,7 +345,18 @@ class TestReader(unittest.TestCase):
time.sleep(0.1)
self.reader_client._read()
self.assertEqual(
[CombinationRecorded(EventCombination.from_string("3,0,1"))],
[
CombinationRecorded(
InputCombination(
InputConfig(
type=3,
code=0,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
)
)
],
l1.calls,
)
self.assertEqual([Signal(MessageType.recording_finished)], l2.calls)
@ -290,13 +387,75 @@ class TestReader(unittest.TestCase):
self.reader_client._read()
self.assertEqual(
[
CombinationRecorded(EventCombination.from_string("1,30,1")),
CombinationRecorded(EventCombination.from_string("1,30,1+3,0,1")),
CombinationRecorded(
EventCombination.from_string("1,30,1+3,0,1+1,51,1")
InputCombination(
InputConfig(
type=EV_KEY,
code=KEY_A,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
)
),
CombinationRecorded(
InputCombination(
(
InputConfig(
type=EV_KEY,
code=KEY_A,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
),
InputConfig(
type=EV_ABS,
code=ABS_X,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
),
)
)
),
CombinationRecorded(
EventCombination.from_string("1,30,1+3,0,-1+1,51,1")
InputCombination(
(
InputConfig(
type=EV_KEY,
code=KEY_A,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
),
InputConfig(
type=EV_ABS,
code=ABS_X,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
),
InputConfig(
type=EV_KEY,
code=KEY_COMMA,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
),
)
)
),
CombinationRecorded(
InputCombination(
(
InputConfig(
type=EV_KEY,
code=KEY_A,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
),
InputConfig(
type=EV_ABS,
code=ABS_X,
analog_threshold=-1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
),
InputConfig(
type=EV_KEY,
code=KEY_COMMA,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
),
)
)
),
],
l1.calls,
@ -328,7 +487,16 @@ class TestReader(unittest.TestCase):
self.reader_client.start_recorder()
time.sleep(0.1)
self.reader_client._read()
self.assertEqual(l1.calls[0].combination, EventCombination((EV_KEY, 1, 1)))
self.assertEqual(
l1.calls[0].combination,
InputCombination(
InputConfig(
type=EV_KEY,
code=1,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
),
)
self.reader_client.set_group(self.groups.find(name="Bar Device"))
time.sleep(0.1)
@ -342,7 +510,16 @@ class TestReader(unittest.TestCase):
push_events(fixtures.bar_device, [new_event(EV_KEY, 2, 1)])
time.sleep(0.1)
self.reader_client._read()
self.assertEqual(l1.calls[1].combination, EventCombination((EV_KEY, 2, 1)))
self.assertEqual(
l1.calls[1].combination,
InputCombination(
InputConfig(
type=EV_KEY,
code=2,
origin_hash=fixtures.bar_device.get_device_hash(),
)
),
)
def test_reading_2(self):
l1 = Listener()
@ -386,7 +563,26 @@ class TestReader(unittest.TestCase):
self.reader_client._read()
self.assertEqual(
l1.calls[-1].combination,
((EV_KEY, CODE_1, 1), (EV_KEY, CODE_3, 1), (EV_ABS, ABS_HAT0X, -1)),
InputCombination(
(
InputConfig(
type=EV_KEY,
code=CODE_1,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
),
InputConfig(
type=EV_KEY,
code=CODE_3,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
),
InputConfig(
type=EV_ABS,
code=ABS_HAT0X,
analog_threshold=-1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
),
)
),
)
def test_blacklisted_events(self):
@ -397,7 +593,7 @@ class TestReader(unittest.TestCase):
fixtures.foo_device_2_mouse,
[
new_event(EV_KEY, BTN_TOOL_DOUBLETAP, 1),
new_event(EV_KEY, CODE_2, 1),
new_event(EV_KEY, BTN_LEFT, 1),
new_event(EV_KEY, BTN_TOOL_DOUBLETAP, 1),
],
force=True,
@ -408,7 +604,14 @@ class TestReader(unittest.TestCase):
time.sleep(0.1)
self.reader_client._read()
self.assertEqual(
l1.calls[-1].combination, EventCombination((EV_KEY, CODE_2, 1))
l1.calls[-1].combination,
InputCombination(
InputConfig(
type=EV_KEY,
code=BTN_LEFT,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
)
),
)
def test_ignore_value_2(self):
@ -426,7 +629,15 @@ class TestReader(unittest.TestCase):
time.sleep(0.2)
self.reader_client._read()
self.assertEqual(
l1.calls[-1].combination, EventCombination((EV_ABS, ABS_HAT0X, 1))
l1.calls[-1].combination,
InputCombination(
InputConfig(
type=EV_ABS,
code=ABS_HAT0X,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
),
)
def test_reading_ignore_up(self):
@ -446,7 +657,14 @@ class TestReader(unittest.TestCase):
time.sleep(0.1)
self.reader_client._read()
self.assertEqual(
l1.calls[-1].combination, EventCombination((EV_KEY, CODE_2, 1))
l1.calls[-1].combination,
InputCombination(
InputConfig(
type=EV_KEY,
code=CODE_2,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
),
)
def test_wrong_device(self):

@ -17,6 +17,7 @@
#
# 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 inputremapper.utils import get_device_hash
from inputremapper.gui.messages.message_broker import MessageBroker
from tests.lib.fixtures import new_event
@ -131,6 +132,12 @@ class TestTest(unittest.TestCase):
reader_client.terminate()
def test_device_hash_from_fixture_is_correct(self):
for fixture in fixtures:
self.assertEqual(
fixture.get_device_hash(), get_device_hash(InputDevice(fixture.path))
)
if __name__ == "__main__":
unittest.main()

Loading…
Cancel
Save