File: mapping_handler.py

package info (click to toggle)
input-remapper 2.1.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 2,856 kB
  • sloc: python: 27,277; sh: 191; xml: 33; makefile: 3
file content (229 lines) | stat: -rw-r--r-- 6,660 bytes parent folder | download | duplicates (3)
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# -*- 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/>.
"""Provides protocols for mapping handlers

*** The architecture behind mapping handlers ***

Handling an InputEvent is done in 3 steps:
 1. Input Event Handling
    A MappingHandler that does Input event handling receives Input Events directly
    from the EventReader.
    To do so it must implement the InputEventHandler protocol.
    An InputEventHandler may handle multiple events (InputEvent.type_and_code)

 2. Event Transformation
    The event gets transformed as described by the mapping.
    e.g.: combining multiple events to a single one
        transforming EV_ABS to EV_REL
        macros
        ...
    Multiple transformations may get chained

 3. Event Injection
    The transformed event gets injected to a global_uinput

MappingHandlers can implement one or more of these steps.

Overview of implemented handlers and the steps they implement:

Step 1:
 - HierarchyHandler

Step 1 and 2:
 - CombinationHandler
 - AbsToBtnHandler
 - RelToBtnHandler

Step 1, 2 and 3:
 - AbsToRelHandler
 - NullHandler

Step 2 and 3:
 - KeyHandler
 - MacroHandler
"""
from __future__ import annotations

import enum
from typing import Dict, Protocol, Set, Optional, List

import evdev

from inputremapper.configs.input_config import InputCombination, InputConfig
from inputremapper.configs.mapping import Mapping
from inputremapper.exceptions import MappingParsingError
from inputremapper.injection.global_uinputs import GlobalUInputs
from inputremapper.input_event import InputEvent
from inputremapper.logging.logger import logger


class EventListener(Protocol):
    async def __call__(self, event: evdev.InputEvent) -> None: ...


class ContextProtocol(Protocol):
    """The parts from context needed for handlers."""

    listeners: Set[EventListener]

    def get_forward_uinput(self, origin_hash) -> evdev.UInput:
        pass


class NotifyCallback(Protocol):
    """Type signature of InputEventHandler.notify

    return True if the event was actually taken care of
    """

    def __call__(
        self,
        event: InputEvent,
        source: evdev.InputDevice,
        suppress: bool = False,
    ) -> bool: ...


class InputEventHandler(Protocol):
    """The protocol any handler, which can be part of an event pipeline, must follow."""

    def notify(
        self,
        event: InputEvent,
        source: evdev.InputDevice,
        suppress: bool = False,
    ) -> bool: ...

    def reset(self) -> None:
        """Reset the state of the handler e.g. release any buttons."""
        ...


class HandlerEnums(enum.Enum):
    # converting to btn
    abs2btn = enum.auto()
    rel2btn = enum.auto()

    macro = enum.auto()
    key = enum.auto()

    # converting to "analog"
    btn2rel = enum.auto()
    rel2rel = enum.auto()
    abs2rel = enum.auto()

    btn2abs = enum.auto()
    rel2abs = enum.auto()
    abs2abs = enum.auto()

    # special handlers
    combination = enum.auto()
    hierarchy = enum.auto()
    axisswitch = enum.auto()
    disable = enum.auto()


class MappingHandler:
    """The protocol an InputEventHandler must follow if it should be
    dynamically integrated in an event-pipeline by the mapping parser
    """

    mapping: Mapping
    # all input events this handler cares about
    # should always be a subset of mapping.input_combination
    input_configs: List[InputConfig]
    _sub_handler: Optional[InputEventHandler]

    # https://bugs.python.org/issue44807
    def __init__(
        self,
        combination: InputCombination,
        mapping: Mapping,
        global_uinputs: GlobalUInputs,
        **_,
    ) -> None:
        """Initialize the handler

        Parameters
        ----------
        combination
            the combination from sub_handler.wrap_with()
        mapping
        """
        self.mapping = mapping
        self.input_configs = list(combination)
        self._sub_handler = None
        self.global_uinputs = global_uinputs

    def notify(
        self,
        event: InputEvent,
        source: evdev.InputDevice,
        suppress: bool = False,
    ) -> bool:
        """Notify this handler about an incoming event.

        Parameters
        ----------
        event
            The newest event that came from `source`, and that should be mapped to
            something else
        source
            Where `event` comes from
        """
        raise NotImplementedError

    def reset(self) -> None:
        """Reset the state of the handler e.g. release any buttons."""
        raise NotImplementedError

    def needs_wrapping(self) -> bool:
        """If this handler needs to be wrapped in another MappingHandler."""
        return len(self.wrap_with()) > 0

    def needs_ranking(self) -> bool:
        """If this handler needs ranking and wrapping with a HierarchyHandler."""
        return False

    def rank_by(self) -> Optional[InputCombination]:
        """The combination for which this handler needs ranking."""

    def wrap_with(self) -> Dict[InputCombination, HandlerEnums]:
        """A dict of InputCombination -> HandlerEnums.

        for each InputCombination this handler should be wrapped
        with the given MappingHandler.
        """
        return {}

    def set_sub_handler(self, handler: InputEventHandler) -> None:
        """Give this handler a sub_handler."""
        self._sub_handler = handler

    def occlude_input_event(self, input_config: InputConfig) -> None:
        """Remove the config from self.input_configs."""
        if not self.input_configs:
            logger.debug_mapping_handler(self)
            raise MappingParsingError(
                "Cannot remove a non existing config", mapping_handler=self
            )

        # should be called for each event a wrapping-handler
        # has in its input_configs InputCombination
        self.input_configs.remove(input_config)