input-remapper/inputremapper/gui/reader.py

253 lines
8.8 KiB
Python
Raw Normal View History

#!/usr/bin/python3
# -*- coding: utf-8 -*-
2022-01-01 12:00:49 +00:00
# input-remapper - GUI for device specific keyboard mappings
2022-01-01 12:52:33 +00:00
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
#
2022-01-01 12:00:49 +00:00
# This file is part of input-remapper.
#
2022-01-01 12:00:49 +00:00
# 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.
#
2022-01-01 12:00:49 +00:00
# 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
2022-01-01 12:00:49 +00:00
# along with input-remapper. If not, see <https://www.gnu.org/licenses/>.
2021-03-21 18:15:20 +00:00
"""Talking to the GUI helper that has root permissions.
2021-03-21 18:15:20 +00:00
see gui.helper.helper
"""
2022-01-31 19:58:37 +00:00
from typing import Optional
2021-03-21 18:15:20 +00:00
from evdev.ecodes import EV_REL
2022-01-31 19:58:37 +00:00
from inputremapper.input_event import InputEvent
2020-12-01 23:02:41 +00:00
2022-01-01 12:00:49 +00:00
from inputremapper.logger import logger
2022-01-31 19:58:37 +00:00
from inputremapper.event_combination import EventCombination
2022-01-01 12:00:49 +00:00
from inputremapper.groups import groups, GAMEPAD
from inputremapper.ipc.pipe import Pipe
2022-02-27 14:43:01 +00:00
from inputremapper.gui.helper import (
MSG_EVENT,
MSG_GROUPS,
CMD_TERMINATE,
CMD_REFRESH_GROUPS,
)
2022-01-01 12:00:49 +00:00
from inputremapper import utils
2022-01-31 19:58:37 +00:00
from inputremapper.gui.active_preset import active_preset
2022-01-01 12:00:49 +00:00
from inputremapper.user import USER
2020-11-22 17:02:55 +00:00
2021-01-07 16:15:12 +00:00
DEBOUNCE_TICKS = 3
2021-01-07 16:15:12 +00:00
def will_report_up(ev_type):
"""Check if this event will ever report a key up (wheels)."""
return ev_type != EV_REL
2021-03-21 18:15:20 +00:00
class Reader:
"""Processes events from the helper for the GUI to use.
Does not serve any purpose for the injection service.
When a button was pressed, the newest keycode can be obtained from this
2021-03-21 18:15:20 +00:00
object. GTK has get_key for keyboard keys, but Reader also
has knowledge of buttons like the middle-mouse button.
"""
2021-09-26 10:44:56 +00:00
def __init__(self):
2021-01-07 16:15:12 +00:00
self.previous_event = None
self.previous_result = None
2020-12-31 20:46:57 +00:00
self._unreleased = {}
2021-01-07 16:15:12 +00:00
self._debounce_remove = {}
self._groups_updated = False
2021-03-21 18:15:20 +00:00
self._cleared_at = 0
self.group = None
2020-11-30 13:34:27 +00:00
2021-03-21 18:15:20 +00:00
self._results = None
self._commands = None
self.connect()
2020-11-30 13:34:27 +00:00
2021-03-21 18:15:20 +00:00
def connect(self):
"""Connect to the helper."""
2022-01-01 12:00:49 +00:00
self._results = Pipe(f"/tmp/input-remapper-{USER}/results")
self._commands = Pipe(f"/tmp/input-remapper-{USER}/commands")
def are_new_groups_available(self):
"""Check if groups contains new devices.
2021-03-21 18:15:20 +00:00
The ui should then update its list.
"""
outdated = self._groups_updated
self._groups_updated = False # assume the ui will react accordingly
2021-03-21 18:15:20 +00:00
return outdated
2022-01-31 19:58:37 +00:00
def _get_event(self, message) -> Optional[InputEvent]:
2021-03-21 18:15:20 +00:00
"""Return an InputEvent if the message contains one. None otherwise."""
2021-09-26 10:44:56 +00:00
message_type = message["type"]
message_body = message["message"]
2021-03-28 11:19:44 +00:00
2022-02-27 14:43:01 +00:00
if message_type == MSG_GROUPS:
if message_body != groups.dumps():
groups.loads(message_body)
2021-09-26 10:44:56 +00:00
logger.debug("Received %d devices", len(groups))
self._groups_updated = True
2021-01-07 16:15:12 +00:00
return None
2021-03-28 11:19:44 +00:00
2022-02-27 14:43:01 +00:00
if message_type == MSG_EVENT:
2022-01-31 19:58:37 +00:00
return InputEvent(*message_body)
2021-01-07 16:15:12 +00:00
2021-03-21 18:15:20 +00:00
logger.error('Received unknown message "%s"', message)
return None
2020-12-31 20:46:57 +00:00
2020-11-30 13:34:27 +00:00
def read(self):
2022-01-31 19:58:37 +00:00
"""Get the newest key/combination as EventCombination object.
2021-01-07 16:15:12 +00:00
Only reports keys from down-events.
On key-down events the pipe returns changed combinations. Release
events won't cause that and the reader will return None as in
"nothing new to report". So In order to change a combination, one
of its keys has to be released and then a different one pressed.
Otherwise making combinations wouldn't be possible. Because at
some point the keys have to be released, and that shouldn't cause
the combination to get trimmed.
2020-12-12 13:12:45 +00:00
"""
2021-01-07 16:15:12 +00:00
# this is in some ways similar to the keycode_mapper and
# joystick_to_mouse, but its much simpler because it doesn't
2021-01-07 16:15:12 +00:00
# have to trigger anything, manage any macros and only
# reports key-down events. This function is called periodically
# by the window.
2020-11-30 13:34:27 +00:00
2021-03-21 18:15:20 +00:00
# remember the previous down-event from the pipe in order to
2021-03-22 09:18:11 +00:00
# be able to tell if the reader should return the updated combination
2021-01-07 16:15:12 +00:00
previous_event = self.previous_event
key_down_received = False
self._debounce_tick()
2021-03-21 18:15:20 +00:00
while self._results.poll():
message = self._results.recv()
event = self._get_event(message)
if event is None:
continue
2021-03-21 18:56:15 +00:00
gamepad = GAMEPAD in self.group.types
2022-01-31 19:58:37 +00:00
if not utils.should_map_as_btn(event, active_preset, gamepad):
2021-03-21 18:56:15 +00:00
continue
2021-03-21 18:15:20 +00:00
if event.value == 0:
2022-02-27 14:43:01 +00:00
logger.debug_key(event, "release")
2022-01-31 19:58:37 +00:00
self._release(event.type_and_code)
2020-12-31 22:16:46 +00:00
continue
2020-12-31 20:46:57 +00:00
2022-02-27 14:43:01 +00:00
if self._unreleased.get(event.type_and_code) == event:
logger.debug_key(event, "duplicate key down")
2022-01-31 19:58:37 +00:00
self._debounce_start(event.event_tuple)
2021-01-07 16:15:12 +00:00
continue
2021-01-07 16:15:12 +00:00
# to keep track of combinations.
# "I have got this release event, what was this for?" A release
# event for a D-Pad axis might be any direction, hence this maps
# from release to input in order to remember it. Since all release
2022-01-31 19:58:37 +00:00
# events have value 0, the value is not used in the combination.
key_down_received = True
2022-02-27 14:43:01 +00:00
logger.debug_key(event, "down")
self._unreleased[event.type_and_code] = event
2022-01-31 19:58:37 +00:00
self._debounce_start(event.event_tuple)
2021-01-07 16:15:12 +00:00
previous_event = event
if not key_down_received:
# This prevents writing a subset of the combination into
# result after keys were released. In order to control the gui,
# they have to be released.
return None
2021-01-07 16:15:12 +00:00
self.previous_event = previous_event
2020-11-22 14:14:43 +00:00
2020-12-31 20:47:56 +00:00
if len(self._unreleased) > 0:
2022-01-31 19:58:37 +00:00
result = EventCombination.from_events(self._unreleased.values())
2021-01-07 16:15:12 +00:00
if result == self.previous_result:
# don't return the same stuff twice
return None
self.previous_result = result
2022-01-31 19:58:37 +00:00
logger.debug_key(result, "read result")
2021-01-07 16:15:12 +00:00
return result
2020-12-31 20:55:38 +00:00
return None
def start_reading(self, group):
2021-03-21 18:15:20 +00:00
"""Start reading keycodes for a device."""
logger.debug('Sending start msg to helper for "%s"', group.key)
self._commands.send(group.key)
self.group = group
2021-03-21 18:15:20 +00:00
self.clear()
def terminate(self):
"""Stop reading keycodes for good."""
2021-09-26 10:44:56 +00:00
logger.debug("Sending close msg to helper")
2022-02-27 14:43:01 +00:00
self._commands.send(CMD_TERMINATE)
2021-03-21 18:15:20 +00:00
def refresh_groups(self):
"""Ask the helper for new device groups."""
2022-02-27 14:43:01 +00:00
self._commands.send(CMD_REFRESH_GROUPS)
2021-04-02 13:08:36 +00:00
2021-03-21 18:15:20 +00:00
def clear(self):
"""Next time when reading don't return the previous keycode."""
2021-09-26 10:44:56 +00:00
logger.debug("Clearing reader")
2021-03-21 18:15:20 +00:00
while self._results.poll():
# clear the results pipe and handle any non-event messages,
# otherwise a 'groups' message might get lost
2021-03-21 18:15:20 +00:00
message = self._results.recv()
self._get_event(message)
self._unreleased = {}
self.previous_event = None
self.previous_result = None
def get_unreleased_keys(self):
2022-01-31 19:58:37 +00:00
"""Get a EventCombination object of the current keyboard state."""
2021-03-21 18:15:20 +00:00
unreleased = list(self._unreleased.values())
if len(unreleased) == 0:
return None
2022-01-31 19:58:37 +00:00
return EventCombination.from_events(unreleased)
2021-03-21 18:15:20 +00:00
def _release(self, type_code):
"""Modify the state to recognize the releasing of the key."""
if type_code in self._unreleased:
del self._unreleased[type_code]
if type_code in self._debounce_remove:
del self._debounce_remove[type_code]
def _debounce_start(self, event_tuple):
"""Act like the key was released if no new event arrives in time."""
if not will_report_up(event_tuple[0]):
self._debounce_remove[event_tuple[:2]] = DEBOUNCE_TICKS
def _debounce_tick(self):
"""If the counter reaches 0, the key is not considered held down."""
for type_code in list(self._debounce_remove.keys()):
if type_code not in self._unreleased:
continue
# clear wheel events from unreleased after some time
if self._debounce_remove[type_code] == 0:
logger.debug_key(self._unreleased[type_code], "Considered as released")
2021-03-21 18:15:20 +00:00
self._release(type_code)
else:
self._debounce_remove[type_code] -= 1
2021-03-21 18:15:20 +00:00
reader = Reader()