mirror of
https://github.com/sezanzeb/input-remapper
synced 2024-11-12 01:10:38 +00:00
223 lines
7.9 KiB
Python
223 lines
7.9 KiB
Python
#!/usr/bin/python3
|
|
# -*- coding: utf-8 -*-
|
|
# input-remapper - GUI for device specific keyboard mappings
|
|
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
|
|
#
|
|
# 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 itertools
|
|
|
|
from typing import Tuple, Iterable
|
|
|
|
import evdev
|
|
from evdev import ecodes
|
|
|
|
from inputremapper.logger import logger
|
|
from inputremapper.configs.system_mapping import system_mapping
|
|
from inputremapper.input_event import InputEvent
|
|
from inputremapper.exceptions import InputEventCreationError
|
|
|
|
# having shift in combinations modifies the configured output,
|
|
# ctrl might not work at all
|
|
DIFFICULT_COMBINATIONS = [
|
|
ecodes.KEY_LEFTSHIFT,
|
|
ecodes.KEY_RIGHTSHIFT,
|
|
ecodes.KEY_LEFTCTRL,
|
|
ecodes.KEY_RIGHTCTRL,
|
|
ecodes.KEY_LEFTALT,
|
|
ecodes.KEY_RIGHTALT,
|
|
]
|
|
|
|
|
|
class EventCombination(Tuple[InputEvent]):
|
|
"""one or multiple InputEvent objects for use as an unique identifier for mappings"""
|
|
|
|
# tuple is immutable, therefore we need to override __new__()
|
|
# https://jfine-python-classes.readthedocs.io/en/latest/subclass-tuple.html
|
|
def __new__(cls, *init_args) -> EventCombination:
|
|
events = []
|
|
for init_arg in init_args:
|
|
event = None
|
|
|
|
for constructor in InputEvent.__get_validators__():
|
|
try:
|
|
event = constructor(init_arg)
|
|
break
|
|
except InputEventCreationError:
|
|
pass
|
|
|
|
if event:
|
|
events.append(event)
|
|
else:
|
|
raise ValueError(f"failed to create InputEvent with {init_arg = }")
|
|
|
|
return super().__new__(cls, events)
|
|
|
|
def __str__(self):
|
|
# only used in tests and logging
|
|
return f"<EventCombination {', '.join([str(e.event_tuple) for e in self])}>"
|
|
|
|
@classmethod
|
|
def __get_validators__(cls):
|
|
"""used by pydantic to create EventCombination objects"""
|
|
yield cls.from_string
|
|
yield cls.from_events
|
|
|
|
@classmethod
|
|
def from_string(cls, init_string: str) -> EventCombination:
|
|
init_args = init_string.split("+")
|
|
return cls(*init_args)
|
|
|
|
@classmethod
|
|
def from_events(
|
|
cls, init_events: Iterable[InputEvent | evdev.InputEvent]
|
|
) -> EventCombination:
|
|
return cls(*init_events)
|
|
|
|
def contains_type_and_code(self, type, code) -> bool:
|
|
"""if a InputEvent contains the type and code"""
|
|
for event in self:
|
|
if event.type_and_code == (type, code):
|
|
return True
|
|
return False
|
|
|
|
def is_problematic(self):
|
|
"""Is this combination going to work properly on all systems?"""
|
|
if len(self) <= 1:
|
|
return False
|
|
|
|
for event in self:
|
|
if event.type != ecodes.EV_KEY:
|
|
continue
|
|
|
|
if event.code in DIFFICULT_COMBINATIONS:
|
|
return True
|
|
|
|
return False
|
|
|
|
def get_permutations(self):
|
|
"""Get a list of EventCombination objects representing all possible permutations.
|
|
|
|
combining a + b + c should have the same result as b + a + c.
|
|
Only the last combination remains the same in the returned result.
|
|
"""
|
|
if len(self) <= 2:
|
|
return [self]
|
|
|
|
permutations = []
|
|
for permutation in itertools.permutations(self[:-1]):
|
|
permutations.append(EventCombination(*permutation, self[-1]))
|
|
|
|
return permutations
|
|
|
|
def json_str(self) -> str:
|
|
return "+".join([event.json_str() for event in self])
|
|
|
|
def beautify(self) -> str:
|
|
"""Get a human readable string representation."""
|
|
result = []
|
|
|
|
for event in self:
|
|
|
|
if event.type not in ecodes.bytype:
|
|
logger.error("Unknown type for %s", event)
|
|
result.append(str(event.code))
|
|
continue
|
|
|
|
if event.code not in ecodes.bytype[event.type]:
|
|
logger.error("Unknown combination code for %s", event)
|
|
result.append(str(event.code))
|
|
continue
|
|
|
|
key_name = None
|
|
|
|
# first try to find the name in xmodmap to not display wrong
|
|
# names due to the keyboard layout
|
|
if event.type == ecodes.EV_KEY:
|
|
key_name = system_mapping.get_name(event.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[event.type][event.code]
|
|
if isinstance(key_name, list):
|
|
key_name = key_name[0]
|
|
|
|
if event.type != ecodes.EV_KEY:
|
|
direction = {
|
|
# 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))
|
|
if direction is not None:
|
|
key_name += f" {direction}"
|
|
|
|
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")
|
|
key_name = key_name.replace("ABS_HAT0Y", "DPad")
|
|
key_name = key_name.replace("ABS_HAT1X", "DPad 2")
|
|
key_name = key_name.replace("ABS_HAT1Y", "DPad 2")
|
|
key_name = key_name.replace("ABS_HAT2X", "DPad 3")
|
|
key_name = key_name.replace("ABS_HAT2Y", "DPad 3")
|
|
|
|
key_name = key_name.replace("ABS_X", "Joystick")
|
|
key_name = key_name.replace("ABS_Y", "Joystick")
|
|
key_name = key_name.replace("ABS_RX", "Joystick 2")
|
|
key_name = key_name.replace("ABS_RY", "Joystick 2")
|
|
|
|
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(" ", " ")
|
|
|
|
result.append(key_name)
|
|
|
|
return " + ".join(result)
|