# -*- coding: utf-8 -*- # input-remapper - GUI for device specific keyboard mappings # Copyright (C) 2025 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, Optional, Hashable, Literal import evdev from evdev import ecodes from inputremapper.utils import get_evdev_constant_name, DeviceHash 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() # Todo: add slots=True as soon as python 3.10 is in common distros @dataclass(frozen=True) class InputEvent: """Events that are generated during runtime. Is a drop-in replacement for evdev.InputEvent """ sec: int usec: int type: int code: int value: int # Our own custom attributes # (They need types for the dataclass to allow them in the constructor) pressed: bool | None = None # haven't figured out yet, depends on threshold config direction: int = 1 # -1 for joystick left, +1 for joystick right and buttons actions: Tuple[EventActions, ...] = () origin_hash: Optional[DeviceHash] = None def __eq__(self, other: InputEvent | evdev.InputEvent | Tuple[int, int, int]): # useful in tests if isinstance(other, InputEvent) or isinstance(other, evdev.InputEvent): return self.event_tuple == (other.type, other.code, other.value) if isinstance(other, tuple): return self.event_tuple == other raise TypeError(f"cannot compare {type(other)} with InputEvent") def is_pressed(self) -> bool: """Get if the event is representing a pressed button, joystick, trigger, a scrolled wheel, a moved mouse, etc. It might depend on stuff like the analog_threshold. """ if self.pressed is not None: return self.pressed # As long as we haven't checked it using the analog thresholds and such, # assume that anything != 0 means it is pressed.' if self.value == 0: return False return True @staticmethod 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 @property def input_match_hash(self) -> Hashable: """a Hashable object which is intended to match the InputEvent with a InputConfig. """ return self.type, self.code, self.origin_hash @classmethod def from_event( cls, event: evdev.InputEvent, origin_hash: Optional[DeviceHash] = None, ) -> InputEvent: """Create a InputEvent from another InputEvent or evdev.InputEvent.""" try: return cls( event.sec, event.usec, event.type, event.code, event.value, origin_hash=origin_hash, ) except AttributeError as exception: raise TypeError( f"Failed to create InputEvent from {event = }" ) from exception @classmethod def from_tuple( cls, event_tuple: Tuple[int, int, int], origin_hash: Optional[DeviceHash] = 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 = } must have length 3" ) return cls.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[DeviceHash] = None): """Create an abs event, like joystick movements.""" return cls.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 cls.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 cls.validate_event( cls( 0, 0, ecodes.EV_KEY, code, value, origin_hash=origin_hash, ) ) @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): 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.""" return self.sec + self.usec / 1000000 def modify( self, sec: Optional[int] = None, usec: Optional[int] = None, type_: Optional[int] = None, code: Optional[int] = None, value: Optional[int] = None, pressed: Optional[bool] = None, direction: Optional[int] = None, actions: Optional[Tuple[EventActions, ...]] = None, origin_hash: Optional[str] = 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, pressed if pressed is not None else self.is_pressed(), direction if direction is not None else self.direction, actions if actions is not None else self.actions, origin_hash=origin_hash if origin_hash is not None else self.origin_hash, # type: ignore )