2021-03-21 18:15:20 +00:00
|
|
|
#!/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@hip70890b.de>
|
2021-03-21 18:15:20 +00:00
|
|
|
#
|
2022-01-01 12:00:49 +00:00
|
|
|
# This file is part of input-remapper.
|
2021-03-21 18:15:20 +00:00
|
|
|
#
|
2022-01-01 12:00:49 +00:00
|
|
|
# input-remapper is free software: you can redistribute it and/or modify
|
2021-03-21 18:15:20 +00:00
|
|
|
# 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,
|
2021-03-21 18:15:20 +00:00
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
|
"""Process that sends stuff to the GUI.
|
|
|
|
|
2022-01-01 12:00:49 +00:00
|
|
|
It should be started via input-remapper-control and pkexec.
|
2021-03-21 18:15:20 +00:00
|
|
|
|
|
|
|
GUIs should not run as root
|
|
|
|
https://wiki.archlinux.org/index.php/Running_GUI_applications_as_root
|
2021-08-31 08:40:47 +00:00
|
|
|
|
|
|
|
The service shouldn't do that even though it has root rights, because that
|
|
|
|
would provide a key-logger that can be accessed by any user at all times,
|
|
|
|
whereas for the helper to start a password is needed and it stops when the ui
|
|
|
|
closes.
|
2021-03-21 18:15:20 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
import select
|
|
|
|
import multiprocessing
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
import evdev
|
2021-04-02 10:16:34 +00:00
|
|
|
from evdev.ecodes import EV_KEY, EV_ABS
|
2021-03-21 18:15:20 +00:00
|
|
|
|
2022-01-01 12:00:49 +00:00
|
|
|
from inputremapper.ipc.pipe import Pipe
|
|
|
|
from inputremapper.logger import logger
|
|
|
|
from inputremapper.groups import groups
|
|
|
|
from inputremapper import utils
|
|
|
|
from inputremapper.user import USER
|
2021-03-21 18:15:20 +00:00
|
|
|
|
|
|
|
|
2022-02-27 14:43:01 +00:00
|
|
|
# received by the helper
|
|
|
|
CMD_TERMINATE = "terminate"
|
|
|
|
CMD_REFRESH_GROUPS = "refresh_groups"
|
|
|
|
|
|
|
|
# sent by the helper to the reader
|
|
|
|
MSG_GROUPS = "groups"
|
|
|
|
MSG_EVENT = "event"
|
2021-03-21 18:15:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
def is_helper_running():
|
|
|
|
"""Check if the helper is running."""
|
|
|
|
try:
|
2022-01-01 12:00:49 +00:00
|
|
|
subprocess.check_output(["pgrep", "-f", "input-remapper-helper"])
|
2021-03-21 18:15:20 +00:00
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
class RootHelper:
|
|
|
|
"""Client that runs as root and works for the GUI.
|
|
|
|
|
|
|
|
Sends device information and keycodes to the GUIs socket.
|
|
|
|
|
|
|
|
Commands are either numbers for generic commands,
|
|
|
|
or strings to start listening on a specific device.
|
|
|
|
"""
|
2021-09-26 10:44:56 +00:00
|
|
|
|
2021-03-21 18:15:20 +00:00
|
|
|
def __init__(self):
|
|
|
|
"""Construct the helper and initialize its sockets."""
|
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")
|
2021-03-21 18:15:20 +00:00
|
|
|
|
2021-04-23 09:51:21 +00:00
|
|
|
self._send_groups()
|
2021-03-21 18:15:20 +00:00
|
|
|
|
2021-04-23 09:51:21 +00:00
|
|
|
self.group = None
|
2021-03-21 18:15:20 +00:00
|
|
|
self._pipe = multiprocessing.Pipe()
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
"""Start doing stuff. Blocks."""
|
|
|
|
while True:
|
|
|
|
self._handle_commands()
|
|
|
|
self._start_reading()
|
|
|
|
|
2021-04-23 09:51:21 +00:00
|
|
|
def _send_groups(self):
|
|
|
|
"""Send the groups to the gui."""
|
2022-02-27 14:43:01 +00:00
|
|
|
self._results.send({"type": MSG_GROUPS, "message": groups.dumps()})
|
2021-04-02 13:08:36 +00:00
|
|
|
|
2021-03-21 18:15:20 +00:00
|
|
|
def _handle_commands(self):
|
|
|
|
"""Handle all unread commands."""
|
|
|
|
# wait for something to do
|
|
|
|
select.select([self._commands], [], [])
|
|
|
|
|
|
|
|
while self._commands.poll():
|
|
|
|
cmd = self._commands.recv()
|
|
|
|
logger.debug('Received command "%s"', cmd)
|
2021-04-23 09:51:21 +00:00
|
|
|
|
2022-02-27 14:43:01 +00:00
|
|
|
if cmd == CMD_TERMINATE:
|
2021-09-26 10:44:56 +00:00
|
|
|
logger.debug("Helper terminates")
|
2021-03-21 18:15:20 +00:00
|
|
|
sys.exit(0)
|
2021-04-23 09:51:21 +00:00
|
|
|
|
2022-02-27 14:43:01 +00:00
|
|
|
if cmd == CMD_REFRESH_GROUPS:
|
2021-04-23 09:51:21 +00:00
|
|
|
groups.refresh()
|
|
|
|
self._send_groups()
|
|
|
|
continue
|
|
|
|
|
|
|
|
group = groups.find(key=cmd)
|
|
|
|
if group is None:
|
|
|
|
groups.refresh()
|
|
|
|
group = groups.find(key=cmd)
|
|
|
|
|
|
|
|
if group is not None:
|
|
|
|
self.group = group
|
|
|
|
continue
|
|
|
|
|
|
|
|
logger.error('Received unknown command "%s"', cmd)
|
2021-03-21 18:15:20 +00:00
|
|
|
|
|
|
|
def _start_reading(self):
|
|
|
|
"""Tell the evdev lib to start looking for keycodes.
|
|
|
|
|
|
|
|
If read is called without prior start_reading, no keycodes
|
|
|
|
will be available.
|
|
|
|
|
|
|
|
This blocks forever until it discovers a new command on the socket.
|
|
|
|
"""
|
|
|
|
rlist = {}
|
|
|
|
|
2021-04-23 09:51:21 +00:00
|
|
|
if self.group is None:
|
2021-09-26 10:44:56 +00:00
|
|
|
logger.error("group is None")
|
2021-04-02 13:08:36 +00:00
|
|
|
return
|
|
|
|
|
2021-03-21 18:15:20 +00:00
|
|
|
virtual_devices = []
|
|
|
|
# Watch over each one of the potentially multiple devices per
|
|
|
|
# hardware
|
2021-04-23 09:51:21 +00:00
|
|
|
for path in self.group.paths:
|
2021-03-21 18:15:20 +00:00
|
|
|
try:
|
|
|
|
device = evdev.InputDevice(path)
|
|
|
|
except FileNotFoundError:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if evdev.ecodes.EV_KEY in device.capabilities():
|
|
|
|
virtual_devices.append(device)
|
|
|
|
|
|
|
|
if len(virtual_devices) == 0:
|
2021-04-23 09:51:21 +00:00
|
|
|
logger.debug('No interesting device for "%s"', self.group.key)
|
2021-03-21 18:15:20 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
for device in virtual_devices:
|
|
|
|
rlist[device.fd] = device
|
|
|
|
|
|
|
|
logger.debug(
|
|
|
|
'Starting reading keycodes from "%s"',
|
2021-09-26 10:44:56 +00:00
|
|
|
'", "'.join([device.name for device in virtual_devices]),
|
2021-03-21 18:15:20 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
rlist[self._commands] = self._commands
|
|
|
|
|
|
|
|
while True:
|
|
|
|
ready_fds = select.select(rlist, [], [])
|
|
|
|
if len(ready_fds[0]) == 0:
|
2021-03-28 11:19:44 +00:00
|
|
|
# whatever, happens for sockets sometimes. Maybe the socket
|
|
|
|
# is closed and select has nothing to select from?
|
2021-03-21 18:15:20 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
for fd in ready_fds[0]:
|
|
|
|
if rlist[fd] == self._commands:
|
|
|
|
# all commands will cause the reader to start over
|
|
|
|
# (possibly for a different device).
|
|
|
|
# _handle_commands will check what is going on
|
|
|
|
return
|
|
|
|
|
|
|
|
device = rlist[fd]
|
|
|
|
|
|
|
|
try:
|
|
|
|
event = device.read_one()
|
2021-04-02 10:16:34 +00:00
|
|
|
if event:
|
|
|
|
self._send_event(event, device)
|
2021-03-21 18:15:20 +00:00
|
|
|
except OSError:
|
|
|
|
logger.debug('Device "%s" disappeared', device.path)
|
|
|
|
return
|
|
|
|
|
2021-03-21 18:56:15 +00:00
|
|
|
def _send_event(self, event, device):
|
2021-03-21 18:15:20 +00:00
|
|
|
"""Write the event into the pipe to the main process.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
event : evdev.InputEvent
|
|
|
|
device : evdev.InputDevice
|
|
|
|
"""
|
|
|
|
# value: 1 for down, 0 for up, 2 for hold.
|
|
|
|
if event.type == EV_KEY and event.value == 2:
|
|
|
|
# ignore hold-down events
|
|
|
|
return
|
|
|
|
|
2021-09-26 10:44:56 +00:00
|
|
|
blacklisted_keys = [evdev.ecodes.BTN_TOOL_DOUBLETAP]
|
2021-03-21 18:15:20 +00:00
|
|
|
|
2021-04-02 14:33:20 +00:00
|
|
|
if event.type == EV_KEY and event.code in blacklisted_keys:
|
2021-03-21 18:15:20 +00:00
|
|
|
return
|
|
|
|
|
2021-04-02 10:16:34 +00:00
|
|
|
if event.type == EV_ABS:
|
|
|
|
abs_range = utils.get_abs_range(device, event.code)
|
2021-09-29 18:17:45 +00:00
|
|
|
event.value = utils.classify_action(event, abs_range)
|
2021-04-02 10:16:34 +00:00
|
|
|
else:
|
2021-09-29 18:17:45 +00:00
|
|
|
event.value = utils.classify_action(event)
|
2021-03-21 18:15:20 +00:00
|
|
|
|
2021-09-26 10:44:56 +00:00
|
|
|
self._results.send(
|
|
|
|
{
|
2022-02-27 14:43:01 +00:00
|
|
|
"type": MSG_EVENT,
|
2021-09-29 18:17:45 +00:00
|
|
|
"message": (event.sec, event.usec, event.type, event.code, event.value),
|
2021-09-26 10:44:56 +00:00
|
|
|
}
|
|
|
|
)
|