1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
|
# -*- 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 typing import Dict, Union, Tuple, Optional, List, Type
import evdev
import inputremapper.exceptions
import inputremapper.utils
from inputremapper.logging.logger import logger
MIN_ABS = -(2**15) # -32768
MAX_ABS = 2**15 # 32768
DEV_NAME = "input-remapper"
DEFAULT_UINPUTS = {
# for event codes see linux/input-event-codes.h
"keyboard": {
evdev.ecodes.EV_KEY: list(evdev.ecodes.KEY.keys() & evdev.ecodes.keys.keys())
},
"gamepad": {
evdev.ecodes.EV_KEY: [*range(0x130, 0x13F)], # BTN_SOUTH - BTN_THUMBR
evdev.ecodes.EV_ABS: [
*(
(i, evdev.AbsInfo(0, MIN_ABS, MAX_ABS, 0, 0, 0))
for i in range(0x00, 0x06)
),
*((i, evdev.AbsInfo(0, -1, 1, 0, 0, 0)) for i in range(0x10, 0x12)),
], # 6-axis and 1 hat switch
},
"mouse": {
evdev.ecodes.EV_KEY: [*range(0x110, 0x118)], # BTN_LEFT - BTN_TASK
evdev.ecodes.EV_REL: [*range(0x00, 0x0D)], # all REL axis
},
}
DEFAULT_UINPUTS["keyboard + mouse"] = {
evdev.ecodes.EV_KEY: [
*DEFAULT_UINPUTS["keyboard"][evdev.ecodes.EV_KEY],
*DEFAULT_UINPUTS["mouse"][evdev.ecodes.EV_KEY],
],
evdev.ecodes.EV_REL: [
*DEFAULT_UINPUTS["mouse"][evdev.ecodes.EV_REL],
],
}
class UInput(evdev.UInput):
_capabilities_cache: Optional[Dict] = None
def __init__(self, *args, **kwargs):
name = kwargs["name"]
logger.debug('creating UInput device: "%s"', name)
super().__init__(*args, **kwargs)
def can_emit(self, event: Tuple[int, int, int]):
"""Check if an event can be emitted by the UIinput.
Wrong events might be injected if the group mappings are wrong,
"""
# this will never change, so we cache it since evdev runs an expensive loop to
# gather the capabilities. (can_emit is called regularly)
if self._capabilities_cache is None:
self._capabilities_cache = self.capabilities(absinfo=False)
return event[1] in self._capabilities_cache.get(event[0], [])
class FrontendUInput:
"""Uinput which can not actually send events, for use in the frontend."""
def __init__(self, *_, events=None, name="py-evdev-uinput", **__):
# see https://python-evdev.readthedocs.io/en/latest/apidoc.html#module-evdev.uinput # noqa pylint: disable=line-too-long
self.events = events
self.name = name
logger.debug('creating fake UInput device: "%s"', self.name)
def capabilities(self):
return self.events
class GlobalUInputs:
"""Manages all UInputs that are shared between all injection processes."""
def __init__(
self,
uinput_factory: Union[Type[UInput], Type[FrontendUInput]],
):
self.devices: Dict[str, Union[UInput, FrontendUInput]] = {}
self._uinput_factory = uinput_factory
def __iter__(self):
return iter(uinput for _, uinput in self.devices.items())
@staticmethod
def can_default_uinput_emit(target: str, type_: int, code: int) -> bool:
"""Check if the uinput with the target name is capable of the event."""
capabilities = DEFAULT_UINPUTS.get(target, {}).get(type_)
return capabilities is not None and code in capabilities
@staticmethod
def find_fitting_default_uinputs(type_: int, code: int) -> List[str]:
"""Find the names of default uinputs that are able to emit this event."""
return [
uinput
for uinput in DEFAULT_UINPUTS
if code in DEFAULT_UINPUTS[uinput].get(type_, [])
]
def reset(self):
self.devices = {}
self.prepare_all()
def prepare_all(self):
"""Generate UInputs."""
for name, events in DEFAULT_UINPUTS.items():
if name in self.devices.keys():
continue
self.devices[name] = self._uinput_factory(
name=f"{DEV_NAME} {name}",
phys=DEV_NAME,
events=events,
)
def prepare_single(self, name: str):
"""Generate a single uinput.
This has to be done in the main process before injections that use it start.
"""
if name not in DEFAULT_UINPUTS:
raise KeyError("Could not find a matching uinput to generate.")
if name in self.devices:
logger.debug('Target "%s" already exists', name)
return
self.devices[name] = self._uinput_factory(
name=f"{DEV_NAME} {name}",
phys=DEV_NAME,
events=DEFAULT_UINPUTS[name],
)
def write(self, event: Tuple[int, int, int], target_uinput):
"""Write event to target uinput."""
uinput = self.get_uinput(target_uinput)
if not uinput:
raise inputremapper.exceptions.UinputNotAvailable(target_uinput)
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) -> Optional[evdev.UInput]:
"""UInput with name
Or None if there is no uinput with this name.
Parameters
----------
name
uniqe name of the uinput device
"""
if name not in self.devices:
logger.error(
f'UInput "{name}" is unknown. '
+ f"Available: {list(self.devices.keys())}"
)
return None
return self.devices.get(name)
|