Files
InputRemapper/inputremapper/input_event.py
Blomios 0ab77d2a64 chore(repo): baseline du fork input-remapper (upstream v2.2.1)
Le dépôt ne contenait que README.md ; ajout de l'intégralité du code
fonctionnel du fork comme socle stable de la branche de release.
L'état runtime d'orchestration (.ideai/) est exclu du suivi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 00:47:12 +02:00

265 lines
8.6 KiB
Python

# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2025 sezanzeb <b8x45ygc9@mozmail.com>
#
# This file is part of input-remapper.
#
# input-remapper is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# input-remapper is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
import 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
)