Fix CombinationHandler releasing (#578)

Co-authored-by: Jonas Bosse <jonas.bosse@posteo.de>
pull/621/head^2
Tobi 1 year ago committed by GitHub
parent 743ebd1bdb
commit 1986d7a2ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -21,5 +21,5 @@ jobs:
pip install black
- name: Analysing the code with black --check --diff
run: |
black --version
black --check --diff ./inputremapper ./tests

@ -48,6 +48,8 @@ Dependencies: `python3-evdev` ≥1.3.0, `gtksourceview4`, `python3-devel`, `pyth
Python packages need to be installed globally for the service to be able to import them. Don't use `--user`
Conda can cause problems due to changed python paths and versions.
```bash
sudo pip install evdev -U # If newest version not in distros repo
sudo pip uninstall key-mapper # In case the old package is still installed

@ -175,6 +175,7 @@ def internals(options):
# daemonize
cmd = f'{cmd} &'
logger.debug(f'Running `{cmd}`')
os.system(cmd)

@ -20,8 +20,7 @@
"""Starts the root reader-service."""
import asyncio
import os
import sys
import atexit
@ -54,7 +53,7 @@ if __name__ == '__main__':
os.kill(os.getpid(), signal.SIGKILL)
atexit.register(on_exit)
# TODO import `groups` instead?
groups = _Groups()
reader_service = ReaderService(groups)
reader_service.run()
loop = asyncio.get_event_loop()
loop.run_until_complete(reader_service.run())

@ -20,15 +20,18 @@
from __future__ import annotations
import itertools
from typing import Tuple, Iterable, Union, List, Dict, Optional, Hashable
from typing import Tuple, Iterable, Union, List, Dict, Optional, Hashable, TypeAlias
from evdev import ecodes
from evdev._ecodes import EV_ABS, EV_KEY, EV_REL
from inputremapper.input_event import InputEvent
from pydantic import BaseModel, root_validator, validator, constr
from pydantic import BaseModel, root_validator, validator
from inputremapper.configs.system_mapping import system_mapping
from inputremapper.gui.messages.message_types import MessageType
from inputremapper.logger import logger
from inputremapper.utils import get_evdev_constant_name
# having shift in combinations modifies the configured output,
# ctrl might not work at all
@ -41,7 +44,9 @@ DIFFICULT_COMBINATIONS = [
ecodes.KEY_RIGHTALT,
]
DeviceHash = constr(to_lower=True)
DeviceHash: TypeAlias = str
EMPTY_TYPE = 99
class InputConfig(BaseModel):
@ -55,9 +60,21 @@ class InputConfig(BaseModel):
# 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
origin_hash: Optional[DeviceHash] = None
analog_threshold: Optional[int] = None
def __str__(self):
return f"InputConfig {get_evdev_constant_name(self.type, self.code)}"
def __repr__(self):
return (
f"<InputConfig {self.type_and_code} "
f"{get_evdev_constant_name(*self.type_and_code)}, "
f"{self.analog_threshold}, "
f"{self.origin_hash}, "
f"at {hex(id(self))}>"
)
@property
def input_match_hash(self) -> Hashable:
"""a Hashable object which is intended to match the InputConfig with a
@ -68,6 +85,10 @@ class InputConfig(BaseModel):
"""
return self.type, self.code, self.origin_hash
@property
def is_empty(self) -> bool:
return self.type == EMPTY_TYPE
@property
def defines_analog_input(self) -> bool:
"""Whether this defines an analog input"""
@ -122,7 +143,7 @@ class InputConfig(BaseModel):
# 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]
key_name = get_evdev_constant_name(self.type, self.code)
if isinstance(key_name, list):
key_name = key_name[0]
@ -223,9 +244,10 @@ class InputConfig(BaseModel):
@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
if analog_threshold == 0 or analog_threshold is None:
return None
return analog_threshold
@root_validator
def _remove_analog_threshold_for_key_input(cls, values):
@ -235,35 +257,62 @@ class InputConfig(BaseModel):
values["analog_threshold"] = None
return values
@root_validator(pre=True)
def validate_origin_hash(cls, values):
origin_hash = values.get("origin_hash")
if origin_hash is None:
# For new presets, origin_hash should be set. For old ones, it can
# be still missing. A lot of tests didn't set an origin_hash.
if values.get("type") != EMPTY_TYPE:
logger.warning("No origin_hash set for %s", values)
return values
values["origin_hash"] = origin_hash.lower()
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"""
"""One or more InputConfigs 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
"""Create a new InputCombination.
Examples
--------
InputCombination([InputConfig, ...])
InputCombination([{type: ..., code: ..., value: ...}, ...])
"""
if not isinstance(configs, Iterable):
raise TypeError("InputCombination requires a list of InputConfigs.")
if isinstance(configs, InputConfig):
return super().__new__(cls, [configs]) # type: ignore
# wrap the argument in square brackets
raise TypeError("InputCombination requires a list of InputConfigs.")
validated_configs = []
for cfg in configs:
if isinstance(cfg, InputConfig):
validated_configs.append(cfg)
for config in configs:
if isinstance(configs, InputEvent):
raise TypeError("InputCombinations require InputConfigs, not Events.")
if isinstance(config, InputConfig):
validated_configs.append(config)
elif isinstance(config, dict):
validated_configs.append(InputConfig(**config))
else:
validated_configs.append(InputConfig(**cfg))
raise TypeError(f'Can\'t handle "{config}"')
if len(validated_configs) == 0:
raise ValueError(f"failed to create InputCombination with {configs = }")
@ -273,10 +322,11 @@ class InputCombination(Tuple[InputConfig, ...]):
return super().__new__(cls, validated_configs) # type: ignore
def __str__(self):
return " + ".join(event.description(exclude_threshold=True) for event in self)
return f'Combination ({" + ".join(str(event) for event in self)})'
def __repr__(self):
return f"<InputCombination {', '.join([str((*e.type_and_code, e.analog_threshold)) for e in self])}>"
combination = ", ".join(repr(event) for event in self)
return f"<InputCombination ({combination}) at {hex(id(self))}>"
@classmethod
def __get_validators__(cls):
@ -291,6 +341,7 @@ class InputCombination(Tuple[InputConfig, ...]):
return cls(init_arg)
def to_config(self) -> Tuple[Dict[str, int], ...]:
"""Turn the object into a tuple of dicts."""
return tuple(input_config.dict(exclude_defaults=True) for input_config in self)
@classmethod
@ -299,7 +350,32 @@ class InputCombination(Tuple[InputConfig, ...]):
Useful for the UI to indicate that this combination is not set
"""
return cls([{"type": 99, "code": 99, "analog_threshold": 99}])
return cls([{"type": EMPTY_TYPE, "code": 99, "analog_threshold": 99}])
@classmethod
def from_tuples(cls, *tuples):
"""Construct an InputCombination from (type, code, analog_threshold) tuples."""
dicts = []
for tuple_ in tuples:
if len(tuple_) == 3:
dicts.append(
{
"type": tuple_[0],
"code": tuple_[1],
"analog_threshold": tuple_[2],
}
)
elif len(tuple_) == 2:
dicts.append(
{
"type": tuple_[0],
"code": tuple_[1],
}
)
else:
raise TypeError
return cls(dicts)
def is_problematic(self) -> bool:
"""Is this combination going to work properly on all systems?"""

@ -52,6 +52,7 @@ from inputremapper.exceptions import MacroParsingError
from inputremapper.gui.gettext import _
from inputremapper.gui.messages.message_types import MessageType
from inputremapper.injection.macros.parse import is_this_a_macro, parse
from inputremapper.utils import get_evdev_constant_name
# TODO: remove pydantic VERSION check as soon as we no longer support
# Ubuntu 20.04 and with it the ancient pydantic 1.2
@ -260,9 +261,9 @@ class UIMapping(BaseModel):
return EV_KEY, system_mapping.get(self.output_symbol)
return None
def get_output_name_constant(self) -> bool:
def get_output_name_constant(self) -> str:
"""Get the evdev name costant for the output."""
return evdev.ecodes.bytype[self.output_type][self.output_code]
return get_evdev_constant_name(self.output_type, self.output_code)
def is_valid(self) -> bool:
"""If the mapping is valid."""
@ -308,6 +309,20 @@ class Mapping(UIMapping):
input_combination: InputCombination
target_uinput: KnownUinput
@classmethod
def from_combination(
cls, input_combination=None, target_uinput="keyboard", output_symbol="a"
):
"""Convenient function to get a valid mapping."""
if not input_combination:
input_combination = [{"type": 99, "code": 99, "analog_threshold": 99}]
return cls(
input_combination=input_combination,
target_uinput=target_uinput,
output_symbol=output_symbol,
)
def is_valid(self) -> bool:
"""If the mapping is valid."""
return True

@ -298,11 +298,11 @@ def _convert_to_individual_mappings():
to [{input_combination: ..., output_symbol: symbol, ...}]
"""
for preset_path, old_preset in all_presets():
for old_preset_path, old_preset in all_presets():
if isinstance(old_preset, list):
continue
preset = Preset(preset_path, UIMapping)
migrated_preset = Preset(old_preset_path, UIMapping)
if "mapping" in old_preset.keys():
for combination, symbol_target in old_preset["mapping"].items():
logger.info(
@ -324,7 +324,7 @@ def _convert_to_individual_mappings():
target_uinput=symbol_target[1],
output_symbol=symbol_target[0],
)
preset.add(mapping)
migrated_preset.add(mapping)
if (
"gamepad" in old_preset.keys()
@ -352,10 +352,10 @@ def _convert_to_individual_mappings():
x_config = cfg.copy()
y_config = cfg.copy()
x_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_X)
[InputConfig(type=EV_ABS, code=ABS_X)]
)
y_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_Y)
[InputConfig(type=EV_ABS, code=ABS_Y)]
)
x_config["output_code"] = REL_X
y_config["output_code"] = REL_Y
@ -364,17 +364,17 @@ def _convert_to_individual_mappings():
if pointer_speed:
mapping_x.gain = pointer_speed
mapping_y.gain = pointer_speed
preset.add(mapping_x)
preset.add(mapping_y)
migrated_preset.add(mapping_x)
migrated_preset.add(mapping_y)
if right_purpose == "mouse":
x_config = cfg.copy()
y_config = cfg.copy()
x_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_RX)
[InputConfig(type=EV_ABS, code=ABS_RX)]
)
y_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_RY)
[InputConfig(type=EV_ABS, code=ABS_RY)]
)
x_config["output_code"] = REL_X
y_config["output_code"] = REL_Y
@ -383,17 +383,17 @@ def _convert_to_individual_mappings():
if pointer_speed:
mapping_x.gain = pointer_speed
mapping_y.gain = pointer_speed
preset.add(mapping_x)
preset.add(mapping_y)
migrated_preset.add(mapping_x)
migrated_preset.add(mapping_y)
if left_purpose == "wheel":
x_config = cfg.copy()
y_config = cfg.copy()
x_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_X)
[InputConfig(type=EV_ABS, code=ABS_X)]
)
y_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_Y)
[InputConfig(type=EV_ABS, code=ABS_Y)]
)
x_config["output_code"] = REL_HWHEEL_HI_RES
y_config["output_code"] = REL_WHEEL_HI_RES
@ -403,17 +403,17 @@ def _convert_to_individual_mappings():
mapping_x.gain = x_scroll_speed
if y_scroll_speed:
mapping_y.gain = y_scroll_speed
preset.add(mapping_x)
preset.add(mapping_y)
migrated_preset.add(mapping_x)
migrated_preset.add(mapping_y)
if right_purpose == "wheel":
x_config = cfg.copy()
y_config = cfg.copy()
x_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_RX)
[InputConfig(type=EV_ABS, code=ABS_RX)]
)
y_config["input_combination"] = InputCombination(
InputConfig(type=EV_ABS, code=ABS_RY)
[InputConfig(type=EV_ABS, code=ABS_RY)]
)
x_config["output_code"] = REL_HWHEEL_HI_RES
y_config["output_code"] = REL_WHEEL_HI_RES
@ -423,10 +423,10 @@ def _convert_to_individual_mappings():
mapping_x.gain = x_scroll_speed
if y_scroll_speed:
mapping_y.gain = y_scroll_speed
preset.add(mapping_x)
preset.add(mapping_y)
migrated_preset.add(mapping_x)
migrated_preset.add(mapping_y)
preset.save()
migrated_preset.save()
def _copy_to_beta():

@ -187,7 +187,7 @@ class Preset(Generic[MappingModel]):
if not self._has_valid_input_combination(mapping):
# we save invalid mappings except for those with an invalid
# input_combination
logger.debug("skipping invalid mapping %s", mapping)
logger.debug("Skipping invalid mapping %s", mapping)
continue
if self._is_mapped_multiple_times(mapping.input_combination):
@ -233,11 +233,19 @@ class Preset(Generic[MappingModel]):
if existing is not None:
return existing
logger.error(
"Combination %s not found. Available: %s",
repr(combination),
list(
self._mappings.keys(),
),
)
return None
def dangerously_mapped_btn_left(self) -> bool:
"""Return True if this mapping disables BTN_Left."""
if InputCombination(InputConfig.btn_left()) not in [
if InputCombination([InputConfig.btn_left()]) not in [
m.input_combination for m in self
]:
return False

@ -106,6 +106,7 @@ class AutoloadHistory:
def remove_timeout(func):
"""Remove timeout to ensure the call works if the daemon is not a proxy."""
# the timeout kwarg is a feature of pydbus. This is needed to make tests work
# that create a Daemon by calling its constructor instead of using pydbus.
def wrapped(*args, **kwargs):
@ -291,8 +292,7 @@ class Daemon:
now = time.time()
if now - 10 > self.refreshed_devices_at:
logger.debug("Refreshing because last info is too old")
# it may take a little bit of time until devices are visible after
# changes
# it may take a bit of time until devices are visible after changes
time.sleep(0.1)
groups.refresh()
self.refreshed_devices_at = now

@ -37,6 +37,7 @@ import multiprocessing
import os
import re
import threading
import traceback
from typing import List, Optional
import evdev
@ -57,6 +58,7 @@ from evdev.ecodes import (
from inputremapper.configs.paths import get_preset_path
from inputremapper.logger import logger
from inputremapper.utils import get_device_hash
TABLET_KEYS = [
evdev.ecodes.BTN_STYLUS,
@ -321,7 +323,7 @@ class _Group:
return group
def __repr__(self):
return f"Group({self.key})"
return f"<Group ({self.key}) at {hex(id(self))}>"
class _FindGroups(threading.Thread):
@ -363,7 +365,12 @@ class _FindGroups(threading.Thread):
# without setting an error"
# - "FileNotFoundError: [Errno 2] No such file or directory:
# '/dev/input/event12'"
logger.error("Failed to access %s: %s", path, str(error))
logger.error(
'Failed to access path "%s": %s %s',
path,
error.__class__.__name__,
str(error),
)
continue
if device.name == "Power Button":
@ -381,9 +388,11 @@ class _FindGroups(threading.Thread):
if key_capa is None and device_type != DeviceType.GAMEPAD:
# skip devices that don't provide buttons that can be mapped
logger.debug('"%s" has no useful capabilities', device.name)
continue
if is_denylisted(device):
logger.debug('"%s" is denylisted', device.name)
continue
key = get_unique_key(device)
@ -391,11 +400,12 @@ class _FindGroups(threading.Thread):
grouped[key] = []
logger.debug(
'Found "%s", "%s", "%s", type: %s',
key,
path,
'Found %s "%s" at "%s", hash "%s", key "%s"',
device_type.value,
device.name,
device_type,
path,
get_device_hash(device),
key,
)
grouped[key].append((device.name, path, device_type))
@ -484,7 +494,7 @@ class _Groups:
def set_groups(self, new_groups: List[_Group]):
"""Overwrite all groups."""
logger.debug("overwriting groups with %s", new_groups)
logger.debug("Overwriting groups with %s", new_groups)
self._groups = new_groups
def list_group_names(self) -> List[str]:
@ -546,4 +556,5 @@ class _Groups:
return None
# TODO global objects are bad practice
groups = _Groups()

@ -27,7 +27,6 @@ from collections import defaultdict
from typing import List, Optional, Dict, Union, Callable, Literal, Set
import cairo
import gi
from evdev.ecodes import (
EV_KEY,
EV_ABS,
@ -60,6 +59,7 @@ from inputremapper.gui.utils import HandlerDisabled, Colors
from inputremapper.injection.mapping_handlers.axis_transform import Transformation
from inputremapper.input_event import InputEvent
from inputremapper.configs.system_mapping import system_mapping, XKB_KEYCODE_OFFSET
from inputremapper.utils import get_evdev_constant_name
Capabilities = Dict[int, List]
@ -267,7 +267,7 @@ class MappingSelectionLabel(Gtk.ListBoxRow):
self.name_input.hide()
def __repr__(self):
return f"MappingSelectionLabel for {self.combination} as {self.name}"
return f"<MappingSelectionLabel for {self.combination} as {self.name} at {hex(id(self))}>"
def _set_not_selected(self):
self.edit_btn.hide()
@ -950,8 +950,7 @@ class OutputAxisSelector:
self.model.clear()
self.model.append(["None, None", _("No Axis")])
for type_, code in types_codes:
key_name = bytype[type_][code]
key_name = get_evdev_constant_name(type_, code)
if isinstance(key_name, list):
key_name = key_name[0]
self.model.append([f"{type_}, {code}", key_name])

@ -16,6 +16,7 @@
#
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations # needed for the TYPE_CHECKING import
import re
@ -259,9 +260,11 @@ class Controller:
self.show_status(
CTX_WARNING,
_("ctrl, alt and shift may not combine properly"),
_("Your system might reinterpret combinations ")
+ _("with those after they are injected, and by doing so ")
+ _("break them."),
_(
"Your system might reinterpret combinations "
+ "with those after they are injected, and by doing so "
+ "break them."
),
)
def move_input_config_in_combination(
@ -559,7 +562,7 @@ class Controller:
def running():
msg = _("Applied preset %s") % self.data_manager.active_preset.name
if self.data_manager.active_preset.get_mapping(
InputCombination(InputConfig.btn_left())
InputCombination([InputConfig.btn_left()])
):
msg += _(", CTRL + DEL to stop")
self.show_status(CTX_APPLY, msg)

@ -147,20 +147,14 @@ class ReaderService:
if exit_code != 0:
raise Exception(f"Failed to pkexec the reader-service, code {exit_code}")
def run(self):
"""Start doing stuff. Blocks."""
async def run(self):
"""Start doing stuff."""
# the reader will check for new commands later, once it is running
# it keeps running for one device or another.
loop = asyncio.get_event_loop()
logger.debug("Discovering initial groups")
self.groups.refresh()
self._send_groups()
loop.run_until_complete(
asyncio.gather(
self._read_commands(),
self._timeout(),
)
)
await asyncio.gather(self._read_commands(), self._timeout())
def _send_groups(self):
"""Send the groups to the gui."""
@ -253,7 +247,7 @@ class ReaderService:
context = self._create_event_pipeline(sources)
# create the event reader and start it
for device in sources:
reader = EventReader(context, device, ForwardDummy, self._stop_event)
reader = EventReader(context, device, self._stop_event)
self._tasks.add(asyncio.create_task(reader.run()))
async def _stop_reading(self):
@ -265,11 +259,11 @@ class ReaderService:
self._stop_event.clear()
def _create_event_pipeline(self, sources: List[evdev.InputDevice]) -> ContextDummy:
"""Create a custom event pipeline for each event code in the
device capabilities.
Instead of sending the events to a uinput they will be sent to the frontend.
"""Create a custom event pipeline for each event code in the capabilities.
Instead of sending the events to an uinput they will be sent to the frontend.
"""
context = ContextDummy()
context_dummy = ContextDummy()
# create a context for each source
for device in sources:
device_hash = get_device_hash(device)
@ -279,7 +273,7 @@ class ReaderService:
input_config = InputConfig(
type=EV_KEY, code=ev_code, origin_hash=device_hash
)
context.add_handler(
context_dummy.add_handler(
input_config, ForwardToUIHandler(self._results_pipe)
)
@ -292,26 +286,26 @@ class ReaderService:
origin_hash=device_hash,
)
mapping = Mapping(
input_combination=InputCombination(input_config),
input_combination=InputCombination([input_config]),
target_uinput="keyboard",
output_symbol="KEY_A",
)
handler: MappingHandler = AbsToBtnHandler(
InputCombination(input_config), mapping
InputCombination([input_config]), mapping
)
handler.set_sub_handler(ForwardToUIHandler(self._results_pipe))
context.add_handler(input_config, handler)
context_dummy.add_handler(input_config, handler)
# negative direction
input_config = input_config.modify(analog_threshold=-30)
mapping = Mapping(
input_combination=InputCombination(input_config),
input_combination=InputCombination([input_config]),
target_uinput="keyboard",
output_symbol="KEY_A",
)
handler = AbsToBtnHandler(InputCombination(input_config), mapping)
handler = AbsToBtnHandler(InputCombination([input_config]), mapping)
handler.set_sub_handler(ForwardToUIHandler(self._results_pipe))
context.add_handler(input_config, handler)
context_dummy.add_handler(input_config, handler)
for ev_code in capabilities.get(EV_REL) or ():
# positive direction
@ -322,53 +316,60 @@ class ReaderService:
origin_hash=device_hash,
)
mapping = Mapping(
input_combination=InputCombination(input_config),
input_combination=InputCombination([input_config]),
target_uinput="keyboard",
output_symbol="KEY_A",
release_timeout=0.3,
force_release_timeout=True,
)
handler = RelToBtnHandler(InputCombination(input_config), mapping)
handler = RelToBtnHandler(InputCombination([input_config]), mapping)
handler.set_sub_handler(ForwardToUIHandler(self._results_pipe))
context.add_handler(input_config, handler)
context_dummy.add_handler(input_config, handler)
# negative direction
input_config = input_config.modify(
analog_threshold=-self.rel_xy_speed[ev_code]
)
mapping = Mapping(
input_combination=InputCombination(input_config),
input_combination=InputCombination([input_config]),
target_uinput="keyboard",
output_symbol="KEY_A",
release_timeout=0.3,
force_release_timeout=True,
)
handler = RelToBtnHandler(InputCombination(input_config), mapping)
handler = RelToBtnHandler(InputCombination([input_config]), mapping)
handler.set_sub_handler(ForwardToUIHandler(self._results_pipe))
context.add_handler(input_config, handler)
context_dummy.add_handler(input_config, handler)
return context_dummy
return context
class ForwardDummy:
@staticmethod
def write(*_):
pass
class ContextDummy:
"""Used for the reader so that no events are actually written to any uinput."""
def __init__(self):
self.listeners = set()
self._notify_callbacks = defaultdict(list)
self.forward_dummy = ForwardDummy()
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]:
def get_notify_callbacks(self, input_event: InputEvent) -> List[NotifyCallback]:
return self._notify_callbacks[input_event.input_match_hash]
def reset(self):
pass
class ForwardDummy:
@staticmethod
def write(*_):
pass
def get_forward_uinput(self, origin_hash) -> evdev.UInput:
"""Don't actually write anything."""
return self.forward_dummy
class ForwardToUIHandler:
@ -382,7 +383,6 @@ class ForwardToUIHandler:
self,
event: InputEvent,
source: evdev.InputDevice,
forward: evdev.UInput,
suppress: bool = False,
) -> bool:
"""Filter duplicates and send into the pipe."""
@ -391,7 +391,7 @@ class ForwardToUIHandler:
if EventActions.negative_trigger in event.actions:
event = event.modify(value=-1)
logger.debug_key(event.event_tuple, "to frontend:")
logger.debug("Sending to %s frontend", event)
self.pipe.send(
{
"type": MSG_EVENT,

@ -19,11 +19,16 @@
"""Stores injection-process wide information."""
from __future__ import annotations
from collections import defaultdict
from typing import List, Dict, Tuple, Set, Hashable
from typing import List, Dict, Set, Hashable
from inputremapper.input_event import InputEvent
import evdev
from inputremapper.configs.input_config import DeviceHash
from inputremapper.input_event import InputEvent
from inputremapper.configs.preset import Preset
from inputremapper.injection.mapping_handlers.mapping_handler import (
EventListener,
@ -33,6 +38,7 @@ from inputremapper.injection.mapping_handlers.mapping_parser import (
parse_mappings,
EventPipelines,
)
from inputremapper.logger import logger
class Context:
@ -53,22 +59,39 @@ class Context:
- makes the injection class shorter and more specific to a certain task,
which is actually spinning up the injection.
Note, that for the reader_service a ContextDummy is used.
Members
-------
preset : Preset
The preset holds all Mappings for the injection process
listeners : Set[EventListener]
a set of callbacks which receive all events
A set of callbacks which receive all events
callbacks : Dict[Tuple[int, int], List[NotifyCallback]]
all entry points to the event pipeline sorted by InputEvent.type_and_code
All entry points to the event pipeline sorted by InputEvent.type_and_code
"""
listeners: Set[EventListener]
_notify_callbacks: Dict[Hashable, List[NotifyCallback]]
_handlers: EventPipelines
_forward_devices: Dict[DeviceHash, evdev.UInput]
_source_devices: Dict[DeviceHash, evdev.InputDevice]
def __init__(
self,
preset: Preset,
source_devices: Dict[DeviceHash, evdev.InputDevice],
forward_devices: Dict[DeviceHash, evdev.UInput],
):
if len(forward_devices) == 0:
logger.warning("Not forward_devices set")
if len(source_devices) == 0:
logger.warning("Not source_devices set")
def __init__(self, preset: Preset):
self.listeners = set()
self._source_devices = source_devices
self._forward_devices = forward_devices
self._notify_callbacks = defaultdict(list)
self._handlers = parse_mappings(preset, self)
@ -83,9 +106,19 @@ class Context:
def _create_callbacks(self) -> None:
"""Add the notify method from all _handlers to self.callbacks."""
for input_config, handler_list in self._handlers.items():
self._notify_callbacks[input_config.input_match_hash].extend(
input_match_hash = input_config.input_match_hash
logger.info("Adding NotifyCallback for %s", input_match_hash)
self._notify_callbacks[input_match_hash].extend(
handler.notify for handler in handler_list
)
def get_entry_points(self, input_event: InputEvent) -> List[NotifyCallback]:
return self._notify_callbacks[input_event.input_match_hash]
def get_notify_callbacks(self, input_event: InputEvent) -> List[NotifyCallback]:
input_match_hash = input_event.input_match_hash
return self._notify_callbacks[input_match_hash]
def get_forward_uinput(self, origin_hash: DeviceHash) -> evdev.UInput:
"""Get the "forward" uinput events from the given origin should go into."""
return self._forward_devices[origin_hash]
def get_source(self, key: DeviceHash) -> evdev.InputDevice:
return self._source_devices[key]

@ -22,11 +22,12 @@
import asyncio
import os
import traceback
from typing import AsyncIterator, Protocol, Set, List
import evdev
from inputremapper.utils import get_device_hash
from inputremapper.utils import get_device_hash, DeviceHash
from inputremapper.injection.mapping_handlers.mapping_handler import (
EventListener,
NotifyCallback,
@ -41,7 +42,10 @@ class Context(Protocol):
def reset(self):
...
def get_entry_points(self, input_event: InputEvent) -> List[NotifyCallback]:
def get_notify_callbacks(self, input_event: InputEvent) -> List[NotifyCallback]:
...
def get_forward_uinput(self, origin_hash: DeviceHash) -> evdev.UInput:
...
@ -60,7 +64,6 @@ class EventReader:
self,
context: Context,
source: evdev.InputDevice,
forward_to: evdev.UInput,
stop_event: asyncio.Event,
) -> None:
"""Initialize all mapping_handlers
@ -69,14 +72,9 @@ class EventReader:
----------
source
where to read keycodes from
forward_to
where to write keycodes to that were not mapped to anything.
Should be an UInput with capabilities that work for all forwarded
events, so ideally they should be copied from source.
"""
self._device_hash = get_device_hash(source)
self._source = source
self._forward_to = forward_to
self.context = context
self.stop_event = stop_event
@ -115,26 +113,24 @@ class EventReader:
yield event
def send_to_handlers(self, event: InputEvent) -> bool:
"""Send the event to callback."""
"""Send the event to the NotifyCallbacks.
Return if anyone took care of the event.
"""
if event.type == evdev.ecodes.EV_MSC:
return False
if event.type == evdev.ecodes.EV_SYN:
return False
results = set()
notify_callbacks = self.context.get_entry_points(event)
handled = False
notify_callbacks = self.context.get_notify_callbacks(event)
if notify_callbacks:
for notify_callback in notify_callbacks:
results.add(
notify_callback(
event,
source=self._source,
forward=self._forward_to,
)
)
handled = notify_callback(event, source=self._source) | handled
return True in results
return handled
async def send_to_listeners(self, event: InputEvent) -> None:
"""Send the event to listeners."""
@ -165,10 +161,12 @@ class EventReader:
def forward(self, event: InputEvent) -> None:
"""Forward an event, which injects it unmodified."""
forward_to = self.context.get_forward_uinput(self._device_hash)
if event.type == evdev.ecodes.EV_KEY:
logger.debug_key(event.event_tuple, "forwarding")
logger.write(event, forward_to)
self._forward_to.write(*event.event_tuple)
forward_to.write(*event.event_tuple)
async def handle(self, event: InputEvent) -> None:
if event.type == evdev.ecodes.EV_KEY and event.value == 2:
@ -194,10 +192,15 @@ class EventReader:
self._source.path,
self._source.fd,
)
async for event in self.read_loop():
await self.handle(
InputEvent.from_event(event, origin_hash=self._device_hash)
)
try:
await self.handle(
InputEvent.from_event(event, origin_hash=self._device_hash)
)
except Exception as e:
logger.error("Handling event %s failed: %s", event, e)
traceback.print_exception(e)
self.context.reset()
logger.info("read loop for %s stopped", self._source.path)

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
from typing import Dict, Union, Tuple
from typing import Dict, Union, Tuple, Optional
import evdev
@ -102,13 +102,22 @@ class GlobalUInputs:
def __iter__(self):
return iter(uinput for _, uinput in self.devices.items())
def reset(self):
self.is_service = inputremapper.utils.is_service()
self._uinput_factory = None
self.devices = {}
self.prepare_all()
def ensure_uinput_factory_set(self):
if self._uinput_factory is not None:
return
# overwrite global_uinputs.is_service in tests to control this
if self.is_service:
logger.debug("Creating regular UInputs")
self._uinput_factory = UInput
else:
logger.debug("Creating FrontendUInputs")
self._uinput_factory = FrontendUInput
def prepare_all(self):
@ -154,10 +163,11 @@ class GlobalUInputs:
if not uinput.can_emit(event):
raise inputremapper.exceptions.EventNotHandled(event)
logger.write(event, uinput)
uinput.write(*event)
uinput.syn()
def get_uinput(self, name: str):
def get_uinput(self, name: str) -> Optional[evdev.UInput]:
"""UInput with name
Or None if there is no uinput with this name.
@ -167,10 +177,14 @@ class GlobalUInputs:
name
uniqe name of the uinput device
"""
if name in self.devices.keys():
return self.devices[name]
if name not in self.devices:
logger.error(
f'UInput "{name}" is unknown. '
+ f"Available: {list(self.devices.keys())}"
)
return None
return None
return self.devices.get(name)
global_uinputs = GlobalUInputs()

@ -33,7 +33,7 @@ from typing import Dict, List, Optional, Tuple, Union
import evdev
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.input_config import InputCombination, InputConfig, DeviceHash
from inputremapper.configs.preset import Preset
from inputremapper.groups import (
_Group,
@ -48,7 +48,6 @@ from inputremapper.logger import logger
from inputremapper.utils import get_device_hash
CapabilitiesDict = Dict[int, List[int]]
GroupSources = List[evdev.InputDevice]
DEV_NAME = "input-remapper"
@ -237,8 +236,8 @@ class Injector(multiprocessing.Process):
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
def _grab_devices(self) -> Dict[DeviceHash, evdev.InputDevice]:
"""Grab all InputDevices that match a mappings' origin_hash."""
# use a dict because the InputDevice is not directly hashable
needed_devices = {}
input_configs = set()
@ -256,10 +255,11 @@ class Injector(multiprocessing.Process):
continue
needed_devices[device.path] = device
grabbed_devices = []
grabbed_devices = {}
for device in needed_devices.values():
if device := self._grab_device(device):
grabbed_devices.append(device)
grabbed_devices[get_device_hash(device)] = device
return grabbed_devices
def _update_preset(self):
@ -409,10 +409,13 @@ class Injector(multiprocessing.Process):
# 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()
forward_devices = {}
for device_hash, device in sources.items():
forward_devices[device_hash] = self._create_forwarding_device(device)
# create this within the process after the event loop creation,
# so that the macros use the correct loop
self.context = Context(self.preset)
self.context = Context(self.preset, sources, forward_devices)
self._stop_event = asyncio.Event()
if len(sources) == 0:
@ -424,13 +427,11 @@ class Injector(multiprocessing.Process):
numlock_state = is_numlock_on()
coroutines = []
for source in sources:
forward_to = self._create_forwarding_device(source)
for device_hash in sources:
# actually doing things
event_reader = EventReader(
self.context,
source,
forward_to,
sources[device_hash],
self._stop_event,
)
coroutines.append(event_reader.run())
@ -460,7 +461,7 @@ class Injector(multiprocessing.Process):
# reached otherwise.
logger.debug("Injector coroutines ended")
for source in sources:
for source in sources.values():
# ungrab at the end to make the next injection process not fail
# its grabs
try:

@ -82,7 +82,7 @@ class Variable:
return macro_variables.get(self.name)
def __repr__(self):
return f'<Variable "{self.name}">'
return f'<Variable "{self.name}" at {hex(id(self))}>'
def _type_check(value: Any, allowed_types, display_name=None, position=None) -> Any:
@ -309,7 +309,7 @@ class Macro:
await asyncio.sleep(self.keystroke_sleep_ms / 1000)
def __repr__(self):
return f'<Macro "{self.code}">'
return f'<Macro "{self.code}" at {hex(id(self))}>'
"""Functions that prepare the macro."""

@ -70,10 +70,10 @@ class AbsToAbsHandler(MappingHandler):
def __str__(self):
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}'
def __repr__(self):
return self.__str__()
return f"<{str(self)} at {hex(id(self))}>"
@property
def child(self): # used for logging
@ -87,10 +87,8 @@ class AbsToAbsHandler(MappingHandler):
self,
event: InputEvent,
source: evdev.InputDevice,
forward: evdev.UInput = None,
suppress: bool = False,
) -> bool:
if event.input_match_hash != self._map_axis.input_match_hash:
return False

@ -18,7 +18,7 @@
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
from typing import Tuple, Optional
from typing import Tuple
import evdev
from evdev.ecodes import EV_ABS
@ -55,13 +55,10 @@ class AbsToBtnHandler(MappingHandler):
def __str__(self):
name = get_evdev_constant_name(*self._input_config.type_and_code)
return (
f'AbsToBtnHandler for "{name}" '
f"{self._input_config.type_and_code} <{id(self)}>:"
)
return f'AbsToBtnHandler for "{name}" ' f"{self._input_config.type_and_code}"
def __repr__(self):
return self.__str__()
return f"<{str(self)} at {hex(id(self))}>"
@property
def child(self): # used for logging
@ -91,7 +88,6 @@ class AbsToBtnHandler(MappingHandler):
self,
event: InputEvent,
source: evdev.InputDevice,
forward: evdev.UInput,
suppress: bool = False,
) -> bool:
if event.input_match_hash != self._input_config.input_match_hash:
@ -119,11 +115,10 @@ class AbsToBtnHandler(MappingHandler):
event = event.modify(value=1, actions=(EventActions.as_key, direction))
self._active = bool(event.value)
# logger.debug_key(event.event_tuple, "sending to sub_handler")
# logger.debug(event.event_tuple, "sending to sub_handler")
return self._sub_handler.notify(
event,
source=source,
forward=forward,
suppress=suppress,
)

@ -166,10 +166,10 @@ class AbsToRelHandler(MappingHandler):
def __str__(self):
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}'
def __repr__(self):
return self.__str__()
return f"<{str(self)} at {hex(id(self))}>"
@property
def child(self): # used for logging
@ -183,7 +183,6 @@ class AbsToRelHandler(MappingHandler):
self,
event: InputEvent,
source: evdev.InputDevice,
forward: evdev.UInput = None,
suppress: bool = False,
) -> bool:
if event.input_match_hash != self._map_axis.input_match_hash:

@ -16,7 +16,8 @@
#
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
from typing import Dict, Tuple, Hashable
from typing import Dict, Tuple, Hashable, TYPE_CHECKING
import evdev
from inputremapper.configs.input_config import InputConfig
@ -27,9 +28,11 @@ from inputremapper.injection.mapping_handlers.mapping_handler import (
MappingHandler,
HandlerEnums,
InputEventHandler,
ContextProtocol,
)
from inputremapper.input_event import InputEvent, EventActions
from inputremapper.logger import logger
from inputremapper.utils import get_device_hash
class AxisSwitchHandler(MappingHandler):
@ -55,6 +58,7 @@ class AxisSwitchHandler(MappingHandler):
self,
combination: InputCombination,
mapping: Mapping,
context: ContextProtocol,
**_,
):
super().__init__(combination, mapping)
@ -73,11 +77,13 @@ class AxisSwitchHandler(MappingHandler):
self._axis_source = None
self._forward_device = None
self.context = context
def __str__(self):
return f"AxisSwitchHandler for {self._map_axis.type_and_code} <{id(self)}>"
return f"AxisSwitchHandler for {self._map_axis.type_and_code}"
def __repr__(self):
return self.__str__()
return f"<{str(self)} at {hex(id(self))}>"
@property
def child(self):
@ -101,7 +107,7 @@ class AxisSwitchHandler(MappingHandler):
if not key_is_pressed:
# recenter the axis
logger.debug_key(self.mapping.input_combination, "stopping axis")
logger.debug("Stopping axis for %s", self.mapping.input_combination)
event = InputEvent(
0,
0,
@ -110,13 +116,13 @@ class AxisSwitchHandler(MappingHandler):
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)
return True
if self._map_axis.type == evdev.ecodes.EV_ABS:
# send the last cached value so that the abs axis
# is at the correct position
logger.debug_key(self.mapping.input_combination, "starting axis")
logger.debug("Starting axis for %s", self.mapping.input_combination)
event = InputEvent(
0,
0,
@ -124,7 +130,7 @@ class AxisSwitchHandler(MappingHandler):
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)
return True
return True
@ -139,10 +145,8 @@ class AxisSwitchHandler(MappingHandler):
self,
event: InputEvent,
source: evdev.InputDevice,
forward: evdev.UInput,
suppress: bool = False,
) -> bool:
if not self._should_map(event):
return False
@ -151,15 +155,18 @@ class AxisSwitchHandler(MappingHandler):
# do some caching so that we can generate the
# recenter event and an initial abs event
if not self._forward_device:
self._forward_device = forward
if self._axis_source is None:
self._axis_source = source
if self._forward_device is None:
device_hash = get_device_hash(source)
self._forward_device = self.context.get_forward_uinput(device_hash)
# always cache the value
self._last_value = event.value
if self._active:
return self._sub_handler.notify(event, source, forward, suppress)
return self._sub_handler.notify(event, source, suppress)
return False

