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"> <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" /> <module name="input-remapper" />
<option name="INTERPRETER_OPTIONS" value="" /> <option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" /> <option name="PARENT_ENVS" value="true" />
@ -8,6 +8,7 @@
<option name="IS_MODULE_SDK" value="true" /> <option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_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_additionalArguments" value="&quot;&quot;" />
<option name="_new_target" value="&quot;$PROJECT_DIR$/tests&quot;" /> <option name="_new_target" value="&quot;$PROJECT_DIR$/tests&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" /> <option name="_new_targetType" value="&quot;PATH&quot;" />

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager"> <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" /> <module name="input-remapper" />
<option name="INTERPRETER_OPTIONS" value="" /> <option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" /> <option name="PARENT_ENVS" value="true" />
@ -8,8 +8,9 @@
<option name="IS_MODULE_SDK" value="true" /> <option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<option name="_new_additionalArguments" value="&quot;--start-dir integration&quot;" /> <option name="_new_pattern" value="&quot;&quot;" />
<option name="_new_target" value="&quot;$PROJECT_DIR$/tests&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;" /> <option name="_new_targetType" value="&quot;PATH&quot;" />
<method v="2" /> <method v="2" />
</configuration> </configuration>

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager"> <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" /> <module name="input-remapper" />
<option name="INTERPRETER_OPTIONS" value="" /> <option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" /> <option name="PARENT_ENVS" value="true" />
@ -8,8 +8,9 @@
<option name="IS_MODULE_SDK" value="true" /> <option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<option name="_new_additionalArguments" value="&quot;--start-dir unit&quot;" /> <option name="_new_pattern" value="&quot;&quot;" />
<option name="_new_target" value="&quot;$PROJECT_DIR$/tests&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;" /> <option name="_new_targetType" value="&quot;PATH&quot;" />
<method v="2" /> <method v="2" />
</configuration> </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 from __future__ import annotations
import enum 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 evdev
import pkg_resources import pkg_resources
@ -46,13 +46,12 @@ from pydantic import (
BaseConfig, BaseConfig,
) )
from inputremapper.configs.input_config import InputCombination
from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME from inputremapper.configs.system_mapping import system_mapping, DISABLE_NAME
from inputremapper.event_combination import EventCombination
from inputremapper.exceptions import MacroParsingError from inputremapper.exceptions import MacroParsingError
from inputremapper.gui.gettext import _ from inputremapper.gui.gettext import _
from inputremapper.gui.messages.message_types import MessageType from inputremapper.gui.messages.message_types import MessageType
from inputremapper.injection.macros.parse import is_this_a_macro, parse 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 # TODO: remove pydantic VERSION check as soon as we no longer support
# Ubuntu 20.04 and with it the ancient pydantic 1.2 # Ubuntu 20.04 and with it the ancient pydantic 1.2
@ -86,16 +85,16 @@ class KnownUinput(str, enum.Enum):
CombinationChangedCallback = Optional[ CombinationChangedCallback = Optional[
Callable[[EventCombination, EventCombination], None] Callable[[InputCombination, InputCombination], None]
] ]
MappingModel = TypeVar("MappingModel", bound="Mapping") MappingModel = TypeVar("MappingModel", bound="UIMapping")
class Cfg(BaseConfig): class Cfg(BaseConfig):
validate_assignment = True validate_assignment = True
use_enum_values = True use_enum_values = True
underscore_attrs_are_private = 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): class ImmutableCfg(Cfg):
@ -116,7 +115,7 @@ class UIMapping(BaseModel):
# Required attributes # Required attributes
# The InputEvent or InputEvent combination which is mapped # 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 # The UInput to which the mapped event will be sent
target_uinput: Optional[Union[str, KnownUinput]] = None 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 # instead wait until it dropped for loger than release_timeout below the threshold
force_release_timeout: bool = False 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: if not needs_workaround:
_combination_changed: Optional[CombinationChangedCallback] = None _combination_changed: Optional[CombinationChangedCallback] = None
@ -170,9 +169,9 @@ class UIMapping(BaseModel):
def __setattr__(self, key: str, value: Any): def __setattr__(self, key: str, value: Any):
"""Call the combination changed callback """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: if key == "_combination_changed" and needs_workaround:
object.__setattr__(self, "_combination_changed", value) object.__setattr__(self, "_combination_changed", value)
return return
@ -181,23 +180,23 @@ class UIMapping(BaseModel):
# the new combination is not yet validated # the new combination is not yet validated
try: try:
new_combi = EventCombination.validate(value) new_combi = InputCombination.validate(value)
except ValueError as exception: except (ValueError, TypeError) as exception:
raise ValidationError( raise ValidationError(
f"failed to Validate {value} as EventCombination", UIMapping f"failed to Validate {value} as InputCombination", UIMapping
) from exception ) from exception
if new_combi == self.event_combination: if new_combi == self.input_combination:
return return
# raises a keyError if the combination or a permutation is already mapped # raises a keyError if the combination or a permutation is already mapped
self._combination_changed(new_combi, self.event_combination) self._combination_changed(new_combi, self.input_combination)
super().__setattr__(key, value) super().__setattr__("input_combination", new_combi)
def __str__(self): def __str__(self):
return str( return str(
self.dict( 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 return self.name
if ( if (
self.event_combination == EventCombination.empty_combination() self.input_combination == InputCombination.empty_combination()
or self.event_combination is None or self.input_combination is None
): ):
return EMPTY_MAPPING_NAME return EMPTY_MAPPING_NAME
return self.event_combination.beautify() return self.input_combination.beautify()
def has_input_defined(self) -> bool: def has_input_defined(self) -> bool:
"""Whether this mapping defines an event-input.""" """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: def is_axis_mapping(self) -> bool:
"""Whether this mapping specifies an output axis.""" """Whether this mapping specifies an output axis."""
return self.output_type in [EV_ABS, EV_REL] 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: def is_wheel_output(self) -> bool:
"""Check if this maps to wheel output.""" """Check if this maps to wheel output."""
return self.output_code in ( return self.output_code in (
@ -270,7 +256,7 @@ class UIMapping(BaseModel):
""" """
if self.output_code and self.output_type: if self.output_code and self.output_type:
return self.output_type, self.output_code 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 EV_KEY, system_mapping.get(self.output_symbol)
return None return None
@ -319,7 +305,7 @@ class Mapping(UIMapping):
""" """
# Override Required attributes to enforce they are set # Override Required attributes to enforce they are set
event_combination: EventCombination input_combination: InputCombination
target_uinput: KnownUinput target_uinput: KnownUinput
def is_valid(self) -> bool: 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' f'The output_symbol "{symbol}" is not a macro and not a valid keycode-name'
) )
@validator("event_combination") @validator("input_combination")
def only_one_analog_input(cls, combination) -> EventCombination: def only_one_analog_input(cls, combination) -> InputCombination:
"""Check that the event_combination specifies a maximum of one """Check that the input_combination specifies a maximum of one
analog to analog mapping analog to analog mapping
""" """
analog_events = [event for event in combination if event.defines_analog_input]
# 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]
if len(analog_events) > 1: if len(analog_events) > 1:
raise ValueError( raise ValueError(
f"Cannot map a combination of multiple analog inputs: {analog_events}" f"Cannot map a combination of multiple analog inputs: {analog_events}"
@ -366,27 +349,21 @@ class Mapping(UIMapping):
return combination return combination
@validator("event_combination") @validator("input_combination")
def trigger_point_in_range(cls, combination) -> EventCombination: def trigger_point_in_range(cls, combination: InputCombination) -> InputCombination:
"""Check if the trigger point for mapping analog axis to buttons is valid.""" """Check if the trigger point for mapping analog axis to buttons is valid."""
for event in combination: for input_config in combination:
if event.type == EV_ABS and abs(event.value) >= 100: if (
input_config.type == EV_ABS
and input_config.analog_threshold
and abs(input_config.analog_threshold) >= 100
):
raise ValueError( raise ValueError(
f"{event = } maps an absolute axis to a button, but the trigger " f"{input_config = } maps an absolute axis to a button, but the trigger "
"point (event.value) is not between -100[%] and 100[%]" "point (event.analog_threshold) is not between -100[%] and 100[%]"
) )
return combination 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 @root_validator
def validate_output_symbol_variant(cls, values): def validate_output_symbol_variant(cls, values):
"""Validate that either type and code or symbol are set for key output.""" """Validate that either type and code or symbol are set for key output."""
@ -428,17 +405,16 @@ class Mapping(UIMapping):
return values return values
@root_validator @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. """Validate that an output type is an axis if we have an input axis.
And vice versa.""" And vice versa."""
combination: EventCombination = values.get("event_combination") assert isinstance(values.get("input_combination"), InputCombination)
event_values = [event.value for event in combination] 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_type = values.get("output_type")
output_symbol = values.get("output_symbol") 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: if not use_as_analog and not output_symbol and output_type != EV_KEY:
raise ValueError( raise ValueError(
"missing macro or key: " "missing macro or key: "

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

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

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

@ -18,11 +18,9 @@
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>. # along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations 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 gi.repository import Gtk
from inputremapper.gui.components.common import FlowBoxEntry, FlowBoxWrapper from inputremapper.gui.components.common import FlowBoxEntry, FlowBoxWrapper

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

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

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

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

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

@ -42,7 +42,9 @@ if TYPE_CHECKING:
class Message(Protocol): class Message(Protocol):
"""The protocol any message must follow to be sent with the MessageBroker.""" """The protocol any message must follow to be sent with the MessageBroker."""
message_type: MessageType @property
def message_type(self) -> MessageType:
...
# useful type aliases # useful type aliases
@ -65,7 +67,10 @@ class MessageBroker:
def signal(self, signal: MessageType): def signal(self, signal: MessageType):
"""Send a signal without any data payload.""" """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): def _publish(self, data: Message, file: str, line: int):
logger.debug( logger.debug(
@ -107,7 +112,7 @@ class MessageBroker:
pass pass
class Signal(Message): class Signal:
"""Send a Message without any associated data over the MassageBus.""" """Send a Message without any associated data over the MassageBus."""
def __init__(self, message_type: MessageType): def __init__(self, message_type: MessageType):

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

@ -23,17 +23,15 @@
see gui.reader_service.ReaderService see gui.reader_service.ReaderService
""" """
from typing import Optional, List, Generator, Dict, Tuple, Set
import time import time
from typing import Optional, List, Generator, Dict, Tuple, Set
import evdev import evdev
import gi import gi
gi.require_version("GLib", "2.0")
from gi.repository import GLib 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.groups import _Groups, _Group
from inputremapper.gui.reader_service import ( from inputremapper.gui.reader_service import (
MSG_EVENT, MSG_EVENT,
@ -153,7 +151,7 @@ class ReaderClient:
# update the generator # update the generator
try: try:
if self._recording_generator is not None: if self._recording_generator is not None:
self._recording_generator.send(InputEvent(*message_body)) self._recording_generator.send(InputEvent(**message_body))
else: else:
# the ReaderService should only send events while the gui # the ReaderService should only send events while the gui
# is recording, so this is unexpected. # is recording, so this is unexpected.
@ -198,13 +196,22 @@ class ReaderClient:
self.message_broker.signal(MessageType.recording_finished) 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: def _recorder(self) -> RecordingGenerator:
"""Generator which receives InputEvents. """Generator which receives InputEvents.
It accumulates them into EventCombinations and sends those on the It accumulates them into EventCombinations and sends those on the
message_broker. It will stop once all keys or inputs are released. message_broker. It will stop once all keys or inputs are released.
""" """
active: Set[Tuple[int, int]] = set() active: Set = set()
accumulator: List[InputEvent] = [] accumulator: List[InputEvent] = []
while True: while True:
event: InputEvent = yield event: InputEvent = yield
@ -213,7 +220,7 @@ class ReaderClient:
if event.value == 0: if event.value == 0:
try: try:
active.remove((event.type, event.code)) active.remove(event.input_match_hash)
except KeyError: except KeyError:
# we haven't seen this before probably a key got released which # we haven't seen this before probably a key got released which
# was pressed before we started recording. ignore it. # was pressed before we started recording. ignore it.
@ -224,21 +231,25 @@ class ReaderClient:
return return
continue continue
active.add(event.type_and_code) active.add(event.input_match_hash)
accu_type_code = [e.type_and_code for e in accumulator] accu_input_hashes = [e.input_match_hash for e in accumulator]
if event.type_and_code in accu_type_code and event not 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 # the value has changed but the event is already in the accumulator
# update the event # update the event
i = accu_type_code.index(event.type_and_code) i = accu_input_hashes.index(event.input_match_hash)
accumulator[i] = event accumulator[i] = event
self.message_broker.publish( self.message_broker.publish(
CombinationRecorded(EventCombination(accumulator)) CombinationRecorded(
InputCombination(map(self._input_event_to_config, accumulator))
)
) )
if event not in accumulator: if event not in accumulator:
accumulator.append(event) accumulator.append(event)
self.message_broker.publish( self.message_broker.publish(
CombinationRecorded(EventCombination(accumulator)) CombinationRecorded(
InputCombination(map(self._input_event_to_config, accumulator))
)
) )
def set_group(self, group: _Group): 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 from __future__ import annotations
import time
import logging
import os
import asyncio import asyncio
import logging
import multiprocessing import multiprocessing
import os
import subprocess import subprocess
import sys import sys
import time
from collections import defaultdict from collections import defaultdict
from typing import Set, List from typing import Set, List
import evdev import evdev
from evdev.ecodes import EV_KEY, EV_ABS, EV_REL, REL_HWHEEL, REL_WHEEL 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.configs.input_config import InputCombination, InputConfig
from inputremapper.event_combination import EventCombination from inputremapper.configs.mapping import Mapping
from inputremapper.groups import _Groups, _Group from inputremapper.groups import _Groups, _Group
from inputremapper.injection.event_reader import EventReader from inputremapper.injection.event_reader import EventReader
from inputremapper.injection.mapping_handlers.abs_to_btn_handler import AbsToBtnHandler 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.injection.mapping_handlers.rel_to_btn_handler import RelToBtnHandler
from inputremapper.input_event import InputEvent, EventActions from inputremapper.input_event import InputEvent, EventActions
from inputremapper.ipc.pipe import Pipe from inputremapper.ipc.pipe import Pipe
@ -267,68 +272,80 @@ class ReaderService:
context = ContextDummy() context = ContextDummy()
# create a context for each source # create a context for each source
for device in sources: for device in sources:
device_hash = get_device_hash(device)
capabilities = device.capabilities(absinfo=False) capabilities = device.capabilities(absinfo=False)
for ev_code in capabilities.get(EV_KEY) or (): for ev_code in capabilities.get(EV_KEY) or ():
context.notify_callbacks[(EV_KEY, ev_code)].append( input_config = InputConfig(
ForwardToUIHandler(self._results_pipe).notify 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 (): for ev_code in capabilities.get(EV_ABS) or ():
# positive direction # positive direction
mapping = UIMapping( input_config = InputConfig(
event_combination=EventCombination((EV_ABS, ev_code, 30)), type=EV_ABS,
code=ev_code,
analog_threshold=30,
origin_hash=device_hash,
)
mapping = Mapping(
input_combination=InputCombination(input_config),
target_uinput="keyboard", target_uinput="keyboard",
output_symbol="KEY_A",
) )
handler: MappingHandler = AbsToBtnHandler( handler: MappingHandler = AbsToBtnHandler(
EventCombination((EV_ABS, ev_code, 30)), mapping InputCombination(input_config), mapping
) )
handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) 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 # negative direction
mapping = UIMapping( input_config = input_config.modify(analog_threshold=-30)
event_combination=EventCombination((EV_ABS, ev_code, -30)), mapping = Mapping(
input_combination=InputCombination(input_config),
target_uinput="keyboard", target_uinput="keyboard",
output_symbol="KEY_A",
) )
handler = AbsToBtnHandler( handler = AbsToBtnHandler(InputCombination(input_config), mapping)
EventCombination((EV_ABS, ev_code, -30)), mapping
)
handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) 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 (): for ev_code in capabilities.get(EV_REL) or ():
# positive direction # positive direction
mapping = UIMapping( input_config = InputConfig(
event_combination=EventCombination( type=EV_REL,
(EV_REL, ev_code, self.rel_xy_speed[ev_code]) 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", target_uinput="keyboard",
output_symbol="KEY_A",
release_timeout=0.3, release_timeout=0.3,
force_release_timeout=True, force_release_timeout=True,
) )
handler = RelToBtnHandler( handler = RelToBtnHandler(InputCombination(input_config), mapping)
EventCombination((EV_REL, ev_code, self.rel_xy_speed[ev_code])),
mapping,
)
handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) 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 # negative direction
mapping = UIMapping( input_config = input_config.modify(
event_combination=EventCombination( analog_threshold=-self.rel_xy_speed[ev_code]
(EV_REL, ev_code, -self.rel_xy_speed[ev_code]) )
), mapping = Mapping(
input_combination=InputCombination(input_config),
target_uinput="keyboard", target_uinput="keyboard",
output_symbol="KEY_A",
release_timeout=0.3, release_timeout=0.3,
force_release_timeout=True, force_release_timeout=True,
) )
handler = RelToBtnHandler( handler = RelToBtnHandler(InputCombination(input_config), mapping)
EventCombination((EV_REL, ev_code, -self.rel_xy_speed[ev_code])),
mapping,
)
handler.set_sub_handler(ForwardToUIHandler(self._results_pipe)) 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 return context
@ -336,7 +353,13 @@ class ReaderService:
class ContextDummy: class ContextDummy:
def __init__(self): def __init__(self):
self.listeners = set() 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): def reset(self):
pass pass
@ -372,13 +395,14 @@ class ForwardToUIHandler:
self.pipe.send( self.pipe.send(
{ {
"type": MSG_EVENT, "type": MSG_EVENT,
"message": ( "message": {
event.sec, "sec": event.sec,
event.usec, "usec": event.usec,
event.type, "type": event.type,
event.code, "code": event.code,
event.value, "value": event.value,
), "origin_hash": event.origin_hash,
},
} }
) )
return True return True

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

@ -25,9 +25,6 @@ from typing import List, Callable, Dict, Optional
import gi 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 gi.repository import Gtk, GLib, Gdk
from inputremapper.logger import logger from inputremapper.logger import logger

@ -20,16 +20,19 @@
"""Stores injection-process wide information.""" """Stores injection-process wide information."""
from collections import defaultdict 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.configs.preset import Preset
from inputremapper.injection.mapping_handlers.mapping_handler import ( from inputremapper.injection.mapping_handlers.mapping_handler import (
InputEventHandler,
EventListener, EventListener,
NotifyCallback, NotifyCallback,
) )
from inputremapper.injection.mapping_handlers.mapping_parser import parse_mappings from inputremapper.injection.mapping_handlers.mapping_parser import (
from inputremapper.input_event import InputEvent parse_mappings,
EventPipelines,
)
class Context: class Context:
@ -61,12 +64,12 @@ class Context:
""" """
listeners: Set[EventListener] listeners: Set[EventListener]
notify_callbacks: Dict[Tuple[int, int], List[NotifyCallback]] _notify_callbacks: Dict[Hashable, List[NotifyCallback]]
_handlers: Dict[InputEvent, Set[InputEventHandler]] _handlers: EventPipelines
def __init__(self, preset: Preset): def __init__(self, preset: Preset):
self.listeners = set() self.listeners = set()
self.notify_callbacks = defaultdict(list) self._notify_callbacks = defaultdict(list)
self._handlers = parse_mappings(preset, self) self._handlers = parse_mappings(preset, self)
self._create_callbacks() self._create_callbacks()
@ -79,7 +82,10 @@ class Context:
def _create_callbacks(self) -> None: def _create_callbacks(self) -> None:
"""Add the notify method from all _handlers to self.callbacks.""" """Add the notify method from all _handlers to self.callbacks."""
for event, handler_list in self._handlers.items(): for input_config, handler_list in self._handlers.items():
self.notify_callbacks[event.type_and_code].extend( self._notify_callbacks[input_config.input_match_hash].extend(
handler.notify for handler in handler_list 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 asyncio
import os import os
from typing import AsyncIterator, Protocol, Set, Dict, Tuple, List from typing import AsyncIterator, Protocol, Set, List
import evdev import evdev
from inputremapper.utils import get_device_hash
from inputremapper.injection.mapping_handlers.mapping_handler import ( from inputremapper.injection.mapping_handlers.mapping_handler import (
EventListener, EventListener,
NotifyCallback, NotifyCallback,
@ -36,11 +37,13 @@ from inputremapper.logger import logger
class Context(Protocol): class Context(Protocol):
listeners: Set[EventListener] listeners: Set[EventListener]
notify_callbacks: Dict[Tuple[int, int], List[NotifyCallback]]
def reset(self): def reset(self):
... ...
def get_entry_points(self, input_event: InputEvent) -> List[NotifyCallback]:
...
class EventReader: class EventReader:
"""Reads input events from a single device and distributes them. """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 Should be an UInput with capabilities that work for all forwarded
events, so ideally they should be copied from source. events, so ideally they should be copied from source.
""" """
self._device_hash = get_device_hash(source)
self._source = source self._source = source
self._forward_to = forward_to self._forward_to = forward_to
self.context = context self.context = context
@ -119,7 +123,7 @@ class EventReader:
return False return False
results = set() results = set()
notify_callbacks = self.context.notify_callbacks.get(event.type_and_code) notify_callbacks = self.context.get_entry_points(event)
if notify_callbacks: if notify_callbacks:
for notify_callback in notify_callbacks: for notify_callback in notify_callbacks:
results.add( results.add(
@ -191,7 +195,9 @@ class EventReader:
self._source.fd, self._source.fd,
) )
async for event in self.read_loop(): 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() self.context.reset()
logger.info("read loop for %s stopped", self._source.path) logger.info("read loop for %s stopped", self._source.path)

@ -20,19 +20,21 @@
"""Keeps injecting keycodes in the background based on the preset.""" """Keeps injecting keycodes in the background based on the preset."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import enum import enum
import multiprocessing import multiprocessing
import sys import sys
import time import time
from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from multiprocessing.connection import Connection from multiprocessing.connection import Connection
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, List, Optional, Tuple, Union
import evdev import evdev
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.preset import Preset from inputremapper.configs.preset import Preset
from inputremapper.event_combination import EventCombination
from inputremapper.groups import ( from inputremapper.groups import (
_Group, _Group,
classify, classify,
@ -43,6 +45,7 @@ from inputremapper.injection.context import Context
from inputremapper.injection.event_reader import EventReader from inputremapper.injection.event_reader import EventReader
from inputremapper.injection.numlock import set_numlock, is_numlock_on, ensure_numlock from inputremapper.injection.numlock import set_numlock, is_numlock_on, ensure_numlock
from inputremapper.logger import logger from inputremapper.logger import logger
from inputremapper.utils import get_device_hash
CapabilitiesDict = Dict[int, List[int]] CapabilitiesDict = Dict[int, List[int]]
GroupSources = List[evdev.InputDevice] GroupSources = List[evdev.InputDevice]
@ -67,7 +70,7 @@ class InjectorState(str, enum.Enum):
def is_in_capabilities( def is_in_capabilities(
combination: EventCombination, capabilities: CapabilitiesDict combination: InputCombination, capabilities: CapabilitiesDict
) -> bool: ) -> bool:
"""Are this combination or one of its sub keys in the capabilities?""" """Are this combination or one of its sub keys in the capabilities?"""
for event in combination: for event in combination:
@ -109,6 +112,7 @@ class Injector(multiprocessing.Process):
group: _Group group: _Group
preset: Preset preset: Preset
context: Optional[Context] context: Optional[Context]
_devices: List[evdev.InputDevice]
_state: InjectorState _state: InjectorState
_msg_pipe: Tuple[Connection, Connection] _msg_pipe: Tuple[Connection, Connection]
_event_readers: List[EventReader] _event_readers: List[EventReader]
@ -187,7 +191,26 @@ class Injector(multiprocessing.Process):
"""Process internal stuff.""" """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 = [ ranking = [
DeviceType.KEYBOARD, DeviceType.KEYBOARD,
DeviceType.GAMEPAD, DeviceType.GAMEPAD,
@ -197,40 +220,41 @@ class Injector(multiprocessing.Process):
DeviceType.CAMERA, DeviceType.CAMERA,
DeviceType.UNKNOWN, 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 if len(candidates) > 1:
devices: List[evdev.InputDevice] = [] # there is more than on input device which can be used for this
for path in self.group.paths: # event we choose only one determined by the ranking
try: return sorted(candidates, key=lambda d: ranking.index(classify(d)))[0]
devices.append(evdev.InputDevice(path)) if len(candidates) == 1:
except (FileNotFoundError, OSError): return candidates.pop()
logger.error('Could not find "%s"', path)
continue 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 # find all devices which have an associated mapping
# use a dict because the InputDevice is not directly hashable # use a dict because the InputDevice is not directly hashable
needed_devices = {} needed_devices = {}
input_configs = set()
# find all unique input_config's
for mapping in self.preset: for mapping in self.preset:
for event in mapping.event_combination: for input_config in mapping.input_combination:
candidates: List[evdev.InputDevice] = [ input_configs.add(input_config)
device
for device in devices # find all unique input_device's
if event.code for input_config in input_configs:
in device.capabilities(absinfo=False).get(event.type, []) if not (device := self._find_input_device(input_config)):
] # there is no point in trying the fallback because
if len(candidates) > 1: # self._update_preset already did that.
# there is more than on input device which can be used for this continue
# event we choose only one determined by the ranking needed_devices[device.path] = device
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
grabbed_devices = [] grabbed_devices = []
for device in needed_devices.values(): for device in needed_devices.values():
@ -238,6 +262,29 @@ class Injector(multiprocessing.Process):
grabbed_devices.append(device) grabbed_devices.append(device)
return grabbed_devices 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]: def _grab_device(self, device: evdev.InputDevice) -> Optional[evdev.InputDevice]:
"""Try to grab the device, return None if not possible. """Try to grab the device, return None if not possible.
@ -261,7 +308,8 @@ class Injector(multiprocessing.Process):
logger.error(str(error)) logger.error(str(error))
return None 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.""" """Copy capabilities for a new device."""
ecodes = evdev.ecodes ecodes = evdev.ecodes
@ -305,6 +353,35 @@ class Injector(multiprocessing.Process):
self._msg_pipe[0].send(InjectorState.STOPPED) self._msg_pipe[0].send(InjectorState.STOPPED)
return 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: def run(self) -> None:
"""The injection worker that keeps injecting until terminated. """The injection worker that keeps injecting until terminated.
@ -323,16 +400,21 @@ class Injector(multiprocessing.Process):
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(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, # create this within the process after the event loop creation,
# so that the macros use the correct loop # so that the macros use the correct loop
self.context = Context(self.preset) self.context = Context(self.preset)
self._stop_event = asyncio.Event() 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: if len(sources) == 0:
# maybe the preset was empty or something # maybe the preset was empty or something
logger.error("Did not grab any device") logger.error("Did not grab any device")
@ -343,33 +425,7 @@ class Injector(multiprocessing.Process):
coroutines = [] coroutines = []
for source in sources: for source in sources:
# copy as much information as possible, because libinput uses the extra forward_to = self._create_forwarding_device(source)
# 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
# actually doing things # actually doing things
event_reader = EventReader( event_reader = EventReader(
self.context, self.context,

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

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

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

@ -16,12 +16,13 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>. # 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 import evdev
from inputremapper.configs.input_config import InputConfig
from inputremapper.configs.input_config import InputCombination
from inputremapper.configs.mapping import Mapping from inputremapper.configs.mapping import Mapping
from inputremapper.event_combination import EventCombination
from inputremapper.injection.mapping_handlers.mapping_handler import ( from inputremapper.injection.mapping_handlers.mapping_handler import (
MappingHandler, MappingHandler,
HandlerEnums, HandlerEnums,
@ -42,8 +43,8 @@ class AxisSwitchHandler(MappingHandler):
output. output.
""" """
_map_axis: Tuple[int, int] # the axis we switch on or off (type and code) _map_axis: InputConfig # the InputConfig for the axis we switch on or off
_trigger_key: Tuple[Tuple[int, int]] # all events that can switch the axis _trigger_keys: Tuple[Hashable, ...] # all events that can switch the axis
_active: bool # whether the axis is on or off _active: bool # whether the axis is on or off
_last_value: int # the value of the last axis event that arrived _last_value: int # the value of the last axis event that arrived
_axis_source: evdev.InputDevice # the cached source of the axis input events _axis_source: evdev.InputDevice # the cached source of the axis input events
@ -52,21 +53,20 @@ class AxisSwitchHandler(MappingHandler):
def __init__( def __init__(
self, self,
combination: EventCombination, combination: InputCombination,
mapping: Mapping, mapping: Mapping,
**_, **_,
): ):
super().__init__(combination, mapping) super().__init__(combination, mapping)
map_axis = [ trigger_keys = tuple(
event.type_and_code for event in combination if not event.is_key_event event.input_match_hash
] for event in combination
trigger_keys = [ if not event.defines_analog_input
event.type_and_code for event in combination if event.is_key_event )
]
assert len(map_axis) != 0
assert len(trigger_keys) >= 1 assert len(trigger_keys) >= 1
self._map_axis = map_axis[0] assert (map_axis := combination.find_analog_input_config())
self._trigger_keys = tuple(trigger_keys) self._map_axis = map_axis
self._trigger_keys = trigger_keys
self._active = False self._active = False
self._last_value = 0 self._last_value = 0
@ -74,7 +74,7 @@ class AxisSwitchHandler(MappingHandler):
self._forward_device = None self._forward_device = None
def __str__(self): 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): def __repr__(self):
return self.__str__() return self.__str__()
@ -101,26 +101,28 @@ class AxisSwitchHandler(MappingHandler):
if not key_is_pressed: if not key_is_pressed:
# recenter the axis # recenter the axis
logger.debug_key(self.mapping.event_combination, "stopping axis") logger.debug_key(self.mapping.input_combination, "stopping axis")
event = InputEvent( event = InputEvent(
0, 0,
0, 0,
*self._map_axis, *self._map_axis.type_and_code,
0, 0,
actions=(EventActions.recenter,), actions=(EventActions.recenter,),
origin_hash=self._map_axis.origin_hash,
) )
self._sub_handler.notify(event, self._axis_source, self._forward_device) self._sub_handler.notify(event, self._axis_source, self._forward_device)
return True 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 # send the last cached value so that the abs axis
# is at the correct position # 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( event = InputEvent(
0, 0,
0, 0,
*self._map_axis, *self._map_axis.type_and_code,
self._last_value, self._last_value,
origin_hash=self._map_axis.origin_hash,
) )
self._sub_handler.notify(event, self._axis_source, self._forward_device) self._sub_handler.notify(event, self._axis_source, self._forward_device)
return True return True
@ -129,8 +131,8 @@ class AxisSwitchHandler(MappingHandler):
def _should_map(self, event: InputEvent): def _should_map(self, event: InputEvent):
return ( return (
event.type_and_code in self._trigger_keys event.input_match_hash in self._trigger_keys
or event.type_and_code == self._map_axis or event.input_match_hash == self._map_axis.input_match_hash
) )
def notify( def notify(
@ -169,6 +171,8 @@ class AxisSwitchHandler(MappingHandler):
def needs_wrapping(self) -> bool: def needs_wrapping(self) -> bool:
return True return True
def wrap_with(self) -> Dict[EventCombination, HandlerEnums]: def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
combination = [event for event in self.input_events if event.is_key_event] combination = [
return {EventCombination(combination): 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 # You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>. # 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 import evdev
from evdev.ecodes import EV_ABS, EV_REL from evdev.ecodes import EV_ABS, EV_REL
from inputremapper.configs.input_config import InputCombination
from inputremapper.configs.mapping import Mapping from inputremapper.configs.mapping import Mapping
from inputremapper.event_combination import EventCombination
from inputremapper.injection.mapping_handlers.mapping_handler import ( from inputremapper.injection.mapping_handlers.mapping_handler import (
MappingHandler, MappingHandler,
InputEventHandler, InputEventHandler,
@ -36,14 +36,14 @@ from inputremapper.logger import logger
class CombinationHandler(MappingHandler): class CombinationHandler(MappingHandler):
"""Keeps track of a combination and notifies a sub handler.""" """Keeps track of a combination and notifies a sub handler."""
# map of (event.type, event.code) -> bool , keep track of the combination state # map of InputEvent.input_match_hash -> bool , keep track of the combination state
_pressed_keys: Dict[Tuple[int, int], bool] _pressed_keys: Dict[Hashable, bool]
_output_state: bool # the last update we sent to a sub-handler _output_state: bool # the last update we sent to a sub-handler
_sub_handler: InputEventHandler _sub_handler: InputEventHandler
def __init__( def __init__(
self, self,
combination: EventCombination, combination: InputCombination,
mapping: Mapping, mapping: Mapping,
**_, **_,
) -> None: ) -> None:
@ -53,15 +53,16 @@ class CombinationHandler(MappingHandler):
self._output_state = False self._output_state = False
# prepare a key map for all events with non-zero value # prepare a key map for all events with non-zero value
for event in combination: for input_config in combination:
assert event.is_key_event assert not input_config.defines_analog_input
self._pressed_keys[event.type_and_code] = False self._pressed_keys[input_config.input_match_hash] = False
assert len(self._pressed_keys) > 0 # no combination handler without a key assert len(self._pressed_keys) > 0 # no combination handler without a key
def __str__(self): def __str__(self):
return ( 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): def __repr__(self):
@ -78,12 +79,11 @@ class CombinationHandler(MappingHandler):
forward: evdev.UInput, forward: evdev.UInput,
suppress: bool = False, suppress: bool = False,
) -> bool: ) -> bool:
type_code = event.type_and_code if event.input_match_hash not in self._pressed_keys.keys():
if type_code not in self._pressed_keys.keys():
return False # we are not responsible for the event return False # we are not responsible for the event
last_state = self.get_active() 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: if self.get_active() == last_state or self.get_active() == self._output_state:
# nothing changed # nothing changed
@ -113,7 +113,7 @@ class CombinationHandler(MappingHandler):
return False return False
logger.debug_key( 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) self._output_state = bool(event.value)
return self._sub_handler.notify(event, source, forward, suppress) 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: if len(self._pressed_keys) == 1 or not self.mapping.release_combination_keys:
return 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() forward.syn()
def needs_ranking(self) -> bool: def needs_ranking(self) -> bool:
return bool(self.input_events) return bool(self.input_configs)
def rank_by(self) -> EventCombination: def rank_by(self) -> InputCombination:
return EventCombination( return InputCombination(
event for event in self.input_events if event.value != 0 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 = {} return_dict = {}
for event in self.input_events: for config in self.input_configs:
if event.type == EV_ABS and event.value != 0: if config.type == EV_ABS and not config.defines_analog_input:
return_dict[EventCombination(event)] = HandlerEnums.abs2btn return_dict[InputCombination(config)] = HandlerEnums.abs2btn
if event.type == EV_REL and event.value != 0: if config.type == EV_REL and not config.defines_analog_input:
return_dict[EventCombination(event)] = HandlerEnums.rel2btn return_dict[InputCombination(config)] = HandlerEnums.rel2btn
return return_dict return return_dict

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

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

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

@ -65,10 +65,10 @@ from typing import Dict, Protocol, Set, Optional, List
import evdev import evdev
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.mapping import Mapping from inputremapper.configs.mapping import Mapping
from inputremapper.event_combination import EventCombination
from inputremapper.exceptions import MappingParsingError from inputremapper.exceptions import MappingParsingError
from inputremapper.input_event import InputEvent, EventActions from inputremapper.input_event import InputEvent
from inputremapper.logger import logger from inputremapper.logger import logger
@ -147,14 +147,14 @@ class MappingHandler:
mapping: Mapping mapping: Mapping
# all input events this handler cares about # all input events this handler cares about
# should always be a subset of mapping.event_combination # should always be a subset of mapping.input_combination
input_events: List[InputEvent] input_configs: List[InputConfig]
_sub_handler: Optional[InputEventHandler] _sub_handler: Optional[InputEventHandler]
# https://bugs.python.org/issue44807 # https://bugs.python.org/issue44807
def __init__( def __init__(
self, self,
combination: EventCombination, combination: InputCombination,
mapping: Mapping, mapping: Mapping,
**_, **_,
) -> None: ) -> None:
@ -166,14 +166,8 @@ class MappingHandler:
the combination from sub_handler.wrap_with() the combination from sub_handler.wrap_with()
mapping 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.mapping = mapping
self.input_events = new_combination self.input_configs = list(combination)
self._sub_handler = None self._sub_handler = None
def notify( def notify(
@ -209,13 +203,13 @@ class MappingHandler:
"""If this handler needs ranking and wrapping with a HierarchyHandler.""" """If this handler needs ranking and wrapping with a HierarchyHandler."""
return False return False
def rank_by(self) -> Optional[EventCombination]: def rank_by(self) -> Optional[InputCombination]:
"""The combination for which this handler needs ranking.""" """The combination for which this handler needs ranking."""
def wrap_with(self) -> Dict[EventCombination, HandlerEnums]: def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
"""A dict of EventCombination -> 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. with the given MappingHandler.
""" """
return {} return {}
@ -224,14 +218,14 @@ class MappingHandler:
"""Give this handler a sub_handler.""" """Give this handler a sub_handler."""
self._sub_handler = handler self._sub_handler = handler
def occlude_input_event(self, event: InputEvent) -> None: def occlude_input_event(self, input_config: InputConfig) -> None:
"""Remove the event from self.input_events.""" """Remove the config from self.input_configs."""
if not self.input_events: if not self.input_configs:
logger.debug_mapping_handler(self) logger.debug_mapping_handler(self)
raise MappingParsingError( 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 # should be called for each event a wrapping-handler
# has in its input_events EventCombination # has in its input_configs InputCombination
self.input_events.remove(event) 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 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.mapping import Mapping
from inputremapper.configs.preset import Preset from inputremapper.configs.preset import Preset
from inputremapper.configs.system_mapping import DISABLE_CODE, DISABLE_NAME from inputremapper.configs.system_mapping import DISABLE_CODE, DISABLE_NAME
from inputremapper.event_combination import EventCombination
from inputremapper.exceptions import MappingParsingError from inputremapper.exceptions import MappingParsingError
from inputremapper.injection.macros.parse import is_this_a_macro 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_abs_handler import AbsToAbsHandler
from inputremapper.injection.mapping_handlers.abs_to_btn_handler import AbsToBtnHandler 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.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 ( from inputremapper.injection.mapping_handlers.axis_switch_handler import (
AxisSwitchHandler, 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.null_handler import NullHandler
from inputremapper.injection.mapping_handlers.rel_to_abs_handler import RelToAbsHandler from inputremapper.injection.mapping_handlers.rel_to_abs_handler import RelToAbsHandler
from inputremapper.injection.mapping_handlers.rel_to_btn_handler import RelToBtnHandler 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.logger import logger
from inputremapper.utils import get_evdev_constant_name 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]]] = { mapping_handler_classes: Dict[HandlerEnums, Optional[Type[MappingHandler]]] = {
# all available mapping_handlers # all available mapping_handlers
@ -95,7 +94,7 @@ def parse_mappings(preset: Preset, context: ContextProtocol) -> EventPipelines:
continue continue
output_handler = constructor( output_handler = constructor(
mapping.event_combination, mapping.input_combination,
mapping, mapping,
context=context, 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 # up in multiple groups if it takes care of multiple InputEvents
event_pipelines: EventPipelines = defaultdict(set) event_pipelines: EventPipelines = defaultdict(set)
for handler in handlers: for handler in handlers:
assert handler.input_events assert handler.input_configs
for event in handler.input_events: for input_config in handler.input_configs:
logger.debug( logger.debug(
"event-pipeline with entry point: %s %s", "event-pipeline with entry point: %s %s",
get_evdev_constant_name(*event.type_and_code), get_evdev_constant_name(*input_config.type_and_code),
event.type_and_code, input_config.input_match_hash,
) )
logger.debug_mapping_handler(handler) logger.debug_mapping_handler(handler)
event_pipelines[event].add(handler) event_pipelines[input_config].add(handler)
return event_pipelines return event_pipelines
@ -168,7 +167,7 @@ def _create_event_pipeline(
handlers.extend(_create_event_pipeline(super_handler, context)) handlers.extend(_create_event_pipeline(super_handler, context))
if handler.input_events: if handler.input_configs:
# the handler was only partially wrapped, # the handler was only partially wrapped,
# we need to return it as a toplevel handler # we need to return it as a toplevel handler
handlers.append(handler) handlers.append(handler)
@ -193,7 +192,7 @@ def _get_output_handler(mapping: Mapping) -> HandlerEnums:
if mapping.output_type == EV_KEY: if mapping.output_type == EV_KEY:
return HandlerEnums.key return HandlerEnums.key
input_event = _maps_axis(mapping.event_combination) input_event = _maps_axis(mapping.input_combination)
if not input_event: if not input_event:
raise MappingParsingError( raise MappingParsingError(
f"This {mapping = } does not map to an axis, key or macro", 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) raise MappingParsingError(f"the output of {mapping = } is unknown", mapping=Mapping)
def _maps_axis(combination: EventCombination) -> Optional[InputEvent]: def _maps_axis(combination: InputCombination) -> Optional[InputConfig]:
"""Whether this EventCombination contains an InputEvent that is treated as """Whether this InputCombination contains an InputEvent that is treated as
an axis and not a binary (key or button) event. an axis and not a binary (key or button) event.
""" """
for event in combination: for event in combination:
if event.value == USE_AS_ANALOG_VALUE: if event.defines_analog_input:
return event return event
return None return None
def _create_hierarchy_handlers( def _create_hierarchy_handlers(
handlers: Dict[EventCombination, Set[MappingHandler]] handlers: Dict[InputCombination, Set[MappingHandler]]
) -> Set[MappingHandler]: ) -> Set[MappingHandler]:
"""Sort handlers by input events and create Hierarchy handlers.""" """Sort handlers by input events and create Hierarchy handlers."""
sorted_handlers = set() sorted_handlers = set()
@ -271,8 +270,8 @@ def _create_hierarchy_handlers(
def _order_combinations( def _order_combinations(
combinations: List[EventCombination], common_event: InputEvent combinations: List[InputCombination], common_config: InputConfig
) -> List[EventCombination]: ) -> List[InputCombination]:
"""Reorder the keys according to some rules. """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 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 combinations
the list which needs ordering the list which needs ordering
common_event common_config
the Key all members of Keys have in common the InputConfig all InputCombination's in combinations have in common
""" """
combinations.sort(key=len) 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 = 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[start:end] = sub_list
combinations.reverse() combinations.reverse()
return combinations 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 """Get all ranges of x for which the elements have constant length
Parameters Parameters

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

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

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

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

@ -21,26 +21,11 @@ from __future__ import annotations
import enum import enum
from dataclasses import dataclass from dataclasses import dataclass
from typing import Tuple, Union, Sequence, Callable, Optional, Any from typing import Tuple, Optional, Hashable
import evdev import evdev
from evdev import ecodes 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): class EventActions(enum.Enum):
"""Additional information an InputEvent can send through the event pipeline.""" """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 # Todo: add slots=True as soon as python 3.10 is in common distros
@dataclass(frozen=True) @dataclass(frozen=True)
class InputEvent: 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 sec: int
usec: int usec: int
type: int type: int
code: int code: int
value: int value: int
actions: Tuple[EventActions, ...] = () actions: Tuple[EventActions, ...] = ()
origin_hash: Optional[str] = None
def __hash__(self): def __eq__(self, other: InputEvent | evdev.InputEvent | Tuple[int, int, int]):
return hash((self.type, self.code, self.value)) # useful in tests
def __eq__(self, other: Any):
if isinstance(other, InputEvent) or isinstance(other, evdev.InputEvent): if isinstance(other, InputEvent) or isinstance(other, evdev.InputEvent):
return self.event_tuple == (other.type, other.code, other.value) return self.event_tuple == (other.type, other.code, other.value)
if isinstance(other, tuple): if isinstance(other, tuple):
return self.event_tuple == other return self.event_tuple == other
return False raise TypeError(f"cannot compare {type(other)} with InputEvent")
@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
if event: @property
return event def input_match_hash(self) -> Hashable:
"""a Hashable object which is intended to match the InputEvent with a
raise ValueError(f"failed to create InputEvent with {init_arg = }") InputConfig.
"""
return self.type, self.code, self.origin_hash
@classmethod @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.""" """Create a InputEvent from another InputEvent or evdev.InputEvent."""
try: 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: except AttributeError as exception:
raise InputEventCreationError( raise TypeError(
f"Failed to create InputEvent from {event = }" f"Failed to create InputEvent from {event = }"
) from exception ) 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 @classmethod
def from_tuple(cls, event_tuple: Tuple[int, int, int]) -> InputEvent: def from_tuple(cls, event_tuple: Tuple[int, int, int]) -> InputEvent:
"""Create a InputEvent from a (type, code, value) tuple.""" """Create a InputEvent from a (type, code, value) tuple."""
try: if len(event_tuple) != 3:
if len(event_tuple) != 3: raise TypeError(
raise InputEventCreationError( f"failed to create InputEvent {event_tuple = }" f" must have length 3"
f"failed to create InputEvent {event_tuple = }"
f" must have length 3"
)
return cls(
0,
0,
int(event_tuple[0]),
int(event_tuple[1]),
int(event_tuple[2]),
) )
except ValueError as exception: return cls(
raise InputEventCreationError( 0,
f"Failed to create InputEvent from {event_tuple = }" 0,
) from exception int(event_tuple[0]),
except TypeError as exception: int(event_tuple[1]),
raise InputEventCreationError( int(event_tuple[2]),
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)
@property @property
def type_and_code(self) -> Tuple[int, int]: def type_and_code(self) -> Tuple[int, int]:
@ -194,14 +138,6 @@ class InputEvent:
def __str__(self): def __str__(self):
return f"InputEvent{self.event_tuple}" 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): def timestamp(self):
"""Return the unix timestamp of when the event was seen.""" """Return the unix timestamp of when the event was seen."""
return self.sec + self.usec / 1000000 return self.sec + self.usec / 1000000
@ -214,6 +150,7 @@ class InputEvent:
code: Optional[int] = None, code: Optional[int] = None,
value: Optional[int] = None, value: Optional[int] = None,
actions: Tuple[EventActions, ...] = None, actions: Tuple[EventActions, ...] = None,
origin_hash: Optional[str] = None,
) -> InputEvent: ) -> InputEvent:
"""Return a new modified event.""" """Return a new modified event."""
return InputEvent( return InputEvent(
@ -223,105 +160,5 @@ class InputEvent:
code if code is not None else self.code, code if code is not None else self.code,
value if value is not None else self.value, value if value is not None else self.value,
actions if actions is not None else self.actions, 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 sys
import time import time
from datetime import datetime from datetime import datetime
from typing import cast, Tuple from typing import cast
import pkg_resources import pkg_resources
@ -77,7 +77,7 @@ class Logger(logging.Logger):
msg = indent * line[1] + line[0] msg = indent * line[1] + line[0]
self._log(logging.DEBUG, msg, args=None) 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. """Log a key-event message.
Example: Example:

@ -21,15 +21,28 @@
"""Utility functions.""" """Utility functions."""
import sys import sys
from hashlib import md5
from typing import Optional from typing import Optional
import evdev import evdev
DeviceHash = str
def is_service() -> bool: def is_service() -> bool:
return sys.argv[0].endswith("input-remapper-service") 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]: def get_evdev_constant_name(type_: int, code: int, *_) -> Optional[str]:
"""Handy function to get the evdev constant name.""" """Handy function to get the evdev constant name."""
# this is more readable than # this is more readable than

@ -1,26 +1,26 @@
# Usage # Usage
To open the UI to modify the mappings, look into your applications menu Look into your applications menu and search for **Input Remapper** to open the UI.
and search for 'Input Remapper'. You should be prompted for your sudo password You should be prompted for your sudo password as special permissions are needed to read
as special permissions are needed to read events from `/dev/input/` files. events from `/dev/input/` files. You can also start it via `input-remapper-gtk`.
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"> <p align="center">
<img src="usage_1.png"/> <img src="usage_1.png"/>
<img src="usage_2.png"/> <img src="usage_2.png"/>
</p> </p>
First, select your device (like your keyboard) on the first page, then create a new In the text input field, type the key to which you would like to map this input.
preset on the second page, and add a mapping. Then you can already edit your inputs, More information about the possible mappings can be found in
as shown in the screenshots. [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 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
line-numbers. line-numbers.
Changes are saved automatically. Changes are saved automatically. Press the "Apply" button to activate (inject) the
Press the "Apply" button to activate (inject) the mapping you created. mapping you created.
If you later want to modify the Input of your mapping you need to use the 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. "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. 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 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 activated in the advanced input configuration, which can be opened by clicking
`Advanced` button. on the `Advanced` button.
A mapping with an input combination is only injected once all combination keys 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 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 ```json
{ {
"autoload": { "autoload": {
"Logitech USB Keyboard": "preset name" "Logitech USB Keyboard": "preset name"
}, },
"version": "1.6" "version": "1.6"
} }
``` ```
`preset name` refers to `~/.config/input-remapper/presets/device name/preset name.json`. `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`. The device name can be found with `sudo input-remapper-control --list-devices`.
#### Preset ### Preset
The preset files are a collection of mappings. The preset files are a collection of mappings.
Here is an example configuration for preset "a" for the "gamepad" device: Here is an example configuration for preset "a" for the "gamepad" device:
`~/.config/input-remapper/presets/gamepad/a.json` `~/.config/input-remapper/presets/gamepad/a.json`
```json ```json
{ [
"1,307,1": { {
"target_uinput": "keyboard", "input_combination": [
"output_symbol": "k(2).k(3)", {"type": 1, "code": 307}
],
"target_uinput": "keyboard",
"output_symbol": "k(2).k(3)",
"macro_key_sleep_ms": 100 "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" "output_symbol": "1"
}, },
"3,1,0": { {
"target_uinput": "mouse", "input_combination": [
"output_type": 2, {"type": 3, "code": 1}
"output_code": 1, ],
"target_uinput": "mouse",
"output_type": 2,
"output_code": 1,
"gain": 0.5 "gain": 0.5
} }
} ]
``` ```
This preset consists of three mappings. This preset consists of three mappings.
* The first maps the key event with code 307 to a macro and sets the time between * 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. 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 second mapping is a combination of a key event with the code 315 and a
* The third maps the y-Axis to the y-Axis on the virtual mouse. 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 As shown above, the mapping is part of the preset. It consists of the input-combination,
and the mapping parameters. which is a list of input-configurations and the mapping parameters.
``` ```
<input-combination>: { {
"input_combination": [
<InputConfig 1>,
<InputConfig 2>
]
<parameter 1>: <value1>, <parameter 1>: <value1>,
<parameter 2>: <value2> <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. #### Input Combination and Configuration
A value of `0` means that the event will be mapped to an axis. A non-zero value means The input-combination is a list of one or more input configurations. To trigger a
that the event will be treated as a key input. 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 A input configuration is a dictionary with some or all of the following parameters:
value 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) | Parameter | Default | Type | Description |
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. | 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: The following table contains all possible parameters and their default values:
| Parameter | Default | Type | Description | | Parameter | Default | Type | Description |
|----------------------------|---------|-----------------|---------------------------------------------------------------------------------------------------------------------------------| |--------------------------|---------|-----------------|-------------------------------------------------------------------------------------------------------------------------|
| target_uinput | | string | The UInput to which the mapped event will be sent | | input_combination | | list | see [above](#input-combination-and-configuration) |
| output_symbol | | string | The symbol or macro string if applicable | | target_uinput | | string | The UInput to which the mapped event will be sent |
| output_type | | int | The event type of the mapped event | | output_symbol | | string | The symbol or macro string if applicable |
| output_code | | int | The event code of the mapped event | | output_type | | int | The event type 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 | | output_code | | int | The event code of the mapped event |
| **Macro settings** | | 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_key_sleep_ms | 0 | positive int | | | **Macro settings** | | | |
| **Axis settings** | | macro_key_sleep_ms | 0 | positive int | |
| deadzone | 0.1 | float ∈ (0, 1) | The deadzone of the input axis | | **Axis settings** | | | |
| gain | 1.0 | float | Scale factor when mapping an axis to an axis | | deadzone | 0.1 | float ∈ (0, 1) | The deadzone of the input axis |
| expo | 0 | float ∈ (-1, 1) | Non liniarity factor see also [GeoGebra](https://www.geogebra.org/calculator/mkdqueky) | | gain | 1.0 | float | Scale factor when mapping an axis to an axis |
| **EV_REL output** | | expo | 0 | float ∈ (-1, 1) | Non liniarity factor see also [GeoGebra](https://www.geogebra.org/calculator/mkdqueky) |
| rel_rate | 60 | positive int | The frequency `[Hz]` at which `EV_REL` events get generated (also effects mouse macro) | | **EV_REL output** | | | |
| **EV_REL as input** | | rel_rate | 60 | positive int | The frequency `[Hz]` at which `EV_REL` events get generated (also effects mouse macro) |
| 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. | | **EV_REL as input** | | | |
| release_timeout | 0.05 | positive float | The time `[s]` until a relative axis is considered stationary if no new events arrive | | 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 ## CLI

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

@ -28,8 +28,6 @@ from evdev.ecodes import KEY_A, KEY_B, KEY_C
import gi import gi
from inputremapper.configs.system_mapping import XKB_KEYCODE_OFFSET
gi.require_version("Gdk", "3.0") gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
gi.require_version("GLib", "2.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.stuff import spy
from tests.lib.logger import logger 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.utils import CTX_ERROR, CTX_WARNING, gtk_iteration
from inputremapper.gui.messages.message_broker import ( from inputremapper.gui.messages.message_broker import (
MessageBroker, MessageBroker,
@ -65,7 +64,7 @@ from inputremapper.gui.components.editor import (
AutoloadSwitch, AutoloadSwitch,
ReleaseCombinationSwitch, ReleaseCombinationSwitch,
CombinationListbox, CombinationListbox,
EventEntry, InputConfigEntry,
AnalogInputSwitch, AnalogInputSwitch,
TriggerThresholdInput, TriggerThresholdInput,
ReleaseTimeoutInput, ReleaseTimeoutInput,
@ -86,7 +85,7 @@ from inputremapper.gui.components.device_groups import (
DeviceGroupSelection, DeviceGroupSelection,
) )
from inputremapper.configs.mapping import MappingData from inputremapper.configs.mapping import MappingData
from inputremapper.event_combination import EventCombination from inputremapper.configs.input_config import InputCombination, InputConfig
class ComponentBaseTest(unittest.TestCase): class ComponentBaseTest(unittest.TestCase):
@ -94,7 +93,7 @@ class ComponentBaseTest(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.message_broker = MessageBroker() self.message_broker = MessageBroker()
self.controller_mock = MagicMock() self.controller_mock: Controller = MagicMock()
def destroy_all_member_widgets(self): def destroy_all_member_widgets(self):
# destroy all Gtk Widgets that are stored in self # destroy all Gtk Widgets that are stored in self
@ -304,7 +303,8 @@ class TestPresetSelection(ComponentBaseTest):
"preset2", "preset2",
( (
MappingData( 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", "preset1",
( (
MappingData( 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", "preset2",
( (
MappingData( 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( MappingData(
name="mapping1", name="mapping1",
event_combination=EventCombination((1, KEY_C, 1)), input_combination=InputCombination(
InputConfig(type=1, code=KEY_C)
),
), ),
MappingData( MappingData(
name="", name="",
event_combination=EventCombination( input_combination=InputCombination(
[(1, KEY_A, 1), (1, KEY_B, 1)] (
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
), ),
), ),
MappingData( MappingData(
name="mapping2", 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") raise Exception("Expected one MappingSelectionLabel to be selected")
def select_row(self, combination: EventCombination): def select_row(self, combination: InputCombination):
def select(label_: MappingSelectionLabel): def select(label_: MappingSelectionLabel):
if label_.combination == combination: if label_.combination == combination:
self.gui.select_row(label_) self.gui.select_row(label_)
@ -397,23 +406,28 @@ class TestMappingListbox(ComponentBaseTest):
def test_activates_correct_row(self): def test_activates_correct_row(self):
self.message_broker.publish( self.message_broker.publish(
MappingData( 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() selected = self.get_selected_row()
self.assertEqual(selected.name, "mapping1") 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): 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( 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): def test_avoids_infinite_recursion(self):
self.message_broker.publish( self.message_broker.publish(
MappingData( 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() self.controller_mock.load_mapping.assert_not_called()
@ -425,42 +439,50 @@ class TestMappingListbox(ComponentBaseTest):
( (
MappingData( MappingData(
name="qux", name="qux",
event_combination=EventCombination((1, KEY_C, 1)), input_combination=InputCombination(
InputConfig(type=1, code=KEY_C)
),
), ),
MappingData( MappingData(
name="foo", name="foo",
event_combination=EventCombination.empty_combination(), input_combination=InputCombination.empty_combination(),
), ),
MappingData( MappingData(
name="bar", 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) 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( self.message_broker.publish(
PresetData( PresetData(
"preset1", "preset1",
( (
MappingData( MappingData(
name="foo", name="foo",
event_combination=EventCombination.empty_combination(), input_combination=InputCombination.empty_combination(),
), ),
MappingData( MappingData(
name="qux", name="qux",
event_combination=EventCombination((1, KEY_C, 1)), input_combination=InputCombination(
InputConfig(type=1, code=KEY_C)
),
), ),
MappingData( MappingData(
name="bar", 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) 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): class TestMappingSelectionLabel(ComponentBaseTest):
@ -471,7 +493,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
self.message_broker, self.message_broker,
self.controller_mock, 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) self.gui.insert(self.mapping_selection_label, -1)
@ -498,7 +525,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
self.message_broker, self.message_broker,
self.controller_mock, self.controller_mock,
"foo", "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") self.assertEqual(self.gui.label.get_label(), "foo")
@ -506,38 +538,69 @@ class TestMappingSelectionLabel(ComponentBaseTest):
self.gui.select_row(self.mapping_selection_label) self.gui.select_row(self.mapping_selection_label)
self.assertEqual( self.assertEqual(
self.mapping_selection_label.combination, 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( self.message_broker.publish(
CombinationUpdate( CombinationUpdate(
EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]), InputCombination(
EventCombination((1, KEY_A, 1)), (
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
InputCombination(InputConfig(type=1, code=KEY_A)),
) )
) )
self.assertEqual( 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): def test_doesnt_update_combination_when_not_selected(self):
self.assertEqual( self.assertEqual(
self.mapping_selection_label.combination, 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( self.message_broker.publish(
CombinationUpdate( CombinationUpdate(
EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]), InputCombination(
EventCombination((1, KEY_A, 1)), (
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
),
InputCombination(InputConfig(type=1, code=KEY_A)),
) )
) )
self.assertEqual( self.assertEqual(
self.mapping_selection_label.combination, 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): def test_updates_name_when_mapping_changed_and_combination_matches(self):
self.message_broker.publish( self.message_broker.publish(
MappingData( 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", name="foo",
) )
) )
@ -546,7 +609,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
def test_ignores_mapping_when_combination_does_not_match(self): def test_ignores_mapping_when_combination_does_not_match(self):
self.message_broker.publish( self.message_broker.publish(
MappingData( 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", name="foo",
) )
) )
@ -559,7 +627,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
# load the mapping associated with the ListBoxRow # load the mapping associated with the ListBoxRow
self.message_broker.publish( self.message_broker.publish(
MappingData( 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()) self.assertTrue(self.mapping_selection_label.edit_btn.get_visible())
@ -567,7 +640,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
# load a different row # load a different row
self.message_broker.publish( self.message_broker.publish(
MappingData( 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()) 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): def test_enter_edit_mode_focuses_name_input(self):
self.message_broker.publish( self.message_broker.publish(
MappingData( 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() self.mapping_selection_label.edit_btn.clicked()
@ -586,7 +669,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
def test_enter_edit_mode_updates_visibility(self): def test_enter_edit_mode_updates_visibility(self):
self.message_broker.publish( self.message_broker.publish(
MappingData( 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() self.assert_selected()
@ -598,7 +686,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
def test_leaves_edit_mode_on_esc(self): def test_leaves_edit_mode_on_esc(self):
self.message_broker.publish( self.message_broker.publish(
MappingData( 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() self.mapping_selection_label.edit_btn.clicked()
@ -616,7 +709,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
def test_update_name(self): def test_update_name(self):
self.message_broker.publish( self.message_broker.publish(
MappingData( 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() 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): def test_name_input_contains_combination_when_name_not_set(self):
self.message_broker.publish( self.message_broker.publish(
MappingData( 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() self.mapping_selection_label.edit_btn.clicked()
@ -637,7 +740,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
def test_name_input_contains_name(self): def test_name_input_contains_name(self):
self.message_broker.publish( self.message_broker.publish(
MappingData( 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", name="foo",
) )
) )
@ -647,7 +755,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
def test_removes_name_when_name_matches_combination(self): def test_removes_name_when_name_matches_combination(self):
self.message_broker.publish( self.message_broker.publish(
MappingData( 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", name="foo",
) )
) )
@ -958,18 +1071,20 @@ class TestReleaseCombinationSwitch(ComponentBaseTest):
class TestEventEntry(ComponentBaseTest): class TestEventEntry(ComponentBaseTest):
def setUp(self) -> None: def setUp(self) -> None:
super().setUp() 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): def test_move_event(self):
self.gui._up_btn.clicked() self.gui._up_btn.clicked()
self.controller_mock.move_event_in_combination.assert_called_once_with( self.controller_mock.move_input_config_in_combination.assert_called_once_with(
InputEvent.from_string("3,0,1"), "up" InputConfig(type=3, code=0, analog_threshold=1), "up"
) )
self.controller_mock.reset_mock() self.controller_mock.reset_mock()
self.gui._down_btn.clicked() self.gui._down_btn.clicked()
self.controller_mock.move_event_in_combination.assert_called_once_with( self.controller_mock.move_input_config_in_combination.assert_called_once_with(
InputEvent.from_string("3,0,1"), "down" 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.message_broker, self.controller_mock, self.gui
) )
self.controller_mock.is_empty_mapping.return_value = False 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( 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(): for entry in self.gui.get_children():
if entry.is_selected(): if entry.is_selected():
return entry 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(): for entry in self.gui.get_children():
if entry.input_event == event: if entry.input_event == input_cfg:
self.gui.select_row(entry) self.gui.select_row(entry)
def test_loads_selected_row(self): def test_loads_selected_row(self):
self.select_row(InputEvent.from_string("1,2,1")) self.select_row(InputConfig(type=1, code=2))
self.controller_mock.load_event.assert_called_once_with( self.controller_mock.load_input_config.assert_called_once_with(
InputEvent.from_string("1,2,1") InputConfig(type=1, code=2)
) )
def test_does_not_create_rows_when_mapping_is_empty(self): def test_does_not_create_rows_when_mapping_is_empty(self):
self.controller_mock.is_empty_mapping.return_value = True 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) self.assertEqual(len(self.gui.get_children()), 0)
def test_selects_row_when_selected_event_message_arrives(self): 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.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): 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() 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) self.controller_mock.set_event_as_analog.assert_called_once_with(False)
def test_updates_state(self): 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.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()) self.assertFalse(self.gui.get_active())
def test_avoids_infinite_recursion(self): def test_avoids_infinite_recursion(self):
self.message_broker.publish(InputEvent.from_string("3,0,0")) self.message_broker.publish(InputConfig(type=3, code=0))
self.message_broker.publish(InputEvent.from_string("3,0,-10")) self.message_broker.publish(InputConfig(type=3, code=0, analog_threshold=-10))
self.controller_mock.set_event_as_analog.assert_not_called() self.controller_mock.set_event_as_analog.assert_not_called()
def test_disables_switch_when_key_event(self): 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.assertLess(self.gui.get_opacity(), 0.6)
self.assertFalse(self.gui.get_sensitive()) self.assertFalse(self.gui.get_sensitive())
def test_enables_switch_when_axis_event(self): def test_enables_switch_when_axis_event(self):
self.message_broker.publish(InputEvent.from_string("1,1,1")) self.message_broker.publish(InputConfig(type=1, code=1))
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_opacity(), 1) self.assertEqual(self.gui.get_opacity(), 1)
self.assertTrue(self.gui.get_sensitive()) self.assertTrue(self.gui.get_sensitive())
self.message_broker.publish(InputEvent.from_string("1,1,1")) self.message_broker.publish(InputConfig(type=1, code=1))
self.message_broker.publish(InputEvent.from_string("2,0,10")) self.message_broker.publish(InputConfig(type=2, code=0, analog_threshold=10))
self.assertEqual(self.gui.get_opacity(), 1) self.assertEqual(self.gui.get_opacity(), 1)
self.assertTrue(self.gui.get_sensitive()) self.assertTrue(self.gui.get_sensitive())
@ -1069,7 +1200,7 @@ class TestTriggerThresholdInput(ComponentBaseTest):
self.input = TriggerThresholdInput( self.input = TriggerThresholdInput(
self.message_broker, self.controller_mock, self.gui 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): def assert_abs_event_config(self):
self.assertEqual(self.gui.get_range(), (-99, 99)) self.assertEqual(self.gui.get_range(), (-99, 99))
@ -1087,23 +1218,23 @@ class TestTriggerThresholdInput(ComponentBaseTest):
def test_updates_event(self): def test_updates_event(self):
self.gui.set_value(15) self.gui.set_value(15)
self.controller_mock.update_event.assert_called_once_with( self.controller_mock.update_input_config.assert_called_once_with(
InputEvent.from_string("3,0,15") InputConfig(type=3, code=0, analog_threshold=15)
) )
def test_sets_value_on_selected_event_message(self): 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) self.assertEqual(self.gui.get_value(), 10)
def test_avoids_infinite_recursion(self): def test_avoids_infinite_recursion(self):
self.message_broker.publish(InputEvent.from_string("3,0,10")) self.message_broker.publish(InputConfig(type=3, code=0, analog_threshold=10))
self.controller_mock.update_event.assert_not_called() self.controller_mock.update_input_config.assert_not_called()
def test_updates_configuration_according_to_selected_event(self): def test_updates_configuration_according_to_selected_event(self):
self.assert_abs_event_config() 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.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() self.assert_key_event_config()
@ -1116,13 +1247,21 @@ class TestReleaseTimeoutInput(ComponentBaseTest):
) )
self.message_broker.publish( self.message_broker.publish(
MappingData( 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): def test_updates_timeout_on_mapping_message(self):
self.message_broker.publish( 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) self.assertEqual(self.gui.get_value(), 1)
@ -1132,28 +1271,64 @@ class TestReleaseTimeoutInput(ComponentBaseTest):
def test_avoids_infinite_recursion(self): def test_avoids_infinite_recursion(self):
self.message_broker.publish( 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() self.controller_mock.update_mapping.assert_not_called()
def test_disables_input_based_on_input_combination(self): def test_disables_input_based_on_input_combination(self):
self.message_broker.publish( 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.assertTrue(self.gui.get_sensitive())
self.assertEqual(self.gui.get_opacity(), 1) self.assertEqual(self.gui.get_opacity(), 1)
self.message_broker.publish( 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.assertFalse(self.gui.get_sensitive())
self.assertLess(self.gui.get_opacity(), 0.6) self.assertLess(self.gui.get_opacity(), 0.6)
self.message_broker.publish( 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( 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.assertFalse(self.gui.get_sensitive())
self.assertLess(self.gui.get_opacity(), 0.6) self.assertLess(self.gui.get_opacity(), 0.6)
@ -1180,7 +1355,10 @@ class TestOutputAxisSelector(ComponentBaseTest):
) )
) )
self.message_broker.publish( 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): def set_active_selection(self, selection: Tuple):
@ -1344,7 +1522,10 @@ class TestSliders(ComponentBaseTest):
self.expo, self.expo,
) )
self.message_broker.publish( 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 @staticmethod
@ -1406,7 +1587,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish( self.message_broker.publish(
MappingData( MappingData(
target_uinput="mouse", target_uinput="mouse",
event_combination="2,0,0", input_combination=InputCombination(InputConfig(type=2, code=0)),
rel_to_abs_input_cutoff=1, rel_to_abs_input_cutoff=1,
output_type=3, output_type=3,
output_code=0, output_code=0,
@ -1425,7 +1606,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish( self.message_broker.publish(
MappingData( MappingData(
target_uinput="mouse", target_uinput="mouse",
event_combination="2,0,0", input_combination=InputCombination(InputConfig(type=2, code=0)),
rel_to_abs_input_cutoff=3, rel_to_abs_input_cutoff=3,
output_type=3, output_type=3,
output_code=0, output_code=0,
@ -1438,7 +1619,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish( self.message_broker.publish(
MappingData( MappingData(
target_uinput="mouse", 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, rel_to_abs_input_cutoff=rel_to_abs_input_cutoff,
output_type=3, output_type=3,
output_code=0, output_code=0,
@ -1455,7 +1636,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish( self.message_broker.publish(
MappingData( MappingData(
target_uinput="mouse", target_uinput="mouse",
event_combination="3,0,0", input_combination=InputCombination(InputConfig(type=3, code=0)),
output_type=3, output_type=3,
output_code=0, output_code=0,
) )
@ -1467,7 +1648,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish( self.message_broker.publish(
MappingData( MappingData(
target_uinput="mouse", target_uinput="mouse",
event_combination="2,0,0", input_combination=InputCombination(InputConfig(type=2, code=0)),
rel_to_abs_input_cutoff=3, rel_to_abs_input_cutoff=3,
output_type=2, output_type=2,
output_code=0, output_code=0,
@ -1479,7 +1660,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish( self.message_broker.publish(
MappingData( MappingData(
target_uinput="mouse", target_uinput="mouse",
event_combination="3,0,0", input_combination=InputCombination(InputConfig(type=3, code=0)),
output_type=3, output_type=3,
output_code=0, output_code=0,
) )
@ -1488,7 +1669,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish( self.message_broker.publish(
MappingData( MappingData(
target_uinput="mouse", target_uinput="mouse",
event_combination="2,0,0", input_combination=InputCombination(InputConfig(type=2, code=0)),
rel_to_abs_input_cutoff=1, rel_to_abs_input_cutoff=1,
output_type=3, output_type=3,
output_code=0, output_code=0,
@ -1505,7 +1686,7 @@ class TestRequireActiveMapping(ComponentBaseTest):
self.box, self.box,
require_recorded_input=False, require_recorded_input=False,
) )
combination = EventCombination([(1, KEY_A, 1)]) combination = InputCombination(InputConfig(type=1, code=KEY_A))
self.message_broker.publish(MappingData()) self.message_broker.publish(MappingData())
self.assert_inactive(self.box) self.assert_inactive(self.box)
@ -1518,7 +1699,7 @@ class TestRequireActiveMapping(ComponentBaseTest):
self.message_broker.publish(PresetData(name="preset", mappings=(combination,))) self.message_broker.publish(PresetData(name="preset", mappings=(combination,)))
self.assert_active(self.box) 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.assert_active(self.box)
self.message_broker.publish(MappingData()) self.message_broker.publish(MappingData())
@ -1531,7 +1712,7 @@ class TestRequireActiveMapping(ComponentBaseTest):
self.box, self.box,
require_recorded_input=True, require_recorded_input=True,
) )
combination = EventCombination([(1, KEY_A, 1)]) combination = InputCombination(InputConfig(type=1, code=KEY_A))
self.message_broker.publish(MappingData()) self.message_broker.publish(MappingData())
self.assert_inactive(self.box) self.assert_inactive(self.box)
@ -1543,7 +1724,7 @@ class TestRequireActiveMapping(ComponentBaseTest):
self.assert_inactive(self.box) self.assert_inactive(self.box)
# the widget will be enabled once a mapping with recorded input is selected # 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) self.assert_active(self.box)
# this mapping doesn't have input recorded, so the box is disabled # 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_4.get_text(), "group / preset / mapping")
self.assertEqual(self.label_5.get_text(), "mapping") self.assertEqual(self.label_5.get_text(), "mapping")
combination = EventCombination([(1, KEY_A, 1), (1, KEY_B, 1)]) combination = InputCombination(
self.message_broker.publish(MappingData(event_combination=combination)) (
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_4.get_text(), "group / preset / a + b")
self.assertEqual(self.label_5.get_text(), "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( 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_4.get_text(), "group / preset / qux")
self.assertEqual(self.label_5.get_text(), "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 # the tests file needs to be imported first to make sure patches are loaded
from contextlib import contextmanager 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.test import get_project_root
from tests.lib.fixtures import new_event from tests.lib.fixtures import new_event
from tests.lib.cleanup import cleanup from tests.lib.cleanup import cleanup
from tests.lib.stuff import spy from tests.lib.stuff import spy
from tests.lib.constants import EVENT_READ_TIMEOUT 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.logger import logger
from tests.lib.fixtures import fixtures from tests.lib.fixtures import fixtures
from tests.lib.pipes import push_event, push_events, uinput_write_history_pipe 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.utils import gtk_iteration, Colors, debounce, debounce_manager
from inputremapper.gui.user_interface import UserInterface from inputremapper.gui.user_interface import UserInterface
from inputremapper.injection.injector import InjectorState 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 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.data_manager.active_mapping.target_uinput, "keyboard")
self.assertEqual(self.target_selection.get_active_id(), "keyboard") self.assertEqual(self.target_selection.get_active_id(), "keyboard")
self.assertEqual( self.assertEqual(
self.data_manager.active_mapping.event_combination, self.data_manager.active_mapping.input_combination,
EventCombination((1, 5, 1)), 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( self.assertGreater(
len(self.user_interface.autocompletion._target_key_capabilities), 0 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): def add_mapping(self, mapping: Optional[Mapping] = None):
self.controller.create_mapping() self.controller.create_mapping()
self.controller.load_mapping(EventCombination.empty_combination()) self.controller.load_mapping(InputCombination.empty_combination())
gtk_iteration() gtk_iteration()
if mapping: if mapping:
self.controller.update_mapping(**mapping.dict(exclude_defaults=True)) 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.data_manager.get_autoload())
self.assertFalse(self.autoload_toggle.get_active()) self.assertFalse(self.autoload_toggle.get_active())
self.assertEqual( 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.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.assertEqual(self.selection_label_listbox.get_selected_row().name, "4")
self.assertIsNone(self.data_manager.active_mapping.name) self.assertIsNone(self.data_manager.active_mapping.name)
@ -659,14 +665,28 @@ class TestGui(GuiTestBase):
push_events( push_events(
fixtures.foo_device_2_keyboard, 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) self.throttle(40)
origin = fixtures.foo_device_2_keyboard.get_device_hash()
mock1.assert_has_calls( mock1.assert_has_calls(
( (
call(CombinationRecorded(EventCombination.from_string("1,30,1"))),
call( 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, any_order=False,
@ -674,12 +694,12 @@ class TestGui(GuiTestBase):
self.assertEqual(mock1.call_count, 2) self.assertEqual(mock1.call_count, 2)
mock2.assert_not_called() 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.throttle(40)
self.assertEqual(mock1.call_count, 2) self.assertEqual(mock1.call_count, 2)
mock2.assert_not_called() 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.throttle(40)
self.assertEqual(mock1.call_count, 2) self.assertEqual(mock1.call_count, 2)
mock2.assert_called_once() mock2.assert_called_once()
@ -687,7 +707,7 @@ class TestGui(GuiTestBase):
self.assertFalse(self.recording_toggle.get_active()) self.assertFalse(self.recording_toggle.get_active())
mock3.assert_called_once() 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 # load a device with more capabilities
self.controller.load_group("Foo Device 2") self.controller.load_group("Foo Device 2")
gtk_iteration() gtk_iteration()
@ -696,82 +716,100 @@ class TestGui(GuiTestBase):
self.controller.start_key_recording() self.controller.start_key_recording()
push_events( push_events(
fixtures.foo_device_2_keyboard, 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) 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. # mapping or something, so it was never overwritten.
origin = fixtures.foo_device_2_keyboard.get_device_hash()
self.assertEqual( self.assertEqual(
self.data_manager.active_mapping.event_combination, self.data_manager.active_mapping.input_combination,
EventCombination.from_string("1,30,1"), InputCombination(InputConfig(type=1, code=30, origin_hash=origin)),
) )
# create a new mapping # create a new mapping
self.controller.create_mapping() self.controller.create_mapping()
gtk_iteration() gtk_iteration()
self.assertEqual( self.assertEqual(
self.data_manager.active_mapping.event_combination, self.data_manager.active_mapping.input_combination,
EventCombination.empty_combination(), InputCombination.empty_combination(),
) )
# try to record the same combination # try to record the same combination
self.controller.start_key_recording() self.controller.start_key_recording()
push_events( push_events(
fixtures.foo_device_2_keyboard, 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) self.throttle(40)
# should still be the empty mapping # should still be the empty mapping
self.assertEqual( self.assertEqual(
self.data_manager.active_mapping.event_combination, self.data_manager.active_mapping.input_combination,
EventCombination.empty_combination(), InputCombination.empty_combination(),
) )
# try to record a different combination # try to record a different combination
self.controller.start_key_recording() 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) self.throttle(40)
# nothing changed yet, as we got the duplicate combination # nothing changed yet, as we got the duplicate combination
self.assertEqual( self.assertEqual(
self.data_manager.active_mapping.event_combination, self.data_manager.active_mapping.input_combination,
EventCombination.empty_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) self.throttle(40)
# now the combination is different # now the combination is different
self.assertEqual( self.assertEqual(
self.data_manager.active_mapping.event_combination, self.data_manager.active_mapping.input_combination,
EventCombination.from_string("1,30,1+1,31,1"), InputCombination(
(
InputConfig(type=1, code=30, origin_hash=origin),
InputConfig(type=1, code=31, origin_hash=origin),
)
),
) )
# let's make the combination even longer # 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.throttle(40)
self.assertEqual( self.assertEqual(
self.data_manager.active_mapping.event_combination, self.data_manager.active_mapping.input_combination,
EventCombination.from_string("1,30,1+1,31,1+1,32,1"), 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 # make sure we stop recording by releasing all keys
push_events( push_events(
fixtures.foo_device_2_keyboard, fixtures.foo_device_2_keyboard,
[ [
InputEvent.from_string("1,31,0"), InputEvent(0, 0, 1, 31, 0),
InputEvent.from_string("1,30,0"), InputEvent(0, 0, 1, 30, 0),
InputEvent.from_string("1,32,0"), InputEvent(0, 0, 1, 32, 0),
], ],
) )
self.throttle(40) self.throttle(40)
# sending a combination update now should not do anything # sending a combination update now should not do anything
self.message_broker.publish( self.message_broker.publish(
CombinationRecorded(EventCombination.from_string("1,35,1")) CombinationRecorded(InputCombination(InputConfig(type=1, code=35)))
) )
gtk_iteration() gtk_iteration()
self.assertEqual( self.assertEqual(
self.data_manager.active_mapping.event_combination, self.data_manager.active_mapping.input_combination,
EventCombination.from_string("1,30,1+1,31,1+1,32,1"), 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): def test_create_simple_mapping(self):
@ -782,11 +820,11 @@ class TestGui(GuiTestBase):
self.assertEqual( self.assertEqual(
self.selection_label_listbox.get_selected_row().combination, self.selection_label_listbox.get_selected_row().combination,
EventCombination.empty_combination(), InputCombination.empty_combination(),
) )
self.assertEqual( self.assertEqual(
self.data_manager.active_mapping.event_combination, self.data_manager.active_mapping.input_combination,
EventCombination.empty_combination(), InputCombination.empty_combination(),
) )
self.assertEqual( self.assertEqual(
self.selection_label_listbox.get_selected_row().name, "Empty Mapping" self.selection_label_listbox.get_selected_row().name, "Empty Mapping"
@ -800,19 +838,20 @@ class TestGui(GuiTestBase):
# 2. record a combination for that mapping # 2. record a combination for that mapping
self.recording_toggle.set_active(True) self.recording_toggle.set_active(True)
gtk_iteration() 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) 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) self.throttle(40)
# check the event_combination # check the input_combination
origin = fixtures.foo_device_2_keyboard.get_device_hash()
self.assertEqual( self.assertEqual(
self.selection_label_listbox.get_selected_row().combination, 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.assertEqual(
self.data_manager.active_mapping.event_combination, self.data_manager.active_mapping.input_combination,
EventCombination.from_string("1,30,1"), InputCombination(InputConfig(type=1, code=30, origin_hash=origin)),
) )
self.assertEqual(self.selection_label_listbox.get_selected_row().name, "a") self.assertEqual(self.selection_label_listbox.get_selected_row().name, "a")
self.assertIsNone(self.data_manager.active_mapping.name) self.assertIsNone(self.data_manager.active_mapping.name)
@ -828,7 +867,9 @@ class TestGui(GuiTestBase):
self.assertEqual( self.assertEqual(
self.data_manager.active_mapping, self.data_manager.active_mapping,
Mapping( Mapping(
event_combination="1,30,1", input_combination=InputCombination(
InputConfig(type=1, code=30, origin_hash=origin)
),
output_symbol="Shift_L", output_symbol="Shift_L",
target_uinput="keyboard", target_uinput="keyboard",
), ),
@ -841,7 +882,7 @@ class TestGui(GuiTestBase):
) )
self.assertEqual( self.assertEqual(
self.selection_label_listbox.get_selected_row().combination, 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 # 4. update target to mouse
@ -850,7 +891,9 @@ class TestGui(GuiTestBase):
self.assertEqual( self.assertEqual(
self.data_manager.active_mapping, self.data_manager.active_mapping,
Mapping( Mapping(
event_combination="1,30,1", input_combination=InputCombination(
InputConfig(type=1, code=30, origin_hash=origin)
),
output_symbol="Shift_L", output_symbol="Shift_L",
target_uinput="mouse", target_uinput="mouse",
), ),
@ -873,12 +916,14 @@ class TestGui(GuiTestBase):
gtk_iteration() gtk_iteration()
# it should be possible to add all of them # it should be possible to add all of them
ev_1 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, -1)) ev_1 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1)
ev_2 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, 1)) ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, 1)
ev_3 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0Y, -1)) ev_3 = (EV_ABS, evdev.ecodes.ABS_HAT0Y, -1)
ev_4 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0Y, 1)) ev_4 = (EV_ABS, evdev.ecodes.ABS_HAT0Y, 1)
def add_mapping(event, symbol): 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() self.controller.create_mapping()
gtk_iteration() gtk_iteration()
self.controller.start_key_recording() self.controller.start_key_recording()
@ -887,33 +932,38 @@ class TestGui(GuiTestBase):
gtk_iteration() gtk_iteration()
self.code_editor.get_buffer().set_text(symbol) self.code_editor.get_buffer().set_text(symbol)
gtk_iteration() 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") config_1 = add_mapping(ev_1, "a")
add_mapping(ev_2, "b") config_2 = add_mapping(ev_2, "b")
add_mapping(ev_3, "c") config_3 = add_mapping(ev_3, "c")
add_mapping(ev_4, "d") config_4 = add_mapping(ev_4, "d")
self.assertEqual( self.assertEqual(
self.data_manager.active_preset.get_mapping( self.data_manager.active_preset.get_mapping(
EventCombination(ev_1) InputCombination(config_1)
).output_symbol, ).output_symbol,
"a", "a",
) )
self.assertEqual( self.assertEqual(
self.data_manager.active_preset.get_mapping( self.data_manager.active_preset.get_mapping(
EventCombination(ev_2) InputCombination(config_2)
).output_symbol, ).output_symbol,
"b", "b",
) )
self.assertEqual( self.assertEqual(
self.data_manager.active_preset.get_mapping( self.data_manager.active_preset.get_mapping(
EventCombination(ev_3) InputCombination(config_3)
).output_symbol, ).output_symbol,
"c", "c",
) )
self.assertEqual( self.assertEqual(
self.data_manager.active_preset.get_mapping( self.data_manager.active_preset.get_mapping(
EventCombination(ev_4) InputCombination(config_4)
).output_symbol, ).output_symbol,
"d", "d",
) )
@ -927,27 +977,47 @@ class TestGui(GuiTestBase):
gtk_iteration() gtk_iteration()
# it should be possible to write a combination # it should be possible to write a combination
ev_1 = InputEvent.from_tuple((EV_KEY, evdev.ecodes.KEY_A, 1)) ev_1 = (EV_KEY, evdev.ecodes.KEY_A, 1)
ev_2 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, 1)) ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, 1)
ev_3 = InputEvent.from_tuple((EV_KEY, evdev.ecodes.KEY_C, 1)) ev_3 = (EV_KEY, evdev.ecodes.KEY_C, 1)
ev_4 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, -1)) ev_4 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1)
combination_1 = EventCombination((ev_1, ev_2, ev_3)) combination_1 = (ev_1, ev_2, ev_3)
combination_2 = EventCombination((ev_2, ev_1, ev_3)) combination_2 = (ev_2, ev_1, ev_3)
# same as 1, but different D-Pad direction # same as 1, but different D-Pad direction
combination_3 = EventCombination((ev_1, ev_4, ev_3)) combination_3 = (ev_1, ev_4, ev_3)
combination_4 = EventCombination((ev_4, ev_1, ev_3)) combination_4 = (ev_4, ev_1, ev_3)
# same as 1, but the last combination is different # same as 1, but the last combination is different
combination_5 = EventCombination((ev_1, ev_3, ev_2)) combination_5 = (ev_1, ev_3, ev_2)
combination_6 = EventCombination((ev_3, ev_1, ev_2)) combination_6 = (ev_3, ev_1, ev_2)
def add_mapping(combi: EventCombination, symbol): 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() self.controller.create_mapping()
gtk_iteration() gtk_iteration()
self.controller.start_key_recording() self.controller.start_key_recording()
previous_event = InputEvent.from_string("1,1,1") previous_event = InputEvent(0, 0, 1, 1, 1)
for event in combi: for event_tuple in combi:
event = InputEvent.from_tuple(event_tuple)
if event.type != previous_event.type: if event.type != previous_event.type:
self.throttle(20) # avoid race condition if we switch fixture self.throttle(20) # avoid race condition if we switch fixture
if event.type == EV_KEY: if event.type == EV_KEY:
@ -957,7 +1027,8 @@ class TestGui(GuiTestBase):
if event.type == EV_REL: if event.type == EV_REL:
push_event(fixtures.foo_device_2_mouse, event) 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: if event.type == EV_KEY:
push_event(fixtures.foo_device_2_keyboard, event.modify(value=0)) push_event(fixtures.foo_device_2_keyboard, event.modify(value=0))
if event.type == EV_ABS: if event.type == EV_ABS:
@ -972,100 +1043,160 @@ class TestGui(GuiTestBase):
add_mapping(combination_1, "a") add_mapping(combination_1, "a")
self.assertEqual( 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", "a",
) )
self.assertEqual( 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", "a",
) )
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_3)) self.assertIsNone(
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_4)) self.data_manager.active_preset.get_mapping(get_combination(combination_3))
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_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 # it won't write the same combination again, even if the
# first two events are in a different order # first two events are in a different order
add_mapping(combination_2, "b") add_mapping(combination_2, "b")
self.assertEqual( 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", "a",
) )
self.assertEqual( 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", "a",
) )
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_3)) self.assertIsNone(
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_4)) self.data_manager.active_preset.get_mapping(get_combination(combination_3))
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_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") add_mapping(combination_3, "c")
self.assertEqual( 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", "a",
) )
self.assertEqual( 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", "a",
) )
self.assertEqual( 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", "c",
) )
self.assertEqual( 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", "c",
) )
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_5)) self.assertIsNone(
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_6)) 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 # same as with combination_2, the existing combination_3 blocks
# combination_4 because they have the same keys and end in the # combination_4 because they have the same keys and end in the
# same key. # same key.
add_mapping(combination_4, "d") add_mapping(combination_4, "d")
self.assertEqual( 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", "a",
) )
self.assertEqual( 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", "a",
) )
self.assertEqual( 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", "c",
) )
self.assertEqual( 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", "c",
) )
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_5)) self.assertIsNone(
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_6)) 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") add_mapping(combination_5, "e")
self.assertEqual( 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", "a",
) )
self.assertEqual( 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", "a",
) )
self.assertEqual( 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", "c",
) )
self.assertEqual( 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", "c",
) )
self.assertEqual( 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", "e",
) )
self.assertEqual( 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", "e",
) )
@ -1078,7 +1209,7 @@ class TestGui(GuiTestBase):
def test_only_one_empty_mapping_possible(self): def test_only_one_empty_mapping_possible(self):
self.assertEqual( self.assertEqual(
self.selection_label_listbox.get_selected_row().combination, 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.selection_label_listbox.get_children()), 1)
self.assertEqual(len(self.data_manager.active_preset), 1) self.assertEqual(len(self.data_manager.active_preset), 1)
@ -1087,7 +1218,7 @@ class TestGui(GuiTestBase):
gtk_iteration() gtk_iteration()
self.assertEqual( self.assertEqual(
self.selection_label_listbox.get_selected_row().combination, 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.selection_label_listbox.get_children()), 2)
self.assertEqual(len(self.data_manager.active_preset), 2) self.assertEqual(len(self.data_manager.active_preset), 2)
@ -1111,7 +1242,7 @@ class TestGui(GuiTestBase):
self.recording_toggle.set_active(True) self.recording_toggle.set_active(True)
gtk_iteration() gtk_iteration()
self.message_broker.publish( self.message_broker.publish(
CombinationRecorded(EventCombination((EV_KEY, KEY_Q, 1))) CombinationRecorded(InputCombination(InputConfig(type=EV_KEY, code=KEY_Q)))
) )
gtk_iteration() gtk_iteration()
self.message_broker.signal(MessageType.recording_finished) self.message_broker.signal(MessageType.recording_finished)
@ -1131,7 +1262,7 @@ class TestGui(GuiTestBase):
self.recording_toggle.set_active(True) self.recording_toggle.set_active(True)
gtk_iteration() gtk_iteration()
self.message_broker.publish( self.message_broker.publish(
CombinationRecorded(EventCombination((EV_KEY, KEY_Q, 1))) CombinationRecorded(InputCombination(InputConfig(type=EV_KEY, code=KEY_Q)))
) )
gtk_iteration() gtk_iteration()
self.message_broker.signal(MessageType.recording_finished) self.message_broker.signal(MessageType.recording_finished)
@ -1140,7 +1271,7 @@ class TestGui(GuiTestBase):
self.controller.create_mapping() self.controller.create_mapping()
gtk_iteration() gtk_iteration()
row: MappingSelectionLabel = self.selection_label_listbox.get_selected_row() 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.assertEqual(row.label.get_text(), "Empty Mapping")
self.assertIs(self.selection_label_listbox.get_row_at_index(2), row) self.assertIs(self.selection_label_listbox.get_row_at_index(2), row)
@ -1173,7 +1304,7 @@ class TestGui(GuiTestBase):
self.controller.create_mapping() self.controller.create_mapping()
gtk_iteration() gtk_iteration()
row = self.selection_label_listbox.get_selected_row() 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.assertEqual(row.label.get_text(), "Empty Mapping")
self.assertIs(self.selection_label_listbox.get_row_at_index(2), row) 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.assertIsNone(self.data_manager.active_mapping.name)
self.assertEqual( self.assertEqual(
self.data_manager.active_mapping.event_combination, self.data_manager.active_mapping.input_combination,
EventCombination.empty_combination(), InputCombination.empty_combination(),
) )
def test_remove_mapping(self): def test_remove_mapping(self):
@ -1232,11 +1363,12 @@ class TestGui(GuiTestBase):
self.controller.load_group("Foo Device 2") self.controller.load_group("Foo Device 2")
gtk_iteration() 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() self.controller.create_mapping()
gtk_iteration() gtk_iteration()
self.controller.start_key_recording() 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( push_events(
fixtures.foo_device_2_keyboard, fixtures.foo_device_2_keyboard,
[event.modify(value=0) for event in combi], [event.modify(value=0) for event in combi],
@ -1246,7 +1378,8 @@ class TestGui(GuiTestBase):
self.code_editor.get_buffer().set_text(symbol) self.code_editor.get_buffer().set_text(symbol)
gtk_iteration() 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") add_mapping(combination, "b")
text = self.get_status_text() text = self.get_status_text()
self.assertIn("shift", text) self.assertIn("shift", text)
@ -1289,11 +1422,11 @@ class TestGui(GuiTestBase):
self.controller.load_preset("preset1") self.controller.load_preset("preset1")
self.throttle(20) 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() gtk_iteration()
self.controller.update_mapping(output_symbol="foo") self.controller.update_mapping(output_symbol="foo")
gtk_iteration() gtk_iteration()
self.controller.load_mapping(EventCombination.from_string("1,2,1")) self.controller.load_mapping(InputCombination(InputConfig(type=1, code=2)))
gtk_iteration() gtk_iteration()
self.controller.update_mapping(output_symbol="qux") self.controller.update_mapping(output_symbol="qux")
gtk_iteration() gtk_iteration()
@ -1316,7 +1449,7 @@ class TestGui(GuiTestBase):
self.assertTrue(error_icon.get_visible()) self.assertTrue(error_icon.get_visible())
self.assertFalse(warning_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() gtk_iteration()
self.controller.update_mapping(output_symbol="b") self.controller.update_mapping(output_symbol="b")
gtk_iteration() gtk_iteration()
@ -1392,7 +1525,7 @@ class TestGui(GuiTestBase):
) )
self.assertIsNotNone(self.data_manager.active_mapping) self.assertIsNotNone(self.data_manager.active_mapping)
self.assertEqual( self.assertEqual(
self.data_manager.active_mapping.event_combination, self.data_manager.active_mapping.input_combination,
self.selection_label_listbox.get_selected_row().combination, self.selection_label_listbox.get_selected_row().combination,
) )
@ -1410,8 +1543,8 @@ class TestGui(GuiTestBase):
self.assertEqual( self.assertEqual(
mappings, mappings,
{ {
EventCombination.from_string("1,1,1"), InputCombination(InputConfig(type=1, code=1)),
EventCombination.from_string("1,2,1"), InputCombination(InputConfig(type=1, code=2)),
}, },
) )
self.assertFalse(self.autoload_toggle.get_active()) self.assertFalse(self.autoload_toggle.get_active())
@ -1425,8 +1558,8 @@ class TestGui(GuiTestBase):
self.assertEqual( self.assertEqual(
mappings, mappings,
{ {
EventCombination.from_string("1,3,1"), InputCombination(InputConfig(type=1, code=3)),
EventCombination.from_string("1,4,1"), InputCombination(InputConfig(type=1, code=4)),
}, },
) )
self.assertTrue(self.autoload_toggle.get_active()) self.assertTrue(self.autoload_toggle.get_active())
@ -1525,7 +1658,7 @@ class TestGui(GuiTestBase):
self.controller.create_mapping() self.controller.create_mapping()
gtk_iteration() gtk_iteration()
self.controller.update_mapping( self.controller.update_mapping(
event_combination=EventCombination(InputEvent.btn_left()), input_combination=InputCombination(InputConfig.btn_left()),
output_symbol="a", output_symbol="a",
) )
gtk_iteration() gtk_iteration()
@ -1828,8 +1961,8 @@ class TestGui(GuiTestBase):
push_events( push_events(
fixtures.bar_device, fixtures.bar_device,
[ [
InputEvent.from_string("1,30,1"), InputEvent(0, 0, 1, 30, 1),
InputEvent.from_string("1,30,0"), InputEvent(0, 0, 1, 30, 0),
], ],
) )
self.throttle(100) # give time for the input to arrive 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") gi.require_version("GtkSource", "4")
from gi.repository import Gtk, Gdk, GLib from gi.repository import Gtk, Gdk, GLib
from inputremapper.gui.utils import gtk_iteration
from tests.lib.cleanup import quick_cleanup 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.messages.message_broker import MessageBroker, MessageType
from inputremapper.gui.user_interface import UserInterface from inputremapper.gui.user_interface import UserInterface
from inputremapper.configs.mapping import MappingData from inputremapper.configs.mapping import MappingData
from inputremapper.event_combination import EventCombination from inputremapper.configs.input_config import InputCombination, InputConfig
class TestUserInterface(unittest.TestCase): class TestUserInterface(unittest.TestCase):
@ -92,7 +92,10 @@ class TestUserInterface(unittest.TestCase):
def test_combination_label_shows_combination(self): def test_combination_label_shows_combination(self):
self.message_broker.publish( self.message_broker.publish(
MappingData( 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() gtk_iteration()

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

@ -66,6 +66,7 @@ def push_event(fixture: Fixture, event, force: bool = False):
fixture fixture
For example 'Foo Device' For example 'Foo Device'
event event
The InputEvent to send
force force
don't check if the event is in fixture.capabilities 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 # You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>. # 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.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 ( from evdev.ecodes import (
EV_REL, EV_REL,
EV_ABS, EV_ABS,
@ -32,7 +33,6 @@ import unittest
from inputremapper.injection.context import Context from inputremapper.injection.context import Context
from inputremapper.configs.preset import Preset from inputremapper.configs.preset import Preset
from inputremapper.event_combination import EventCombination
from inputremapper.configs.mapping import Mapping from inputremapper.configs.mapping import Mapping
@ -44,23 +44,23 @@ class TestContext(unittest.TestCase):
def test_callbacks(self): def test_callbacks(self):
preset = Preset() preset = Preset()
cfg = { cfg = {
"event_combination": ",".join((str(EV_ABS), str(ABS_X), "0")), "input_combination": get_combination_config((EV_ABS, ABS_X)),
"target_uinput": "mouse", "target_uinput": "mouse",
"output_type": EV_REL, "output_type": EV_REL,
"output_code": REL_HWHEEL_HI_RES, "output_code": REL_HWHEEL_HI_RES,
} }
preset.add(Mapping(**cfg)) # abs x -> wheel 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 cfg["output_code"] = REL_WHEEL_HI_RES
preset.add(Mapping(**cfg)) # abs y -> wheel preset.add(Mapping(**cfg)) # abs y -> wheel
preset.add(get_key_mapping(EventCombination((1, 31, 1)), "keyboard", "k(a)")) preset.add(get_key_mapping(get_combination_config((1, 31)), "keyboard", "k(a)"))
preset.add(get_key_mapping(EventCombination((1, 32, 1)), "keyboard", "b")) preset.add(get_key_mapping(get_combination_config((1, 32)), "keyboard", "b"))
# overlapping combination for (1, 32, 1) # overlapping combination for (1, 32, 1)
preset.add( preset.add(
get_key_mapping( get_key_mapping(
EventCombination(((1, 32, 1), (1, 33, 1), (1, 34, 1))), get_combination_config((1, 32), (1, 33), (1, 34)),
"keyboard", "keyboard",
"c", "c",
) )
@ -68,27 +68,29 @@ class TestContext(unittest.TestCase):
# map abs x to key "b" # map abs x to key "b"
preset.add( 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) context = Context(preset)
# expected callbacks and their lengths: # expected callbacks and their lengths:
callbacks = { callbacks = {
( # ABS_X -> "d" and ABS_X -> wheel have the same type and code
EV_ABS, InputConfig(type=EV_ABS, code=ABS_X).input_match_hash: 2,
ABS_X, InputConfig(type=EV_ABS, code=ABS_Y).input_match_hash: 1,
): 2, # ABS_X -> "d" and ABS_X -> wheel have the same type and code InputConfig(type=1, code=31).input_match_hash: 1,
(EV_ABS, ABS_Y): 1,
(1, 31): 1,
# even though we have 2 mappings with this type and code, we only expect one callback # 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 # because they both map to keys. We don't want to trigger two mappings with the same key press
(1, 32): 1, InputConfig(type=1, code=32).input_match_hash: 1,
(1, 33): 1, InputConfig(type=1, code=33).input_match_hash: 1,
(1, 34): 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(): 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 # 7 unique input events in the preset
self.assertEqual(7, len(context._handlers)) self.assertEqual(7, len(context._handlers))

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

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

@ -20,8 +20,9 @@
from tests.test import is_service_running from tests.test import is_service_running
from tests.lib.logger import logger
from tests.lib.cleanup import cleanup 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.pipes import push_events, uinput_write_history_pipe
from tests.lib.tmp import tmp from tests.lib.tmp import tmp
from tests.lib.fixtures import fixtures, get_key_mapping 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.configs.global_config import global_config
from inputremapper.groups import groups from inputremapper.groups import groups
from inputremapper.configs.paths import get_config_path, mkdir, get_preset_path 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.configs.preset import Preset
from inputremapper.injection.injector import InjectorState from inputremapper.injection.injector import InjectorState
from inputremapper.daemon import Daemon from inputremapper.daemon import Daemon
@ -114,8 +115,7 @@ class TestDaemon(unittest.TestCase):
preset_name = "foo" preset_name = "foo"
ev_1 = (EV_KEY, BTN_A) ev = (EV_ABS, ABS_X)
ev_2 = (EV_ABS, ABS_X)
group = groups.find(name="gamepad") group = groups.find(name="gamepad")
@ -123,8 +123,18 @@ class TestDaemon(unittest.TestCase):
group2 = groups.find(name="Bar Device") group2 = groups.find(name="Bar Device")
preset = Preset(group.get_preset_path(preset_name)) preset = Preset(group.get_preset_path(preset_name))
preset.add(get_key_mapping(EventCombination([*ev_1, 1]), "keyboard", "a")) preset.add(
preset.add(get_key_mapping(EventCombination([*ev_2, -1]), "keyboard", "b")) 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() preset.save()
global_config.set_autoload_preset(group.key, preset_name) global_config.set_autoload_preset(group.key, preset_name)
@ -140,6 +150,7 @@ class TestDaemon(unittest.TestCase):
# has been cleanedUp in setUp # has been cleanedUp in setUp
self.assertNotIn("keyboard", global_uinputs.devices) self.assertNotIn("keyboard", global_uinputs.devices)
logger.info(f"start injector for {group.key}")
self.daemon.start_injecting(group.key, preset_name) self.daemon.start_injecting(group.key, preset_name)
# created on demand # created on demand
@ -155,6 +166,7 @@ class TestDaemon(unittest.TestCase):
self.assertEqual(event.code, BTN_B) self.assertEqual(event.code, BTN_B)
self.assertEqual(event.value, 1) self.assertEqual(event.value, 1)
logger.info(f"stopping injector for {group.key}")
self.daemon.stop_injecting(group.key) self.daemon.stop_injecting(group.key)
time.sleep(0.2) time.sleep(0.2)
self.assertEqual(self.daemon.get_state(group.key), InjectorState.STOPPED) self.assertEqual(self.daemon.get_state(group.key), InjectorState.STOPPED)
@ -167,11 +179,12 @@ class TestDaemon(unittest.TestCase):
raise raise
"""Injection 2""" """Injection 2"""
logger.info(f"start injector for {group.key}")
self.daemon.start_injecting(group.key, preset_name) self.daemon.start_injecting(group.key, preset_name)
time.sleep(0.1) time.sleep(0.1)
# -1234 will be classified as -1 by the injector # -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) time.sleep(0.1)
self.assertTrue(uinput_write_history_pipe[0].poll()) self.assertTrue(uinput_write_history_pipe[0].poll())
@ -214,7 +227,11 @@ class TestDaemon(unittest.TestCase):
system_mapping._set("a", KEY_A) system_mapping._set("a", KEY_A)
preset = Preset(get_preset_path(group_name, preset_name)) 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 # make the daemon load the file instead
with open(get_config_path("xmodmap.json"), "w") as file: 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") path = os.path.join(config_dir, "presets", name, f"{preset_name}.json")
preset = Preset(path) 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() preset.save()
system_mapping.clear() system_mapping.clear()
@ -334,7 +355,11 @@ class TestDaemon(unittest.TestCase):
pereset = Preset(group.get_preset_path(preset_name)) pereset = Preset(group.get_preset_path(preset_name))
pereset.add( 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() pereset.save()
@ -398,7 +423,11 @@ class TestDaemon(unittest.TestCase):
preset = Preset(group.get_preset_path(preset_name)) preset = Preset(group.get_preset_path(preset_name))
preset.add( 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() preset.save()
@ -451,7 +480,13 @@ class TestDaemon(unittest.TestCase):
preset_name = "preset7" preset_name = "preset7"
group = groups.find(key="Foo Device 2") group = groups.find(key="Foo Device 2")
preset = Preset(group.get_preset_path(preset_name)) 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() preset.save()
global_config.set_autoload_preset(group.key, preset_name) global_config.set_autoload_preset(group.key, preset_name)
@ -468,7 +503,13 @@ class TestDaemon(unittest.TestCase):
group = groups.find(key="Foo Device 2") group = groups.find(key="Foo Device 2")
preset = Preset(group.get_preset_path(preset_name)) 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() preset.save()
global_config.set_autoload_preset(group.key, preset_name) 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.global_config import global_config
from inputremapper.configs.mapping import UIMapping, MappingData from inputremapper.configs.mapping import UIMapping, MappingData
from inputremapper.configs.system_mapping import system_mapping 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.exceptions import DataManagementError
from inputremapper.groups import _Groups from inputremapper.groups import _Groups
from inputremapper.gui.messages.message_broker import ( 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.gui.reader_client import ReaderClient
from inputremapper.injection.global_uinputs import GlobalUInputs from inputremapper.injection.global_uinputs import GlobalUInputs
from inputremapper.input_event import InputEvent
from tests.lib.cleanup import quick_cleanup from tests.lib.cleanup import quick_cleanup
from tests.lib.patches import FakeDaemonProxy 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.paths import get_preset_path
from inputremapper.configs.preset import Preset from inputremapper.configs.preset import Preset
@ -163,13 +162,17 @@ class TestDataManager(unittest.TestCase):
self.data_manager.load_preset(name="preset1") self.data_manager.load_preset(name="preset1")
listener = Listener() listener = Listener()
self.message_broker.subscribe(MessageType.mapping, 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] mapping: MappingData = listener.calls[0]
control_preset = Preset(get_preset_path("Foo Device", "preset1")) control_preset = Preset(get_preset_path("Foo Device", "preset1"))
control_preset.load() control_preset.load()
self.assertEqual( 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, mapping.output_symbol,
) )
@ -181,7 +184,9 @@ class TestDataManager(unittest.TestCase):
control_preset.empty() control_preset.empty()
control_preset.load() control_preset.load()
self.assertEqual( 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)", "key(a)",
) )
@ -373,13 +378,17 @@ class TestDataManager(unittest.TestCase):
def test_load_mapping(self): def test_load_mapping(self):
"""should be able to load a mapping""" """should be able to load a mapping"""
preset, _, _ = prepare_presets() 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_group(group_key="Foo Device")
self.data_manager.load_preset(name="preset1") self.data_manager.load_preset(name="preset1")
listener = Listener() listener = Listener()
self.message_broker.subscribe(MessageType.mapping, 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] mapping = listener.calls[0]
self.assertEqual(mapping, expected_mapping) self.assertEqual(mapping, expected_mapping)
@ -393,7 +402,7 @@ class TestDataManager(unittest.TestCase):
self.assertRaises( self.assertRaises(
KeyError, KeyError,
self.data_manager.load_mapping, 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): def test_cannot_load_mapping_without_preset(self):
@ -404,13 +413,13 @@ class TestDataManager(unittest.TestCase):
self.assertRaises( self.assertRaises(
DataManagementError, DataManagementError,
self.data_manager.load_mapping, 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.data_manager.load_group("Foo Device")
self.assertRaises( self.assertRaises(
DataManagementError, DataManagementError,
self.data_manager.load_mapping, self.data_manager.load_mapping,
combination=EventCombination("1,1,1"), combination=InputCombination(InputConfig(type=1, code=1)),
) )
def test_load_event(self): def test_load_event(self):
@ -419,11 +428,11 @@ class TestDataManager(unittest.TestCase):
self.message_broker.subscribe(MessageType.selected_event, mock) self.message_broker.subscribe(MessageType.selected_event, mock)
self.data_manager.load_group("Foo Device") self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1") 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)))
self.data_manager.load_event(InputEvent.from_string("1,1,1")) self.data_manager.load_input_config(InputConfig(type=1, code=1))
mock.assert_called_once_with(InputEvent.from_string("1,1,1")) mock.assert_called_once_with(InputConfig(type=1, code=1))
self.assertEqual( 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): 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_group("Foo Device")
self.data_manager.load_preset("preset1") self.data_manager.load_preset("preset1")
with self.assertRaises(DataManagementError): 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): def test_cannot_load_event_when_not_in_mapping_combination(self):
prepare_presets() prepare_presets()
self.data_manager.load_group("Foo Device") self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1") 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): 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): def test_update_event(self):
prepare_presets() prepare_presets()
self.data_manager.load_group("Foo Device") self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1") 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)))
self.data_manager.load_event(InputEvent.from_string("1,1,1")) self.data_manager.load_input_config(InputConfig(type=1, code=1))
self.data_manager.update_event(InputEvent.from_string("1,5,1")) self.data_manager.update_input_config(InputConfig(type=1, code=5))
self.assertEqual( 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): def test_update_event_sends_messages(self):
prepare_presets() prepare_presets()
self.data_manager.load_group("Foo Device") self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1") 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)))
self.data_manager.load_event(InputEvent.from_string("1,1,1")) self.data_manager.load_input_config(InputConfig(type=1, code=1))
mock = MagicMock() mock = MagicMock()
self.message_broker.subscribe(MessageType.selected_event, mock) self.message_broker.subscribe(MessageType.selected_event, mock)
self.message_broker.subscribe(MessageType.combination_update, mock) self.message_broker.subscribe(MessageType.combination_update, mock)
self.message_broker.subscribe(MessageType.mapping, 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 = [ expected = [
call( 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(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) mock.assert_has_calls(expected, any_order=False)
@ -477,25 +489,27 @@ class TestDataManager(unittest.TestCase):
prepare_presets() prepare_presets()
self.data_manager.load_group("Foo Device") self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1") 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)))
self.data_manager.load_event(InputEvent.from_string("1,1,1")) self.data_manager.load_input_config(InputConfig(type=1, code=1))
with self.assertRaises(KeyError): 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): def test_cannot_update_event_when_not_loaded(self):
prepare_presets() prepare_presets()
self.data_manager.load_group("Foo Device") self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1") 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): 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): def test_update_mapping_emits_mapping_changed(self):
"""update mapping should emit a mapping_changed event""" """update mapping should emit a mapping_changed event"""
prepare_presets() prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2") self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2") 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() listener = Listener()
self.message_broker.subscribe(MessageType.mapping, listener) self.message_broker.subscribe(MessageType.mapping, listener)
@ -515,7 +529,9 @@ class TestDataManager(unittest.TestCase):
prepare_presets() prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2") self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2") 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( self.data_manager.update_mapping(
name="foo", name="foo",
@ -526,7 +542,7 @@ class TestDataManager(unittest.TestCase):
preset = Preset(get_preset_path("Foo Device", "preset2"), UIMapping) preset = Preset(get_preset_path("Foo Device", "preset2"), UIMapping)
preset.load() 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.format_name(), "foo")
self.assertEqual(mapping.output_symbol, "f") self.assertEqual(mapping.output_symbol, "f")
self.assertEqual(mapping.release_timeout, 0.3) self.assertEqual(mapping.release_timeout, 0.3)
@ -536,7 +552,9 @@ class TestDataManager(unittest.TestCase):
prepare_presets() prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2") self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2") 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( self.data_manager.update_mapping(
output_symbol="bar", # not a macro and not a valid symbol 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 = Preset(get_preset_path("Foo Device", "preset2"), UIMapping)
preset.load() 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.assertIsNotNone(mapping.get_error())
self.assertEqual(mapping.output_symbol, "bar") 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_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2") 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() listener = Listener()
self.message_broker.subscribe(MessageType.mapping, listener) self.message_broker.subscribe(MessageType.mapping, listener)
self.message_broker.subscribe(MessageType.combination_update, listener) self.message_broker.subscribe(MessageType.combination_update, listener)
# we expect a message for combination update first, and then for mapping # we expect a message for combination update first, and then for mapping
self.data_manager.update_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].message_type, MessageType.combination_update)
self.assertEqual( self.assertEqual(
listener.calls[0].old_combination, listener.calls[0].old_combination,
EventCombination.from_string("1,4,1"), InputCombination(InputConfig(type=1, code=4)),
) )
self.assertEqual( self.assertEqual(
listener.calls[0].new_combination, 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].message_type, MessageType.mapping)
self.assertEqual( self.assertEqual(
listener.calls[1].event_combination, listener.calls[1].input_combination,
EventCombination.from_string("1,5,1+1,6,1"), InputCombination(get_combination_config((1, 5), (1, 6))),
) )
def test_cannot_update_mapping_combination(self): def test_cannot_update_mapping_combination(self):
@ -584,12 +604,14 @@ class TestDataManager(unittest.TestCase):
prepare_presets() prepare_presets()
self.data_manager.load_group(group_key="Foo Device 2") self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2") 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( self.assertRaises(
KeyError, KeyError,
self.data_manager.update_mapping, 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): def test_cannot_update_mapping(self):
@ -624,7 +646,7 @@ class TestDataManager(unittest.TestCase):
self.message_broker.subscribe(MessageType.preset, listener) self.message_broker.subscribe(MessageType.preset, listener)
self.data_manager.create_mapping() # emits preset_changed 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(listener.calls[0].name, "preset2")
self.assertEqual(len(listener.calls[0].mappings), 3) 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_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2") 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() listener = Listener()
self.message_broker.subscribe(MessageType.preset, 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.delete_mapping() # emits preset
self.data_manager.save() 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 mappings = listener.calls[0].mappings
preset_name = listener.calls[0].name preset_name = listener.calls[0].name
expected_preset = Preset(get_preset_path("Foo Device", "preset2")) 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}") self.assertAlmostEqual(x, y2, msg=f"test expo symmetry for {init_args}")
def test_origin_symmetry(self): 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) f(x) = - f(-x)
within the constraints: min = -max within the constraints: min = -max
""" """
@ -111,7 +111,7 @@ class TestAxisTransformation(unittest.TestCase):
self.assertAlmostEqual( self.assertAlmostEqual(
f(x), f(x),
-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): def test_gain(self):

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

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

@ -39,18 +39,22 @@ from evdev.ecodes import (
from inputremapper.configs.mapping import Mapping from inputremapper.configs.mapping import Mapping
from inputremapper.configs.preset import Preset from inputremapper.configs.preset import Preset
from inputremapper.configs.system_mapping import system_mapping 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.context import Context
from inputremapper.injection.event_reader import EventReader from inputremapper.injection.event_reader import EventReader
from inputremapper.injection.global_uinputs import global_uinputs 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.cleanup import quick_cleanup
from tests.lib.fixtures import get_key_mapping
class TestEventReader(unittest.IsolatedAsyncioTestCase): class TestEventReader(unittest.IsolatedAsyncioTestCase):
def setUp(self): 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.stop_event = asyncio.Event()
self.preset = Preset() self.preset = Preset()
@ -77,18 +81,37 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase):
self.preset.add( self.preset.add(
get_key_mapping( get_key_mapping(
EventCombination([EV_KEY, trigger, 1]), InputCombination(
InputConfig(
type=EV_KEY,
code=trigger,
origin_hash=fixtures.gamepad.get_device_hash(),
)
),
"keyboard", "keyboard",
"if_single(key(a), key(KEY_LEFTSHIFT))", "if_single(key(a), key(KEY_LEFTSHIFT))",
) )
) )
self.preset.add( 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 # left x to mouse x
cfg = { 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", "target_uinput": "mouse",
"output_type": EV_REL, "output_type": EV_REL,
"output_code": REL_X, "output_code": REL_X,
@ -96,17 +119,23 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase):
self.preset.add(Mapping(**cfg)) self.preset.add(Mapping(**cfg))
# left y to mouse y # 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 cfg["output_code"] = REL_Y
self.preset.add(Mapping(**cfg)) self.preset.add(Mapping(**cfg))
# right x to wheel x # 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 cfg["output_code"] = REL_HWHEEL_HI_RES
self.preset.add(Mapping(**cfg)) self.preset.add(Mapping(**cfg))
# right y to wheel y # 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 cfg["output_code"] = REL_WHEEL_HI_RES
self.preset.add(Mapping(**cfg)) self.preset.add(Mapping(**cfg))
@ -139,13 +168,30 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase):
trigger = evdev.ecodes.BTN_A trigger = evdev.ecodes.BTN_A
self.preset.add( self.preset.add(
get_key_mapping( get_key_mapping(
EventCombination([EV_KEY, trigger, 1]), InputCombination(
InputConfig(
type=EV_KEY,
code=trigger,
origin_hash=fixtures.gamepad.get_device_hash(),
)
),
"keyboard", "keyboard",
"if_single(k(a), k(KEY_LEFTSHIFT))", "if_single(k(a), k(KEY_LEFTSHIFT))",
) )
) )
self.preset.add( 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) # 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.patches import uinputs
from tests.lib.cleanup import quick_cleanup from tests.lib.cleanup import quick_cleanup
from tests.lib.constants import EVENT_READ_TIMEOUT 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 uinput_write_history_pipe
from tests.lib.pipes import read_write_history_pipe, push_events from tests.lib.pipes import read_write_history_pipe, push_events
from tests.lib.fixtures import ( from tests.lib.fixtures import (
@ -61,7 +61,7 @@ from inputremapper.configs.system_mapping import (
DISABLE_NAME, DISABLE_NAME,
) )
from inputremapper.configs.preset import Preset 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.macros.parse import parse
from inputremapper.injection.context import Context from inputremapper.injection.context import Context
from inputremapper.groups import groups, classify, DeviceType from inputremapper.groups import groups, classify, DeviceType
@ -107,11 +107,22 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
quick_cleanup() 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): def test_grab(self):
# path is from the fixtures # path is from the fixtures
path = "/dev/input/event10" path = "/dev/input/event10"
preset = Preset() 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) self.injector = Injector(groups.find(key="Foo Device 2"), preset)
# this test needs to pass around all other constraints of # this test needs to pass around all other constraints of
@ -127,7 +138,13 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
def test_fail_grab(self): def test_fail_grab(self):
self.make_it_fail = 999 self.make_it_fail = 999
preset = Preset() 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) self.injector = Injector(groups.find(key="Foo Device 2"), preset)
path = "/dev/input/event10" path = "/dev/input/event10"
@ -148,9 +165,15 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
def test_grab_device_1(self): def test_grab_device_1(self):
preset = Preset() preset = Preset()
preset.add( 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.context = Context(preset)
self.injector.group.paths = [ self.injector.group.paths = [
"/dev/input/event10", "/dev/input/event10",
@ -165,16 +188,18 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
def test_forward_gamepad_events(self): def test_forward_gamepad_events(self):
# forward abs joystick events # forward abs joystick events
preset = Preset() 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) self.injector.context = Context(preset)
path = "/dev/input/event30" 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() devices = self.injector._grab_devices()
self.assertEqual(len(devices), 1) self.assertEqual(len(devices), 1)
self.assertEqual(devices[0].path, path) self.assertEqual(devices[0].path, path)
@ -184,25 +209,34 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
def test_skip_unused_device(self): def test_skip_unused_device(self):
# skips a device because its capabilities are not used in the preset # skips a device because its capabilities are not used in the preset
preset = Preset() preset = Preset()
preset.add(get_key_mapping(EventCombination([EV_KEY, 10, 1]), "keyboard", "a")) preset.add(
self.injector = Injector(groups.find(key="Foo Device 2"), preset) 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) 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() devices = self.injector._grab_devices()
self.assertEqual(devices, []) self.assertEqual(len(devices), 1)
self.assertEqual(self.failed, 0) self.assertEqual(self.failed, 2)
def test_skip_unknown_device(self): def test_skip_unknown_device(self):
preset = Preset() 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 # 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) self.injector.context = Context(preset)
path = "/dev/input/event11"
self.injector.group.paths = [path]
devices = self.injector._grab_devices() devices = self.injector._grab_devices()
# skips the device alltogether, so no grab attempts fail # skips the device alltogether, so no grab attempts fail
@ -226,9 +260,26 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
@mock.patch("evdev.InputDevice.ungrab") @mock.patch("evdev.InputDevice.ungrab")
def test_capabilities_and_uinput_presence(self, ungrab_patch): def test_capabilities_and_uinput_presence(self, ungrab_patch):
preset = Preset() 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( 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", "keyboard",
"key(b)", "key(b)",
) )
@ -239,11 +290,28 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
self.injector.run() self.injector.run()
self.assertEqual( 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, m1,
) )
self.assertEqual( 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, m2,
) )
@ -286,14 +354,34 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
preset = Preset() preset = Preset()
preset.add( preset.add(
get_key_mapping( 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", "keyboard",
"k(KEY_Q).k(w)", "k(KEY_Q).k(w)",
) )
) )
preset.add( preset.add(
get_key_mapping( 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", "keyboard",
"a", "a",
) )
@ -303,23 +391,52 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
preset.add( preset.add(
get_key_mapping( 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", "keyboard",
"b", "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( push_events(
fixtures.gamepad, fixtures.foo_device_2_keyboard,
[ [
# should execute a macro... # should execute a macro...
new_event(EV_KEY, 8, 1), # forwarded new_event(EV_KEY, 8, 1), # forwarded
new_event(EV_KEY, 9, 1), # triggers macro new_event(EV_KEY, 9, 1), # triggers macro
new_event(EV_KEY, 8, 0), # releases macro new_event(EV_KEY, 8, 0), # releases macro
new_event(EV_KEY, 9, 0), # forwarded 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 # gamepad stuff. trigger a combination
new_event(EV_ABS, ABS_HAT0X, -1), new_event(EV_ABS, ABS_HAT0X, -1),
new_event(EV_ABS, ABS_HAT0X, 0), new_event(EV_ABS, ABS_HAT0X, 0),
],
)
time.sleep(0.1)
push_events(
fixtures.foo_device_2_keyboard,
[
# just pass those over without modifying # just pass those over without modifying
new_event(EV_KEY, 10, 1), new_event(EV_KEY, 10, 1),
new_event(EV_KEY, 10, 0), new_event(EV_KEY, 10, 0),
@ -328,14 +445,8 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
force=True, force=True,
) )
self.injector = Injector(groups.find(name="gamepad"), preset) # the injector needs time to process this
self.assertEqual(self.injector.get_state(), InjectorState.UNKNOWN) time.sleep(0.1)
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)
# sending anything arbitrary does not stop the process # sending anything arbitrary does not stop the process
# (is_alive checked later after some time) # (is_alive checked later after some time)
@ -400,18 +511,18 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
self.assertEqual(self.injector.get_state(), InjectorState.RUNNING) self.assertEqual(self.injector.get_state(), InjectorState.RUNNING)
def test_is_in_capabilities(self): def test_is_in_capabilities(self):
key = EventCombination((1, 2, 1)) key = InputCombination(get_combination_config((1, 2, 1)))
capabilities = {1: [9, 2, 5]} capabilities = {1: [9, 2, 5]}
self.assertTrue(is_in_capabilities(key, capabilities)) 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]} capabilities = {1: [9, 2, 5]}
# only one of the codes of the combination is required. # only one of the codes of the combination is required.
# The goal is to make combinations= across those sub-devices possible, # The goal is to make combinations= across those sub-devices possible,
# that make up one hardware device # that make up one hardware device
self.assertTrue(is_in_capabilities(key, capabilities)) 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]} capabilities = {1: [9, 2, 5]}
self.assertTrue(is_in_capabilities(key, capabilities)) self.assertTrue(is_in_capabilities(key, capabilities))
@ -459,10 +570,16 @@ class TestModifyCapabilities(unittest.TestCase):
return self._capabilities return self._capabilities
preset = Preset() preset = Preset()
preset.add(get_key_mapping(EventCombination([EV_KEY, 80, 1]), "keyboard", "a"))
preset.add( preset.add(
get_key_mapping( 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", "keyboard",
DISABLE_NAME, DISABLE_NAME,
), ),
@ -473,14 +590,22 @@ class TestModifyCapabilities(unittest.TestCase):
preset.add( preset.add(
get_key_mapping( 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 # going to be ignored, because EV_REL cannot be mapped, that's
# mouse movements. # mouse movements.
preset.add( 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") self.a = system_mapping.get("a")
@ -506,14 +631,6 @@ class TestModifyCapabilities(unittest.TestCase):
quick_cleanup() quick_cleanup()
def test_copy_capabilities(self): 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 # 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 # remove it until somebody complains, since its presence broke stuff
self.injector = Injector(mock.Mock(), self.preset) 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 import evdev
from dataclasses import FrozenInstanceError from dataclasses import FrozenInstanceError
from inputremapper.input_event import InputEvent from inputremapper.input_event import InputEvent
from inputremapper.exceptions import InputEventCreationError
class TestInputEvent(unittest.TestCase): class TestInputEvent(unittest.TestCase):
@ -44,27 +43,7 @@ class TestInputEvent(unittest.TestCase):
self.assertEqual(e1.code, e2.code) self.assertEqual(e1.code, e2.code)
self.assertEqual(e1.value, e2.value) self.assertEqual(e1.value, e2.value)
self.assertRaises(InputEventCreationError, InputEvent.from_event, "1,2,3") self.assertRaises(TypeError, InputEvent.from_event, "1,2,3")
def test_from_string(self):
s1 = "1,2,3"
s2 = "1 ,2, 3 "
s3 = (1, 2, 3)
s4 = "1,2,3,4"
s5 = "1,2,_3"
e1 = InputEvent.from_string(s1)
e2 = InputEvent.from_string(s2)
self.assertEqual(e1, e2)
self.assertEqual(e1.sec, 0)
self.assertEqual(e1.usec, 0)
self.assertEqual(e1.type, 1)
self.assertEqual(e1.code, 2)
self.assertEqual(e1.value, 3)
self.assertRaises(InputEventCreationError, InputEvent.from_string, s3)
self.assertRaises(InputEventCreationError, InputEvent.from_string, s4)
self.assertRaises(InputEventCreationError, InputEvent.from_string, s5)
def test_from_event_tuple(self): def test_from_event_tuple(self):
t1 = (1, 2, 3) t1 = (1, 2, 3)
@ -81,11 +60,8 @@ class TestInputEvent(unittest.TestCase):
self.assertEqual(e1.code, 2) self.assertEqual(e1.code, 2)
self.assertEqual(e1.value, 3) self.assertEqual(e1.value, 3)
self.assertRaises(InputEventCreationError, InputEvent.from_string, t3)
self.assertRaises(InputEventCreationError, InputEvent.from_string, t4)
def test_properties(self): def test_properties(self):
e1 = InputEvent.btn_left() e1 = InputEvent.from_tuple((evdev.ecodes.EV_KEY, evdev.ecodes.BTN_LEFT, 1))
self.assertEqual( self.assertEqual(
e1.event_tuple, e1.event_tuple,
(evdev.ecodes.EV_KEY, evdev.ecodes.BTN_LEFT, 1), (evdev.ecodes.EV_KEY, evdev.ecodes.BTN_LEFT, 1),

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

@ -12,9 +12,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>. # 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.cleanup import quick_cleanup
from tests.lib.tmp import tmp from tests.lib.tmp import tmp
from tests.lib.fixtures import get_combination_config
import os import os
import unittest import unittest
@ -37,12 +38,13 @@ from evdev.ecodes import (
REL_HWHEEL_HI_RES, REL_HWHEEL_HI_RES,
) )
from inputremapper.configs.mapping import UIMapping
from inputremapper.configs.migrations import migrate, config_version from inputremapper.configs.migrations import migrate, config_version
from inputremapper.configs.preset import Preset from inputremapper.configs.preset import Preset
from inputremapper.configs.global_config import global_config from inputremapper.configs.global_config import global_config
from inputremapper.configs.paths import touch, CONFIG_PATH, mkdir, get_preset_path from inputremapper.configs.paths import touch, CONFIG_PATH, mkdir, get_preset_path
from inputremapper.logger import IS_BETA 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.user import HOME
from inputremapper.logger import VERSION from inputremapper.logger import VERSION
@ -178,7 +180,7 @@ class TestMigrations(unittest.TestCase):
mappings like mappings like
{(type, code): symbol} or {(type, code, value): symbol} should migrate {(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") path = os.path.join(CONFIG_PATH, "presets", "Foo Device", "test.json")
os.makedirs(os.path.dirname(path), exist_ok=True) os.makedirs(os.path.dirname(path), exist_ok=True)
@ -209,69 +211,81 @@ class TestMigrations(unittest.TestCase):
preset.load() preset.load()
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 1, 1])), preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=1))),
UIMapping( UIMapping(
event_combination=EventCombination([EV_KEY, 1, 1]), input_combination=InputCombination(InputConfig(type=EV_KEY, code=1)),
target_uinput="keyboard", target_uinput="keyboard",
output_symbol="a", output_symbol="a",
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 2, 1])), preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=2))),
UIMapping( UIMapping(
event_combination=EventCombination([EV_KEY, 2, 1]), input_combination=InputCombination(InputConfig(type=EV_KEY, code=2)),
target_uinput="gamepad", target_uinput="gamepad",
output_symbol="BTN_B", output_symbol="BTN_B",
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 3, 1])), preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=3))),
UIMapping( UIMapping(
event_combination=EventCombination([EV_KEY, 3, 1]), input_combination=InputCombination(InputConfig(type=EV_KEY, code=3)),
target_uinput="keyboard", target_uinput="keyboard",
output_symbol="BTN_1\n# Broken mapping:\n# No target can handle all specified keycodes", output_symbol="BTN_1\n# Broken mapping:\n# No target can handle all specified keycodes",
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 4, 1])), preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=4))),
UIMapping( UIMapping(
event_combination=EventCombination([EV_KEY, 4, 1]), input_combination=InputCombination(InputConfig(type=EV_KEY, code=4)),
target_uinput="keyboard", target_uinput="keyboard",
output_symbol="d", output_symbol="d",
), ),
) )
self.assertEqual( 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( 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", target_uinput="keyboard",
output_symbol="b", output_symbol="b",
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping( 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( UIMapping(
event_combination=EventCombination( input_combination=InputCombination(
((EV_ABS, 1, 1), (EV_ABS, 2, -1), (EV_ABS, 3, 1)), get_combination_config(
(EV_ABS, 1, 1), (EV_ABS, 2, -1), (EV_ABS, 3, 1)
),
), ),
target_uinput="keyboard", target_uinput="keyboard",
output_symbol="c", output_symbol="c",
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 5, 1])), preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=5))),
UIMapping( UIMapping(
event_combination=EventCombination([EV_KEY, 5, 1]), input_combination=InputCombination(InputConfig(type=EV_KEY, code=5)),
target_uinput="foo", target_uinput="foo",
output_symbol="e", output_symbol="e",
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 6, 1])), preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=6))),
UIMapping( UIMapping(
event_combination=EventCombination([EV_KEY, 6, 1]), input_combination=InputCombination(InputConfig(type=EV_KEY, code=6)),
target_uinput="keyboard", target_uinput="keyboard",
output_symbol="key(a, b)", output_symbol="key(a, b)",
), ),
@ -302,41 +316,41 @@ class TestMigrations(unittest.TestCase):
preset.load() preset.load()
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 1, 1])), preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=1))),
UIMapping( UIMapping(
event_combination=EventCombination([EV_KEY, 1, 1]), input_combination=InputCombination(InputConfig(type=EV_KEY, code=1)),
target_uinput="keyboard", target_uinput="keyboard",
output_symbol="otherwise + otherwise", output_symbol="otherwise + otherwise",
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 2, 1])), preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=2))),
UIMapping( UIMapping(
event_combination=EventCombination([EV_KEY, 2, 1]), input_combination=InputCombination(InputConfig(type=EV_KEY, code=2)),
target_uinput="keyboard", target_uinput="keyboard",
output_symbol="bar($otherwise)", output_symbol="bar($otherwise)",
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 3, 1])), preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=3))),
UIMapping( UIMapping(
event_combination=EventCombination([EV_KEY, 3, 1]), input_combination=InputCombination(InputConfig(type=EV_KEY, code=3)),
target_uinput="keyboard", target_uinput="keyboard",
output_symbol="foo(else=qux)", output_symbol="foo(else=qux)",
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 4, 1])), preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=4))),
UIMapping( UIMapping(
event_combination=EventCombination([EV_KEY, 4, 1]), input_combination=InputCombination(InputConfig(type=EV_KEY, code=4)),
target_uinput="foo", target_uinput="foo",
output_symbol="qux(otherwise).bar(else=1)", output_symbol="qux(otherwise).bar(else=1)",
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination([EV_KEY, 5, 1])), preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=5))),
UIMapping( UIMapping(
event_combination=EventCombination([EV_KEY, 5, 1]), input_combination=InputCombination(InputConfig(type=EV_KEY, code=5)),
target_uinput="keyboard", target_uinput="keyboard",
output_symbol="foo(otherwise1=2qux)", output_symbol="foo(otherwise1=2qux)",
), ),
@ -400,9 +414,11 @@ class TestMigrations(unittest.TestCase):
# 2 mappings for wheel # 2 mappings for wheel
self.assertEqual(len(preset), 4) self.assertEqual(len(preset), 4)
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_X, 0))), preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_X))),
UIMapping( UIMapping(
event_combination=EventCombination((EV_ABS, ABS_X, 0)), input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_X)
),
target_uinput="mouse", target_uinput="mouse",
output_type=EV_REL, output_type=EV_REL,
output_code=REL_X, output_code=REL_X,
@ -410,9 +426,11 @@ class TestMigrations(unittest.TestCase):
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_Y, 0))), preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_Y))),
UIMapping( UIMapping(
event_combination=EventCombination((EV_ABS, ABS_Y, 0)), input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_Y)
),
target_uinput="mouse", target_uinput="mouse",
output_type=EV_REL, output_type=EV_REL,
output_code=REL_Y, output_code=REL_Y,
@ -420,9 +438,11 @@ class TestMigrations(unittest.TestCase):
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_RX, 0))), preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RX))),
UIMapping( UIMapping(
event_combination=EventCombination((EV_ABS, ABS_RX, 0)), input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_RX)
),
target_uinput="mouse", target_uinput="mouse",
output_type=EV_REL, output_type=EV_REL,
output_code=REL_HWHEEL_HI_RES, output_code=REL_HWHEEL_HI_RES,
@ -430,9 +450,11 @@ class TestMigrations(unittest.TestCase):
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_RY, 0))), preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RY))),
UIMapping( UIMapping(
event_combination=EventCombination((EV_ABS, ABS_RY, 0)), input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_RY)
),
target_uinput="mouse", target_uinput="mouse",
output_type=EV_REL, output_type=EV_REL,
output_code=REL_WHEEL_HI_RES, output_code=REL_WHEEL_HI_RES,
@ -468,9 +490,11 @@ class TestMigrations(unittest.TestCase):
# 2 mappings for wheel # 2 mappings for wheel
self.assertEqual(len(preset), 4) self.assertEqual(len(preset), 4)
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_RX, 0))), preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RX))),
UIMapping( UIMapping(
event_combination=EventCombination((EV_ABS, ABS_RX, 0)), input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_RX)
),
target_uinput="mouse", target_uinput="mouse",
output_type=EV_REL, output_type=EV_REL,
output_code=REL_X, output_code=REL_X,
@ -478,9 +502,11 @@ class TestMigrations(unittest.TestCase):
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_RY, 0))), preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RY))),
UIMapping( UIMapping(
event_combination=EventCombination((EV_ABS, ABS_RY, 0)), input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_RY)
),
target_uinput="mouse", target_uinput="mouse",
output_type=EV_REL, output_type=EV_REL,
output_code=REL_Y, output_code=REL_Y,
@ -488,9 +514,11 @@ class TestMigrations(unittest.TestCase):
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_X, 0))), preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_X))),
UIMapping( UIMapping(
event_combination=EventCombination((EV_ABS, ABS_X, 0)), input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_X)
),
target_uinput="mouse", target_uinput="mouse",
output_type=EV_REL, output_type=EV_REL,
output_code=REL_HWHEEL_HI_RES, output_code=REL_HWHEEL_HI_RES,
@ -498,9 +526,11 @@ class TestMigrations(unittest.TestCase):
), ),
) )
self.assertEqual( self.assertEqual(
preset.get_mapping(EventCombination((EV_ABS, ABS_Y, 0))), preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_Y))),
UIMapping( UIMapping(
event_combination=EventCombination((EV_ABS, ABS_Y, 0)), input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_Y)
),
target_uinput="mouse", target_uinput="mouse",
output_type=EV_REL, output_type=EV_REL,
output_code=REL_WHEEL_HI_RES, 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.mapping import UIMapping
from inputremapper.configs.paths import get_preset_path, get_config_path, CONFIG_PATH from inputremapper.configs.paths import get_preset_path, get_config_path, CONFIG_PATH
from inputremapper.configs.preset import Preset from inputremapper.configs.preset import Preset
from inputremapper.event_combination import EventCombination from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.input_event import InputEvent
from tests.lib.cleanup import quick_cleanup 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): class TestPreset(unittest.TestCase):
@ -43,19 +42,21 @@ class TestPreset(unittest.TestCase):
quick_cleanup() quick_cleanup()
def test_is_mapped_multiple_times(self): 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() permutations = combination.get_permutations()
self.assertEqual(len(permutations), 6) self.assertEqual(len(permutations), 6)
self.preset._mappings[permutations[0]] = Mapping( self.preset._mappings[permutations[0]] = Mapping(
event_combination=permutations[0], input_combination=permutations[0],
target_uinput="keyboard", target_uinput="keyboard",
output_symbol="a", output_symbol="a",
) )
self.assertFalse(self.preset._is_mapped_multiple_times(permutations[2])) self.assertFalse(self.preset._is_mapped_multiple_times(permutations[2]))
self.preset._mappings[permutations[1]] = Mapping( self.preset._mappings[permutations[1]] = Mapping(
event_combination=permutations[1], input_combination=permutations[1],
target_uinput="keyboard", target_uinput="keyboard",
output_symbol="a", output_symbol="a",
) )
@ -76,7 +77,7 @@ class TestPreset(unittest.TestCase):
# load again from the disc # load again from the disc
self.preset.load() self.preset.load()
self.assertEqual( self.assertEqual(
self.preset.get_mapping(EventCombination([99, 99, 99])), self.preset.get_mapping(InputCombination.empty_combination()),
get_key_mapping(), get_key_mapping(),
) )
self.assertFalse(self.preset.has_unsaved_changes()) self.assertFalse(self.preset.has_unsaved_changes())
@ -92,13 +93,13 @@ class TestPreset(unittest.TestCase):
self.assertFalse(self.preset.has_unsaved_changes()) self.assertFalse(self.preset.has_unsaved_changes())
# modify the mapping # modify the mapping
mapping = self.preset.get_mapping(EventCombination([99, 99, 99])) mapping = self.preset.get_mapping(InputCombination.empty_combination())
mapping.gain = 0.5 mapping.gain = 0.5
self.assertTrue(self.preset.has_unsaved_changes()) self.assertTrue(self.preset.has_unsaved_changes())
self.preset.load() self.preset.load()
self.preset.path = get_preset_path("bar", "foo") 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 # empty preset and empty file
self.assertFalse(self.preset.has_unsaved_changes()) self.assertFalse(self.preset.has_unsaved_changes())
@ -117,14 +118,14 @@ class TestPreset(unittest.TestCase):
self.assertEqual(len(self.preset), 0) self.assertEqual(len(self.preset), 0)
def test_save_load(self): def test_save_load(self):
one = InputEvent.from_tuple((EV_KEY, 10, 1)) one = InputConfig(type=EV_KEY, code=10)
two = InputEvent.from_tuple((EV_KEY, 11, 1)) two = InputConfig(type=EV_KEY, code=11)
three = InputEvent.from_tuple((EV_KEY, 12, 1)) three = InputConfig(type=EV_KEY, code=12)
self.preset.add(get_key_mapping(EventCombination(one), "keyboard", "1")) self.preset.add(get_key_mapping(InputCombination(one), "keyboard", "1"))
self.preset.add(get_key_mapping(EventCombination(two), "keyboard", "2")) self.preset.add(get_key_mapping(InputCombination(two), "keyboard", "2"))
self.preset.add( 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.path = get_preset_path("Foo Device", "test")
self.preset.save() self.preset.save()
@ -139,16 +140,16 @@ class TestPreset(unittest.TestCase):
self.assertEqual(len(loaded), 3) self.assertEqual(len(loaded), 3)
self.assertRaises(TypeError, loaded.get_mapping, one) self.assertRaises(TypeError, loaded.get_mapping, one)
self.assertEqual( self.assertEqual(
loaded.get_mapping(EventCombination(one)), loaded.get_mapping(InputCombination(one)),
get_key_mapping(EventCombination(one), "keyboard", "1"), get_key_mapping(InputCombination(one), "keyboard", "1"),
) )
self.assertEqual( self.assertEqual(
loaded.get_mapping(EventCombination(two)), loaded.get_mapping(InputCombination(two)),
get_key_mapping(EventCombination(two), "keyboard", "2"), get_key_mapping(InputCombination(two), "keyboard", "2"),
) )
self.assertEqual( self.assertEqual(
loaded.get_mapping(EventCombination((two, three))), loaded.get_mapping(InputCombination((two, three))),
get_key_mapping(EventCombination((two, three)), "keyboard", "3"), get_key_mapping(InputCombination((two, three)), "keyboard", "3"),
) )
# load missing file # load missing file
@ -156,13 +157,10 @@ class TestPreset(unittest.TestCase):
self.assertRaises(FileNotFoundError, preset.load) self.assertRaises(FileNotFoundError, preset.load)
def test_modify_mapping(self): def test_modify_mapping(self):
# the reader would not report values like 111 or 222, only 1 or -1. ev_1 = InputCombination(InputConfig(type=EV_KEY, code=1))
# the preset just does what it is told, so it accepts them. ev_3 = InputCombination(InputConfig(type=EV_KEY, code=2))
ev_1 = EventCombination((EV_KEY, 1, 111))
ev_2 = EventCombination((EV_KEY, 1, 222))
ev_3 = EventCombination((EV_KEY, 2, 111))
# only values between -99 and 99 are allowed as mapping for EV_ABS or EV_REL # 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 # add the first mapping
self.preset.add(get_key_mapping(ev_1, "keyboard", "a")) 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 # change ev_1 to ev_3 and change a to b
mapping = self.preset.get_mapping(ev_1) mapping = self.preset.get_mapping(ev_1)
mapping.event_combination = ev_3 mapping.input_combination = ev_3
mapping.output_symbol = "b" mapping.output_symbol = "b"
self.assertIsNone(self.preset.get_mapping(ev_1)) self.assertIsNone(self.preset.get_mapping(ev_1))
self.assertEqual( self.assertEqual(
@ -204,7 +202,7 @@ class TestPreset(unittest.TestCase):
# try to change combination of 4 to 3 # try to change combination of 4 to 3
mapping = self.preset.get_mapping(ev_4) mapping = self.preset.get_mapping(ev_4)
with self.assertRaises(KeyError): with self.assertRaises(KeyError):
mapping.event_combination = ev_3 mapping.input_combination = ev_3
self.assertEqual( self.assertEqual(
self.preset.get_mapping(ev_3), self.preset.get_mapping(ev_3),
@ -228,13 +226,13 @@ class TestPreset(unittest.TestCase):
self.assertFalse(content) self.assertFalse(content)
def test_combinations(self): def test_combinations(self):
ev_1 = InputEvent.from_tuple((EV_KEY, 1, 111)) ev_1 = InputConfig(type=EV_KEY, code=1, analog_threshold=111)
ev_2 = InputEvent.from_tuple((EV_KEY, 1, 222)) ev_2 = InputConfig(type=EV_KEY, code=1, analog_threshold=222)
ev_3 = InputEvent.from_tuple((EV_KEY, 2, 111)) ev_3 = InputConfig(type=EV_KEY, code=2, analog_threshold=111)
ev_4 = InputEvent.from_tuple((EV_ABS, 1, 99)) ev_4 = InputConfig(type=EV_ABS, code=1, analog_threshold=99)
combi_1 = EventCombination((ev_1, ev_2, ev_3)) combi_1 = InputCombination((ev_1, ev_2, ev_3))
combi_2 = EventCombination((ev_2, ev_1, ev_3)) combi_2 = InputCombination((ev_2, ev_1, ev_3))
combi_3 = EventCombination((ev_1, ev_2, ev_4)) combi_3 = InputCombination((ev_1, ev_2, ev_4))
self.preset.add(get_key_mapping(combi_1, "keyboard", "a")) self.preset.add(get_key_mapping(combi_1, "keyboard", "a"))
self.assertEqual( self.assertEqual(
@ -277,7 +275,7 @@ class TestPreset(unittest.TestCase):
mapping = self.preset.get_mapping(combi_1) mapping = self.preset.get_mapping(combi_1)
mapping.output_symbol = "c" mapping.output_symbol = "c"
with self.assertRaises(KeyError): with self.assertRaises(KeyError):
mapping.event_combination = combi_3 mapping.input_combination = combi_3
self.assertEqual( self.assertEqual(
self.preset.get_mapping(combi_1), self.preset.get_mapping(combi_1),
@ -294,10 +292,10 @@ class TestPreset(unittest.TestCase):
def test_remove(self): def test_remove(self):
# does nothing # does nothing
ev_1 = EventCombination((EV_KEY, 40, 1)) ev_1 = InputCombination(InputConfig(type=EV_KEY, code=40))
ev_2 = EventCombination((EV_KEY, 30, 1)) ev_2 = InputCombination(InputConfig(type=EV_KEY, code=30))
ev_3 = EventCombination((EV_KEY, 20, 1)) ev_3 = InputCombination(InputConfig(type=EV_KEY, code=20))
ev_4 = EventCombination((EV_KEY, 10, 1)) ev_4 = InputCombination(InputConfig(type=EV_KEY, code=10))
self.assertRaises(TypeError, self.preset.remove, (EV_KEY, 10, 1)) self.assertRaises(TypeError, self.preset.remove, (EV_KEY, 10, 1))
self.preset.remove(ev_1) self.preset.remove(ev_1)
@ -328,13 +326,25 @@ class TestPreset(unittest.TestCase):
def test_empty(self): def test_empty(self):
self.preset.add( 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( 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( 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.assertEqual(len(self.preset), 3)
self.preset.path = get_config_path("test.json") self.preset.path = get_config_path("test.json")
@ -348,13 +358,25 @@ class TestPreset(unittest.TestCase):
def test_clear(self): def test_clear(self):
self.preset.add( 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( 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( 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.assertEqual(len(self.preset), 3)
self.preset.path = get_config_path("test.json") self.preset.path = get_config_path("test.json")
@ -370,7 +392,7 @@ class TestPreset(unittest.TestCase):
# btn left is mapped # btn left is mapped
self.preset.add( self.preset.add(
get_key_mapping( get_key_mapping(
EventCombination(InputEvent.btn_left()), InputCombination(InputConfig.btn_left()),
"keyboard", "keyboard",
"1", "1",
) )
@ -378,7 +400,7 @@ class TestPreset(unittest.TestCase):
self.assertTrue(self.preset.dangerously_mapped_btn_left()) self.assertTrue(self.preset.dangerously_mapped_btn_left())
self.preset.add( self.preset.add(
get_key_mapping( get_key_mapping(
EventCombination([EV_KEY, 41, 1]), InputCombination(InputConfig(type=EV_KEY, code=41)),
"keyboard", "keyboard",
"2", "2",
) )
@ -388,14 +410,16 @@ class TestPreset(unittest.TestCase):
# another mapping maps to btn_left # another mapping maps to btn_left
self.preset.add( self.preset.add(
get_key_mapping( get_key_mapping(
EventCombination([EV_KEY, 42, 1]), InputCombination(InputConfig(type=EV_KEY, code=42)),
"mouse", "mouse",
"btn_left", "btn_left",
) )
) )
self.assertFalse(self.preset.dangerously_mapped_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" mapping.output_symbol = "BTN_Left"
self.assertFalse(self.preset.dangerously_mapped_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()) self.assertTrue(self.preset.dangerously_mapped_btn_left())
# btn_left is not mapped # 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()) self.assertFalse(self.preset.dangerously_mapped_btn_left())
def test_save_load_with_invalid_mappings(self): def test_save_load_with_invalid_mappings(self):
@ -414,12 +438,12 @@ class TestPreset(unittest.TestCase):
self.assertFalse(ui_preset.is_valid()) self.assertFalse(ui_preset.is_valid())
# make the mapping 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.output_symbol = "a"
m.target_uinput = "keyboard" m.target_uinput = "keyboard"
self.assertTrue(ui_preset.is_valid()) 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) ui_preset.add(m2)
self.assertFalse(ui_preset.is_valid()) self.assertFalse(ui_preset.is_valid())
ui_preset.save() ui_preset.save()
@ -429,12 +453,12 @@ class TestPreset(unittest.TestCase):
preset.load() preset.load()
self.assertEqual(len(preset), 1) self.assertEqual(len(preset), 1)
a = preset.get_mapping(m.event_combination).dict() a = preset.get_mapping(m.input_combination).dict()
b = m.dict() b = m.dict()
a.pop("mapping_type") a.pop("mapping_type")
b.pop("mapping_type") b.pop("mapping_type")
self.assertEqual(a, b) 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 # both presets load
ui_preset.clear() ui_preset.clear()
@ -442,13 +466,13 @@ class TestPreset(unittest.TestCase):
ui_preset.load() ui_preset.load()
self.assertEqual(len(ui_preset), 2) 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() b = m.dict()
a.pop("mapping_type") a.pop("mapping_type")
b.pop("mapping_type") b.pop("mapping_type")
self.assertEqual(a, b) self.assertEqual(a, b)
# self.assertEqual(ui_preset.get_mapping(m.event_combination), m) # self.assertEqual(ui_preset.get_mapping(m.input_combination), m)
self.assertEqual(ui_preset.get_mapping(m2.event_combination), m2) self.assertEqual(ui_preset.get_mapping(m2.input_combination), m2)
if __name__ == "__main__": if __name__ == "__main__":

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

@ -17,6 +17,7 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>. # 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 inputremapper.gui.messages.message_broker import MessageBroker
from tests.lib.fixtures import new_event from tests.lib.fixtures import new_event
@ -131,6 +132,12 @@ class TestTest(unittest.TestCase):
reader_client.terminate() 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__": if __name__ == "__main__":
unittest.main() unittest.main()

Loading…
Cancel
Save