#!/usr/bin/python3 # -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings # Copyright (C) 2022 sezanzeb # # 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 . from __future__ import annotations import enum from dataclasses import dataclass from typing import Tuple, Union, Sequence, Callable, Optional import evdev from evdev import ecodes from inputremapper.configs.system_mapping import system_mapping from inputremapper.exceptions import InputEventCreationError from inputremapper.gui.messages.message_broker import MessageType from inputremapper.logger import logger InputEventValidationType = Union[ str, Tuple[int, int, int], evdev.InputEvent, ] # if "Use as analog" is set in the advanced mapping editor, the value will be set to 0 USE_AS_ANALOG_VALUE = 0 class EventActions(enum.Enum): """Additional information an InputEvent can send through the event pipeline""" as_key = enum.auto() # treat this event as a key event recenter = enum.auto() # recenter the axis when receiving this none = enum.auto() # used in combination with as_key, for originally abs or rel events positive_trigger = enum.auto() # original event was positive direction negative_trigger = enum.auto() # original event was negative direction # Todo: add slots=True as soon as python 3.10 is in common distros @dataclass(frozen=True) class InputEvent: """The evnet used by inputremapper as a drop in replacement for evdev.InputEvent """ message_type = MessageType.selected_event sec: int usec: int type: int code: int value: int actions: Tuple[EventActions, ...] = () def __hash__(self): return hash((self.type, self.code, self.value)) def __eq__(self, other): if isinstance(other, InputEvent) or isinstance(other, evdev.InputEvent): return self.event_tuple == (other.type, other.code, other.value) if isinstance(other, tuple): return self.event_tuple == other return False @classmethod def __get_validators__(cls): """Used by pydantic and EventCombination to create InputEvent objects.""" yield cls.validate @classmethod def validate(cls, init_arg: InputEventValidationType) -> InputEvent: """Try all the different methods, and raise an error if none succeed.""" if isinstance(init_arg, InputEvent): return init_arg event = None validators: Sequence[Callable[..., InputEvent]] = ( cls.from_event, cls.from_string, cls.from_tuple, ) for validator in validators: try: event = validator(init_arg) break except InputEventCreationError: pass if event: return event raise ValueError(f"failed to create InputEvent with {init_arg = }") @classmethod def from_event(cls, event: evdev.InputEvent) -> InputEvent: """Create a InputEvent from another InputEvent or evdev.InputEvent.""" try: return cls(event.sec, event.usec, event.type, event.code, event.value) except AttributeError: raise InputEventCreationError( f"failed to create InputEvent from {event = }" ) @classmethod def from_string(cls, string: str) -> InputEvent: """Create a InputEvent from a string like 'type, code, value'.""" try: t, c, v = string.split(",") return cls(0, 0, int(t), int(c), int(v)) except (ValueError, AttributeError): raise InputEventCreationError( f"failed to create InputEvent from {string = !r}" ) @classmethod def from_tuple(cls, event_tuple: Tuple[int, int, int]) -> InputEvent: """Create a InputEvent from a (type, code, value) tuple.""" try: if len(event_tuple) != 3: raise InputEventCreationError( f"failed to create InputEvent {event_tuple = }" f" must have length 3" ) return cls( 0, 0, int(event_tuple[0]), int(event_tuple[1]), int(event_tuple[2]), ) except ValueError: raise InputEventCreationError( f"failed to create InputEvent from {event_tuple = }" ) except TypeError: raise InputEventCreationError( f"failed to create InputEvent from {type(event_tuple) = }" ) @classmethod def btn_left(cls): return cls(0, 0, evdev.ecodes.EV_KEY, evdev.ecodes.BTN_LEFT, 1) @property def type_and_code(self) -> Tuple[int, int]: """Event type, code.""" return self.type, self.code @property def event_tuple(self) -> Tuple[int, int, int]: """Event type, code, value.""" return self.type, self.code, self.value @property def is_key_event(self) -> bool: """Whether this is interpreted as a key event.""" return self.type == evdev.ecodes.EV_KEY or EventActions.as_key in self.actions @property def is_wheel_event(self) -> bool: """Whether this is interpreted as a key event.""" return self.type == evdev.ecodes.EV_REL and self.code in [ ecodes.REL_WHEEL, ecodes.REL_HWHEEL, ] @property def is_wheel_hi_res_event(self) -> bool: """Whether this is interpreted as a key event.""" return self.type == evdev.ecodes.EV_REL and self.code in [ ecodes.REL_WHEEL_HI_RES, ecodes.REL_HWHEEL_HI_RES, ] def __str__(self): return f"InputEvent{self.event_tuple}" def description(self, exclude_threshold=False, exclude_direction=False) -> str: """get a human-readable description of the event""" return ( f"{self.get_name()} " f"{self.get_direction() if not exclude_direction else ''} " f"{self.get_threshold() if not exclude_threshold else ''}".strip() ) def timestamp(self): """Return the unix timestamp of when the event was seen.""" return self.sec + self.usec / 1000000 def modify( self, sec: int = None, usec: int = None, type: int = None, code: int = None, value: int = None, actions: Tuple[EventActions, ...] = None, ) -> InputEvent: """Return a new modified event.""" return InputEvent( sec if sec is not None else self.sec, usec if usec is not None else self.usec, type if type is not None else self.type, code if code is not None else self.code, value if value is not None else self.value, actions if actions is not None else self.actions, ) def json_key(self) -> str: return ",".join([str(self.type), str(self.code), str(self.value)]) def get_name(self) -> Optional[str]: """human-readable name""" if self.type not in ecodes.bytype: logger.warning("Unknown type for %s", self) return f"unknown {self.type, self.code}" if self.code not in ecodes.bytype[self.type]: logger.warning("Unknown code for %s", self) return f"unknown {self.type, self.code}" key_name = None # first try to find the name in xmodmap to not display wrong # names due to the keyboard layout if self.type == ecodes.EV_KEY: key_name = system_mapping.get_name(self.code) if key_name is None: # if no result, look in the linux combination constants. On a german # keyboard for example z and y are switched, which will therefore # cause the wrong letter to be displayed. key_name = ecodes.bytype[self.type][self.code] if isinstance(key_name, list): key_name = key_name[0] key_name = key_name.replace("ABS_Z", "Trigger Left") key_name = key_name.replace("ABS_RZ", "Trigger Right") key_name = key_name.replace("ABS_HAT0X", "DPad-X") key_name = key_name.replace("ABS_HAT0Y", "DPad-Y") key_name = key_name.replace("ABS_HAT1X", "DPad-2-X") key_name = key_name.replace("ABS_HAT1Y", "DPad-2-Y") key_name = key_name.replace("ABS_HAT2X", "DPad-3-X") key_name = key_name.replace("ABS_HAT2Y", "DPad-3-Y") key_name = key_name.replace("ABS_X", "Joystick-X") key_name = key_name.replace("ABS_Y", "Joystick-Y") key_name = key_name.replace("ABS_RX", "Joystick-RX") key_name = key_name.replace("ABS_RY", "Joystick-RY") key_name = key_name.replace("BTN_", "Button ") key_name = key_name.replace("KEY_", "") key_name = key_name.replace("REL_", "") key_name = key_name.replace("HWHEEL", "Wheel") key_name = key_name.replace("WHEEL", "Wheel") key_name = key_name.replace("_", " ") key_name = key_name.replace(" ", " ") return key_name def get_direction(self) -> str: if self.type == ecodes.EV_KEY: return "" try: event = self.modify(value=self.value // abs(self.value)) except ZeroDivisionError: return "" return { # D-Pad (ecodes.ABS_HAT0X, -1): "Left", (ecodes.ABS_HAT0X, 1): "Right", (ecodes.ABS_HAT0Y, -1): "Up", (ecodes.ABS_HAT0Y, 1): "Down", (ecodes.ABS_HAT1X, -1): "Left", (ecodes.ABS_HAT1X, 1): "Right", (ecodes.ABS_HAT1Y, -1): "Up", (ecodes.ABS_HAT1Y, 1): "Down", (ecodes.ABS_HAT2X, -1): "Left", (ecodes.ABS_HAT2X, 1): "Right", (ecodes.ABS_HAT2Y, -1): "Up", (ecodes.ABS_HAT2Y, 1): "Down", # joystick (ecodes.ABS_X, 1): "Right", (ecodes.ABS_X, -1): "Left", (ecodes.ABS_Y, 1): "Down", (ecodes.ABS_Y, -1): "Up", (ecodes.ABS_RX, 1): "Right", (ecodes.ABS_RX, -1): "Left", (ecodes.ABS_RY, 1): "Down", (ecodes.ABS_RY, -1): "Up", # wheel (ecodes.REL_WHEEL, -1): "Down", (ecodes.REL_WHEEL, 1): "Up", (ecodes.REL_HWHEEL, -1): "Left", (ecodes.REL_HWHEEL, 1): "Right", }.get((event.code, event.value)) or ("+" if event.value > 0 else "-") def get_threshold(self) -> str: if self.value == 0: return "" return { ecodes.EV_REL: f"{abs(self.value)}", ecodes.EV_ABS: f"{abs(self.value)}%", }.get(self.type) or ""