@ -17,7 +17,8 @@
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
from typing import Dict, Tuple, Hashable
from __future__ import annotations # needed for the TYPE_CHECKING import
from typing import TYPE_CHECKING, Dict, Hashable
import evdev
from evdev.ecodes import EV_ABS, EV_REL
@ -32,6 +33,9 @@ from inputremapper.injection.mapping_handlers.mapping_handler import (
from inputremapper.input_event import InputEvent
from inputremapper.logger import logger
if TYPE_CHECKING:
from inputremapper.injection.context import Context
class CombinationHandler(MappingHandler):
"""Keeps track of a combination and notifies a sub handler."""
@ -40,52 +44,71 @@ class CombinationHandler(MappingHandler):
_pressed_keys: Dict[Hashable, bool]
_output_state: bool # the last update we sent to a sub-handler
_sub_handler: InputEventHandler
_handled_input_hashes: list[Hashable]
def __init__(
self,
combination: InputCombination,
mapping: Mapping,
context: Context,
**_,
) -> None:
logger.debug(mapping)
logger.debug(str(mapping))
super().__init__(combination, mapping)
self._pressed_keys = {}
self._output_state = False
self._context = context
# prepare a key map for all events with non-zero value
for input_config in combination:
assert not input_config.defines_analog_input
self._pressed_keys[input_config.input_match_hash] = False
self._handled_input_hashes = [
input_config.input_match_hash for input_config in combination
]
assert len(self._pressed_keys) > 0 # no combination handler without a key
def __str__(self):
return (
f'CombinationHandler for "{self.mapping.input_combination}" '
f"{tuple(t for t in self._pressed_keys.keys())} <{id(self)}>:"
f'CombinationHandler for "{str(self.mapping.input_combination)}" '
f"{tuple(t for t in self._pressed_keys.keys())}"
)
def __repr__(self):
return self.__str__()
description = (
f'CombinationHandler for "{repr(self.mapping.input_combination)}" '
f"{tuple(t for t in self._pressed_keys.keys())}"
)
return f"<{description} at {hex(id(self))}>"
@property
def child(self): # used for logging
def child(self):
# used for logging
return self._sub_handler
def notify(
self,
event: InputEvent,
source: evdev.InputDevice,
forward: evdev.UInput,
suppress: bool = False,
) -> bool:
if event.input_match_hash not in self._pressed_keys.keys():
return False # we are not responsible for the event
if event.input_match_hash not in self._handled_input_hashes:
# we are not responsible for the event
return False
last_state = self.get_active()
self._pressed_keys[event.input_match_hash] = event.value == 1
was_activated = self.is_activated()
if self.get_active() == last_state or self.get_active() == self._output_state:
# update the state
# The value of non-key input should have been changed to either 0 or 1 at this
# point by other handlers.
is_pressed = event.value == 1
self._pressed_keys[event.input_match_hash] = is_pressed
# maybe this changes the activation status (triggered/not-triggered)
is_activated = self.is_activated()
if is_activated == was_activated or is_activated == self._output_state:
# nothing changed
if self._output_state:
# combination is active, consume the event
@ -94,9 +117,9 @@ class CombinationHandler(MappingHandler):
# combination inactive, forward the event
return False
if self.get_active():
if is_activated:
# send key up events to the forwarded uinput
self.forward_release(forward)
self.forward_release()
event = event.modify(value=1)
else:
if self._output_state or self.mapping.is_axis_mapping():
@ -112,11 +135,9 @@ class CombinationHandler(MappingHandler):
if suppress:
return False
logger.debug_key(
self.mapping.input_combination, "triggered: sending to sub-handler"
)
logger.debug("Sending %s to sub-handler", self.mapping.input_combination)
self._output_state = bool(event.value)
return self._sub_handler.notify(event, source, forward, suppress)
return self._sub_handler.notify(event, source, suppress)
def reset(self) -> None:
self._sub_handler.reset()
@ -124,14 +145,14 @@ class CombinationHandler(MappingHandler):
self._pressed_keys[key] = False
self._output_state = False
def get_active(self) -> bool:
def is_activated(self) -> bool:
"""Return if all keys in the keymap are set to True."""
return False not in self._pressed_keys.values()
def forward_release(self, forward: evdev.UInput) -> None:
"""Forward a button release for all keys if this is a combination
def forward_release(self) -> None:
"""Forward a button release for all keys if this is a combination.
this might cause duplicate key-up events but those are ignored by evdev anyway
This might cause duplicate key-up events but those are ignored by evdev anyway
"""
if len(self._pressed_keys) == 1 or not self.mapping.release_combination_keys:
return
@ -140,25 +161,37 @@ class CombinationHandler(MappingHandler):
lambda cfg: self._pressed_keys.get(cfg.input_match_hash),
self.mapping.input_combination,
)
logger.debug("Forwarding release for %s", self.mapping.input_combination)
for input_config in keys_to_release:
forward.write(*input_config.type_and_code, 0)
forward.syn()
origin_hash = input_config.origin_hash
if origin_hash is None:
logger.error(
f"Can't forward due to missing origin_hash in {repr(input_config)}"
)
continue
forward_to = self._context.get_forward_uinput(origin_hash)
logger.write(input_config, forward_to)
forward_to.write(*input_config.type_and_code, 0)
forward_to.syn()
def needs_ranking(self) -> bool:
return bool(self.input_configs)
def rank_by(self) -> InputCombination:
return InputCombination(
event for event in self.input_configs if not event.defines_analog_input
[event for event in self.input_configs if not event.defines_analog_input]
)
def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
return_dict = {}
for config in self.input_configs:
if config.type == EV_ABS and not config.defines_analog_input:
return_dict[InputCombination(config)] = HandlerEnums.abs2btn
return_dict[InputCombination([config])] = HandlerEnums.abs2btn
if config.type == EV_REL and not config.defines_analog_input:
return_dict[InputCombination(config)] = HandlerEnums.rel2btn
return_dict[InputCombination([config])] = HandlerEnums.rel2btn
return return_dict

@ -44,16 +44,16 @@ class HierarchyHandler(MappingHandler):
) -> None:
self.handlers = handlers
self._input_config = input_config
combination = InputCombination(input_config)
combination = InputCombination([input_config])
# use the mapping from the first child TODO: find a better solution
mapping = handlers[0].mapping
super().__init__(combination, mapping)
def __str__(self):
return f"HierarchyHandler for {self._input_config} <{id(self)}>:"
return f"HierarchyHandler for {self._input_config}"
def __repr__(self):
return self.__str__()
return f"<{str(self)} at {hex(id(self))}>"
@property
def child(self): # used for logging
@ -63,7 +63,6 @@ class HierarchyHandler(MappingHandler):
self,
event: InputEvent,
source: evdev.InputDevice = None,
forward: evdev.UInput = None,
suppress: bool = False,
) -> bool:
if event.input_match_hash != self._input_config.input_match_hash:
@ -72,9 +71,9 @@ class HierarchyHandler(MappingHandler):
success = False
for handler in self.handlers:
if not success:
success = handler.notify(event, source, forward)
success = handler.notify(event, source)
else:
handler.notify(event, source, forward, suppress=True)
handler.notify(event, source, suppress=True)
return success
def reset(self) -> None:
@ -86,12 +85,12 @@ class HierarchyHandler(MappingHandler):
self._input_config.type == EV_ABS
and not self._input_config.defines_analog_input
):
return {InputCombination(self._input_config): HandlerEnums.abs2btn}
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 {InputCombination([self._input_config]): HandlerEnums.rel2btn}
return {}
def set_sub_handler(self, handler: InputEventHandler) -> None:

@ -56,10 +56,10 @@ class KeyHandler(MappingHandler):
self._active = False
def __str__(self):
return f"KeyHandler <{id(self)}>:"
return f"KeyHandler to {self._maps_to}"
def __repr__(self):
return self.__str__()
return f"<{str(self)} at {hex(id(self))}>"
@property
def child(self): # used for logging
@ -72,7 +72,6 @@ class KeyHandler(MappingHandler):
event_tuple = (*self._maps_to, event.value)
try:
global_uinputs.write(event_tuple, self.mapping.target_uinput)
logger.debug_key(event_tuple, "sending to %s", self.mapping.target_uinput)
self._active = bool(event.value)
return True
except exceptions.Error:

@ -53,10 +53,10 @@ class MacroHandler(MappingHandler):
self._macro = parse(self.mapping.output_symbol, context, mapping)
def __str__(self):
return f"MacroHandler <{id(self)}>:"
return f"MacroHandler"
def __repr__(self):
return self.__str__()
return f"<{str(self)} at {hex(id(self))}>"
@property
def child(self): # used for logging
@ -78,11 +78,6 @@ class MacroHandler(MappingHandler):
def handler(type_, code, value) -> None:
"""Handler for macros."""
logger.debug_key(
(type_, code, value),
"sending from macro to %s",
self.mapping.target_uinput,
)
global_uinputs.write((type_, code, value), self.mapping.target_uinput)
asyncio.ensure_future(self.run_macro(handler))

@ -78,10 +78,13 @@ class EventListener(Protocol):
class ContextProtocol(Protocol):
"""The parts from context needed for macros."""
"""The parts from context needed for handlers."""
listeners: Set[EventListener]
def get_forward_uinput(self, origin_hash) -> evdev.UInput:
pass
class NotifyCallback(Protocol):
"""Type signature of InputEventHandler.notify
@ -93,7 +96,6 @@ class NotifyCallback(Protocol):
self,
event: InputEvent,
source: evdev.InputDevice,
forward: evdev.UInput,
suppress: bool = False,
) -> bool:
...
@ -106,7 +108,6 @@ class InputEventHandler(Protocol):
self,
event: InputEvent,
source: evdev.InputDevice,
forward: evdev.UInput,
suppress: bool = False,
) -> bool:
...
@ -174,7 +175,6 @@ class MappingHandler:
self,
event: InputEvent,
source: evdev.InputDevice,
forward: evdev.UInput,
suppress: bool = False,
) -> bool:
"""Notify this handler about an incoming event.
@ -186,8 +186,6 @@ class MappingHandler:
something else
source
Where `event` comes from
forward
Where to write keycodes to that were not mapped to anything
"""
raise NotImplementedError

@ -55,7 +55,6 @@ class NullHandler(MappingHandler):
self,
event: InputEvent,
source: evdev.InputDevice,
forward: evdev.UInput,
suppress: bool = False,
) -> bool:
return True

@ -106,10 +106,10 @@ class RelToAbsHandler(MappingHandler):
self._observed_rate = DEFAULT_REL_RATE
def __str__(self):
return f"RelToAbsHandler for {self._map_axis} <{id(self)}>:"
return f"RelToAbsHandler for {self._map_axis}"
def __repr__(self):
return self.__str__()
return f"<{str(self)} at {hex(id(self))}>"
@property
def child(self): # used for logging
@ -161,7 +161,7 @@ class RelToAbsHandler(MappingHandler):
self,
event: InputEvent,
source: evdev.InputDevice,
forward: evdev.UInput = None,
forward_to: evdev.UInput = None,
suppress: bool = False,
) -> bool:
self._observe_rate(event)

@ -61,10 +61,10 @@ class RelToBtnHandler(MappingHandler):
assert len(combination) == 1
def __str__(self):
return f'RelToBtnHandler for "{self._input_config}" <{id(self)}>:'
return f'RelToBtnHandler for "{self._input_config}"'
def __repr__(self):
return self.__str__()
return f"<{str(self)} at {hex(id(self))}>"
@property
def child(self): # used for logging
@ -73,7 +73,6 @@ class RelToBtnHandler(MappingHandler):
async def _stage_release(
self,
source: InputEvent,
forward: evdev.InputDevice,
suppress: bool,
):
while time.time() < self._last_activation + self.mapping.release_timeout:
@ -91,18 +90,16 @@ class RelToBtnHandler(MappingHandler):
actions=(EventActions.as_key,),
origin_hash=self._input_config.origin_hash,
)
logger.debug_key(event.event_tuple, "sending to sub_handler")
self._sub_handler.notify(event, source, forward, suppress)
logger.debug("Sending %s to sub_handler", event)
self._sub_handler.notify(event, source, suppress)
self._active = False
def notify(
self,
event: InputEvent,
source: evdev.InputDevice,
forward: evdev.UInput,
suppress: bool = False,
) -> bool:
assert event.type == EV_REL
if event.input_match_hash != self._input_config.input_match_hash:
return False
@ -117,7 +114,7 @@ class RelToBtnHandler(MappingHandler):
# consume the event
return True
event = event.modify(value=0, actions=(EventActions.as_key,))
logger.debug_key(event.event_tuple, "sending to sub_handler")
logger.debug("Sending %s to sub_handler", event)
self._abort_release = True
else:
# don't consume the event.
@ -126,7 +123,7 @@ class RelToBtnHandler(MappingHandler):
else:
# the axis is above the threshold
if not self._active:
asyncio.ensure_future(self._stage_release(source, forward, suppress))
asyncio.ensure_future(self._stage_release(source, suppress))
if value >= threshold > 0:
direction = EventActions.positive_trigger
else:
@ -135,10 +132,8 @@ class RelToBtnHandler(MappingHandler):
event = event.modify(value=1, actions=(EventActions.as_key, direction))
self._active = bool(event.value)
# logger.debug_key(event.event_tuple, "sending to sub_handler")
return self._sub_handler.notify(
event, source=source, forward=forward, suppress=suppress
)
# logger.debug("Sending %s to sub_handler", event)
return self._sub_handler.notify(event, source=source, suppress=suppress)
def reset(self) -> None:
if self._active:

@ -116,10 +116,10 @@ class RelToRelHandler(MappingHandler):
)
def __str__(self):
return f"RelToRelHandler for {self._input_config} <{id(self)}>:"
return f"RelToRelHandler for {self._input_config}"
def __repr__(self):
return self.__str__()
return f"<{str(self)} at {hex(id(self))}>"
@property
def child(self): # used for logging
@ -133,7 +133,7 @@ class RelToRelHandler(MappingHandler):
self,
event: InputEvent,
source: evdev.InputDevice,
forward: evdev.UInput = None,
forward_to: evdev.UInput = None,
suppress: bool = False,
) -> bool:
if not self._should_map(event):

@ -21,11 +21,13 @@ from __future__ import annotations
import enum
from dataclasses import dataclass
from typing import Tuple, Optional, Hashable
from typing import Tuple, Optional, Hashable, Literal
import evdev
from evdev import ecodes
from inputremapper.utils import get_evdev_constant_name
class EventActions(enum.Enum):
"""Additional information an InputEvent can send through the event pipeline."""
@ -39,6 +41,21 @@ class EventActions(enum.Enum):
negative_trigger = enum.auto() # original event was negative direction
def validate_event(event):
"""Test if the event is valid."""
if not isinstance(event.type, int):
raise TypeError(f"Expected type to be an int, but got {event.type}")
if not isinstance(event.code, int):
raise TypeError(f"Expected code to be an int, but got {event.code}")
if not isinstance(event.value, int):
# this happened to me because I screwed stuff up
raise TypeError(f"Expected value to be an int, but got {event.value}")
return event
# Todo: add slots=True as soon as python 3.10 is in common distros
@dataclass(frozen=True)
class InputEvent:
@ -54,6 +71,7 @@ class InputEvent:
value: int
actions: Tuple[EventActions, ...] = ()
origin_hash: Optional[str] = None
forward_to: Optional[evdev.UInput] = None
def __eq__(self, other: InputEvent | evdev.InputEvent | Tuple[int, int, int]):
# useful in tests
@ -90,18 +108,71 @@ class InputEvent:
) from exception
@classmethod
def from_tuple(cls, event_tuple: Tuple[int, int, int]) -> InputEvent:
def from_tuple(
cls, event_tuple: Tuple[int, int, int], origin_hash: Optional[str] = None
) -> InputEvent:
"""Create a InputEvent from a (type, code, value) tuple."""
# use this as rarely as possible. Construct objects early on and pass them
# around instead of passing around integers
if len(event_tuple) != 3:
raise TypeError(
f"failed to create InputEvent {event_tuple = }" f" must have length 3"
f"failed to create InputEvent {event_tuple = } must have length 3"
)
return validate_event(
cls(
0,
0,
int(event_tuple[0]),
int(event_tuple[1]),
int(event_tuple[2]),
origin_hash=origin_hash,
)
)
@classmethod
def abs(cls, code: int, value: int, origin_hash: Optional[str] = None):
"""Create an abs event, like joystick movements."""
return validate_event(
cls(
0,
0,
ecodes.EV_ABS,
code,
value,
origin_hash=origin_hash,
)
)
@classmethod
def rel(cls, code: int, value: int, origin_hash: Optional[str] = None):
"""Create a rel event, like mouse movements."""
return validate_event(
cls(
0,
0,
ecodes.EV_REL,
code,
value,
origin_hash=origin_hash,
)
)
@classmethod
def key(cls, code: int, value: Literal[0, 1], origin_hash: Optional[str] = None):
"""Create a key event, like keyboard keys or gamepad buttons.
A value of 1 means "press", a value of 0 means "release".
"""
return validate_event(
cls(
0,
0,
ecodes.EV_KEY,
code,
value,
origin_hash=origin_hash,
)
return cls(
0,
0,
int(event_tuple[0]),
int(event_tuple[1]),
int(event_tuple[2]),
)
@property
@ -136,7 +207,11 @@ class InputEvent:
]
def __str__(self):
return f"InputEvent{self.event_tuple}"
name = get_evdev_constant_name(self.type, self.code)
return f"InputEvent for {self.event_tuple} {name}"
def __repr__(self):
return f"<{str(self)} at {hex(id(self))}>"
def timestamp(self):
"""Return the unix timestamp of when the event was seen."""

