Skip to content

Commit

Permalink
Fix CombinationHandler releasing (sezanzeb#578)
Browse files Browse the repository at this point in the history
Co-authored-by: Jonas Bosse <jonas.bosse@posteo.de>
  • Loading branch information
sezanzeb and jonasBoss authored Feb 19, 2023
1 parent 743ebd1 commit 1986d7a
Show file tree
Hide file tree
Showing 66 changed files with 2,403 additions and 1,631 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ jobs:
pip install black
- name: Analysing the code with black --check --diff
run: |
black --version
black --check --diff ./inputremapper ./tests
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions bin/input-remapper-control
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ def internals(options):

# daemonize
cmd = f'{cmd} &'
logger.debug(f'Running `{cmd}`')
os.system(cmd)


Expand Down
7 changes: 3 additions & 4 deletions bin/input-remapper-reader-service
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@


"""Starts the root reader-service."""


import asyncio
import os
import sys
import atexit
Expand Down Expand Up @@ -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())
116 changes: 96 additions & 20 deletions inputremapper/configs/input_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,7 +44,9 @@
ecodes.KEY_RIGHTALT,
]

DeviceHash = constr(to_lower=True)
DeviceHash: TypeAlias = str

EMPTY_TYPE = 99


class InputConfig(BaseModel):
Expand All @@ -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
Expand All @@ -68,6 +85,10 @@ def input_match_hash(self) -> Hashable:
"""
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"""
Expand Down Expand Up @@ -122,7 +143,7 @@ def _get_name(self) -> Optional[str]:
# 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]

Expand Down Expand Up @@ -223,9 +244,10 @@ def __hash__(self):
@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):
Expand All @@ -235,35 +257,62 @@ def _remove_analog_threshold_for_key_input(cls, values):
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 = }")
Expand All @@ -273,10 +322,11 @@ def __new__(cls, configs: InputCombinationInit) -> InputCombination:
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):
Expand All @@ -291,6 +341,7 @@ def validate(cls, init_arg) -> InputCombination:
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
Expand All @@ -299,7 +350,32 @@ def empty_combination(cls) -> InputCombination:
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?"""
Expand Down
19 changes: 17 additions & 2 deletions inputremapper/configs/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
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
Expand Down Expand Up @@ -260,9 +261,9 @@ def get_output_type_code(self) -> Optional[Tuple[int, int]]:
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."""
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 1986d7a

Please sign in to comment.