@ -37,6 +37,7 @@ except ImportError:
start = time.time()
previous_key_debug_log = None
previous_write_debug_log = None
def parse_mapping_handler(mapping_handler):
@ -55,7 +56,7 @@ def parse_mapping_handler(mapping_handler):
lines_and_indent.extend(sub_list)
break
lines_and_indent.append([str(mapping_handler), indent])
lines_and_indent.append([repr(mapping_handler), indent])
try:
mapping_handler = mapping_handler.child
except AttributeError:
@ -77,11 +78,8 @@ class Logger(logging.Logger):
msg = indent * line[1] + line[0]
self._log(logging.DEBUG, msg, args=None)
def debug_key(self, key, msg, *args):
"""Log a key-event message.
Example:
... DEBUG event_reader.py:143: forwarding ···················· (1, 71, 1)
def write(self, key, uinput):
"""Log that an event is being written
Parameters
----------
@ -93,21 +91,18 @@ class Logger(logging.Logger):
if not self.isEnabledFor(logging.DEBUG):
return
global previous_key_debug_log
global previous_write_debug_log
msg = msg % args
str_key = str(key)
str_key = repr(key)
str_key = str_key.replace(",)", ")")
spacing = " " + "·" * max(0, 30 - len(msg))
if len(spacing) == 1:
spacing = ""
msg = f"{msg}{spacing} {str_key}"
if msg == previous_key_debug_log:
msg = f'Writing {str_key} to "{uinput.name}"'
if msg == previous_write_debug_log:
# avoid some super spam from EV_ABS events
return
previous_key_debug_log = msg
previous_write_debug_log = msg
self._log(logging.DEBUG, msg, args=None)
@ -247,6 +242,8 @@ logger.setLevel(logging.INFO)
logging.getLogger("asyncio").setLevel(logging.WARNING)
# using pkg_resources to figure out the version fails in many cases,
# so we hardcode it instead
VERSION = "1.6.0-beta"
EVDEV_VERSION = None
try:

@ -43,13 +43,19 @@ def get_device_hash(device: evdev.InputDevice) -> DeviceHash:
return md5(s.encode()).hexdigest().lower()
def get_evdev_constant_name(type_: int, code: int, *_) -> Optional[str]:
"""Handy function to get the evdev constant name."""
# this is more readable than
def get_evdev_constant_name(type_: Optional[int], code: Optional[int], *_) -> str:
"""Handy function to get the evdev constant name for display purposes.
Returns "unknown" for unknown events.
"""
# using this function is more readable than
# type_, code = event.type_and_code
# name = evdev.ecodes.bytype[type_][code]
name = evdev.ecodes.bytype.get(type_, {}).get(code)
if isinstance(name, list):
return name[0]
name = name[0]
if name is None:
return "unknown"
return name

@ -17,7 +17,7 @@
<text x="32.5" y="14">coverage</text>
</g>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="83.0" y="15" fill="#010101" fill-opacity=".3">93%</text>
<text x="82.0" y="14">93%</text>
<text x="83.0" y="15" fill="#010101" fill-opacity=".3">92%</text>
<text x="82.0" y="14">92%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -117,3 +117,4 @@ https://miro.com/app/board/uXjVPLa8ilM=/?share_link_id=272180986764
- [Python Unix Domain Sockets](https://pymotw.com/2/socket/uds.html)
- [GNOME HIG](https://developer.gnome.org/hig/stable/)
- [GtkSource Example](https://github.com/wolfthefallen/py-GtkSourceCompletion-example)
- [linux/input-event-codes.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h)

@ -17,7 +17,7 @@
<text x="22.0" y="14">pylint</text>
</g>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="63.0" y="15" fill="#010101" fill-opacity=".3">9.27</text>
<text x="62.0" y="14">9.27</text>
<text x="63.0" y="15" fill="#010101" fill-opacity=".3">8.88</text>
<text x="62.0" y="14">8.88</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -304,7 +304,9 @@ class TestPresetSelection(ComponentBaseTest):
(
MappingData(
name="m1",
input_combination=InputCombination(InputConfig(type=1, code=2)),
input_combination=InputCombination(
[InputConfig(type=1, code=2)]
),
),
),
)
@ -317,7 +319,9 @@ class TestPresetSelection(ComponentBaseTest):
(
MappingData(
name="m1",
input_combination=InputCombination(InputConfig(type=1, code=2)),
input_combination=InputCombination(
[InputConfig(type=1, code=2)]
),
),
),
)
@ -331,7 +335,9 @@ class TestPresetSelection(ComponentBaseTest):
(
MappingData(
name="m1",
input_combination=InputCombination(InputConfig(type=1, code=2)),
input_combination=InputCombination(
[InputConfig(type=1, code=2)]
),
),
),
)
@ -358,22 +364,22 @@ class TestMappingListbox(ComponentBaseTest):
MappingData(
name="mapping1",
input_combination=InputCombination(
InputConfig(type=1, code=KEY_C)
[InputConfig(type=1, code=KEY_C)]
),
),
MappingData(
name="",
input_combination=InputCombination(
(
[
InputConfig(type=1, code=KEY_A),
InputConfig(type=1, code=KEY_B),
)
]
),
),
MappingData(
name="mapping2",
input_combination=InputCombination(
InputConfig(type=1, code=KEY_B)
[InputConfig(type=1, code=KEY_B)]
),
),
),
@ -407,27 +413,27 @@ class TestMappingListbox(ComponentBaseTest):
self.message_broker.publish(
MappingData(
name="mapping1",
input_combination=InputCombination(InputConfig(type=1, code=KEY_C)),
input_combination=InputCombination([InputConfig(type=1, code=KEY_C)]),
)
)
selected = self.get_selected_row()
self.assertEqual(selected.name, "mapping1")
self.assertEqual(
selected.combination,
InputCombination(InputConfig(type=1, code=KEY_C)),
InputCombination([InputConfig(type=1, code=KEY_C)]),
)
def test_loads_mapping(self):
self.select_row(InputCombination(InputConfig(type=1, code=KEY_B)))
self.select_row(InputCombination([InputConfig(type=1, code=KEY_B)]))
self.controller_mock.load_mapping.assert_called_once_with(
InputCombination(InputConfig(type=1, code=KEY_B))
InputCombination([InputConfig(type=1, code=KEY_B)])
)
def test_avoids_infinite_recursion(self):
self.message_broker.publish(
MappingData(
name="mapping1",
input_combination=InputCombination(InputConfig(type=1, code=KEY_C)),
input_combination=InputCombination([InputConfig(type=1, code=KEY_C)]),
)
)
self.controller_mock.load_mapping.assert_not_called()
@ -440,7 +446,7 @@ class TestMappingListbox(ComponentBaseTest):
MappingData(
name="qux",
input_combination=InputCombination(
InputConfig(type=1, code=KEY_C)
[InputConfig(type=1, code=KEY_C)]
),
),
MappingData(
@ -450,7 +456,7 @@ class TestMappingListbox(ComponentBaseTest):
MappingData(
name="bar",
input_combination=InputCombination(
InputConfig(type=1, code=KEY_B)
[InputConfig(type=1, code=KEY_B)]
),
),
),
@ -469,13 +475,13 @@ class TestMappingListbox(ComponentBaseTest):
MappingData(
name="qux",
input_combination=InputCombination(
InputConfig(type=1, code=KEY_C)
[InputConfig(type=1, code=KEY_C)]
),
),
MappingData(
name="bar",
input_combination=InputCombination(
InputConfig(type=1, code=KEY_B)
[InputConfig(type=1, code=KEY_B)]
),
),
),
@ -512,10 +518,9 @@ class TestMappingSelectionLabel(ComponentBaseTest):
def test_repr(self):
self.mapping_selection_label.name = "name"
self.assertEqual(
repr(self.mapping_selection_label),
"MappingSelectionLabel for a + b as name",
)
self.assertIn("name", repr(self.mapping_selection_label))
self.assertIn("KEY_A", repr(self.mapping_selection_label))
self.assertIn("KEY_B", repr(self.mapping_selection_label))
def test_shows_combination_without_name(self):
self.assertEqual(self.mapping_selection_label.label.get_label(), "a + b")
@ -553,12 +558,12 @@ class TestMappingSelectionLabel(ComponentBaseTest):
InputConfig(type=1, code=KEY_B),
)
),
InputCombination(InputConfig(type=1, code=KEY_A)),
InputCombination([InputConfig(type=1, code=KEY_A)]),
)
)
self.assertEqual(
self.mapping_selection_label.combination,
InputCombination(InputConfig(type=1, code=KEY_A)),
InputCombination([InputConfig(type=1, code=KEY_A)]),
)
def test_doesnt_update_combination_when_not_selected(self):
@ -579,7 +584,7 @@ class TestMappingSelectionLabel(ComponentBaseTest):
InputConfig(type=1, code=KEY_B),
)
),
InputCombination(InputConfig(type=1, code=KEY_A)),
InputCombination([InputConfig(type=1, code=KEY_A)]),
)
)
self.assertEqual(
@ -1248,7 +1253,7 @@ class TestReleaseTimeoutInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
input_combination=InputCombination(
InputConfig(type=2, code=0, analog_threshold=1)
[InputConfig(type=2, code=0, analog_threshold=1)]
),
target_uinput="keyboard",
)
@ -1258,7 +1263,7 @@ class TestReleaseTimeoutInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
input_combination=InputCombination(
InputConfig(type=2, code=0, analog_threshold=1)
[InputConfig(type=2, code=0, analog_threshold=1)]
),
release_timeout=1,
)
@ -1273,7 +1278,7 @@ class TestReleaseTimeoutInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
input_combination=InputCombination(
InputConfig(type=2, code=0, analog_threshold=1)
[InputConfig(type=2, code=0, analog_threshold=1)]
),
release_timeout=1,
)
@ -1357,7 +1362,7 @@ class TestOutputAxisSelector(ComponentBaseTest):
self.message_broker.publish(
MappingData(
target_uinput="mouse",
input_combination=InputCombination(InputConfig(type=1, code=1)),
input_combination=InputCombination([InputConfig(type=1, code=1)]),
)
)
@ -1523,7 +1528,7 @@ class TestSliders(ComponentBaseTest):
)
self.message_broker.publish(
MappingData(
input_combination=InputCombination(InputConfig(type=3, code=0)),
input_combination=InputCombination([InputConfig(type=3, code=0)]),
target_uinput="mouse",
)
)
@ -1587,7 +1592,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
target_uinput="mouse",
input_combination=InputCombination(InputConfig(type=2, code=0)),
input_combination=InputCombination([InputConfig(type=2, code=0)]),
rel_to_abs_input_cutoff=1,
output_type=3,
output_code=0,
@ -1606,7 +1611,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
target_uinput="mouse",
input_combination=InputCombination(InputConfig(type=2, code=0)),
input_combination=InputCombination([InputConfig(type=2, code=0)]),
rel_to_abs_input_cutoff=3,
output_type=3,
output_code=0,
@ -1619,7 +1624,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
target_uinput="mouse",
input_combination=InputCombination(InputConfig(type=2, code=0)),
input_combination=InputCombination([InputConfig(type=2, code=0)]),
rel_to_abs_input_cutoff=rel_to_abs_input_cutoff,
output_type=3,
output_code=0,
@ -1636,7 +1641,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
target_uinput="mouse",
input_combination=InputCombination(InputConfig(type=3, code=0)),
input_combination=InputCombination([InputConfig(type=3, code=0)]),
output_type=3,
output_code=0,
)
@ -1648,7 +1653,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
target_uinput="mouse",
input_combination=InputCombination(InputConfig(type=2, code=0)),
input_combination=InputCombination([InputConfig(type=2, code=0)]),
rel_to_abs_input_cutoff=3,
output_type=2,
output_code=0,
@ -1660,7 +1665,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
target_uinput="mouse",
input_combination=InputCombination(InputConfig(type=3, code=0)),
input_combination=InputCombination([InputConfig(type=3, code=0)]),
output_type=3,
output_code=0,
)
@ -1669,7 +1674,7 @@ class TestRelativeInputCutoffInput(ComponentBaseTest):
self.message_broker.publish(
MappingData(
target_uinput="mouse",
input_combination=InputCombination(InputConfig(type=2, code=0)),
input_combination=InputCombination([InputConfig(type=2, code=0)]),
rel_to_abs_input_cutoff=1,
output_type=3,
output_code=0,
@ -1686,7 +1691,7 @@ class TestRequireActiveMapping(ComponentBaseTest):
self.box,
require_recorded_input=False,
)
combination = InputCombination(InputConfig(type=1, code=KEY_A))
combination = InputCombination([InputConfig(type=1, code=KEY_A)])
self.message_broker.publish(MappingData())
self.assert_inactive(self.box)
@ -1712,7 +1717,7 @@ class TestRequireActiveMapping(ComponentBaseTest):
self.box,
require_recorded_input=True,
)
combination = InputCombination(InputConfig(type=1, code=KEY_A))
combination = InputCombination([InputConfig(type=1, code=KEY_A)])
self.message_broker.publish(MappingData())
self.assert_inactive(self.box)
@ -1848,7 +1853,7 @@ class TestBreadcrumbs(ComponentBaseTest):
self.assertEqual(self.label_4.get_text(), "group / preset / a + b")
self.assertEqual(self.label_5.get_text(), "a + b")
combination = InputCombination(InputConfig(type=1, code=KEY_A))
combination = InputCombination([InputConfig(type=1, code=KEY_A)])
self.message_broker.publish(
MappingData(name="qux", input_combination=combination)
)

@ -18,17 +18,19 @@
# 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 asyncio
# the tests file needs to be imported first to make sure patches are loaded
from contextlib import contextmanager
from typing import Tuple, List, Optional, Iterable
from inputremapper.injection.global_uinputs import global_uinputs
from tests.lib.global_uinputs import reset_global_uinputs_for_service
from tests.test import get_project_root
from tests.lib.fixtures import new_event
from tests.lib.cleanup import cleanup
from tests.lib.stuff import spy
from tests.lib.constants import EVENT_READ_TIMEOUT
from tests.lib.fixtures import prepare_presets, get_combination_config
from tests.lib.fixtures import prepare_presets
from tests.lib.logger import logger
from tests.lib.fixtures import fixtures
from tests.lib.pipes import push_event, push_events, uinput_write_history_pipe
@ -124,6 +126,15 @@ def launch(
)
def start_reader_service():
def process():
reader_service = ReaderService(_Groups())
loop = asyncio.new_event_loop()
loop.run_until_complete(reader_service.run())
multiprocessing.Process(target=process).start()
@contextmanager
def patch_launch():
"""patch the launch function such that we don't connect to
@ -136,7 +147,8 @@ def patch_launch():
# instead of running pkexec, fork instead. This will make
# the reader-service aware of all the test patches
if "pkexec input-remapper-control --command start-reader-service" in cmd:
multiprocessing.Process(target=ReaderService(_Groups()).run).start()
logger.info("pkexec-patch starting ReaderService process")
start_reader_service()
return 0
return original_os_system(cmd)
@ -185,7 +197,8 @@ class TestGroupsFromReaderService(unittest.TestCase):
# instead of running pkexec, fork instead. This will make
# the reader-service aware of all the test patches
if "pkexec input-remapper-control --command start-reader-service" in cmd:
self.reader_service_started() # don't start the reader-service just log that it was.
# don't start the reader-service just log that it was.
self.reader_service_started()
return 0
return self.original_os_system(cmd)
@ -213,7 +226,7 @@ class TestGroupsFromReaderService(unittest.TestCase):
self.assertEqual(len(self.data_manager.get_group_keys()), 0)
# start the reader-service delayed
multiprocessing.Process(target=ReaderService(_Groups()).run).start()
start_reader_service()
# perform some iterations so that the reader ends up reading from the pipes
# which will make it receive devices.
for _ in range(10):
@ -338,7 +351,7 @@ class GuiTestBase(unittest.TestCase):
self.assertEqual(self.target_selection.get_active_id(), "keyboard")
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(InputConfig(type=1, code=5)),
InputCombination([InputConfig(type=1, code=5)]),
)
self.assertEqual(
self.data_manager.active_input_config, InputConfig(type=1, code=5)
@ -526,12 +539,14 @@ class TestGui(GuiTestBase):
self.assertFalse(self.autoload_toggle.get_active())
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination,
InputCombination(InputConfig(type=1, code=5)),
InputCombination([InputConfig(type=1, code=5)]),
)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(
InputConfig(type=1, code=5),
[
InputConfig(type=1, code=5),
]
),
)
self.assertEqual(self.selection_label_listbox.get_selected_row().name, "4")
@ -665,26 +680,29 @@ class TestGui(GuiTestBase):
push_events(
fixtures.foo_device_2_keyboard,
[InputEvent(0, 0, 1, 30, 1), InputEvent(0, 0, 1, 31, 1)],
[
InputEvent(0, 0, 1, 30, 1),
InputEvent(0, 0, 1, 31, 1),
],
)
self.throttle(40)
self.throttle(60)
origin = fixtures.foo_device_2_keyboard.get_device_hash()
mock1.assert_has_calls(
(
call(
CombinationRecorded(
InputCombination(
InputConfig(type=1, code=30, origin_hash=origin)
[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),
)
]
)
)
),
@ -695,12 +713,12 @@ class TestGui(GuiTestBase):
mock2.assert_not_called()
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 31, 0)])
self.throttle(40)
self.throttle(60)
self.assertEqual(mock1.call_count, 2)
mock2.assert_not_called()
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 0)])
self.throttle(40)
self.throttle(60)
self.assertEqual(mock1.call_count, 2)
mock2.assert_called_once()
@ -718,14 +736,14 @@ class TestGui(GuiTestBase):
fixtures.foo_device_2_keyboard,
[InputEvent(0, 0, 1, 30, 1), InputEvent(0, 0, 1, 30, 0)],
)
self.throttle(40)
self.throttle(60)
# if this fails with <InputCombination (1, 5, 1)>: this is the initial
# mapping or something, so it was never overwritten.
origin = fixtures.foo_device_2_keyboard.get_device_hash()
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(InputConfig(type=1, code=30, origin_hash=origin)),
InputCombination([InputConfig(type=1, code=30, origin_hash=origin)]),
)
# create a new mapping
@ -742,7 +760,7 @@ class TestGui(GuiTestBase):
fixtures.foo_device_2_keyboard,
[InputEvent(0, 0, 1, 30, 1), InputEvent(0, 0, 1, 30, 0)],
)
self.throttle(40)
self.throttle(60)
# should still be the empty mapping
self.assertEqual(
self.data_manager.active_mapping.input_combination,
@ -752,36 +770,36 @@ class TestGui(GuiTestBase):
# try to record a different combination
self.controller.start_key_recording()
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 1)])
self.throttle(40)
self.throttle(60)
# nothing changed yet, as we got the duplicate combination
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination.empty_combination(),
)
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 31, 1)])
self.throttle(40)
self.throttle(60)
# now the combination is different
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(
(
[
InputConfig(type=1, code=30, origin_hash=origin),
InputConfig(type=1, code=31, origin_hash=origin),
)
]
),
)
# let's make the combination even longer
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 32, 1)])
self.throttle(40)
self.throttle(60)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(
(
[
InputConfig(type=1, code=30, origin_hash=origin),
InputConfig(type=1, code=31, origin_hash=origin),
InputConfig(type=1, code=32, origin_hash=origin),
)
]
),
)
@ -794,21 +812,21 @@ class TestGui(GuiTestBase):
InputEvent(0, 0, 1, 32, 0),
],
)
self.throttle(40)
self.throttle(60)
# sending a combination update now should not do anything
self.message_broker.publish(
CombinationRecorded(InputCombination(InputConfig(type=1, code=35)))
CombinationRecorded(InputCombination([InputConfig(type=1, code=35)]))
)
gtk_iteration()
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(
(
[
InputConfig(type=1, code=30, origin_hash=origin),
InputConfig(type=1, code=31, origin_hash=origin),
InputConfig(type=1, code=32, origin_hash=origin),
)
]
),
)
@ -839,19 +857,19 @@ class TestGui(GuiTestBase):
self.recording_toggle.set_active(True)
gtk_iteration()
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 1)])
self.throttle(40)
self.throttle(60)
push_events(fixtures.foo_device_2_keyboard, [InputEvent(0, 0, 1, 30, 0)])
self.throttle(40)
self.throttle(60)
# check the input_combination
origin = fixtures.foo_device_2_keyboard.get_device_hash()
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination,
InputCombination(InputConfig(type=1, code=30, origin_hash=origin)),
InputCombination([InputConfig(type=1, code=30, origin_hash=origin)]),
)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(InputConfig(type=1, code=30, origin_hash=origin)),
InputCombination([InputConfig(type=1, code=30, origin_hash=origin)]),
)
self.assertEqual(self.selection_label_listbox.get_selected_row().name, "a")
self.assertIsNone(self.data_manager.active_mapping.name)
@ -868,7 +886,7 @@ class TestGui(GuiTestBase):
self.data_manager.active_mapping,
Mapping(
input_combination=InputCombination(
InputConfig(type=1, code=30, origin_hash=origin)
[InputConfig(type=1, code=30, origin_hash=origin)]
),
output_symbol="Shift_L",
target_uinput="keyboard",
@ -882,7 +900,7 @@ class TestGui(GuiTestBase):
)
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination,
InputCombination(InputConfig(type=1, code=30, origin_hash=origin)),
InputCombination([InputConfig(type=1, code=30, origin_hash=origin)]),
)
# 4. update target to mouse
@ -892,7 +910,7 @@ class TestGui(GuiTestBase):
self.data_manager.active_mapping,
Mapping(
input_combination=InputCombination(
InputConfig(type=1, code=30, origin_hash=origin)
[InputConfig(type=1, code=30, origin_hash=origin)]
),
output_symbol="Shift_L",
target_uinput="mouse",
@ -916,26 +934,27 @@ class TestGui(GuiTestBase):
gtk_iteration()
# it should be possible to add all of them
ev_1 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1)
ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, 1)
ev_3 = (EV_ABS, evdev.ecodes.ABS_HAT0Y, -1)
ev_4 = (EV_ABS, evdev.ecodes.ABS_HAT0Y, 1)
ev_1 = InputEvent.abs(evdev.ecodes.ABS_HAT0X, -1)
ev_2 = InputEvent.abs(evdev.ecodes.ABS_HAT0X, 1)
ev_3 = InputEvent.abs(evdev.ecodes.ABS_HAT0Y, -1)
ev_4 = InputEvent.abs(evdev.ecodes.ABS_HAT0Y, 1)
def add_mapping(event_tuple, symbol) -> InputCombination:
def add_mapping(event, symbol) -> InputCombination:
"""adds mapping and returns the expected input combination"""
event = InputEvent.from_tuple(event_tuple)
self.controller.create_mapping()
gtk_iteration()
self.controller.start_key_recording()
push_events(fixtures.foo_device_2_gamepad, [event, event.modify(value=0)])
self.throttle(40)
self.throttle(60)
gtk_iteration()
self.code_editor.get_buffer().set_text(symbol)
gtk_iteration()
return InputCombination(
InputConfig.from_input_event(event).modify(
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash()
)
[
InputConfig.from_input_event(event).modify(
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash()
)
]
)
config_1 = add_mapping(ev_1, "a")
@ -977,10 +996,26 @@ class TestGui(GuiTestBase):
gtk_iteration()
# it should be possible to write a combination
ev_1 = (EV_KEY, evdev.ecodes.KEY_A, 1)
ev_2 = (EV_ABS, evdev.ecodes.ABS_HAT0X, 1)
ev_3 = (EV_KEY, evdev.ecodes.KEY_C, 1)
ev_4 = (EV_ABS, evdev.ecodes.ABS_HAT0X, -1)
ev_1 = InputEvent.key(
evdev.ecodes.KEY_A,
1,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
ev_2 = InputEvent.abs(
evdev.ecodes.ABS_HAT0X,
1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
ev_3 = InputEvent.key(
evdev.ecodes.KEY_C,
1,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
ev_4 = InputEvent.abs(
evdev.ecodes.ABS_HAT0X,
-1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
combination_1 = (ev_1, ev_2, ev_3)
combination_2 = (ev_2, ev_1, ev_3)
@ -992,34 +1027,23 @@ class TestGui(GuiTestBase):
combination_5 = (ev_1, ev_3, ev_2)
combination_6 = (ev_3, ev_1, ev_2)
def get_combination(combi: Iterable[Tuple[int, int, int]]) -> InputCombination:
def get_combination(combi: Iterable[InputEvent]) -> InputCombination:
"""Create an InputCombination from a list of events.
Ensures the origin_hash is set correctly.
"""
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()
)
for event in combi:
config = InputConfig.from_input_event(event)
configs.append(config)
return InputCombination(configs)
def add_mapping(combi: Iterable[Tuple[int, int, int]], symbol):
def add_mapping(combi: Iterable[InputEvent], symbol):
logger.info("add_mapping %s", combi)
self.controller.create_mapping()
gtk_iteration()
self.controller.start_key_recording()
previous_event = InputEvent(0, 0, 1, 1, 1)
for event_tuple in combi:
event = InputEvent.from_tuple(event_tuple)
if event.type != previous_event.type:
self.throttle(20) # avoid race condition if we switch fixture
for event in combi:
if event.type == EV_KEY:
push_event(fixtures.foo_device_2_keyboard, event)
if event.type == EV_ABS:
@ -1027,8 +1051,11 @@ class TestGui(GuiTestBase):
if event.type == EV_REL:
push_event(fixtures.foo_device_2_mouse, event)
for event_tuple in combi:
event = InputEvent.from_tuple(event_tuple)
# avoid race condition if we switch fixture in push_event. The order
# of events needs to be correct.
self.throttle(20)
for event in combi:
if event.type == EV_KEY:
push_event(fixtures.foo_device_2_keyboard, event.modify(value=0))
if event.type == EV_ABS:
@ -1036,12 +1063,13 @@ class TestGui(GuiTestBase):
if event.type == EV_REL:
pass
self.throttle(40)
self.throttle(60)
gtk_iteration()
self.code_editor.get_buffer().set_text(symbol)
gtk_iteration()
add_mapping(combination_1, "a")
self.assertEqual(
self.data_manager.active_preset.get_mapping(
get_combination(combination_1)
@ -1209,7 +1237,7 @@ class TestGui(GuiTestBase):
def test_only_one_empty_mapping_possible(self):
self.assertEqual(
self.selection_label_listbox.get_selected_row().combination,
InputCombination(InputConfig(type=1, code=5)),
InputCombination([InputConfig(type=1, code=5)]),
)
self.assertEqual(len(self.selection_label_listbox.get_children()), 1)
self.assertEqual(len(self.data_manager.active_preset), 1)
@ -1242,7 +1270,9 @@ class TestGui(GuiTestBase):
self.recording_toggle.set_active(True)
gtk_iteration()
self.message_broker.publish(
CombinationRecorded(InputCombination(InputConfig(type=EV_KEY, code=KEY_Q)))
CombinationRecorded(
InputCombination([InputConfig(type=EV_KEY, code=KEY_Q)])
)
)
gtk_iteration()
self.message_broker.signal(MessageType.recording_finished)
@ -1262,7 +1292,9 @@ class TestGui(GuiTestBase):
self.recording_toggle.set_active(True)
gtk_iteration()
self.message_broker.publish(
CombinationRecorded(InputCombination(InputConfig(type=EV_KEY, code=KEY_Q)))
CombinationRecorded(
InputCombination([InputConfig(type=EV_KEY, code=KEY_Q)])
)
)
gtk_iteration()
self.message_broker.signal(MessageType.recording_finished)
@ -1373,7 +1405,7 @@ class TestGui(GuiTestBase):
fixtures.foo_device_2_keyboard,
[event.modify(value=0) for event in combi],
)
self.throttle(40)
self.throttle(60)
gtk_iteration()
self.code_editor.get_buffer().set_text(symbol)
gtk_iteration()
@ -1422,11 +1454,11 @@ class TestGui(GuiTestBase):
self.controller.load_preset("preset1")
self.throttle(20)
self.controller.load_mapping(InputCombination(InputConfig(type=1, code=1)))
self.controller.load_mapping(InputCombination([InputConfig(type=1, code=1)]))
gtk_iteration()
self.controller.update_mapping(output_symbol="foo")
gtk_iteration()
self.controller.load_mapping(InputCombination(InputConfig(type=1, code=2)))
self.controller.load_mapping(InputCombination([InputConfig(type=1, code=2)]))
gtk_iteration()
self.controller.update_mapping(output_symbol="qux")
gtk_iteration()
@ -1449,7 +1481,7 @@ class TestGui(GuiTestBase):
self.assertTrue(error_icon.get_visible())
self.assertFalse(warning_icon.get_visible())
self.controller.load_mapping(InputCombination(InputConfig(type=1, code=1)))
self.controller.load_mapping(InputCombination([InputConfig(type=1, code=1)]))
gtk_iteration()
self.controller.update_mapping(output_symbol="b")
gtk_iteration()
@ -1543,8 +1575,8 @@ class TestGui(GuiTestBase):
self.assertEqual(
mappings,
{
InputCombination(InputConfig(type=1, code=1)),
InputCombination(InputConfig(type=1, code=2)),
InputCombination([InputConfig(type=1, code=1)]),
InputCombination([InputConfig(type=1, code=2)]),
},
)
self.assertFalse(self.autoload_toggle.get_active())
@ -1558,8 +1590,8 @@ class TestGui(GuiTestBase):
self.assertEqual(
mappings,
{
InputCombination(InputConfig(type=1, code=3)),
InputCombination(InputConfig(type=1, code=4)),
InputCombination([InputConfig(type=1, code=3)]),
InputCombination([InputConfig(type=1, code=4)]),
},
)
self.assertTrue(self.autoload_toggle.get_active())
@ -1658,7 +1690,7 @@ class TestGui(GuiTestBase):
self.controller.create_mapping()
gtk_iteration()
self.controller.update_mapping(
input_combination=InputCombination(InputConfig.btn_left()),
input_combination=InputCombination([InputConfig.btn_left()]),
output_symbol="a",
)
gtk_iteration()
@ -1719,6 +1751,11 @@ class TestGui(GuiTestBase):
self.assertIn("Stop", text)
def test_start_injecting(self):
# It's 2023 everyone! That means this test randomly stopped working because it
# used FrontendUInputs instead of regular UInputs. I guess a fucking ghost
# was fixing this for us during 2022, but it seems to have disappeared.
reset_global_uinputs_for_service()
self.controller.load_group("Foo Device 2")
with spy(self.daemon, "set_config_dir") as spy1:
@ -1750,8 +1787,8 @@ class TestGui(GuiTestBase):
push_events(
fixtures.foo_device_2_keyboard,
[
new_event(evdev.events.EV_KEY, 5, 1),
new_event(evdev.events.EV_KEY, 5, 0),
InputEvent.key(5, 1),
InputEvent.key(5, 0),
],
)
@ -1773,6 +1810,8 @@ class TestGui(GuiTestBase):
self.assertNotIn("input-remapper", device_group_entry.name)
def test_stop_injecting(self):
reset_global_uinputs_for_service()
self.controller.load_group("Foo Device 2")
self.start_injector_btn.clicked()
gtk_iteration()
@ -1782,6 +1821,7 @@ class TestGui(GuiTestBase):
gtk_iteration()
if self.data_manager.get_state() == InjectorState.RUNNING:
break
# fail here so we don't block forever
self.assertEqual(self.data_manager.get_state(), InjectorState.RUNNING)
@ -1795,8 +1835,8 @@ class TestGui(GuiTestBase):
push_events(
fixtures.foo_device_2_keyboard,
[
new_event(evdev.events.EV_KEY, 5, 1),
new_event(evdev.events.EV_KEY, 5, 0),
InputEvent.key(5, 1),
InputEvent.key(5, 0),
],
)
@ -1819,8 +1859,8 @@ class TestGui(GuiTestBase):
push_events(
fixtures.foo_device_2_keyboard,
[
new_event(evdev.events.EV_KEY, 5, 1),
new_event(evdev.events.EV_KEY, 5, 0),
InputEvent.key(5, 1),
InputEvent.key(5, 0),
],
)
time.sleep(0.2)

@ -93,7 +93,7 @@ class TestUserInterface(unittest.TestCase):
self.message_broker.publish(
MappingData(
input_combination=InputCombination(
InputConfig(type=EV_KEY, code=KEY_A)
[InputConfig(type=EV_KEY, code=KEY_A)]
),
name="foo",
)

@ -29,6 +29,10 @@ import psutil
from pickle import UnpicklingError
from unittest.mock import patch
# don't import anything from input_remapper gloablly here, because some files execute
# code when imported, which can screw up patches. I wish we had a dependency injection
# framework that patches together the dependencies during runtime...
from tests.lib.logger import logger
from tests.lib.pipes import (
uinput_write_history_pipe,
@ -79,9 +83,10 @@ def quick_cleanup(log=True):
from inputremapper.gui.utils import debounce_manager
from inputremapper.configs.paths import get_config_path
from inputremapper.injection.global_uinputs import global_uinputs
from tests.lib.global_uinputs import reset_global_uinputs_for_service
if log:
print("Quick cleanup...")
logger.info("Quick cleanup...")
debounce_manager.stop_all()
@ -151,10 +156,10 @@ def quick_cleanup(log=True):
uinput.write_count = 0
uinput.write_history = []
global_uinputs.is_service = True
reset_global_uinputs_for_service()
if log:
print("Quick cleanup done")
logger.info("Quick cleanup done")
def cleanup():
@ -163,9 +168,8 @@ def cleanup():
Using this is slower, usually quick_cleanup() is sufficient.
"""
from inputremapper.groups import groups
from inputremapper.injection.global_uinputs import global_uinputs
print("Cleanup...")
logger.info("Cleanup...")
os.system("pkill -f input-remapper-service")
os.system("pkill -f input-remapper-control")
@ -173,7 +177,5 @@ def cleanup():
quick_cleanup(log=False)
groups.refresh()
with patch.object(sys, "argv", ["input-remapper-service"]):
global_uinputs.prepare_all()
print("Cleanup done")
logger.info("Cleanup done")

@ -23,12 +23,13 @@ from __future__ import annotations
import dataclasses
import json
from hashlib import md5
from typing import Dict, Optional, Tuple, Iterable
from typing import Dict, Optional
import time
import evdev
from tests.lib.logger import logger
# input-remapper is only interested in devices that have EV_KEY, add some
# random other stuff to test that they are ignored.
phys_foo = "usb-0000:03:00.0-1/input2"
@ -39,8 +40,8 @@ keyboard_keys = sorted(evdev.ecodes.keys.keys())[:255]
@dataclasses.dataclass(frozen=True)
class Fixture:
path: str
capabilities: Dict = dataclasses.field(default_factory=dict)
path: str = ""
name: str = "unset"
info: evdev.device.DeviceInfo = evdev.device.DeviceInfo(None, None, None, None)
phys: str = "unset"
@ -51,7 +52,14 @@ class Fixture:
def get_device_hash(self):
s = str(self.capabilities) + self.name
return md5(s.encode()).hexdigest()
device_hash = md5(s.encode()).hexdigest()
logger.info(
'Hash for fixture "%s" "%s": "%s"',
self.path,
self.name,
device_hash,
)
return device_hash
class _Fixtures:
@ -69,7 +77,7 @@ class _Fixtures:
)
# Another "Foo Device", which will get an incremented key.
# If possible write tests using this one, because name != key here and
# that would be important to test as well. Otherwise the tests can't
# that would be important to test as well. Otherwise, the tests can't
# see if the groups correct attribute is used in functions and paths.
dev_input_event11 = Fixture(
capabilities={
@ -177,7 +185,7 @@ class _Fixtures:
path="/dev/input/event31",
)
# input-remapper devices are not displayed in the ui, some instance
# of input-remapper started injecting apparently.
# of input-remapper started injecting, apparently.
dev_input_event40 = Fixture(
capabilities={evdev.ecodes.EV_KEY: keyboard_keys},
phys="input-remapper/input1",
@ -239,6 +247,10 @@ class _Fixtures:
def __iter__(self):
return iter([*self._iter, *self._dynamic_fixtures.values()])
def get_paths(self):
"""Get a list of all available device paths."""
return list(self._dynamic_fixtures.keys())
def reset(self):
self._dynamic_fixtures = {}
@ -308,54 +320,19 @@ class _Fixtures:
fixtures = _Fixtures()
def get_combination_config(
*event_tuples: Tuple[int, int] | Tuple[int, int, int]
) -> Iterable[Dict[str, int]]:
"""convenient function to get a iterable of dicts, InputEvent.event_tuple's"""
for event in event_tuples:
if len(event) == 3:
yield {k: v for k, v in zip(("type", "code", "analog_threshold"), event)}
elif len(event) == 2:
yield {k: v for k, v in zip(("type", "code"), event)}
else:
raise TypeError
def get_ui_mapping(combination=None, target_uinput="keyboard", output_symbol="a"):
"""Convenient function to get a valid mapping."""
from inputremapper.configs.mapping import UIMapping
if not combination:
combination = get_combination_config((99, 99))
return UIMapping(
input_combination=combination,
target_uinput=target_uinput,
output_symbol=output_symbol,
)
def new_event(type, code, value, timestamp):
"""Create a new InputEvent.
Handy because of the annoying sec and usec arguments of the regular
evdev.InputEvent constructor.
def get_key_mapping(combination=None, target_uinput="keyboard", output_symbol="a"):
"""Convenient function to get a valid mapping."""
from inputremapper.configs.mapping import Mapping
if not combination:
combination = [{"type": 99, "code": 99, "analog_threshold": 99}]
return Mapping(
input_combination=combination,
target_uinput=target_uinput,
output_symbol=output_symbol,
)
def new_event(type, code, value, timestamp=None, offset=0):
"""Create a new input_event."""
from tests.lib.patches import InputEvent
Prefer using `InputEvent.key()`, `InputEvent.abs()`, `InputEvent.rel()` or just
`InputEvent(0, 0, 1234, 2345, 3456)`.
"""
from inputremapper.input_event import InputEvent
if timestamp is None:
timestamp = time.time() + offset
timestamp = time.time()
sec = int(timestamp)
usec = timestamp % 1 * 1000000
@ -368,27 +345,32 @@ def prepare_presets():
"Foo Device 2/preset3" is the newest and "Foo Device 2/preset2" is set to autoload
"""
from inputremapper.configs.preset import Preset
from inputremapper.configs.mapping import Mapping
from inputremapper.configs.paths import get_config_path, get_preset_path
from inputremapper.configs.global_config import global_config
from inputremapper.configs.input_config import InputCombination
preset1 = Preset(get_preset_path("Foo Device", "preset1"))
preset1.add(
get_key_mapping(combination=get_combination_config((1, 1)), output_symbol="b")
Mapping.from_combination(
InputCombination.from_tuples((1, 1)),
output_symbol="b",
)
)
preset1.add(get_key_mapping(combination=get_combination_config((1, 2))))
preset1.add(Mapping.from_combination(InputCombination.from_tuples((1, 2))))
preset1.save()
time.sleep(0.1)
preset2 = Preset(get_preset_path("Foo Device", "preset2"))
preset2.add(get_key_mapping(combination=get_combination_config((1, 3))))
preset2.add(get_key_mapping(combination=get_combination_config((1, 4))))
preset2.add(Mapping.from_combination(InputCombination.from_tuples((1, 3))))
preset2.add(Mapping.from_combination(InputCombination.from_tuples((1, 4))))
preset2.save()
# make sure the timestamp of preset 3 is the newest,
# so that it will be automatically loaded by the GUI
time.sleep(0.1)
preset3 = Preset(get_preset_path("Foo Device", "preset3"))
preset3.add(get_key_mapping(combination=get_combination_config((1, 5))))
preset3.add(Mapping.from_combination(InputCombination.from_tuples((1, 5))))
preset3.save()
with open(get_config_path("config.json"), "w") as file:

@ -0,0 +1,35 @@
#!/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 sys
from unittest.mock import patch
from inputremapper.injection.global_uinputs import global_uinputs
def reset_global_uinputs_for_service():
with patch.object(sys, "argv", ["input-remapper-service"]):
# patch argv for global_uinputs to think it is a service
global_uinputs.reset()
def reset_global_uinputs_for_gui():
with patch.object(sys, "argv", ["input-remapper-gtk"]):
global_uinputs.reset()

@ -29,6 +29,7 @@ from pickle import UnpicklingError
import evdev
from inputremapper.utils import get_evdev_constant_name
from tests.lib.constants import EVENT_READ_TIMEOUT, MIN_ABS, MAX_ABS
from tests.lib.fixtures import Fixture, fixtures, new_event
from tests.lib.pipes import (
@ -56,9 +57,16 @@ class InputDevice:
def __init__(self, path):
if path != "justdoit" and not fixtures.get(path):
# beware that fixtures keys and the path attribute of a fixture can
# theoretically be different. I don't know if this is the case right now
logger.error(
'path "%s" was not found in fixtures. available: %s',
path,
list(fixtures.get_paths()),
)
raise FileNotFoundError()
if path == "justdoit":
self._fixture = Fixture()
self._fixture = Fixture(path="justdoit")
else:
self._fixture = fixtures[path]
@ -214,35 +222,43 @@ class UInput:
def write(self, type, code, value):
self.write_count += 1
event = new_event(type, code, value)
event = new_event(type, code, value, time.time())
uinput_write_history.append(event)
uinput_write_history_pipe[1].send(event)
self.write_history.append(event)
logger.info("%s written", (type, code, value))
logger.info(
'%s %s written to "%s"',
(type, code, value),
get_evdev_constant_name(type, code),
self.name,
)
def syn(self):
pass
# TODO inherit from input-remappers InputEvent?
# makes convert_to_internal_events obsolete
class InputEvent(evdev.InputEvent):
def __init__(self, sec, usec, type, code, value):
self.t = (type, code, value)
super().__init__(sec, usec, type, code, value)
def copy(self):
return InputEvent(self.sec, self.usec, self.type, self.code, self.value)
def patch_evdev():
def list_devices():
return [fixture_.path for fixture_ in fixtures]
class PatchedInputEvent(evdev.InputEvent):
def __init__(self, sec, usec, type, code, value):
self.t = (type, code, value)
super().__init__(sec, usec, type, code, value)
def copy(self):
return PatchedInputEvent(
self.sec,
self.usec,
self.type,
self.code,
self.value,
)
evdev.list_devices = list_devices
evdev.InputDevice = InputDevice
evdev.UInput = UInput
evdev.InputEvent = InputEvent
evdev.InputEvent = PatchedInputEvent
def patch_events():

@ -18,6 +18,8 @@
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
"""Reading events from fixtures, making fixtures act like they are sending events."""
from __future__ import annotations
import multiprocessing

@ -23,13 +23,6 @@ import copy
from unittest.mock import patch
def convert_to_internal_events(events):
"""Convert an iterable of InputEvent to a list of inputremapper.InputEvent."""
from inputremapper.input_event import InputEvent as InternalInputEvent
return [InternalInputEvent.from_event(event) for event in events]
def spy(obj, name):
"""Convenient wrapper for patch.object(..., ..., wraps=...)."""
return patch.object(obj, name, wraps=obj.__getattribute__(name))

@ -126,9 +126,10 @@ patch_is_running()
# patch_warnings()
def main():
update_inputremapper_verbosity()
update_inputremapper_verbosity()
def main():
cleanup()
# https://docs.python.org/3/library/argparse.html
parser = argparse.ArgumentParser(description=__doc__)

@ -17,10 +17,8 @@
#
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
from inputremapper.configs.input_config import InputConfig
from inputremapper.input_event import InputEvent
from tests.lib.cleanup import quick_cleanup
from tests.lib.fixtures import get_key_mapping, get_combination_config
from evdev.ecodes import (
EV_REL,
EV_ABS,
@ -34,6 +32,7 @@ import unittest
from inputremapper.injection.context import Context
from inputremapper.configs.preset import Preset
from inputremapper.configs.mapping import Mapping
from inputremapper.configs.input_config import InputConfig, InputCombination
class TestContext(unittest.TestCase):
@ -44,23 +43,31 @@ class TestContext(unittest.TestCase):
def test_callbacks(self):
preset = Preset()
cfg = {
"input_combination": get_combination_config((EV_ABS, ABS_X)),
"input_combination": InputCombination.from_tuples((EV_ABS, ABS_X)),
"target_uinput": "mouse",
"output_type": EV_REL,
"output_code": REL_HWHEEL_HI_RES,
}
preset.add(Mapping(**cfg)) # abs x -> wheel
cfg["input_combination"] = get_combination_config((EV_ABS, ABS_Y))
cfg["input_combination"] = InputCombination.from_tuples((EV_ABS, ABS_Y))
cfg["output_code"] = REL_WHEEL_HI_RES
preset.add(Mapping(**cfg)) # abs y -> wheel
preset.add(get_key_mapping(get_combination_config((1, 31)), "keyboard", "k(a)"))
preset.add(get_key_mapping(get_combination_config((1, 32)), "keyboard", "b"))
preset.add(
Mapping.from_combination(
InputCombination.from_tuples((1, 31)), "keyboard", "k(a)"
)
)
preset.add(
Mapping.from_combination(
InputCombination.from_tuples((1, 32)), "keyboard", "b"
)
)
# overlapping combination for (1, 32, 1)
preset.add(
get_key_mapping(
get_combination_config((1, 32), (1, 33), (1, 34)),
Mapping.from_combination(
InputCombination.from_tuples((1, 32), (1, 33), (1, 34)),
"keyboard",
"c",
)
@ -68,29 +75,37 @@ class TestContext(unittest.TestCase):
# map abs x to key "b"
preset.add(
get_key_mapping(
get_combination_config((EV_ABS, ABS_X, 20)),
Mapping.from_combination(
InputCombination.from_tuples((EV_ABS, ABS_X, 20)),
"keyboard",
"d",
),
)
context = Context(preset)
# expected callbacks and their lengths:
callbacks = {
context = Context(preset, {}, {})
expected_num_callbacks = {
# ABS_X -> "d" and ABS_X -> wheel have the same type and code
InputConfig(type=EV_ABS, code=ABS_X).input_match_hash: 2,
InputConfig(type=EV_ABS, code=ABS_Y).input_match_hash: 1,
InputConfig(type=1, code=31).input_match_hash: 1,
# even though we have 2 mappings with this type and code, we only expect one callback
# because they both map to keys. We don't want to trigger two mappings with the same key press
InputConfig(type=1, code=32).input_match_hash: 1,
InputConfig(type=1, code=33).input_match_hash: 1,
InputConfig(type=1, code=34).input_match_hash: 1,
InputEvent.abs(ABS_X, 1): 2,
InputEvent.abs(ABS_Y, 1): 1,
InputEvent.key(31, 1): 1,
# even though we have 2 mappings with this type and code, we only expect
# one callback because they both map to keys. We don't want to trigger two
# mappings with the same key press
InputEvent.key(32, 1): 1,
InputEvent.key(33, 1): 1,
InputEvent.key(34, 1): 1,
}
self.assertEqual(set(callbacks.keys()), set(context._notify_callbacks.keys()))
for key, val in callbacks.items():
self.assertEqual(val, len(context._notify_callbacks[key]))
self.assertEqual(
set([event.input_match_hash for event in expected_num_callbacks.keys()]),
set(context._notify_callbacks.keys()),
)
for input_event, num_callbacks in expected_num_callbacks.items():
self.assertEqual(
num_callbacks,
len(context.get_notify_callbacks(input_event)),
)
# 7 unique input events in the preset
self.assertEqual(7, len(context._handlers))

@ -87,6 +87,7 @@ class TestControl(unittest.TestCase):
start_history = []
stop_counter = 0
# using an actual injector is not within the scope of this test
class Injector:
def stop_injecting(self, *args, **kwargs):

@ -53,7 +53,7 @@ from inputremapper.configs.mapping import UIMapping, MappingData
from tests.lib.cleanup import quick_cleanup
from tests.lib.stuff import spy
from tests.lib.patches import FakeDaemonProxy
from tests.lib.fixtures import fixtures, prepare_presets, get_combination_config
from tests.lib.fixtures import fixtures, prepare_presets
from inputremapper.configs.global_config import GlobalConfig
from inputremapper.gui.controller import Controller, MAPPING_DEFAULTS
from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME
@ -457,7 +457,7 @@ class TestController(unittest.TestCase):
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=4))
combination=InputCombination([InputConfig(type=1, code=4)])
)
with patch.object(self.data_manager, "update_mapping") as mock:
@ -514,7 +514,7 @@ class TestController(unittest.TestCase):
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
self.message_broker.subscribe(
MessageType.user_confirm_request, lambda msg: msg.respond(True)
)
@ -524,7 +524,7 @@ class TestController(unittest.TestCase):
preset = Preset(get_preset_path("Foo Device", "preset2"))
preset.load()
self.assertIsNone(
preset.get_mapping(InputCombination(InputConfig(type=1, code=3)))
preset.get_mapping(InputCombination([InputConfig(type=1, code=3)]))
)
def test_does_not_delete_mapping_when_not_confirmed(self):
@ -533,7 +533,7 @@ class TestController(unittest.TestCase):
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
self.user_interface.confirm_delete.configure_mock(
return_value=Gtk.ResponseType.CANCEL
)
@ -544,7 +544,7 @@ class TestController(unittest.TestCase):
preset = Preset(get_preset_path("Foo Device", "preset2"))
preset.load()
self.assertIsNotNone(
preset.get_mapping(InputCombination(InputConfig(type=1, code=3)))
preset.get_mapping(InputCombination([InputConfig(type=1, code=3)]))
)
def test_should_update_combination(self):
@ -552,7 +552,7 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
calls: List[CombinationUpdate] = []
@ -561,13 +561,13 @@ class TestController(unittest.TestCase):
self.message_broker.subscribe(MessageType.combination_update, f)
self.controller.update_combination(
InputCombination(InputConfig(type=1, code=10))
InputCombination([InputConfig(type=1, code=10)])
)
self.assertEqual(
calls[0],
CombinationUpdate(
InputCombination(InputConfig(type=1, code=3)),
InputCombination(InputConfig(type=1, code=10)),
InputCombination([InputConfig(type=1, code=3)]),
InputCombination([InputConfig(type=1, code=10)]),
),
)
@ -576,7 +576,7 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
calls: List[CombinationUpdate] = []
@ -585,7 +585,7 @@ class TestController(unittest.TestCase):
self.message_broker.subscribe(MessageType.combination_update, f)
self.controller.update_combination(
InputCombination(InputConfig(type=1, code=4))
InputCombination([InputConfig(type=1, code=4)])
)
self.assertEqual(len(calls), 0)
@ -632,7 +632,7 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
calls: List[CombinationUpdate] = []
@ -643,25 +643,25 @@ class TestController(unittest.TestCase):
self.controller.start_key_recording()
self.message_broker.publish(
CombinationRecorded(InputCombination(InputConfig(type=1, code=10)))
CombinationRecorded(InputCombination([InputConfig(type=1, code=10)]))
)
self.assertEqual(
calls[0],
CombinationUpdate(
InputCombination(InputConfig(type=1, code=3)),
InputCombination(InputConfig(type=1, code=10)),
InputCombination([InputConfig(type=1, code=3)]),
InputCombination([InputConfig(type=1, code=10)]),
),
)
self.message_broker.publish(
CombinationRecorded(
InputCombination(get_combination_config((1, 10), (1, 3)))
InputCombination(InputCombination.from_tuples((1, 10), (1, 3)))
)
)
self.assertEqual(
calls[1],
CombinationUpdate(
InputCombination(InputConfig(type=1, code=10)),
InputCombination(get_combination_config((1, 10), (1, 3))),
InputCombination([InputConfig(type=1, code=10)]),
InputCombination(InputCombination.from_tuples((1, 10), (1, 3))),
),
)
@ -669,7 +669,7 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
calls: List[CombinationUpdate] = []
@ -679,7 +679,7 @@ class TestController(unittest.TestCase):
self.message_broker.subscribe(MessageType.combination_update, f)
self.message_broker.publish(
CombinationRecorded(InputCombination(InputConfig(type=1, code=10)))
CombinationRecorded(InputCombination([InputConfig(type=1, code=10)]))
)
self.assertEqual(len(calls), 0)
@ -687,7 +687,7 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
calls: List[CombinationUpdate] = []
@ -698,12 +698,12 @@ class TestController(unittest.TestCase):
self.controller.start_key_recording()
self.message_broker.publish(
CombinationRecorded(InputCombination(InputConfig(type=1, code=10)))
CombinationRecorded(InputCombination([InputConfig(type=1, code=10)]))
)
self.message_broker.signal(MessageType.recording_finished)
self.message_broker.publish(
CombinationRecorded(
InputCombination(get_combination_config((1, 10), (1, 3)))
InputCombination(InputCombination.from_tuples((1, 10), (1, 3)))
)
)
@ -713,7 +713,7 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=3)))
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
calls: List[CombinationUpdate] = []
@ -724,12 +724,12 @@ class TestController(unittest.TestCase):
self.controller.start_key_recording()
self.message_broker.publish(
CombinationRecorded(InputCombination(InputConfig(type=1, code=10)))
CombinationRecorded(InputCombination([InputConfig(type=1, code=10)]))
)
self.controller.stop_key_recording()
self.message_broker.publish(
CombinationRecorded(
InputCombination(get_combination_config((1, 10), (1, 3)))
InputCombination(InputCombination.from_tuples((1, 10), (1, 3)))
)
)
@ -762,7 +762,7 @@ class TestController(unittest.TestCase):
self.data_manager.load_preset("foo")
self.data_manager.create_mapping()
self.data_manager.update_mapping(
input_combination=InputCombination(InputConfig.btn_left()),
input_combination=InputCombination([InputConfig.btn_left()]),
target_uinput="keyboard",
output_symbol="a",
)
@ -788,7 +788,7 @@ class TestController(unittest.TestCase):
self.data_manager.load_preset("foo")
self.data_manager.create_mapping()
self.data_manager.update_mapping(
input_combination=InputCombination(InputConfig.btn_left()),
input_combination=InputCombination([InputConfig.btn_left()]),
target_uinput="keyboard",
output_symbol="a",
)
@ -805,14 +805,14 @@ class TestController(unittest.TestCase):
self.data_manager.load_preset("foo")
self.data_manager.create_mapping()
self.data_manager.update_mapping(
input_combination=InputCombination(InputConfig.btn_left()),
input_combination=InputCombination([InputConfig.btn_left()]),
target_uinput="keyboard",
output_symbol="a",
)
self.data_manager.create_mapping()
self.data_manager.load_mapping(InputCombination.empty_combination())
self.data_manager.update_mapping(
input_combination=InputCombination(InputConfig(type=1, code=5)),
input_combination=InputCombination([InputConfig(type=1, code=5)]),
target_uinput="mouse",
output_symbol="BTN_LEFT",
)
@ -947,10 +947,12 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=InputCombination(
get_combination_config((1, 1), (1, 2), (1, 3))
InputCombination.from_tuples((1, 1), (1, 2), (1, 3))
)
)
@ -959,7 +961,7 @@ class TestController(unittest.TestCase):
)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 2), (1, 1), (1, 3))),
InputCombination(InputCombination.from_tuples((1, 2), (1, 1), (1, 3))),
)
# now nothing changes
self.controller.move_input_config_in_combination(
@ -967,17 +969,19 @@ class TestController(unittest.TestCase):
)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 2), (1, 1), (1, 3))),
InputCombination(InputCombination.from_tuples((1, 2), (1, 1), (1, 3))),
)
def test_move_event_down(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=InputCombination(
get_combination_config((1, 1), (1, 2), (1, 3))
InputCombination.from_tuples((1, 1), (1, 2), (1, 3))
)
)
@ -986,7 +990,7 @@ class TestController(unittest.TestCase):
)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 1), (1, 3), (1, 2))),
InputCombination(InputCombination.from_tuples((1, 1), (1, 3), (1, 2))),
)
# now nothing changes
self.controller.move_input_config_in_combination(
@ -994,30 +998,34 @@ class TestController(unittest.TestCase):
)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 1), (1, 3), (1, 2))),
InputCombination(InputCombination.from_tuples((1, 1), (1, 3), (1, 2))),
)
def test_move_event_in_combination_of_len_1(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.controller.move_input_config_in_combination(
InputConfig(type=1, code=3), "down"
)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 3))),
InputCombination(InputCombination.from_tuples((1, 3))),
)
def test_move_event_loads_it_again(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=InputCombination(
get_combination_config((1, 1), (1, 2), (1, 3))
InputCombination.from_tuples((1, 1), (1, 2), (1, 3))
)
)
mock = MagicMock()
@ -1031,7 +1039,9 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.load_input_config(InputConfig(type=1, code=3))
mock = MagicMock()
self.message_broker.subscribe(MessageType.selected_event, mock)
@ -1042,7 +1052,9 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.load_input_config(InputConfig(type=1, code=3))
mock = MagicMock()
self.message_broker.subscribe(MessageType.selected_event, mock)
@ -1065,33 +1077,37 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=get_combination_config((1, 3), (1, 4))
input_combination=InputCombination.from_tuples((1, 3), (1, 4))
)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 3), (1, 4))),
InputCombination(InputCombination.from_tuples((1, 3), (1, 4))),
)
self.data_manager.load_input_config(InputConfig(type=1, code=4))
self.controller.remove_event()
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 3))),
InputCombination(InputCombination.from_tuples((1, 3))),
)
def test_remove_event_loads_a_event(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=get_combination_config((1, 3), (1, 4))
input_combination=InputCombination.from_tuples((1, 3), (1, 4))
)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((1, 3), (1, 4))),
InputCombination(InputCombination.from_tuples((1, 3), (1, 4))),
)
self.data_manager.load_input_config(InputConfig(type=1, code=4))
@ -1104,9 +1120,11 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=get_combination_config((1, 3), (1, 4))
input_combination=InputCombination.from_tuples((1, 3), (1, 4))
)
self.data_manager.load_input_config(InputConfig(type=1, code=3))
@ -1127,10 +1145,10 @@ class TestController(unittest.TestCase):
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.update_mapping(
input_combination=get_combination_config((3, 0, 10))
input_combination=InputCombination.from_tuples((3, 0, 10))
)
self.data_manager.load_mapping(
InputCombination(get_combination_config((3, 0, 10)))
InputCombination(InputCombination.from_tuples((3, 0, 10)))
)
self.data_manager.load_input_config(
InputConfig(type=3, code=0, analog_threshold=10)
@ -1148,9 +1166,11 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=get_combination_config((3, 0, 10))
input_combination=InputCombination.from_tuples((3, 0, 10))
)
self.data_manager.load_input_config(
InputConfig(type=3, code=0, analog_threshold=10)
@ -1159,23 +1179,25 @@ class TestController(unittest.TestCase):
self.controller.set_event_as_analog(True)
self.assertEqual(
self.data_manager.active_mapping.input_combination,
InputCombination(get_combination_config((3, 0))),
InputCombination(InputCombination.from_tuples((3, 0))),
)
def test_set_event_as_analog_adds_rel_threshold(self):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=get_combination_config((2, 0))
input_combination=InputCombination.from_tuples((2, 0))
)
self.data_manager.load_input_config(InputConfig(type=2, code=0))
self.controller.set_event_as_analog(False)
combinations = [
InputCombination(get_combination_config((2, 0, 1))),
InputCombination(get_combination_config((2, 0, -1))),
InputCombination(InputCombination.from_tuples((2, 0, 1))),
InputCombination(InputCombination.from_tuples((2, 0, -1))),
]
self.assertIn(self.data_manager.active_mapping.input_combination, combinations)
@ -1183,16 +1205,18 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=get_combination_config((3, 0))
input_combination=InputCombination.from_tuples((3, 0))
)
self.data_manager.load_input_config(InputConfig(type=3, code=0))
self.controller.set_event_as_analog(False)
combinations = [
InputCombination(get_combination_config((3, 0, 10))),
InputCombination(get_combination_config((3, 0, -10))),
InputCombination(InputCombination.from_tuples((3, 0, 10))),
InputCombination(InputCombination.from_tuples((3, 0, -10))),
]
self.assertIn(self.data_manager.active_mapping.input_combination, combinations)
@ -1200,7 +1224,9 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.load_input_config(InputConfig(type=1, code=3))
mock = MagicMock()
@ -1217,9 +1243,11 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=get_combination_config((3, 0, 10))
input_combination=InputCombination.from_tuples((3, 0, 10))
)
self.data_manager.load_input_config(
InputConfig(type=3, code=0, analog_threshold=10)
@ -1240,9 +1268,11 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=get_combination_config((3, 0))
input_combination=InputCombination.from_tuples((3, 0))
)
self.data_manager.load_input_config(InputConfig(type=3, code=0))
@ -1261,7 +1291,9 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
request: UserConfirmRequest = None
def f(r: UserConfirmRequest):
@ -1276,7 +1308,9 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(output_symbol=None)
request: UserConfirmRequest = None
@ -1292,9 +1326,11 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=get_combination_config((1, 3), (2, 1, 1)),
input_combination=InputCombination.from_tuples((1, 3), (2, 1, 1)),
output_symbol=None,
)
request: UserConfirmRequest = None
@ -1311,9 +1347,11 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=get_combination_config((1, 3), (2, 1, 1)),
input_combination=InputCombination.from_tuples((1, 3), (2, 1, 1)),
output_symbol=None,
)
@ -1326,7 +1364,7 @@ class TestController(unittest.TestCase):
mapping_type="analog",
output_symbol=None,
input_combination=InputCombination(
get_combination_config((1, 3), (2, 1))
InputCombination.from_tuples((1, 3), (2, 1))
),
)
@ -1334,7 +1372,9 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.message_broker.subscribe(
MessageType.user_confirm_request, lambda r: r.respond(False)
@ -1344,7 +1384,7 @@ class TestController(unittest.TestCase):
mock.assert_not_called()
self.data_manager.update_mapping(
input_combination=get_combination_config((1, 3), (2, 1)),
input_combination=InputCombination.from_tuples((1, 3), (2, 1)),
output_symbol=None,
mapping_type="analog",
)
@ -1356,7 +1396,9 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.message_broker.subscribe(
MessageType.user_confirm_request, lambda r: r.respond(True)
@ -1369,9 +1411,11 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=get_combination_config((1, 3), (2, 1)),
input_combination=InputCombination.from_tuples((1, 3), (2, 1)),
output_symbol=None,
mapping_type="analog",
)
@ -1389,9 +1433,11 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=get_combination_config((1, 3), (2, 1)),
input_combination=InputCombination.from_tuples((1, 3), (2, 1)),
output_symbol=None,
)
mock = MagicMock()
@ -1403,9 +1449,11 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=get_combination_config((1, 3), (2, 1, 1)),
input_combination=InputCombination.from_tuples((1, 3), (2, 1, 1)),
mapping_type="analog",
output_symbol=None,
)
@ -1418,9 +1466,11 @@ class TestController(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device 2")
self.data_manager.load_preset("preset2")
self.data_manager.load_mapping(InputCombination(get_combination_config((1, 3))))
self.data_manager.load_mapping(
InputCombination(InputCombination.from_tuples((1, 3)))
)
self.data_manager.update_mapping(
input_combination=get_combination_config((1, 3), (2, 1)),
input_combination=InputCombination.from_tuples((1, 3), (2, 1)),
output_symbol=None,
mapping_type="analog",
)

@ -17,15 +17,16 @@
#
# 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 evdev._ecodes import EV_ABS
from inputremapper.input_event import InputEvent
from tests.test import is_service_running
from tests.lib.logger import logger
from tests.lib.cleanup import cleanup
from tests.lib.fixtures import new_event, get_combination_config
from tests.lib.fixtures import Fixture
from tests.lib.pipes import push_events, uinput_write_history_pipe
from tests.lib.tmp import tmp
from tests.lib.fixtures import fixtures, get_key_mapping
from tests.lib.fixtures import fixtures
import os
import unittest
@ -34,10 +35,11 @@ import subprocess
import json
import evdev
from evdev.ecodes import EV_KEY, EV_ABS, KEY_B, KEY_A, ABS_X, BTN_A, BTN_B
from evdev.ecodes import EV_KEY, KEY_B, KEY_A, ABS_X, BTN_A, BTN_B
from pydbus import SystemBus
from inputremapper.configs.system_mapping import system_mapping
from inputremapper.configs.mapping import Mapping
from inputremapper.configs.global_config import global_config
from inputremapper.groups import groups
from inputremapper.configs.paths import get_config_path, mkdir, get_preset_path
@ -115,8 +117,6 @@ class TestDaemon(unittest.TestCase):
preset_name = "foo"
ev = (EV_ABS, ABS_X)
group = groups.find(name="gamepad")
# unrelated group that shouldn't be affected at all
@ -124,15 +124,21 @@ class TestDaemon(unittest.TestCase):
preset = Preset(group.get_preset_path(preset_name))
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=BTN_A)),
"keyboard",
"a",
Mapping.from_combination(
input_combination=InputCombination(
[InputConfig(type=EV_KEY, code=BTN_A)]
),
target_uinput="keyboard",
output_symbol="a",
)
)
preset.add(
get_key_mapping(
InputCombination(get_combination_config((*ev, -1))), "keyboard", "b"
Mapping.from_combination(
input_combination=InputCombination(
[InputConfig(type=EV_ABS, code=ABS_X, analog_threshold=-1)]
),
target_uinput="keyboard",
output_symbol="b",
)
)
preset.save()
@ -141,7 +147,10 @@ class TestDaemon(unittest.TestCase):
"""Injection 1"""
# should forward the event unchanged
push_events(fixtures.gamepad, [new_event(EV_KEY, BTN_B, 1)])
push_events(
fixtures.gamepad,
[InputEvent.key(BTN_B, 1, fixtures.gamepad.get_device_hash())],
)
self.daemon = Daemon()
@ -184,7 +193,10 @@ class TestDaemon(unittest.TestCase):
time.sleep(0.1)
# -1234 will be classified as -1 by the injector
push_events(fixtures.gamepad, [new_event(*ev, -1234)])
push_events(
fixtures.gamepad,
[InputEvent.abs(ABS_X, -1234, fixtures.gamepad.get_device_hash())],
)
time.sleep(0.1)
self.assertTrue(uinput_write_history_pipe[0].poll())
@ -213,7 +225,7 @@ class TestDaemon(unittest.TestCase):
os.remove(get_config_path("xmodmap.json"))
preset_name = "foo"
ev = (EV_KEY, 9)
key_code = 9
group_name = "9876 name"
# expected key of the group
@ -228,8 +240,10 @@ class TestDaemon(unittest.TestCase):
preset = Preset(get_preset_path(group_name, preset_name))
preset.add(
get_key_mapping(
InputCombination(get_combination_config(ev)), "keyboard", "a"
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=key_code)]),
"keyboard",
"a",
)
)
@ -246,13 +260,15 @@ class TestDaemon(unittest.TestCase):
groups.refresh()
# the daemon is supposed to find this device by calling refresh
fixtures[self.new_fixture_path] = {
"capabilities": {evdev.ecodes.EV_KEY: [ev[1]]},
"phys": "9876 phys",
"info": evdev.device.DeviceInfo(4, 5, 6, 7),
"name": group_name,
}
push_events(fixtures[self.new_fixture_path], [new_event(*ev, 1)])
fixture = Fixture(
capabilities={evdev.ecodes.EV_KEY: [key_code]},
phys="9876 phys",
info=evdev.device.DeviceInfo(4, 5, 6, 7),
name=group_name,
path=self.new_fixture_path,
)
fixtures[self.new_fixture_path] = fixture
push_events(fixture, [InputEvent.key(key_code, 1, fixture.get_device_hash())])
self.daemon.start_injecting(group_key, preset_name)
# test if the injector called groups.refresh successfully
@ -264,16 +280,16 @@ class TestDaemon(unittest.TestCase):
self.assertTrue(uinput_write_history_pipe[0].poll())
event = uinput_write_history_pipe[0].recv()
self.assertEqual(event.t, (EV_KEY, KEY_A, 1))
self.assertEqual(event, (EV_KEY, KEY_A, 1))
self.daemon.stop_injecting(group_key)
time.sleep(0.2)
self.assertEqual(self.daemon.get_state(group_key), InjectorState.STOPPED)
def test_refresh_for_unknown_key(self):
device = "9876 name"
device_9876 = "9876 name"
# this test only makes sense if this device is unknown yet
self.assertIsNone(groups.find(name=device))
self.assertIsNone(groups.find(name=device_9876))
self.daemon = Daemon()
@ -282,25 +298,26 @@ class TestDaemon(unittest.TestCase):
self.daemon.refresh()
fixtures[self.new_fixture_path] = {
"capabilities": {evdev.ecodes.EV_KEY: [evdev.ecodes.KEY_A]},
"phys": "9876 phys",
"info": evdev.device.DeviceInfo(4, 5, 6, 7),
"name": device,
}
fixtures[self.new_fixture_path] = Fixture(
capabilities={evdev.ecodes.EV_KEY: [evdev.ecodes.KEY_A]},
phys="9876 phys",
info=evdev.device.DeviceInfo(4, 5, 6, 7),
name=device_9876,
path=self.new_fixture_path,
)
self.daemon._autoload("25v7j9q4vtj")
# this is unknown, so the daemon will scan the devices again
# test if the injector called groups.refresh successfully
self.assertIsNotNone(groups.find(name=device))
self.assertIsNotNone(groups.find(name=device_9876))
def test_xmodmap_file(self):
"""Create a custom xmodmap file, expect the daemon to read keycodes from it."""
from_keycode = evdev.ecodes.KEY_A
target = "keyboard"
to_name = "q"
to_keycode = 100
event = (EV_KEY, from_keycode, 1)
name = "Bar Device"
preset_name = "foo"
@ -312,15 +329,26 @@ class TestDaemon(unittest.TestCase):
preset = Preset(path)
preset.add(
get_key_mapping(
InputCombination(get_combination_config(event)), target, to_name
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=from_keycode)]),
target,
to_name,
)
)
preset.save()
system_mapping.clear()
push_events(fixtures.bar_device, [new_event(*event)])
push_events(
fixtures.bar_device,
[
InputEvent.key(
from_keycode,
1,
origin_hash=fixtures.bar_device.get_device_hash(),
)
],
)
# an existing config file is needed otherwise set_config_dir refuses
# to use the directory
@ -328,10 +356,13 @@ class TestDaemon(unittest.TestCase):
global_config.path = config_path
global_config._save_config()
# finally, create the xmodmap file
xmodmap_path = os.path.join(config_dir, "xmodmap.json")
with open(xmodmap_path, "w") as file:
file.write(f'{{"{to_name}":{to_keycode}}}')
# test setup complete
self.daemon = Daemon()
self.daemon.set_config_dir(config_dir)
@ -355,8 +386,8 @@ class TestDaemon(unittest.TestCase):
pereset = Preset(group.get_preset_path(preset_name))
pereset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=KEY_A)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=KEY_A)]),
"keyboard",
"a",
)
@ -423,8 +454,8 @@ class TestDaemon(unittest.TestCase):
preset = Preset(group.get_preset_path(preset_name))
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=KEY_A)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=KEY_A)]),
"keyboard",
"a",
)
@ -481,8 +512,8 @@ class TestDaemon(unittest.TestCase):
group = groups.find(key="Foo Device 2")
preset = Preset(group.get_preset_path(preset_name))
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=3, code=2, analog_threshold=1)),
Mapping.from_combination(
InputCombination([InputConfig(type=3, code=2, analog_threshold=1)]),
"keyboard",
"a",
)
@ -504,8 +535,8 @@ class TestDaemon(unittest.TestCase):
preset = Preset(group.get_preset_path(preset_name))
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=3, code=2, analog_threshold=1)),
Mapping.from_combination(
InputCombination([InputConfig(type=3, code=2, analog_threshold=1)]),
"keyboard",
"a",
)

@ -42,7 +42,7 @@ from inputremapper.gui.reader_client import ReaderClient
from inputremapper.injection.global_uinputs import GlobalUInputs
from tests.lib.cleanup import quick_cleanup
from tests.lib.patches import FakeDaemonProxy
from tests.lib.fixtures import prepare_presets, get_combination_config
from tests.lib.fixtures import prepare_presets
from inputremapper.configs.paths import get_preset_path
from inputremapper.configs.preset import Preset
@ -163,7 +163,7 @@ class TestDataManager(unittest.TestCase):
listener = Listener()
self.message_broker.subscribe(MessageType.mapping, listener)
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=1))
combination=InputCombination([InputConfig(type=1, code=1)])
)
mapping: MappingData = listener.calls[0]
@ -171,7 +171,7 @@ class TestDataManager(unittest.TestCase):
control_preset.load()
self.assertEqual(
control_preset.get_mapping(
InputCombination(InputConfig(type=1, code=1))
InputCombination([InputConfig(type=1, code=1)])
).output_symbol,
mapping.output_symbol,
)
@ -185,7 +185,7 @@ class TestDataManager(unittest.TestCase):
control_preset.load()
self.assertEqual(
control_preset.get_mapping(
InputCombination(InputConfig(type=1, code=1))
InputCombination([InputConfig(type=1, code=1)])
).output_symbol,
"key(a)",
)
@ -379,7 +379,7 @@ class TestDataManager(unittest.TestCase):
"""should be able to load a mapping"""
preset, _, _ = prepare_presets()
expected_mapping = preset.get_mapping(
InputCombination(InputConfig(type=1, code=1))
InputCombination([InputConfig(type=1, code=1)])
)
self.data_manager.load_group(group_key="Foo Device")
@ -387,7 +387,7 @@ class TestDataManager(unittest.TestCase):
listener = Listener()
self.message_broker.subscribe(MessageType.mapping, listener)
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=1))
combination=InputCombination([InputConfig(type=1, code=1)])
)
mapping = listener.calls[0]
@ -402,7 +402,7 @@ class TestDataManager(unittest.TestCase):
self.assertRaises(
KeyError,
self.data_manager.load_mapping,
combination=InputCombination(InputConfig(type=1, code=1)),
combination=InputCombination([InputConfig(type=1, code=1)]),
)
def test_cannot_load_mapping_without_preset(self):
@ -413,13 +413,13 @@ class TestDataManager(unittest.TestCase):
self.assertRaises(
DataManagementError,
self.data_manager.load_mapping,
combination=InputCombination(InputConfig(type=1, code=1)),
combination=InputCombination([InputConfig(type=1, code=1)]),
)
self.data_manager.load_group("Foo Device")
self.assertRaises(
DataManagementError,
self.data_manager.load_mapping,
combination=InputCombination(InputConfig(type=1, code=1)),
combination=InputCombination([InputConfig(type=1, code=1)]),
)
def test_load_event(self):
@ -428,7 +428,7 @@ class TestDataManager(unittest.TestCase):
self.message_broker.subscribe(MessageType.selected_event, mock)
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1)))
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)]))
self.data_manager.load_input_config(InputConfig(type=1, code=1))
mock.assert_called_once_with(InputConfig(type=1, code=1))
self.assertEqual(
@ -446,7 +446,7 @@ class TestDataManager(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1)))
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)]))
with self.assertRaises(ValueError):
self.data_manager.load_input_config(InputConfig(type=1, code=5))
@ -454,7 +454,7 @@ class TestDataManager(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1)))
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)]))
self.data_manager.load_input_config(InputConfig(type=1, code=1))
self.data_manager.update_input_config(InputConfig(type=1, code=5))
self.assertEqual(
@ -465,7 +465,7 @@ class TestDataManager(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1)))
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)]))
self.data_manager.load_input_config(InputConfig(type=1, code=1))
mock = MagicMock()
@ -476,8 +476,8 @@ class TestDataManager(unittest.TestCase):
expected = [
call(
CombinationUpdate(
InputCombination(InputConfig(type=1, code=1)),
InputCombination(InputConfig(type=1, code=5)),
InputCombination([InputConfig(type=1, code=1)]),
InputCombination([InputConfig(type=1, code=5)]),
)
),
call(self.data_manager.active_mapping.get_bus_message()),
@ -489,7 +489,7 @@ class TestDataManager(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1)))
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)]))
self.data_manager.load_input_config(InputConfig(type=1, code=1))
with self.assertRaises(KeyError):
self.data_manager.update_input_config(InputConfig(type=1, code=2))
@ -498,7 +498,7 @@ class TestDataManager(unittest.TestCase):
prepare_presets()
self.data_manager.load_group("Foo Device")
self.data_manager.load_preset("preset1")
self.data_manager.load_mapping(InputCombination(InputConfig(type=1, code=1)))
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=1)]))
with self.assertRaises(DataManagementError):
self.data_manager.update_input_config(InputConfig(type=1, code=2))
@ -508,7 +508,7 @@ class TestDataManager(unittest.TestCase):
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=4))
combination=InputCombination([InputConfig(type=1, code=4)])
)
listener = Listener()
@ -530,7 +530,7 @@ class TestDataManager(unittest.TestCase):
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=4))
combination=InputCombination([InputConfig(type=1, code=4)])
)
self.data_manager.update_mapping(
@ -542,7 +542,7 @@ class TestDataManager(unittest.TestCase):
preset = Preset(get_preset_path("Foo Device", "preset2"), UIMapping)
preset.load()
mapping = preset.get_mapping(InputCombination(InputConfig(type=1, code=4)))
mapping = preset.get_mapping(InputCombination([InputConfig(type=1, code=4)]))
self.assertEqual(mapping.format_name(), "foo")
self.assertEqual(mapping.output_symbol, "f")
self.assertEqual(mapping.release_timeout, 0.3)
@ -553,7 +553,7 @@ class TestDataManager(unittest.TestCase):
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=4))
combination=InputCombination([InputConfig(type=1, code=4)])
)
self.data_manager.update_mapping(
@ -563,7 +563,7 @@ class TestDataManager(unittest.TestCase):
preset = Preset(get_preset_path("Foo Device", "preset2"), UIMapping)
preset.load()
mapping = preset.get_mapping(InputCombination(InputConfig(type=1, code=4)))
mapping = preset.get_mapping(InputCombination([InputConfig(type=1, code=4)]))
self.assertIsNotNone(mapping.get_error())
self.assertEqual(mapping.output_symbol, "bar")
@ -573,7 +573,7 @@ class TestDataManager(unittest.TestCase):
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=4))
combination=InputCombination([InputConfig(type=1, code=4)])
)
listener = Listener()
self.message_broker.subscribe(MessageType.mapping, listener)
@ -581,21 +581,23 @@ class TestDataManager(unittest.TestCase):
# we expect a message for combination update first, and then for mapping
self.data_manager.update_mapping(
input_combination=InputCombination(get_combination_config((1, 5), (1, 6)))
input_combination=InputCombination(
InputCombination.from_tuples((1, 5), (1, 6))
)
)
self.assertEqual(listener.calls[0].message_type, MessageType.combination_update)
self.assertEqual(
listener.calls[0].old_combination,
InputCombination(InputConfig(type=1, code=4)),
InputCombination([InputConfig(type=1, code=4)]),
)
self.assertEqual(
listener.calls[0].new_combination,
InputCombination(get_combination_config((1, 5), (1, 6))),
InputCombination(InputCombination.from_tuples((1, 5), (1, 6))),
)
self.assertEqual(listener.calls[1].message_type, MessageType.mapping)
self.assertEqual(
listener.calls[1].input_combination,
InputCombination(get_combination_config((1, 5), (1, 6))),
InputCombination(InputCombination.from_tuples((1, 5), (1, 6))),
)
def test_cannot_update_mapping_combination(self):
@ -605,13 +607,13 @@ class TestDataManager(unittest.TestCase):
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=4))
combination=InputCombination([InputConfig(type=1, code=4)])
)
self.assertRaises(
KeyError,
self.data_manager.update_mapping,
input_combination=InputCombination(InputConfig(type=1, code=3)),
input_combination=InputCombination([InputConfig(type=1, code=3)]),
)
def test_cannot_update_mapping(self):
@ -671,7 +673,7 @@ class TestDataManager(unittest.TestCase):
self.data_manager.load_group(group_key="Foo Device 2")
self.data_manager.load_preset(name="preset2")
self.data_manager.load_mapping(
combination=InputCombination(InputConfig(type=1, code=3))
combination=InputCombination([InputConfig(type=1, code=3)])
)
listener = Listener()
@ -682,7 +684,7 @@ class TestDataManager(unittest.TestCase):
self.data_manager.save()
deleted_mapping = old_preset.get_mapping(
InputCombination(InputConfig(type=1, code=3))
InputCombination([InputConfig(type=1, code=3)])
)
mappings = listener.calls[0].mappings
preset_name = listener.calls[0].name

File diff suppressed because it is too large Load Diff

@ -39,6 +39,7 @@ from evdev.ecodes import (
REL_Y,
REL_WHEEL,
)
from inputremapper.injection.mapping_handlers.combination_handler import (
CombinationHandler,
)
@ -61,10 +62,12 @@ from inputremapper.injection.mapping_handlers.macro_handler import MacroHandler
from inputremapper.injection.mapping_handlers.mapping_handler import MappingHandler
from inputremapper.injection.mapping_handlers.rel_to_abs_handler import RelToAbsHandler
from inputremapper.input_event import InputEvent, EventActions
from tests.lib.cleanup import cleanup
from tests.lib.logger import logger
from tests.lib.patches import InputDevice
from tests.lib.constants import MAX_ABS
from tests.lib.stuff import convert_to_internal_events
from tests.lib.fixtures import fixtures
class BaseTests:
@ -103,13 +106,14 @@ class TestAxisSwitchHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
output_type=2,
output_code=1,
),
MagicMock(),
)
class TestAbsToBtnHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(
InputConfig(type=3, code=5, analog_threshold=10)
[InputConfig(type=3, code=5, analog_threshold=10)]
)
self.handler = AbsToBtnHandler(
input_combination,
@ -123,7 +127,7 @@ class TestAbsToBtnHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
class TestAbsToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(InputConfig(type=EV_ABS, code=ABS_X))
input_combination = InputCombination([InputConfig(type=EV_ABS, code=ABS_X)])
self.handler = AbsToAbsHandler(
input_combination,
Mapping(
@ -138,7 +142,6 @@ class TestAbsToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
self.handler.notify(
InputEvent(0, 0, EV_ABS, ABS_X, MAX_ABS),
source=InputDevice("/dev/input/event15"),
forward=evdev.UInput(),
)
self.handler.reset()
history = global_uinputs.get_uinput("gamepad").write_history
@ -150,7 +153,7 @@ class TestAbsToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
class TestRelToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(InputConfig(type=EV_REL, code=REL_X))
input_combination = InputCombination([InputConfig(type=EV_REL, code=REL_X)])
self.handler = RelToAbsHandler(
input_combination,
Mapping(
@ -165,7 +168,6 @@ class TestRelToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
self.handler.notify(
InputEvent(0, 0, EV_REL, REL_X, 123),
source=InputDevice("/dev/input/event15"),
forward=evdev.UInput(),
)
self.handler.reset()
history = global_uinputs.get_uinput("gamepad").write_history
@ -186,13 +188,11 @@ class TestRelToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
self.handler.notify(
InputEvent(0, delta, EV_REL, REL_X, 100),
source=InputDevice("/dev/input/event15"),
forward=evdev.UInput(),
)
self.handler.notify(
InputEvent(0, delta * 2, EV_REL, REL_X, 100),
source=InputDevice("/dev/input/event15"),
forward=evdev.UInput(),
)
self.assertEqual(self.handler._observed_rate, expected_rate)
@ -204,13 +204,11 @@ class TestRelToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
self.handler.notify(
InputEvent(0, 50, EV_REL, REL_X, 100),
source=InputDevice("/dev/input/event15"),
forward=evdev.UInput(),
)
self.handler.notify(
InputEvent(0, 50, EV_REL, REL_X, 100),
source=InputDevice("/dev/input/event15"),
forward=evdev.UInput(),
)
self.assertEqual(self.handler._observed_rate, DEFAULT_REL_RATE)
@ -218,7 +216,7 @@ class TestRelToAbsHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
class TestAbsToRelHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(InputConfig(type=EV_ABS, code=ABS_X))
input_combination = InputCombination([InputConfig(type=EV_ABS, code=ABS_X)])
self.handler = AbsToRelHandler(
input_combination,
Mapping(
@ -233,7 +231,6 @@ class TestAbsToRelHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
self.handler.notify(
InputEvent(0, 0, EV_ABS, ABS_X, MAX_ABS),
source=InputDevice("/dev/input/event15"),
forward=evdev.UInput(),
)
await asyncio.sleep(0.2)
self.handler.reset()
@ -249,12 +246,40 @@ class TestCombinationHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
handler: CombinationHandler
def setUp(self):
mouse = fixtures.foo_device_2_mouse
self.mouse_hash = mouse.get_device_hash()
keyboard = fixtures.foo_device_2_keyboard
self.keyboard_hash = keyboard.get_device_hash()
gamepad = fixtures.gamepad
self.gamepad_hash = gamepad.get_device_hash()
input_combination = InputCombination(
(
InputConfig(type=2, code=0, analog_threshold=10),
InputConfig(type=1, code=3),
InputConfig(
type=EV_REL,
code=5,
analog_threshold=10,
origin_hash=self.mouse_hash,
),
InputConfig(
type=EV_KEY,
code=3,
origin_hash=self.keyboard_hash,
),
InputConfig(
type=EV_KEY,
code=4,
origin_hash=self.gamepad_hash,
),
)
)
self.input_combination = input_combination
self.context_mock = MagicMock()
self.handler = CombinationHandler(
input_combination,
Mapping(
@ -262,8 +287,100 @@ class TestCombinationHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
target_uinput="mouse",
output_symbol="BTN_LEFT",
),
self.context_mock,
)
def test_forward_correctly(self):
# In the past, if a mapping has inputs from two different sub devices, it
# always failed to send the release events to the correct one.
# Nowadays, self._context.get_forward_uinput(origin_hash) is used to
# release them correctly.
mock = MagicMock()
self.handler.set_sub_handler(mock)
# insert our own test-uinput to see what is being written to it
uinputs = {
self.mouse_hash: evdev.UInput(),
self.keyboard_hash: evdev.UInput(),
self.gamepad_hash: evdev.UInput(),
}
self.context_mock.get_forward_uinput = lambda origin_hash: uinputs[origin_hash]
# 1. trigger the combination
self.handler.notify(
InputEvent.rel(
code=self.input_combination[0].code,
value=1,
origin_hash=self.input_combination[0].origin_hash,
),
source=fixtures.foo_device_2_mouse,
)
self.handler.notify(
InputEvent.key(
code=self.input_combination[1].code,
value=1,
origin_hash=self.input_combination[1].origin_hash,
),
source=fixtures.foo_device_2_keyboard,
)
self.handler.notify(
InputEvent.key(
code=self.input_combination[2].code,
value=1,
origin_hash=self.input_combination[2].origin_hash,
),
source=fixtures.gamepad,
)
# 2. expect release events to be written to the correct devices, as indicated
# by the origin_hash of the InputConfigs
self.assertListEqual(
uinputs[self.mouse_hash].write_history,
[InputEvent.rel(self.input_combination[0].code, 0)],
)
self.assertListEqual(
uinputs[self.keyboard_hash].write_history,
[InputEvent.key(self.input_combination[1].code, 0)],
)
self.assertListEqual(
uinputs[self.gamepad_hash].write_history,
[InputEvent.key(self.input_combination[2].code, 0)],
)
def test_no_forwards(self):
# if a combination is not triggered, nothing is released
mock = MagicMock()
self.handler.set_sub_handler(mock)
# insert our own test-uinput to see what is being written to it
uinputs = {
self.mouse_hash: evdev.UInput(),
self.keyboard_hash: evdev.UInput(),
}
self.context_mock.get_forward_uinput = lambda origin_hash: uinputs[origin_hash]
# 1. inject any two events
self.handler.notify(
InputEvent.rel(
code=self.input_combination[0].code,
value=1,
origin_hash=self.input_combination[0].origin_hash,
),
source=fixtures.foo_device_2_mouse,
)
self.handler.notify(
InputEvent.key(
code=self.input_combination[1].code,
value=1,
origin_hash=self.input_combination[1].origin_hash,
),
source=fixtures.foo_device_2_keyboard,
)
# 2. expect no release events to be written
self.assertListEqual(uinputs[self.mouse_hash].write_history, [])
self.assertListEqual(uinputs[self.keyboard_hash].write_history, [])
class TestHierarchyHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
@ -303,19 +420,14 @@ class TestKeyHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
self.handler.notify(
InputEvent(0, 0, EV_REL, REL_X, 1, actions=(EventActions.as_key,)),
source=InputDevice("/dev/input/event11"),
forward=evdev.UInput(),
)
history = convert_to_internal_events(
global_uinputs.get_uinput("mouse").write_history
)
self.assertEqual(history[0], InputEvent.from_tuple((EV_KEY, BTN_LEFT, 1)))
history = global_uinputs.get_uinput("mouse").write_history
self.assertEqual(history[0], InputEvent.key(BTN_LEFT, 1))
self.assertEqual(len(history), 1)
self.handler.reset()
history = convert_to_internal_events(
global_uinputs.get_uinput("mouse").write_history
)
self.assertEqual(history[1], InputEvent.from_tuple((EV_KEY, BTN_LEFT, 0)))
history = global_uinputs.get_uinput("mouse").write_history
self.assertEqual(history[1], InputEvent.key(BTN_LEFT, 0))
self.assertEqual(len(history), 2)
@ -342,31 +454,26 @@ class TestMacroHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
self.handler.notify(
InputEvent(0, 0, EV_REL, REL_X, 1, actions=(EventActions.as_key,)),
source=InputDevice("/dev/input/event11"),
forward=evdev.UInput(),
)
await asyncio.sleep(0.1)
history = convert_to_internal_events(
global_uinputs.get_uinput("mouse").write_history
)
self.assertIn(InputEvent.from_tuple((EV_KEY, BTN_LEFT, 1)), history)
self.assertIn(InputEvent.from_tuple((EV_KEY, BTN_RIGHT, 1)), history)
history = global_uinputs.get_uinput("mouse").write_history
self.assertIn(InputEvent.key(BTN_LEFT, 1), history)
self.assertIn(InputEvent.key(BTN_RIGHT, 1), history)
self.assertEqual(len(history), 2)
self.handler.reset()
await asyncio.sleep(0.1)
history = convert_to_internal_events(
global_uinputs.get_uinput("mouse").write_history
)
self.assertIn(InputEvent.from_tuple((EV_KEY, BTN_LEFT, 0)), history[-2:])
self.assertIn(InputEvent.from_tuple((EV_KEY, BTN_RIGHT, 0)), history[-2:])
history = global_uinputs.get_uinput("mouse").write_history
self.assertIn(InputEvent.key(BTN_LEFT, 0), history[-2:])
self.assertIn(InputEvent.key(BTN_RIGHT, 0), history[-2:])
self.assertEqual(len(history), 4)
class TestRelToBtnHandler(BaseTests, unittest.IsolatedAsyncioTestCase):
def setUp(self):
input_combination = InputCombination(
InputConfig(type=2, code=0, analog_threshold=10)
[InputConfig(type=2, code=0, analog_threshold=10)]
)
self.handler = RelToBtnHandler(
input_combination,
@ -382,7 +489,7 @@ class TestRelToRelHanlder(BaseTests, unittest.IsolatedAsyncioTestCase):
handler: RelToRelHandler
def setUp(self):
input_combination = InputCombination(InputConfig(type=EV_REL, code=REL_X))
input_combination = InputCombination([InputConfig(type=EV_REL, code=REL_X)])
self.handler = RelToRelHandler(
input_combination,
Mapping(

@ -43,12 +43,9 @@ from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.injection.context import Context
from inputremapper.injection.event_reader import EventReader
from inputremapper.injection.global_uinputs import global_uinputs
from tests.lib.fixtures import (
new_event,
get_combination_config,
get_key_mapping,
fixtures,
)
from inputremapper.input_event import InputEvent
from inputremapper.utils import get_device_hash
from tests.lib.fixtures import fixtures
from tests.lib.cleanup import quick_cleanup
@ -58,15 +55,17 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase):
self.stop_event = asyncio.Event()
self.preset = Preset()
global_uinputs.is_service = True
global_uinputs.prepare_all()
def tearDown(self):
quick_cleanup()
async def setup(self, source, mapping):
"""Set a EventReader up for the test and run it in the background."""
forward_to = evdev.UInput()
context = Context(mapping)
context = Context(mapping, {}, {})
context.uinput = evdev.UInput()
event_reader = EventReader(context, source, forward_to, self.stop_event)
event_reader = EventReader(context, source, self.stop_event)
asyncio.ensure_future(event_reader.run())
await asyncio.sleep(0.1)
return context, event_reader
@ -80,27 +79,31 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase):
trigger = evdev.ecodes.BTN_A
self.preset.add(
get_key_mapping(
Mapping.from_combination(
InputCombination(
InputConfig(
type=EV_KEY,
code=trigger,
origin_hash=fixtures.gamepad.get_device_hash(),
)
[
InputConfig(
type=EV_KEY,
code=trigger,
origin_hash=fixtures.gamepad.get_device_hash(),
)
]
),
"keyboard",
"if_single(key(a), key(KEY_LEFTSHIFT))",
)
)
self.preset.add(
get_key_mapping(
Mapping.from_combination(
InputCombination(
InputConfig(
type=EV_ABS,
code=ABS_Y,
analog_threshold=1,
origin_hash=fixtures.gamepad.get_device_hash(),
)
[
InputConfig(
type=EV_ABS,
code=ABS_Y,
analog_threshold=1,
origin_hash=fixtures.gamepad.get_device_hash(),
)
]
),
"keyboard",
"b",
@ -108,86 +111,111 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase):
)
# left x to mouse x
cfg = {
"input_combination": InputConfig(
type=EV_ABS, code=ABS_X, origin_hash=fixtures.gamepad.get_device_hash()
),
config = {
"input_combination": [
InputConfig(
type=EV_ABS,
code=ABS_X,
origin_hash=fixtures.gamepad.get_device_hash(),
)
],
"target_uinput": "mouse",
"output_type": EV_REL,
"output_code": REL_X,
}
self.preset.add(Mapping(**cfg))
self.preset.add(Mapping(**config))
# left y to mouse y
cfg["input_combination"] = InputConfig(
type=EV_ABS, code=ABS_Y, origin_hash=fixtures.gamepad.get_device_hash()
)
cfg["output_code"] = REL_Y
self.preset.add(Mapping(**cfg))
config["input_combination"] = [
InputConfig(
type=EV_ABS,
code=ABS_Y,
origin_hash=fixtures.gamepad.get_device_hash(),
)
]
config["output_code"] = REL_Y
self.preset.add(Mapping(**config))
# right x to wheel x
cfg["input_combination"] = InputConfig(
type=EV_ABS, code=ABS_RX, origin_hash=fixtures.gamepad.get_device_hash()
)
cfg["output_code"] = REL_HWHEEL_HI_RES
self.preset.add(Mapping(**cfg))
config["input_combination"] = [
InputConfig(
type=EV_ABS,
code=ABS_RX,
origin_hash=fixtures.gamepad.get_device_hash(),
)
]
config["output_code"] = REL_HWHEEL_HI_RES
self.preset.add(Mapping(**config))
# right y to wheel y
cfg["input_combination"] = InputConfig(
type=EV_ABS, code=ABS_RY, origin_hash=fixtures.gamepad.get_device_hash()
)
cfg["output_code"] = REL_WHEEL_HI_RES
self.preset.add(Mapping(**cfg))
config["input_combination"] = [
InputConfig(
type=EV_ABS,
code=ABS_RY,
origin_hash=fixtures.gamepad.get_device_hash(),
)
]
config["output_code"] = REL_WHEEL_HI_RES
self.preset.add(Mapping(**config))
context, _ = await self.setup(self.gamepad_source, self.preset)
gamepad_hash = get_device_hash(self.gamepad_source)
self.gamepad_source.push_events(
[
new_event(EV_KEY, trigger, 1), # start the macro
new_event(EV_ABS, ABS_Y, 10), # ignored
new_event(EV_KEY, evdev.ecodes.BTN_B, 2), # ignored
new_event(EV_KEY, evdev.ecodes.BTN_B, 0), # ignored
# stop it, the only way to trigger `then`
new_event(EV_KEY, trigger, 0),
InputEvent.key(evdev.ecodes.BTN_Y, 0, gamepad_hash), # start the macro
InputEvent.key(trigger, 1, gamepad_hash), # start the macro
InputEvent.abs(ABS_Y, 10, gamepad_hash), # ignored
InputEvent.key(evdev.ecodes.BTN_B, 2, gamepad_hash), # ignored
InputEvent.key(evdev.ecodes.BTN_B, 0, gamepad_hash), # ignored
# release the trigger, which runs `then` of if_single
InputEvent.key(trigger, 0, gamepad_hash),
]
)
await asyncio.sleep(0.1)
self.stop_event.set() # stop the reader
self.assertEqual(len(context.listeners), 0)
history = [a.t for a in global_uinputs.get_uinput("keyboard").write_history]
history = global_uinputs.get_uinput("keyboard").write_history
self.assertIn((EV_KEY, code_a, 1), history)
self.assertIn((EV_KEY, code_a, 0), history)
self.assertNotIn((EV_KEY, code_shift, 1), history)
self.assertNotIn((EV_KEY, code_shift, 0), history)
# after if_single takes an action, the listener should have been removed
self.assertSetEqual(context.listeners, set())
async def test_if_single_joystick_under_threshold(self):
"""Triggers then because the joystick events value is too low."""
# TODO: Move this somewhere more sensible
code_a = system_mapping.get("a")
trigger = evdev.ecodes.BTN_A
self.preset.add(
get_key_mapping(
Mapping.from_combination(
InputCombination(
InputConfig(
type=EV_KEY,
code=trigger,
origin_hash=fixtures.gamepad.get_device_hash(),
)
[
InputConfig(
type=EV_KEY,
code=trigger,
origin_hash=fixtures.gamepad.get_device_hash(),
)
]
),
"keyboard",
"if_single(k(a), k(KEY_LEFTSHIFT))",
)
)
self.preset.add(
get_key_mapping(
Mapping.from_combination(
InputCombination(
InputConfig(
type=EV_ABS,
code=ABS_Y,
analog_threshold=1,
origin_hash=fixtures.gamepad.get_device_hash(),
)
[
InputConfig(
type=EV_ABS,
code=ABS_Y,
analog_threshold=1,
origin_hash=fixtures.gamepad.get_device_hash(),
)
]
),
"keyboard",
"b",
@ -200,14 +228,14 @@ class TestEventReader(unittest.IsolatedAsyncioTestCase):
self.gamepad_source.push_events(
[
new_event(EV_KEY, trigger, 1), # start the macro
new_event(EV_ABS, ABS_Y, 1), # ignored because value too low
new_event(EV_KEY, trigger, 0), # stop, only way to trigger `then`
InputEvent.key(trigger, 1), # start the macro
InputEvent.abs(ABS_Y, 1), # ignored because value too low
InputEvent.key(trigger, 0), # stop, only way to trigger `then`
]
)
await asyncio.sleep(0.1)
self.assertEqual(len(context.listeners), 0)
history = [a.t for a in global_uinputs.get_uinput("keyboard").write_history]
history = global_uinputs.get_uinput("keyboard").write_history
# the key that triggered if_single should be injected after
# if_single had a chance to inject keys (if the macro is fast enough),

@ -17,8 +17,7 @@
#
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
from inputremapper.input_event import InputEvent
from tests.lib.cleanup import cleanup
import sys
@ -71,19 +70,19 @@ class TestGlobalUinputs(unittest.TestCase):
implicitly tests get_uinput and UInput.can_emit
"""
ev_1 = (EV_KEY, KEY_A, 1)
ev_2 = (EV_ABS, ABS_X, 10)
ev_1 = InputEvent.key(KEY_A, 1)
ev_2 = InputEvent.abs(ABS_X, 10)
keyboard = global_uinputs.get_uinput("keyboard")
global_uinputs.write(ev_1, "keyboard")
global_uinputs.write(ev_1.event_tuple, "keyboard")
self.assertEqual(keyboard.write_count, 1)
with self.assertRaises(EventNotHandled):
global_uinputs.write(ev_2, "keyboard")
global_uinputs.write(ev_2.event_tuple, "keyboard")
with self.assertRaises(UinputNotAvailable):
global_uinputs.write(ev_1, "foo")
global_uinputs.write(ev_1.event_tuple, "foo")
def test_creates_frontend_uinputs(self):
frontend_uinputs = GlobalUInputs()

@ -17,19 +17,21 @@
#
# 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 pydantic import ValidationError
from tests.lib.fixtures import new_event
from inputremapper.input_event import InputEvent
from tests.lib.global_uinputs import (
reset_global_uinputs_for_service,
reset_global_uinputs_for_gui,
)
from tests.lib.patches import uinputs
from tests.lib.cleanup import quick_cleanup
from tests.lib.constants import EVENT_READ_TIMEOUT
from tests.lib.fixtures import fixtures, get_combination_config
from tests.lib.fixtures import fixtures
from tests.lib.pipes import uinput_write_history_pipe
from tests.lib.pipes import read_write_history_pipe, push_events
from tests.lib.fixtures import (
keyboard_keys,
get_key_mapping,
)
from tests.lib.fixtures import keyboard_keys
import unittest
from unittest import mock
@ -61,6 +63,7 @@ from inputremapper.configs.system_mapping import (
DISABLE_NAME,
)
from inputremapper.configs.preset import Preset
from inputremapper.configs.mapping import Mapping
from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.injection.macros.parse import parse
from inputremapper.injection.context import Context
@ -117,8 +120,8 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
path = "/dev/input/event10"
preset = Preset()
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=10)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=10)]),
"keyboard",
"a",
)
@ -127,7 +130,7 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
self.injector = Injector(groups.find(key="Foo Device 2"), preset)
# this test needs to pass around all other constraints of
# _grab_device
self.injector.context = Context(preset)
self.injector.context = Context(preset, {}, {})
device = self.injector._grab_device(evdev.InputDevice(path))
gamepad = classify(device) == DeviceType.GAMEPAD
self.assertFalse(gamepad)
@ -139,8 +142,8 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
self.make_it_fail = 999
preset = Preset()
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=10)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=10)]),
"keyboard",
"a",
)
@ -148,7 +151,7 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
self.injector = Injector(groups.find(key="Foo Device 2"), preset)
path = "/dev/input/event10"
self.injector.context = Context(preset)
self.injector.context = Context(preset, {}, {})
device = self.injector._grab_device(evdev.InputDevice(path))
self.assertIsNone(device)
self.assertGreaterEqual(self.failed, 1)
@ -163,18 +166,27 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
self.assertEqual(self.injector.get_state(), InjectorState.NO_GRAB)
def test_grab_device_1(self):
device_hash = fixtures.gamepad.get_device_hash()
preset = Preset()
preset.add(
get_key_mapping(
Mapping.from_combination(
InputCombination(
InputConfig(type=EV_ABS, code=ABS_HAT0X, analog_threshold=1)
[
InputConfig(
type=EV_ABS,
code=ABS_HAT0X,
analog_threshold=1,
origin_hash=device_hash,
)
]
),
"keyboard",
"a",
),
)
self.initialize_injector(groups.find(name="gamepad"), preset)
self.injector.context = Context(preset)
self.injector.context = Context(preset, {}, {})
self.injector.group.paths = [
"/dev/input/event10",
"/dev/input/event30",
@ -183,41 +195,45 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
grabbed = self.injector._grab_devices()
self.assertEqual(len(grabbed), 1)
self.assertEqual(grabbed[0].path, "/dev/input/event30")
self.assertEqual(grabbed[device_hash].path, "/dev/input/event30")
def test_forward_gamepad_events(self):
device_hash = fixtures.gamepad.get_device_hash()
# forward abs joystick events
preset = Preset()
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=BTN_A)),
"keyboard",
"a",
Mapping.from_combination(
input_combination=InputCombination(
[InputConfig(type=EV_KEY, code=BTN_A, origin_hash=device_hash)]
),
target_uinput="keyboard",
output_symbol="a",
),
)
self.initialize_injector(groups.find(name="gamepad"), preset)
self.injector.context = Context(preset)
self.injector.context = Context(preset, {}, {})
path = "/dev/input/event30"
devices = self.injector._grab_devices()
self.assertEqual(len(devices), 1)
self.assertEqual(devices[0].path, path)
gamepad = classify(devices[0]) == DeviceType.GAMEPAD
self.assertEqual(devices[device_hash].path, path)
gamepad = classify(devices[device_hash]) == DeviceType.GAMEPAD
self.assertTrue(gamepad)
def test_skip_unused_device(self):
# skips a device because its capabilities are not used in the preset
preset = Preset()
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=10)),
Mapping.from_combination(
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, {}, {})
# grabs only one device even though the group has 4 devices
devices = self.injector._grab_devices()
@ -227,8 +243,8 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
def test_skip_unknown_device(self):
preset = Preset()
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=1234)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=1234)]),
"keyboard",
"a",
)
@ -236,12 +252,12 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
# skips a device because its capabilities are not used in the preset
self.initialize_injector(groups.find(key="Foo Device 2"), preset)
self.injector.context = Context(preset)
self.injector.context = Context(preset, {}, {})
devices = self.injector._grab_devices()
# skips the device alltogether, so no grab attempts fail
self.assertEqual(self.failed, 0)
self.assertEqual(devices, [])
self.assertEqual(devices, {})
def test_get_udev_name(self):
self.injector = Injector(groups.find(key="Foo Device 2"), Preset())
@ -260,25 +276,29 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
@mock.patch("evdev.InputDevice.ungrab")
def test_capabilities_and_uinput_presence(self, ungrab_patch):
preset = Preset()
m1 = get_key_mapping(
m1 = Mapping.from_combination(
InputCombination(
InputConfig(
type=EV_KEY,
code=KEY_A,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
[
InputConfig(
type=EV_KEY,
code=KEY_A,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
]
),
"keyboard",
"c",
)
m2 = get_key_mapping(
m2 = Mapping.from_combination(
InputCombination(
InputConfig(
type=EV_REL,
code=REL_HWHEEL,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
)
[
InputConfig(
type=EV_REL,
code=REL_HWHEEL,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
)
]
),
"keyboard",
"key(b)",
@ -292,11 +312,13 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
self.assertEqual(
self.injector.preset.get_mapping(
InputCombination(
InputConfig(
type=EV_KEY,
code=KEY_A,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
[
InputConfig(
type=EV_KEY,
code=KEY_A,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
]
)
),
m1,
@ -304,29 +326,21 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
self.assertEqual(
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(),
)
[
InputConfig(
type=EV_REL,
code=REL_HWHEEL,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
)
]
)
),
m2,
)
self.assertListEqual(
sorted(uinputs.keys()),
sorted(
[
# reading and preventing original events from reaching the
# display server
"input-remapper Foo Device foo forwarded",
"input-remapper Foo Device forwarded",
]
),
)
# reading and preventing original events from reaching the
# display server
forwarded_foo = uinputs.get("input-remapper Foo Device foo forwarded")
forwarded = uinputs.get("input-remapper Foo Device forwarded")
self.assertIsNotNone(forwarded_foo)
@ -353,9 +367,9 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
preset = Preset()
preset.add(
get_key_mapping(
Mapping.from_combination(
InputCombination(
(
[
InputConfig(
type=EV_KEY,
code=8,
@ -366,21 +380,23 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
code=9,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
),
)
]
),
"keyboard",
"k(KEY_Q).k(w)",
)
)
preset.add(
get_key_mapping(
Mapping.from_combination(
InputCombination(
InputConfig(
type=EV_ABS,
code=ABS_HAT0X,
analog_threshold=-1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
[
InputConfig(
type=EV_ABS,
code=ABS_HAT0X,
analog_threshold=-1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
]
),
"keyboard",
"a",
@ -390,13 +406,15 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
input_b = 10
with self.assertRaises(ValidationError):
preset.add(
get_key_mapping(
Mapping.from_combination(
InputCombination(
InputConfig(
type=EV_KEY,
code=input_b,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
[
InputConfig(
type=EV_KEY,
code=input_b,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
]
),
"keyboard",
"b",
@ -416,10 +434,10 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
fixtures.foo_device_2_keyboard,
[
# should execute a macro...
new_event(EV_KEY, 8, 1), # forwarded
new_event(EV_KEY, 9, 1), # triggers macro
new_event(EV_KEY, 8, 0), # releases macro
new_event(EV_KEY, 9, 0), # forwarded
InputEvent.key(8, 1), # forwarded
InputEvent.key(9, 1), # triggers macro
InputEvent.key(8, 0), # releases macro
InputEvent.key(9, 0), # forwarded
],
)
@ -428,8 +446,8 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
fixtures.foo_device_2_gamepad,
[
# gamepad stuff. trigger a combination
new_event(EV_ABS, ABS_HAT0X, -1),
new_event(EV_ABS, ABS_HAT0X, 0),
InputEvent.abs(ABS_HAT0X, -1),
InputEvent.abs(ABS_HAT0X, 0),
],
)
@ -438,9 +456,9 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
fixtures.foo_device_2_keyboard,
[
# just pass those over without modifying
new_event(EV_KEY, 10, 1),
new_event(EV_KEY, 10, 0),
new_event(3124, 3564, 6542),
InputEvent.key(10, 1),
InputEvent.key(10, 0),
InputEvent(0, 0, 3124, 3564, 6542),
],
force=True,
)
@ -511,18 +529,18 @@ class TestInjector(unittest.IsolatedAsyncioTestCase):
self.assertEqual(self.injector.get_state(), InjectorState.RUNNING)
def test_is_in_capabilities(self):
key = InputCombination(get_combination_config((1, 2, 1)))
key = InputCombination(InputCombination.from_tuples((1, 2, 1)))
capabilities = {1: [9, 2, 5]}
self.assertTrue(is_in_capabilities(key, capabilities))
key = InputCombination(get_combination_config((1, 2, 1), (1, 3, 1)))
key = InputCombination(InputCombination.from_tuples((1, 2, 1), (1, 3, 1)))
capabilities = {1: [9, 2, 5]}
# only one of the codes of the combination is required.
# The goal is to make combinations= across those sub-devices possible,
# that make up one hardware device
self.assertTrue(is_in_capabilities(key, capabilities))
key = InputCombination(get_combination_config((1, 2, 1), (1, 5, 1)))
key = InputCombination(InputCombination.from_tuples((1, 2, 1), (1, 5, 1)))
capabilities = {1: [9, 2, 5]}
self.assertTrue(is_in_capabilities(key, capabilities))
@ -571,15 +589,15 @@ class TestModifyCapabilities(unittest.TestCase):
preset = Preset()
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=80)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=80)]),
"keyboard",
"a",
)
)
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=81)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=81)]),
"keyboard",
DISABLE_NAME,
),
@ -589,8 +607,8 @@ class TestModifyCapabilities(unittest.TestCase):
macro = parse(macro_code, preset)
preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=60)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=60)]),
"keyboard",
macro_code,
),
@ -599,9 +617,9 @@ class TestModifyCapabilities(unittest.TestCase):
# going to be ignored, because EV_REL cannot be mapped, that's
# mouse movements.
preset.add(
get_key_mapping(
Mapping.from_combination(
InputCombination(
InputConfig(type=EV_REL, code=1234, analog_threshold=3)
[InputConfig(type=EV_REL, code=1234, analog_threshold=3)]
),
"keyboard",
"b",

@ -44,7 +44,6 @@ from evdev.ecodes import (
)
from inputremapper.configs.input_config import InputCombination, InputConfig
from tests.lib.fixtures import get_combination_config
class TestInputConfig(unittest.TestCase):
@ -322,48 +321,99 @@ class TestInputConfig(unittest.TestCase):
class TestInputCombination(unittest.TestCase):
def test_eq(self):
a = InputCombination(
[
InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"),
InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"),
]
)
b = InputCombination(
[
InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"),
InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"),
]
)
self.assertEqual(a, b)
def test_not_eq(self):
a = InputCombination(
[
InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="2345"),
InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="bcde"),
]
)
b = InputCombination(
[
InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"),
InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"),
]
)
self.assertNotEqual(a, b)
def test_can_be_used_as_dict_key(self):
dict_ = {
InputCombination(
[
InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"),
InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"),
]
): "foo"
}
key = InputCombination(
[
InputConfig(type=EV_REL, code=REL_X, value=1, origin_hash="1234"),
InputConfig(type=EV_KEY, code=KEY_A, value=1, origin_hash="abcd"),
]
)
self.assertEqual(dict_.get(key), "foo")
def test_get_permutations(self):
key_1 = InputCombination(get_combination_config((1, 3, 1)))
key_1 = InputCombination(InputCombination.from_tuples((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)))
key_2 = InputCombination(InputCombination.from_tuples((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))
InputCombination.from_tuples((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))),
InputCombination(
InputCombination.from_tuples((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))),
InputCombination(
InputCombination.from_tuples((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))
InputCombination.from_tuples((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))
InputCombination.from_tuples((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))
InputCombination.from_tuples((1, 3, 1), (1, KEY_LEFTCTRL, 1))
)
self.assertTrue(key_3.is_problematic())
key_4 = InputCombination(get_combination_config((1, 3, 1)))
key_4 = InputCombination(InputCombination.from_tuples((1, 3, 1)))
self.assertFalse(key_4.is_problematic())
key_5 = InputCombination(get_combination_config((1, 3, 1), (1, 5, 1)))
key_5 = InputCombination(InputCombination.from_tuples((1, 3, 1), (1, 5, 1)))
self.assertFalse(key_5.is_problematic())
def test_init(self):
@ -383,7 +433,7 @@ class TestInputCombination(unittest.TestCase):
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([InputConfig(type=1, code=2, analog_threshold=3)])
InputCombination(
(
{"type": 1, "code": 2},
@ -393,7 +443,7 @@ class TestInputCombination(unittest.TestCase):
)
def test_to_config(self):
c1 = InputCombination(InputConfig(type=1, code=2, analog_threshold=3))
c1 = InputCombination([InputConfig(type=1, code=2, analog_threshold=3)])
c2 = InputCombination(
(
InputConfig(type=1, code=2, analog_threshold=3),
@ -410,60 +460,74 @@ class TestInputCombination(unittest.TestCase):
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(),
InputCombination(
InputCombination.from_tuples((EV_KEY, KEY_A, 1))
).beautify(),
"a",
)
self.assertEqual(
InputCombination(get_combination_config((EV_KEY, KEY_A, 1))).beautify(),
InputCombination(
InputCombination.from_tuples((EV_KEY, KEY_A, 1))
).beautify(),
"a",
)
self.assertEqual(
InputCombination(
get_combination_config((EV_ABS, ABS_HAT0Y, -1))
InputCombination.from_tuples((EV_ABS, ABS_HAT0Y, -1))
).beautify(),
"DPad-Y Up",
)
self.assertEqual(
InputCombination(get_combination_config((EV_KEY, BTN_A, 1))).beautify(),
InputCombination(
InputCombination.from_tuples((EV_KEY, BTN_A, 1))
).beautify(),
"Button A",
)
self.assertEqual(
InputCombination(get_combination_config((EV_KEY, 1234, 1))).beautify(),
InputCombination(
InputCombination.from_tuples((EV_KEY, 1234, 1))
).beautify(),
"unknown (1, 1234)",
)
self.assertEqual(
InputCombination(
get_combination_config((EV_ABS, ABS_HAT0X, -1))
InputCombination.from_tuples((EV_ABS, ABS_HAT0X, -1))
).beautify(),
"DPad-X Left",
)
self.assertEqual(
InputCombination(
get_combination_config((EV_ABS, ABS_HAT0Y, -1))
InputCombination.from_tuples((EV_ABS, ABS_HAT0Y, -1))
).beautify(),
"DPad-Y Up",
)
self.assertEqual(
InputCombination(get_combination_config((EV_KEY, BTN_A, 1))).beautify(),
InputCombination(
InputCombination.from_tuples((EV_KEY, BTN_A, 1))
).beautify(),
"Button A",
)
self.assertEqual(
InputCombination(get_combination_config((EV_ABS, ABS_X, 1))).beautify(),
InputCombination(
InputCombination.from_tuples((EV_ABS, ABS_X, 1))
).beautify(),
"Joystick-X Right",
)
self.assertEqual(
InputCombination(get_combination_config((EV_ABS, ABS_RY, 1))).beautify(),
InputCombination(
InputCombination.from_tuples((EV_ABS, ABS_RY, 1))
).beautify(),
"Joystick-RY Down",
)
self.assertEqual(
InputCombination(
get_combination_config((EV_REL, REL_HWHEEL, 1))
InputCombination.from_tuples((EV_REL, REL_HWHEEL, 1))
).beautify(),
"Wheel Right",
)
self.assertEqual(
InputCombination(
get_combination_config((EV_REL, REL_WHEEL, -1))
InputCombination.from_tuples((EV_REL, REL_WHEEL, -1))
).beautify(),
"Wheel Down",
)
@ -471,7 +535,7 @@ class TestInputCombination(unittest.TestCase):
# combinations
self.assertEqual(
InputCombination(
get_combination_config(
InputCombination.from_tuples(
(EV_KEY, BTN_A, 1),
(EV_KEY, BTN_B, 1),
(EV_KEY, BTN_C, 1),

@ -18,14 +18,14 @@
# 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 tests.lib.tmp import tmp
import evdev
import os
import shutil
import unittest
import logging
from tests.lib.tmp import tmp
from inputremapper.logger import logger, update_verbosity, log_info, ColorfulFormatter
from inputremapper.configs.paths import remove
@ -53,19 +53,15 @@ class TestLogger(unittest.TestCase):
path = os.path.join(tmp, "logger-test")
remove(path)
def test_key_debug(self):
def test_write(self):
uinput = evdev.UInput(name="foo")
path = os.path.join(tmp, "logger-test")
add_filehandler(path)
logger.debug_key(((1, 2, 1),), "foo %s bar", 1234)
logger.debug_key(((1, 200, -1), (1, 5, 1)), "foo %s", (1, 2))
logger.write((evdev.ecodes.EV_KEY, evdev.ecodes.KEY_A, 1), uinput)
with open(path, "r") as f:
content = f.read().lower()
self.assertIn(
"foo 1234 bar ·················· ((1, 2, 1))",
content,
)
content = f.read()
self.assertIn(
"foo (1, 2) ···················· ((1, 200, -1), (1, 5, 1))",
f'Writing (1, 30, 1) to "foo"',
content,
)

@ -65,7 +65,7 @@ from inputremapper.injection.macros.parse import (
get_macro_argument_names,
get_num_parameters,
)
from tests.lib.fixtures import new_event
from inputremapper.input_event import InputEvent
from tests.lib.logger import logger
from tests.lib.cleanup import quick_cleanup
@ -82,7 +82,7 @@ class MacroTestBase(unittest.IsolatedAsyncioTestCase):
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.context = Context(Preset())
self.context = Context(Preset(), source_devices={}, forward_devices={})
def tearDown(self):
self.result = []
@ -722,17 +722,17 @@ class TestMacros(MacroTestBase):
macro.press_trigger()
asyncio.ensure_future(macro.run(self.handler))
await (asyncio.sleep(0.1))
await asyncio.sleep(0.1)
self.assertTrue(macro.is_holding())
self.assertEqual(len(self.result), 2)
await (asyncio.sleep(0.1))
await asyncio.sleep(0.1)
# doesn't do fancy stuff, is blocking until the release
self.assertEqual(len(self.result), 2)
"""up"""
macro.release_trigger()
await (asyncio.sleep(0.05))
await asyncio.sleep(0.05)
self.assertFalse(macro.is_holding())
self.assertEqual(len(self.result), 4)
@ -745,7 +745,7 @@ class TestMacros(MacroTestBase):
macro = parse("key(1).hold().key(3)", self.context, DummyMapping)
asyncio.ensure_future(macro.run(self.handler))
await (asyncio.sleep(0.1))
await asyncio.sleep(0.1)
self.assertFalse(macro.is_holding())
# since press_trigger was never called it just does the macro
# completely
@ -764,7 +764,7 @@ class TestMacros(MacroTestBase):
"""down"""
macro.press_trigger()
await (asyncio.sleep(0.05))
await asyncio.sleep(0.05)
self.assertTrue(macro.is_holding())
asyncio.ensure_future(macro.run(self.handler))
@ -777,7 +777,7 @@ class TestMacros(MacroTestBase):
"""up"""
macro.release_trigger()
await (asyncio.sleep(0.05))
await asyncio.sleep(0.05)
self.assertFalse(macro.is_holding())
self.assertEqual(len(self.result), 2)
@ -969,7 +969,7 @@ class TestMacros(MacroTestBase):
asyncio.ensure_future(macro_2.run(self.handler))
sleep = 0.1
await (asyncio.sleep(sleep))
await asyncio.sleep(sleep)
self.assertTrue(macro_1.is_holding())
self.assertTrue(macro_2.is_holding())
macro_1.release_trigger()
@ -1287,9 +1287,9 @@ class TestIfSingle(MacroTestBase):
x = system_mapping.get("x")
y = system_mapping.get("y")
await self.trigger_sequence(macro, new_event(EV_KEY, a, 1))
await self.trigger_sequence(macro, InputEvent.key(a, 1))
await asyncio.sleep(0.1)
await self.release_sequence(macro, new_event(EV_KEY, a, 0))
await self.release_sequence(macro, InputEvent.key(a, 0))
# the key that triggered the macro is released
await asyncio.sleep(0.1)
@ -1313,7 +1313,7 @@ class TestIfSingle(MacroTestBase):
y = system_mapping.get("y")
# pressing the macro key
await self.trigger_sequence(macro, new_event(EV_KEY, a, 1))
await self.trigger_sequence(macro, InputEvent.key(a, 1))
await asyncio.sleep(0.05)
# if_single only looks out for newly pressed keys,
@ -1321,13 +1321,13 @@ class TestIfSingle(MacroTestBase):
# pressed before if_single. This was decided because it is a lot
# less tricky and more fluently to use if you type fast
for listener in self.context.listeners:
asyncio.ensure_future(listener(new_event(EV_KEY, b, 0)))
asyncio.ensure_future(listener(InputEvent.key(b, 0)))
await asyncio.sleep(0.05)
self.assertListEqual(self.result, [])
# releasing the actual key triggers if_single
await asyncio.sleep(0.05)
await self.release_sequence(macro, new_event(EV_KEY, a, 0))
await self.release_sequence(macro, InputEvent.key(a, 0))
await asyncio.sleep(0.05)
self.assertListEqual(self.result, [(EV_KEY, x, 1), (EV_KEY, x, 0)])
self.assertFalse(macro.running)
@ -1351,11 +1351,11 @@ class TestIfSingle(MacroTestBase):
y = system_mapping.get("y")
# press the trigger key
await self.trigger_sequence(macro, new_event(EV_KEY, a, 1))
await self.trigger_sequence(macro, InputEvent.key(a, 1))
await asyncio.sleep(0.1)
# press another key
for listener in self.context.listeners:
asyncio.ensure_future(listener(new_event(EV_KEY, b, 1)))
asyncio.ensure_future(listener(InputEvent.key(b, 1)))
await asyncio.sleep(0.1)
self.assertListEqual(self.result, [(EV_KEY, y, 1), (EV_KEY, y, 0)])
@ -1371,11 +1371,11 @@ class TestIfSingle(MacroTestBase):
x = system_mapping.get("x")
# press trigger key
await self.trigger_sequence(macro, new_event(EV_KEY, a, 1))
await self.trigger_sequence(macro, InputEvent.key(a, 1))
await asyncio.sleep(0.1)
# press another key
for listener in self.context.listeners:
asyncio.ensure_future(listener(new_event(EV_KEY, b, 1)))
asyncio.ensure_future(listener(InputEvent.key(b, 1)))
await asyncio.sleep(0.1)
self.assertListEqual(self.result, [])
@ -1392,7 +1392,7 @@ class TestIfSingle(MacroTestBase):
a = system_mapping.get("a")
y = system_mapping.get("y")
await self.trigger_sequence(macro, new_event(EV_KEY, a, 1))
await self.trigger_sequence(macro, InputEvent.key(a, 1))
# no timeout yet
await asyncio.sleep(0.2)
@ -1413,12 +1413,12 @@ class TestIfSingle(MacroTestBase):
code_a = system_mapping.get("a")
trigger = 1
await self.trigger_sequence(macro, new_event(EV_KEY, trigger, 1))
await self.trigger_sequence(macro, InputEvent.key(trigger, 1))
await asyncio.sleep(0.1)
for listener in self.context.listeners:
asyncio.ensure_future(listener(new_event(EV_ABS, ABS_Y, 10)))
asyncio.ensure_future(listener(InputEvent.abs(ABS_Y, 10)))
await asyncio.sleep(0.1)
await self.release_sequence(macro, new_event(EV_KEY, trigger, 0))
await self.release_sequence(macro, InputEvent.key(trigger, 0))
await asyncio.sleep(0.1)
self.assertFalse(macro.running)
self.assertListEqual(self.result, [(EV_KEY, code_a, 1), (EV_KEY, code_a, 0)])

@ -47,7 +47,7 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
}
m = Mapping(**cfg)
self.assertEqual(
m.input_combination, InputCombination(InputConfig(type=1, code=2))
m.input_combination, InputCombination([InputConfig(type=1, code=2)])
)
self.assertEqual(m.target_uinput, "keyboard")
self.assertEqual(m.output_symbol, "a")
@ -65,7 +65,7 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
def test_is_wheel_output(self):
mapping = Mapping(
input_combination=InputCombination(InputConfig(type=EV_REL, code=REL_X)),
input_combination=InputCombination([InputConfig(type=EV_REL, code=REL_X)]),
target_uinput="keyboard",
output_type=EV_REL,
output_code=REL_Y,
@ -74,7 +74,7 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
self.assertFalse(mapping.is_high_res_wheel_output())
mapping = Mapping(
input_combination=InputCombination(InputConfig(type=EV_REL, code=REL_X)),
input_combination=InputCombination([InputConfig(type=EV_REL, code=REL_X)]),
target_uinput="keyboard",
output_type=EV_REL,
output_code=REL_WHEEL,
@ -83,7 +83,7 @@ class TestMapping(unittest.IsolatedAsyncioTestCase):
self.assertFalse(mapping.is_high_res_wheel_output())
mapping = Mapping(
input_combination=InputCombination(InputConfig(type=EV_REL, code=REL_X)),
input_combination=InputCombination([InputConfig(type=EV_REL, code=REL_X)]),
target_uinput="keyboard",
output_type=EV_REL,
output_code=REL_WHEEL_HI_RES,
@ -421,7 +421,7 @@ class TestUIMapping(unittest.IsolatedAsyncioTestCase):
def test_has_input_defined(self):
m = UIMapping()
self.assertFalse(m.has_input_defined())
m.input_combination = InputCombination(InputConfig(type=EV_KEY, code=1))
m.input_combination = InputCombination([InputConfig(type=EV_KEY, code=1)])
self.assertTrue(m.has_input_defined())

@ -15,7 +15,6 @@
from tests.lib.cleanup import quick_cleanup
from tests.lib.tmp import tmp
from tests.lib.fixtures import get_combination_config
import os
import unittest
@ -211,33 +210,33 @@ class TestMigrations(unittest.TestCase):
preset.load()
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=1))),
preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=1)])),
UIMapping(
input_combination=InputCombination(InputConfig(type=EV_KEY, code=1)),
input_combination=InputCombination([InputConfig(type=EV_KEY, code=1)]),
target_uinput="keyboard",
output_symbol="a",
),
)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=2))),
preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=2)])),
UIMapping(
input_combination=InputCombination(InputConfig(type=EV_KEY, code=2)),
input_combination=InputCombination([InputConfig(type=EV_KEY, code=2)]),
target_uinput="gamepad",
output_symbol="BTN_B",
),
)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=3))),
preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=3)])),
UIMapping(
input_combination=InputCombination(InputConfig(type=EV_KEY, code=3)),
input_combination=InputCombination([InputConfig(type=EV_KEY, code=3)]),
target_uinput="keyboard",
output_symbol="BTN_1\n# Broken mapping:\n# No target can handle all specified keycodes",
),
)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=4))),
preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=4)])),
UIMapping(
input_combination=InputCombination(InputConfig(type=EV_KEY, code=4)),
input_combination=InputCombination([InputConfig(type=EV_KEY, code=4)]),
target_uinput="keyboard",
output_symbol="d",
),
@ -245,12 +244,12 @@ class TestMigrations(unittest.TestCase):
self.assertEqual(
preset.get_mapping(
InputCombination(
InputConfig(type=EV_ABS, code=ABS_HAT0X, analog_threshold=-1)
[InputConfig(type=EV_ABS, code=ABS_HAT0X, analog_threshold=-1)]
)
),
UIMapping(
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_HAT0X, analog_threshold=-1)
[InputConfig(type=EV_ABS, code=ABS_HAT0X, analog_threshold=-1)]
),
target_uinput="keyboard",
output_symbol="b",
@ -259,14 +258,14 @@ class TestMigrations(unittest.TestCase):
self.assertEqual(
preset.get_mapping(
InputCombination(
get_combination_config(
InputCombination.from_tuples(
(EV_ABS, 1, 1), (EV_ABS, 2, -1), (EV_ABS, 3, 1)
)
),
),
UIMapping(
input_combination=InputCombination(
get_combination_config(
InputCombination.from_tuples(
(EV_ABS, 1, 1), (EV_ABS, 2, -1), (EV_ABS, 3, 1)
),
),
@ -275,17 +274,17 @@ class TestMigrations(unittest.TestCase):
),
)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=5))),
preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=5)])),
UIMapping(
input_combination=InputCombination(InputConfig(type=EV_KEY, code=5)),
input_combination=InputCombination([InputConfig(type=EV_KEY, code=5)]),
target_uinput="foo",
output_symbol="e",
),
)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=6))),
preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=6)])),
UIMapping(
input_combination=InputCombination(InputConfig(type=EV_KEY, code=6)),
input_combination=InputCombination([InputConfig(type=EV_KEY, code=6)]),
target_uinput="keyboard",
output_symbol="key(a, b)",
),
@ -316,41 +315,41 @@ class TestMigrations(unittest.TestCase):
preset.load()
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=1))),
preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=1)])),
UIMapping(
input_combination=InputCombination(InputConfig(type=EV_KEY, code=1)),
input_combination=InputCombination([InputConfig(type=EV_KEY, code=1)]),
target_uinput="keyboard",
output_symbol="otherwise + otherwise",
),
)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=2))),
preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=2)])),
UIMapping(
input_combination=InputCombination(InputConfig(type=EV_KEY, code=2)),
input_combination=InputCombination([InputConfig(type=EV_KEY, code=2)]),
target_uinput="keyboard",
output_symbol="bar($otherwise)",
),
)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=3))),
preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=3)])),
UIMapping(
input_combination=InputCombination(InputConfig(type=EV_KEY, code=3)),
input_combination=InputCombination([InputConfig(type=EV_KEY, code=3)]),
target_uinput="keyboard",
output_symbol="foo(else=qux)",
),
)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=4))),
preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=4)])),
UIMapping(
input_combination=InputCombination(InputConfig(type=EV_KEY, code=4)),
input_combination=InputCombination([InputConfig(type=EV_KEY, code=4)]),
target_uinput="foo",
output_symbol="qux(otherwise).bar(else=1)",
),
)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_KEY, code=5))),
preset.get_mapping(InputCombination([InputConfig(type=EV_KEY, code=5)])),
UIMapping(
input_combination=InputCombination(InputConfig(type=EV_KEY, code=5)),
input_combination=InputCombination([InputConfig(type=EV_KEY, code=5)]),
target_uinput="keyboard",
output_symbol="foo(otherwise1=2qux)",
),
@ -414,10 +413,12 @@ class TestMigrations(unittest.TestCase):
# 2 mappings for wheel
self.assertEqual(len(preset), 4)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_X))),
preset.get_mapping(
InputCombination([InputConfig(type=EV_ABS, code=ABS_X)])
),
UIMapping(
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_X)
[InputConfig(type=EV_ABS, code=ABS_X)]
),
target_uinput="mouse",
output_type=EV_REL,
@ -426,10 +427,12 @@ class TestMigrations(unittest.TestCase):
),
)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_Y))),
preset.get_mapping(
InputCombination([InputConfig(type=EV_ABS, code=ABS_Y)])
),
UIMapping(
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_Y)
[InputConfig(type=EV_ABS, code=ABS_Y)]
),
target_uinput="mouse",
output_type=EV_REL,
@ -438,10 +441,12 @@ class TestMigrations(unittest.TestCase):
),
)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RX))),
preset.get_mapping(
InputCombination([InputConfig(type=EV_ABS, code=ABS_RX)])
),
UIMapping(
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_RX)
[InputConfig(type=EV_ABS, code=ABS_RX)]
),
target_uinput="mouse",
output_type=EV_REL,
@ -450,10 +455,12 @@ class TestMigrations(unittest.TestCase):
),
)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RY))),
preset.get_mapping(
InputCombination([InputConfig(type=EV_ABS, code=ABS_RY)])
),
UIMapping(
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_RY)
[InputConfig(type=EV_ABS, code=ABS_RY)]
),
target_uinput="mouse",
output_type=EV_REL,
@ -490,10 +497,12 @@ class TestMigrations(unittest.TestCase):
# 2 mappings for wheel
self.assertEqual(len(preset), 4)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RX))),
preset.get_mapping(
InputCombination([InputConfig(type=EV_ABS, code=ABS_RX)])
),
UIMapping(
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_RX)
[InputConfig(type=EV_ABS, code=ABS_RX)]
),
target_uinput="mouse",
output_type=EV_REL,
@ -502,10 +511,12 @@ class TestMigrations(unittest.TestCase):
),
)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_RY))),
preset.get_mapping(
InputCombination([InputConfig(type=EV_ABS, code=ABS_RY)])
),
UIMapping(
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_RY)
[InputConfig(type=EV_ABS, code=ABS_RY)]
),
target_uinput="mouse",
output_type=EV_REL,
@ -514,10 +525,12 @@ class TestMigrations(unittest.TestCase):
),
)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_X))),
preset.get_mapping(
InputCombination([InputConfig(type=EV_ABS, code=ABS_X)])
),
UIMapping(
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_X)
[InputConfig(type=EV_ABS, code=ABS_X)]
),
target_uinput="mouse",
output_type=EV_REL,
@ -526,10 +539,12 @@ class TestMigrations(unittest.TestCase):
),
)
self.assertEqual(
preset.get_mapping(InputCombination(InputConfig(type=EV_ABS, code=ABS_Y))),
preset.get_mapping(
InputCombination([InputConfig(type=EV_ABS, code=ABS_Y)])
),
UIMapping(
input_combination=InputCombination(
InputConfig(type=EV_ABS, code=ABS_Y)
[InputConfig(type=EV_ABS, code=ABS_Y)]
),
target_uinput="mouse",
output_type=EV_REL,

@ -30,7 +30,6 @@ from inputremapper.configs.paths import get_preset_path, get_config_path, CONFIG
from inputremapper.configs.preset import Preset
from inputremapper.configs.input_config import InputCombination, InputConfig
from tests.lib.cleanup import quick_cleanup
from tests.lib.fixtures import get_key_mapping, get_combination_config
class TestPreset(unittest.TestCase):
@ -43,7 +42,7 @@ class TestPreset(unittest.TestCase):
def test_is_mapped_multiple_times(self):
combination = InputCombination(
get_combination_config((1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4))
InputCombination.from_tuples((1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4))
)
permutations = combination.get_permutations()
self.assertEqual(len(permutations), 6)
@ -64,7 +63,7 @@ class TestPreset(unittest.TestCase):
def test_has_unsaved_changes(self):
self.preset.path = get_preset_path("foo", "bar2")
self.preset.add(get_key_mapping())
self.preset.add(Mapping.from_combination())
self.assertTrue(self.preset.has_unsaved_changes())
self.preset.save()
self.assertFalse(self.preset.has_unsaved_changes())
@ -78,7 +77,7 @@ class TestPreset(unittest.TestCase):
self.preset.load()
self.assertEqual(
self.preset.get_mapping(InputCombination.empty_combination()),
get_key_mapping(),
Mapping.from_combination(),
)
self.assertFalse(self.preset.has_unsaved_changes())
@ -99,7 +98,7 @@ class TestPreset(unittest.TestCase):
self.preset.load()
self.preset.path = get_preset_path("bar", "foo")
self.preset.remove(get_key_mapping().input_combination)
self.preset.remove(Mapping.from_combination().input_combination)
# empty preset and empty file
self.assertFalse(self.preset.has_unsaved_changes())
@ -122,10 +121,14 @@ class TestPreset(unittest.TestCase):
two = InputConfig(type=EV_KEY, code=11)
three = InputConfig(type=EV_KEY, code=12)
self.preset.add(get_key_mapping(InputCombination(one), "keyboard", "1"))
self.preset.add(get_key_mapping(InputCombination(two), "keyboard", "2"))
self.preset.add(
get_key_mapping(InputCombination((two, three)), "keyboard", "3"),
Mapping.from_combination(InputCombination([one]), "keyboard", "1")
)
self.preset.add(
Mapping.from_combination(InputCombination([two]), "keyboard", "2")
)
self.preset.add(
Mapping.from_combination(InputCombination((two, three)), "keyboard", "3"),
)
self.preset.path = get_preset_path("Foo Device", "test")
self.preset.save()
@ -140,16 +143,16 @@ class TestPreset(unittest.TestCase):
self.assertEqual(len(loaded), 3)
self.assertRaises(TypeError, loaded.get_mapping, one)
self.assertEqual(
loaded.get_mapping(InputCombination(one)),
get_key_mapping(InputCombination(one), "keyboard", "1"),
loaded.get_mapping(InputCombination([one])),
Mapping.from_combination(InputCombination([one]), "keyboard", "1"),
)
self.assertEqual(
loaded.get_mapping(InputCombination(two)),
get_key_mapping(InputCombination(two), "keyboard", "2"),
loaded.get_mapping(InputCombination([two])),
Mapping.from_combination(InputCombination([two]), "keyboard", "2"),
)
self.assertEqual(
loaded.get_mapping(InputCombination((two, three))),
get_key_mapping(InputCombination((two, three)), "keyboard", "3"),
loaded.get_mapping(InputCombination([two, three])),
Mapping.from_combination(InputCombination([two, three]), "keyboard", "3"),
)
# load missing file
@ -157,13 +160,13 @@ class TestPreset(unittest.TestCase):
self.assertRaises(FileNotFoundError, preset.load)
def test_modify_mapping(self):
ev_1 = InputCombination(InputConfig(type=EV_KEY, code=1))
ev_3 = InputCombination(InputConfig(type=EV_KEY, code=2))
ev_1 = InputCombination([InputConfig(type=EV_KEY, code=1)])
ev_3 = InputCombination([InputConfig(type=EV_KEY, code=2)])
# only values between -99 and 99 are allowed as mapping for EV_ABS or EV_REL
ev_4 = InputCombination(InputConfig(type=EV_ABS, code=1, analog_threshold=99))
ev_4 = InputCombination([InputConfig(type=EV_ABS, code=1, analog_threshold=99)])
# add the first mapping
self.preset.add(get_key_mapping(ev_1, "keyboard", "a"))
self.preset.add(Mapping.from_combination(ev_1, "keyboard", "a"))
self.assertTrue(self.preset.has_unsaved_changes())
self.assertEqual(len(self.preset), 1)
@ -174,19 +177,19 @@ class TestPreset(unittest.TestCase):
self.assertIsNone(self.preset.get_mapping(ev_1))
self.assertEqual(
self.preset.get_mapping(ev_3),
get_key_mapping(ev_3, "keyboard", "b"),
Mapping.from_combination(ev_3, "keyboard", "b"),
)
self.assertEqual(len(self.preset), 1)
# add 4
self.preset.add(get_key_mapping(ev_4, "keyboard", "c"))
self.preset.add(Mapping.from_combination(ev_4, "keyboard", "c"))
self.assertEqual(
self.preset.get_mapping(ev_3),
get_key_mapping(ev_3, "keyboard", "b"),
Mapping.from_combination(ev_3, "keyboard", "b"),
)
self.assertEqual(
self.preset.get_mapping(ev_4),
get_key_mapping(ev_4, "keyboard", "c"),
Mapping.from_combination(ev_4, "keyboard", "c"),
)
self.assertEqual(len(self.preset), 2)
@ -195,7 +198,7 @@ class TestPreset(unittest.TestCase):
mapping.output_symbol = "d"
self.assertEqual(
self.preset.get_mapping(ev_4),
get_key_mapping(ev_4, "keyboard", "d"),
Mapping.from_combination(ev_4, "keyboard", "d"),
)
self.assertEqual(len(self.preset), 2)
@ -206,18 +209,18 @@ class TestPreset(unittest.TestCase):
self.assertEqual(
self.preset.get_mapping(ev_3),
get_key_mapping(ev_3, "keyboard", "b"),
Mapping.from_combination(ev_3, "keyboard", "b"),
)
self.assertEqual(
self.preset.get_mapping(ev_4),
get_key_mapping(ev_4, "keyboard", "d"),
Mapping.from_combination(ev_4, "keyboard", "d"),
)
self.assertEqual(len(self.preset), 2)
def test_avoids_redundant_saves(self):
with patch.object(self.preset, "has_unsaved_changes", lambda: False):
self.preset.path = get_preset_path("foo", "bar2")
self.preset.add(get_key_mapping())
self.preset.add(Mapping.from_combination())
self.preset.save()
with open(get_preset_path("foo", "bar2"), "r") as f:
@ -234,42 +237,42 @@ class TestPreset(unittest.TestCase):
combi_2 = InputCombination((ev_2, ev_1, ev_3))
combi_3 = InputCombination((ev_1, ev_2, ev_4))
self.preset.add(get_key_mapping(combi_1, "keyboard", "a"))
self.preset.add(Mapping.from_combination(combi_1, "keyboard", "a"))
self.assertEqual(
self.preset.get_mapping(combi_1),
get_key_mapping(combi_1, "keyboard", "a"),
Mapping.from_combination(combi_1, "keyboard", "a"),
)
self.assertEqual(
self.preset.get_mapping(combi_2),
get_key_mapping(combi_1, "keyboard", "a"),
Mapping.from_combination(combi_1, "keyboard", "a"),
)
# since combi_1 and combi_2 are equivalent, this raises a KeyError
self.assertRaises(
KeyError,
self.preset.add,
get_key_mapping(combi_2, "keyboard", "b"),
Mapping.from_combination(combi_2, "keyboard", "b"),
)
self.assertEqual(
self.preset.get_mapping(combi_1),
get_key_mapping(combi_1, "keyboard", "a"),
Mapping.from_combination(combi_1, "keyboard", "a"),
)
self.assertEqual(
self.preset.get_mapping(combi_2),
get_key_mapping(combi_1, "keyboard", "a"),
Mapping.from_combination(combi_1, "keyboard", "a"),
)
self.preset.add(get_key_mapping(combi_3, "keyboard", "c"))
self.preset.add(Mapping.from_combination(combi_3, "keyboard", "c"))
self.assertEqual(
self.preset.get_mapping(combi_1),
get_key_mapping(combi_1, "keyboard", "a"),
Mapping.from_combination(combi_1, "keyboard", "a"),
)
self.assertEqual(
self.preset.get_mapping(combi_2),
get_key_mapping(combi_1, "keyboard", "a"),
Mapping.from_combination(combi_1, "keyboard", "a"),
)
self.assertEqual(
self.preset.get_mapping(combi_3),
get_key_mapping(combi_3, "keyboard", "c"),
Mapping.from_combination(combi_3, "keyboard", "c"),
)
mapping = self.preset.get_mapping(combi_1)
@ -279,69 +282,69 @@ class TestPreset(unittest.TestCase):
self.assertEqual(
self.preset.get_mapping(combi_1),
get_key_mapping(combi_1, "keyboard", "c"),
Mapping.from_combination(combi_1, "keyboard", "c"),
)
self.assertEqual(
self.preset.get_mapping(combi_2),
get_key_mapping(combi_1, "keyboard", "c"),
Mapping.from_combination(combi_1, "keyboard", "c"),
)
self.assertEqual(
self.preset.get_mapping(combi_3),
get_key_mapping(combi_3, "keyboard", "c"),
Mapping.from_combination(combi_3, "keyboard", "c"),
)
def test_remove(self):
# does nothing
ev_1 = InputCombination(InputConfig(type=EV_KEY, code=40))
ev_2 = InputCombination(InputConfig(type=EV_KEY, code=30))
ev_3 = InputCombination(InputConfig(type=EV_KEY, code=20))
ev_4 = InputCombination(InputConfig(type=EV_KEY, code=10))
ev_1 = InputCombination([InputConfig(type=EV_KEY, code=40)])
ev_2 = InputCombination([InputConfig(type=EV_KEY, code=30)])
ev_3 = InputCombination([InputConfig(type=EV_KEY, code=20)])
ev_4 = InputCombination([InputConfig(type=EV_KEY, code=10)])
self.assertRaises(TypeError, self.preset.remove, (EV_KEY, 10, 1))
self.preset.remove(ev_1)
self.assertFalse(self.preset.has_unsaved_changes())
self.assertEqual(len(self.preset), 0)
self.preset.add(get_key_mapping(combination=ev_1))
self.preset.add(Mapping.from_combination(input_combination=ev_1))
self.assertEqual(len(self.preset), 1)
self.preset.remove(ev_1)
self.assertEqual(len(self.preset), 0)
self.preset.add(get_key_mapping(ev_4, "keyboard", "KEY_KP1"))
self.preset.add(Mapping.from_combination(ev_4, "keyboard", "KEY_KP1"))
self.assertTrue(self.preset.has_unsaved_changes())
self.preset.add(get_key_mapping(ev_3, "keyboard", "KEY_KP2"))
self.preset.add(get_key_mapping(ev_2, "keyboard", "KEY_KP3"))
self.preset.add(Mapping.from_combination(ev_3, "keyboard", "KEY_KP2"))
self.preset.add(Mapping.from_combination(ev_2, "keyboard", "KEY_KP3"))
self.assertEqual(len(self.preset), 3)
self.preset.remove(ev_3)
self.assertEqual(len(self.preset), 2)
self.assertEqual(
self.preset.get_mapping(ev_4),
get_key_mapping(ev_4, "keyboard", "KEY_KP1"),
Mapping.from_combination(ev_4, "keyboard", "KEY_KP1"),
)
self.assertIsNone(self.preset.get_mapping(ev_3))
self.assertEqual(
self.preset.get_mapping(ev_2),
get_key_mapping(ev_2, "keyboard", "KEY_KP3"),
Mapping.from_combination(ev_2, "keyboard", "KEY_KP3"),
)
def test_empty(self):
self.preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=10)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=10)]),
"keyboard",
"1",
),
)
self.preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=11)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=11)]),
"keyboard",
"2",
),
)
self.preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=12)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=12)]),
"keyboard",
"3",
),
@ -358,22 +361,22 @@ class TestPreset(unittest.TestCase):
def test_clear(self):
self.preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=10)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=10)]),
"keyboard",
"1",
),
)
self.preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=11)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=11)]),
"keyboard",
"2",
),
)
self.preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=12)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=12)]),
"keyboard",
"3",
),
@ -391,16 +394,16 @@ class TestPreset(unittest.TestCase):
def test_dangerously_mapped_btn_left(self):
# btn left is mapped
self.preset.add(
get_key_mapping(
InputCombination(InputConfig.btn_left()),
Mapping.from_combination(
InputCombination([InputConfig.btn_left()]),
"keyboard",
"1",
)
)
self.assertTrue(self.preset.dangerously_mapped_btn_left())
self.preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=41)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=41)]),
"keyboard",
"2",
)
@ -409,8 +412,8 @@ class TestPreset(unittest.TestCase):
# another mapping maps to btn_left
self.preset.add(
get_key_mapping(
InputCombination(InputConfig(type=EV_KEY, code=42)),
Mapping.from_combination(
InputCombination([InputConfig(type=EV_KEY, code=42)]),
"mouse",
"btn_left",
)
@ -418,7 +421,7 @@ class TestPreset(unittest.TestCase):
self.assertFalse(self.preset.dangerously_mapped_btn_left())
mapping = self.preset.get_mapping(
InputCombination(InputConfig(type=EV_KEY, code=42))
InputCombination([InputConfig(type=EV_KEY, code=42)])
)
mapping.output_symbol = "BTN_Left"
self.assertFalse(self.preset.dangerously_mapped_btn_left())
@ -428,7 +431,7 @@ class TestPreset(unittest.TestCase):
self.assertTrue(self.preset.dangerously_mapped_btn_left())
# btn_left is not mapped
self.preset.remove(InputCombination(InputConfig.btn_left()))
self.preset.remove(InputCombination([InputConfig.btn_left()]))
self.assertFalse(self.preset.dangerously_mapped_btn_left())
def test_save_load_with_invalid_mappings(self):
@ -443,7 +446,9 @@ class TestPreset(unittest.TestCase):
m.target_uinput = "keyboard"
self.assertTrue(ui_preset.is_valid())
m2 = UIMapping(input_combination=InputCombination(InputConfig(type=1, code=2)))
m2 = UIMapping(
input_combination=InputCombination([InputConfig(type=1, code=2)])
)
ui_preset.add(m2)
self.assertFalse(ui_preset.is_valid())
ui_preset.save()

@ -18,12 +18,14 @@
# 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 asyncio
import os
import json
import multiprocessing
import time
import unittest
from typing import List, Optional
from unittest import mock
from unittest.mock import patch, MagicMock
from evdev.ecodes import (
@ -33,7 +35,6 @@ from evdev.ecodes import (
KEY_COMMA,
BTN_TOOL_DOUBLETAP,
KEY_A,
EV_REL,
REL_WHEEL,
REL_X,
ABS_X,
@ -50,7 +51,8 @@ from inputremapper.gui.messages.message_broker import (
from inputremapper.gui.messages.message_data import CombinationRecorded
from inputremapper.gui.messages.message_types import MessageType
from inputremapper.gui.reader_client import ReaderClient
from inputremapper.gui.reader_service import ReaderService
from inputremapper.gui.reader_service import ReaderService, ContextDummy
from inputremapper.input_event import InputEvent
from tests.lib.fixtures import new_event
from tests.lib.cleanup import quick_cleanup
from tests.lib.constants import (
@ -60,7 +62,8 @@ from tests.lib.constants import (
MIN_ABS,
)
from tests.lib.pipes import push_event, push_events
from tests.lib.fixtures import fixtures, get_combination_config
from tests.lib.fixtures import fixtures
from tests.lib.stuff import spy
CODE_1 = 100
CODE_2 = 101
@ -86,7 +89,7 @@ def wait(func, timeout=1.0):
break
class TestReader(unittest.TestCase):
class TestReaderAsyncio(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.reader_service = None
self.groups = _Groups()
@ -100,8 +103,76 @@ class TestReader(unittest.TestCase):
except (BrokenPipeError, OSError):
pass
if self.reader_service is not None:
self.reader_service.join()
async def create_reader_service(self, groups: Optional[_Groups] = None):
# this will cause pending events to be copied over to the reader-service
# process
if not groups:
groups = self.groups
self.reader_service = ReaderService(groups)
asyncio.ensure_future(self.reader_service.run())
async def test_should_forward_to_dummy(self):
# It forwards to a ForwardDummy, because the gui process
# 1. can't inject and
# 2. is not even supposed to inject anything
# thanks to not using multiprocessing as opposed to the other tests, we can
# access this stuff
context = None
original_create_event_pipeline = ReaderService._create_event_pipeline
def remember_context(*args, **kwargs):
nonlocal context
context = original_create_event_pipeline(*args, **kwargs)
return context
with mock.patch(
"inputremapper.gui.reader_service.ReaderService._create_event_pipeline",
remember_context,
):
await self.create_reader_service()
listener = Listener()
self.message_broker.subscribe(MessageType.combination_recorded, listener)
self.reader_client.set_group(self.groups.find(key="Foo Device 2"))
self.reader_client.start_recorder()
await asyncio.sleep(0.1)
self.assertIsInstance(context, ContextDummy)
with spy(
context.forward_dummy,
"write",
) as write_spy:
events = [InputEvent.rel(REL_X, -1)]
push_events(fixtures.foo_device_2_mouse, events)
await asyncio.sleep(0.1)
self.reader_client._read()
self.assertEqual(0, len(listener.calls))
# we want `write` to be called on the forward_dummy, because we want
# those events to just disappear.
self.assertEqual(write_spy.call_count, len(events))
self.assertEqual([call[0] for call in write_spy.call_args_list], events)
class TestReaderMultiprocessing(unittest.TestCase):
def setUp(self):
self.reader_service_process = None
self.groups = _Groups()
self.message_broker = MessageBroker()
self.reader_client = ReaderClient(self.message_broker, self.groups)
def tearDown(self):
quick_cleanup()
try:
self.reader_client.terminate()
except (BrokenPipeError, OSError):
pass
if self.reader_service_process is not None:
self.reader_service_process.join()
def create_reader_service(self, groups: Optional[_Groups] = None):
# this will cause pending events to be copied over to the reader-service
@ -111,10 +182,15 @@ class TestReader(unittest.TestCase):
def start_reader_service():
reader_service = ReaderService(groups)
reader_service.run()
# this is a new process, so create a new event loop, or something
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(reader_service.run())
self.reader_service = multiprocessing.Process(target=start_reader_service)
self.reader_service.start()
self.reader_service_process = multiprocessing.Process(
target=start_reader_service
)
self.reader_service_process.start()
time.sleep(0.1)
def test_reading(self):
@ -126,13 +202,13 @@ class TestReader(unittest.TestCase):
self.reader_client.set_group(self.groups.find(key="Foo Device 2"))
self.reader_client.start_recorder()
push_events(fixtures.foo_device_2_gamepad, [new_event(EV_ABS, ABS_HAT0X, 1)])
push_events(fixtures.foo_device_2_gamepad, [InputEvent.abs(ABS_HAT0X, 1)])
# we need to sleep because we have two different fixtures,
# which will lead to race conditions
time.sleep(0.1)
# relative axis events should be released automagically after 0.3s
push_events(fixtures.foo_device_2_mouse, [new_event(EV_REL, REL_X, 5)])
push_events(fixtures.foo_device_2_mouse, [InputEvent.rel(REL_X, 5)])
time.sleep(0.1)
# read all pending events. Having a glib mainloop would be better,
# as it would call read automatically periodically
@ -141,17 +217,19 @@ class TestReader(unittest.TestCase):
[
CombinationRecorded(
InputCombination(
InputConfig(
type=3,
code=16,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
[
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,
@ -164,7 +242,7 @@ class TestReader(unittest.TestCase):
analog_threshold=1,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
),
)
]
)
),
],
@ -173,7 +251,7 @@ class TestReader(unittest.TestCase):
# release the hat switch should emit the recording finished event
# as both the hat and relative axis are released by now
push_events(fixtures.foo_device_2_gamepad, [new_event(EV_ABS, ABS_HAT0X, 0)])
push_events(fixtures.foo_device_2_gamepad, [InputEvent.abs(ABS_HAT0X, 0)])
time.sleep(0.3)
self.reader_client._read()
self.assertEqual([Signal(MessageType.recording_finished)], l2.calls)
@ -188,7 +266,7 @@ class TestReader(unittest.TestCase):
self.reader_client.set_group(self.groups.find(key="Foo Device 2"))
self.reader_client.start_recorder()
push_events(fixtures.foo_device_2_mouse, [new_event(EV_REL, REL_X, -5)])
push_events(fixtures.foo_device_2_mouse, [InputEvent.rel(REL_X, -5)])
time.sleep(0.1)
self.reader_client._read()
@ -196,12 +274,14 @@ class TestReader(unittest.TestCase):
[
CombinationRecorded(
InputCombination(
InputConfig(
type=2,
code=0,
analog_threshold=-1,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
)
[
InputConfig(
type=2,
code=0,
analog_threshold=-1,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
)
]
)
)
],
@ -220,7 +300,7 @@ class TestReader(unittest.TestCase):
self.reader_client.set_group(self.groups.find(key="Foo Device 2"))
self.reader_client.start_recorder()
push_events(fixtures.foo_device_2_mouse, [new_event(EV_REL, REL_X, -1)])
push_events(fixtures.foo_device_2_mouse, [InputEvent.rel(REL_X, -1)])
time.sleep(0.1)
self.reader_client._read()
self.assertEqual(0, len(l1.calls))
@ -234,7 +314,7 @@ class TestReader(unittest.TestCase):
push_events(
fixtures.foo_device_2_mouse,
[new_event(EV_REL, REL_WHEEL, -1), new_event(EV_REL, REL_HWHEEL, 1)],
[InputEvent.rel(REL_WHEEL, -1), InputEvent.rel(REL_HWHEEL, 1)],
)
time.sleep(0.1)
self.reader_client._read()
@ -243,17 +323,19 @@ class TestReader(unittest.TestCase):
[
CombinationRecorded(
InputCombination(
InputConfig(
type=2,
code=8,
analog_threshold=-1,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
)
[
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,
@ -266,7 +348,7 @@ class TestReader(unittest.TestCase):
analog_threshold=1,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
),
)
]
)
),
],
@ -280,11 +362,11 @@ class TestReader(unittest.TestCase):
self.reader_client.set_group(self.groups.find(key="Foo Device 2"))
self.reader_client.start_recorder()
push_events(fixtures.foo_device_2_keyboard, [new_event(EV_KEY, KEY_A, 1)])
push_events(fixtures.foo_device_2_keyboard, [InputEvent.key(KEY_A, 1)])
time.sleep(0.1)
self.reader_client._read()
# the duplicate event should be ignored
push_events(fixtures.foo_device_2_keyboard, [new_event(EV_KEY, KEY_A, 1)])
push_events(fixtures.foo_device_2_keyboard, [InputEvent.key(KEY_A, 1)])
time.sleep(0.1)
self.reader_client._read()
@ -292,12 +374,14 @@ class TestReader(unittest.TestCase):
[
CombinationRecorded(
InputCombination(
InputConfig(
type=1,
code=30,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
[
InputConfig(
type=1,
code=30,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
]
)
)
],
@ -316,7 +400,7 @@ class TestReader(unittest.TestCase):
# over 30% should trigger
push_events(
fixtures.foo_device_2_gamepad,
[new_event(EV_ABS, ABS_X, int(MAX_ABS * 0.4))],
[InputEvent.abs(ABS_X, int(MAX_ABS * 0.4))],
)
time.sleep(0.1)
self.reader_client._read()
@ -324,12 +408,14 @@ class TestReader(unittest.TestCase):
[
CombinationRecorded(
InputCombination(
InputConfig(
type=3,
code=0,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
[
InputConfig(
type=3,
code=0,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
]
)
)
],
@ -340,7 +426,7 @@ class TestReader(unittest.TestCase):
# less the 30% should release
push_events(
fixtures.foo_device_2_gamepad,
[new_event(EV_ABS, ABS_X, int(MAX_ABS * 0.2))],
[InputEvent.abs(ABS_X, int(MAX_ABS * 0.2))],
)
time.sleep(0.1)
self.reader_client._read()
@ -348,12 +434,14 @@ class TestReader(unittest.TestCase):
[
CombinationRecorded(
InputCombination(
InputConfig(
type=3,
code=0,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
[
InputConfig(
type=3,
code=0,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
]
)
)
],
@ -368,19 +456,19 @@ class TestReader(unittest.TestCase):
self.reader_client.set_group(self.groups.find(key="Foo Device 2"))
self.reader_client.start_recorder()
push_event(fixtures.foo_device_2_keyboard, new_event(EV_KEY, KEY_A, 1))
push_event(fixtures.foo_device_2_keyboard, InputEvent.key(KEY_A, 1))
time.sleep(0.1)
push_event(
fixtures.foo_device_2_gamepad, new_event(EV_ABS, ABS_X, int(MAX_ABS * 0.4))
fixtures.foo_device_2_gamepad, InputEvent.abs(ABS_X, int(MAX_ABS * 0.4))
)
time.sleep(0.1)
push_event(fixtures.foo_device_2_keyboard, new_event(EV_KEY, KEY_COMMA, 1))
push_event(fixtures.foo_device_2_keyboard, InputEvent.key(KEY_COMMA, 1))
time.sleep(0.1)
push_events(
fixtures.foo_device_2_gamepad,
[
new_event(EV_ABS, ABS_X, int(MAX_ABS * 0.1)),
new_event(EV_ABS, ABS_X, int(MIN_ABS * 0.4)),
InputEvent.abs(ABS_X, int(MAX_ABS * 0.1)),
InputEvent.abs(ABS_X, int(MIN_ABS * 0.4)),
],
)
time.sleep(0.1)
@ -389,16 +477,18 @@ class TestReader(unittest.TestCase):
[
CombinationRecorded(
InputCombination(
InputConfig(
type=EV_KEY,
code=KEY_A,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
[
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,
@ -410,12 +500,12 @@ class TestReader(unittest.TestCase):
analog_threshold=1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
),
)
]
)
),
CombinationRecorded(
InputCombination(
(
[
InputConfig(
type=EV_KEY,
code=KEY_A,
@ -432,12 +522,12 @@ class TestReader(unittest.TestCase):
code=KEY_COMMA,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
),
)
]
)
),
CombinationRecorded(
InputCombination(
(
[
InputConfig(
type=EV_KEY,
code=KEY_A,
@ -454,7 +544,7 @@ class TestReader(unittest.TestCase):
code=KEY_COMMA,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
),
)
]
)
),
],
@ -468,7 +558,7 @@ class TestReader(unittest.TestCase):
push_events(
fixtures.foo_device_2_keyboard,
[
new_event(EV_KEY, 1, 1),
InputEvent.key(1, 1),
]
* 10,
)
@ -476,8 +566,8 @@ class TestReader(unittest.TestCase):
push_events(
fixtures.bar_device,
[
new_event(EV_KEY, 2, 1),
new_event(EV_KEY, 2, 0),
InputEvent.key(2, 1),
InputEvent.key(2, 0),
]
* 3,
)
@ -490,11 +580,13 @@ class TestReader(unittest.TestCase):
self.assertEqual(
l1.calls[0].combination,
InputCombination(
InputConfig(
type=EV_KEY,
code=1,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
[
InputConfig(
type=EV_KEY,
code=1,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
]
),
)
@ -507,17 +599,19 @@ class TestReader(unittest.TestCase):
self.assertEqual(len(l1.calls), 1)
self.reader_client.start_recorder()
push_events(fixtures.bar_device, [new_event(EV_KEY, 2, 1)])
push_events(fixtures.bar_device, [InputEvent.key(2, 1)])
time.sleep(0.1)
self.reader_client._read()
self.assertEqual(
l1.calls[1].combination,
InputCombination(
InputConfig(
type=EV_KEY,
code=2,
origin_hash=fixtures.bar_device.get_device_hash(),
)
[
InputConfig(
type=EV_KEY,
code=2,
origin_hash=fixtures.bar_device.get_device_hash(),
)
]
),
)
@ -564,7 +658,7 @@ class TestReader(unittest.TestCase):
self.assertEqual(
l1.calls[-1].combination,
InputCombination(
(
[
InputConfig(
type=EV_KEY,
code=CODE_1,
@ -581,7 +675,7 @@ class TestReader(unittest.TestCase):
analog_threshold=-1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
),
)
]
),
)
@ -592,9 +686,9 @@ class TestReader(unittest.TestCase):
push_events(
fixtures.foo_device_2_mouse,
[
new_event(EV_KEY, BTN_TOOL_DOUBLETAP, 1),
new_event(EV_KEY, BTN_LEFT, 1),
new_event(EV_KEY, BTN_TOOL_DOUBLETAP, 1),
InputEvent.key(BTN_TOOL_DOUBLETAP, 1),
InputEvent.key(BTN_LEFT, 1),
InputEvent.key(BTN_TOOL_DOUBLETAP, 1),
],
force=True,
)
@ -606,11 +700,13 @@ class TestReader(unittest.TestCase):
self.assertEqual(
l1.calls[-1].combination,
InputCombination(
InputConfig(
type=EV_KEY,
code=BTN_LEFT,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
)
[
InputConfig(
type=EV_KEY,
code=BTN_LEFT,
origin_hash=fixtures.foo_device_2_mouse.get_device_hash(),
)
]
),
)
@ -620,7 +716,7 @@ class TestReader(unittest.TestCase):
# this is not a combination, because (EV_KEY CODE_3, 2) is ignored
push_events(
fixtures.foo_device_2_gamepad,
[new_event(EV_ABS, ABS_HAT0X, 1), new_event(EV_KEY, CODE_3, 2)],
[InputEvent.abs(ABS_HAT0X, 1), InputEvent.key(CODE_3, 2)],
force=True,
)
self.create_reader_service()
@ -631,12 +727,14 @@ class TestReader(unittest.TestCase):
self.assertEqual(
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(),
)
[
InputConfig(
type=EV_ABS,
code=ABS_HAT0X,
analog_threshold=1,
origin_hash=fixtures.foo_device_2_gamepad.get_device_hash(),
)
]
),
)
@ -659,11 +757,13 @@ class TestReader(unittest.TestCase):
self.assertEqual(
l1.calls[-1].combination,
InputCombination(
InputConfig(
type=EV_KEY,
code=CODE_2,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
[
InputConfig(
type=EV_KEY,
code=CODE_2,
origin_hash=fixtures.foo_device_2_keyboard.get_device_hash(),
)
]
),
)
@ -674,9 +774,9 @@ class TestReader(unittest.TestCase):
push_events(
fixtures.foo_device_2_keyboard,
[
new_event(EV_KEY, CODE_1, 1),
new_event(EV_KEY, CODE_2, 1),
new_event(EV_KEY, CODE_3, 1),
InputEvent.key(CODE_1, 1),
InputEvent.key(CODE_2, 1),
InputEvent.key(CODE_3, 1),
],
)
self.create_reader_service()
@ -696,9 +796,9 @@ class TestReader(unittest.TestCase):
push_events(
fixtures.input_remapper_bar_device,
[
new_event(EV_KEY, CODE_1, 1),
new_event(EV_KEY, CODE_2, 1),
new_event(EV_KEY, CODE_3, 1),
InputEvent.key(CODE_1, 1),
InputEvent.key(CODE_2, 1),
InputEvent.key(CODE_3, 1),
],
)
self.create_reader_service()
@ -712,7 +812,7 @@ class TestReader(unittest.TestCase):
self.create_reader_service()
self.reader_client.set_group(self.groups.find(key="Foo Device 2"))
push_events(fixtures.foo_device_2_keyboard, [new_event(EV_KEY, CODE_3, 1)])
push_events(fixtures.foo_device_2_keyboard, [InputEvent.key(CODE_3, 1)])
time.sleep(START_READING_DELAY + EVENT_READ_TIMEOUT)
self.assertTrue(self.reader_client._results_pipe.poll())
@ -721,7 +821,7 @@ class TestReader(unittest.TestCase):
self.assertFalse(self.reader_client._results_pipe.poll())
# no new events arrive after terminating
push_events(fixtures.foo_device_2_keyboard, [new_event(EV_KEY, CODE_3, 1)])
push_events(fixtures.foo_device_2_keyboard, [InputEvent.key(CODE_3, 1)])
time.sleep(EVENT_READ_TIMEOUT * 3)
self.assertFalse(self.reader_client._results_pipe.poll())
@ -862,48 +962,48 @@ class TestReader(unittest.TestCase):
# that exposes user-input forever
with patch.object(ReaderService, "_maximum_lifetime", 1):
self.create_reader_service()
self.assertTrue(self.reader_service.is_alive())
self.assertTrue(self.reader_service_process.is_alive())
time.sleep(0.5)
self.assertTrue(self.reader_service.is_alive())
self.assertTrue(self.reader_service_process.is_alive())
time.sleep(1)
self.assertFalse(self.reader_service.is_alive())
self.assertFalse(self.reader_service_process.is_alive())
def test_reader_service_waits_for_client_to_finish(self):
# if the client is currently reading, it waits a bit longer until the
# client finishes reading
with patch.object(ReaderService, "_maximum_lifetime", 1):
self.create_reader_service()
self.assertTrue(self.reader_service.is_alive())
self.assertTrue(self.reader_service_process.is_alive())
self.reader_client.set_group(self.groups.find(key="Foo Device 2"))
self.reader_client.start_recorder()
time.sleep(2)
# still alive, without start_recorder it should have already exited
self.assertTrue(self.reader_service.is_alive())
self.assertTrue(self.reader_service_process.is_alive())
self.reader_client.stop_recorder()
time.sleep(1)
self.assertFalse(self.reader_service.is_alive())
self.assertFalse(self.reader_service_process.is_alive())
def test_reader_service_wont_wait_forever(self):
# if the client is reading forever, stop it after another timeout
with patch.object(ReaderService, "_maximum_lifetime", 1):
with patch.object(ReaderService, "_timeout_tolerance", 1):
self.create_reader_service()
self.assertTrue(self.reader_service.is_alive())
self.assertTrue(self.reader_service_process.is_alive())
self.reader_client.set_group(self.groups.find(key="Foo Device 2"))
self.reader_client.start_recorder()
time.sleep(1.5)
# still alive, without start_recorder it should have already exited
self.assertTrue(self.reader_service.is_alive())
self.assertTrue(self.reader_service_process.is_alive())
time.sleep(1)
# now it stopped, even though the reader is still reading
self.assertFalse(self.reader_service.is_alive())
self.assertFalse(self.reader_service_process.is_alive())
if __name__ == "__main__":

@ -17,10 +17,8 @@
#
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
from inputremapper.utils import get_device_hash
import asyncio
from inputremapper.gui.messages.message_broker import MessageBroker
from tests.lib.fixtures import new_event
from tests.lib.cleanup import cleanup, quick_cleanup
from tests.lib.constants import EVENT_READ_TIMEOUT, START_READING_DELAY
from tests.lib.logger import logger
@ -39,6 +37,9 @@ from evdev.ecodes import EV_ABS, EV_KEY
from inputremapper.groups import groups, _Groups
from inputremapper.gui.reader_client import ReaderClient
from inputremapper.gui.reader_service import ReaderService
from inputremapper.input_event import InputEvent
from inputremapper.utils import get_device_hash
from inputremapper.gui.messages.message_broker import MessageBroker
class TestTest(unittest.TestCase):
@ -95,7 +96,8 @@ class TestTest(unittest.TestCase):
# there is no point in using the global groups object
# because the reader-service runs in a different process
reader_service = ReaderService(_Groups())
reader_service.run()
loop = asyncio.new_event_loop()
loop.run_until_complete(reader_service.run())
self.reader_service = multiprocessing.Process(target=start_reader_service)
self.reader_service.start()
@ -113,7 +115,7 @@ class TestTest(unittest.TestCase):
reader_client.start_recorder()
time.sleep(START_READING_DELAY)
event = new_event(EV_KEY, 102, 1)
event = InputEvent.key(102, 1)
push_events(fixtures.foo_device_2_keyboard, [event])
wait_for_results()
self.assertTrue(reader_client._results_pipe.poll())
@ -123,7 +125,7 @@ class TestTest(unittest.TestCase):
# can push more events to the reader-service that is inside a separate
# process, which end up being sent to the reader
event = new_event(EV_KEY, 102, 0)
event = InputEvent.key(102, 0)
logger.info("push_events")
push_events(fixtures.foo_device_2_keyboard, [event])
wait_for_results()

@ -0,0 +1,41 @@
#!/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_ABS, ABS_X, BTN_WEST, BTN_Y, EV_KEY, KEY_A
from inputremapper.utils import get_evdev_constant_name
class TestUtil(unittest.TestCase):
def test_get_evdev_constant_name(self):
# BTN_WEST and BTN_Y both are code 308. I don't care which one is chosen
# in the return value, but it should return one of them without crashing.
self.assertEqual(get_evdev_constant_name(EV_KEY, BTN_Y), "BTN_WEST")
self.assertEqual(get_evdev_constant_name(EV_KEY, BTN_WEST), "BTN_WEST")
self.assertEqual(get_evdev_constant_name(123, KEY_A), "unknown")
self.assertEqual(get_evdev_constant_name(EV_KEY, 9999), "unknown")
self.assertEqual(get_evdev_constant_name(EV_KEY, KEY_A), "KEY_A")
self.assertEqual(get_evdev_constant_name(EV_ABS, ABS_X), "ABS_X")
Loading…
Cancel
Save