support for linux kernel keycode constants

This commit is contained in:
sezanzeb 2020-11-30 16:22:17 +01:00 committed by sezanzeb
parent 5edf376562
commit d338ebe7f8
9 changed files with 144 additions and 59 deletions

View File

@ -30,9 +30,19 @@ Documentation:
- `m` holds a modifier while executing the second parameter
- `.` executes two actions behind each other
##### Names
For a list of supported keystrokes and their names, check the output of
`xmodmap -pke`
- Alphanumeric `a` to `z` and `0` to `9`
- Modifiers `Alt_L` `Control_L` `Control_R` `Shift_L` `Shift_R`
If you can't find what you need, consult [linux/input-event-codes.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h)
- Mouse buttons `BTN_LEFT` `BTN_RIGHT` `BTN_MIDDLE` `BTN_SIDE`
- Macro special keys `KEY_MACRO1` `KEY_MACRO2` ...
## Installation
After your installation, independent of the method, you should add yourself

View File

@ -688,7 +688,8 @@
"KP_0" - "KP_9"
"Shift_L", "Shift_R"
"Alt_L", "Alt_R"
"Control_R"</property>
"Control_R"
"Mouse_1" - "Mouse_5"</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label" translatable="yes">Mapping</property>

View File

@ -32,7 +32,7 @@ import evdev
from keymapper.logger import logger
from keymapper.getdevices import get_devices
from keymapper.state import system_mapping
from keymapper.state import system_mapping, KEYCODE_OFFSET
from keymapper.dev.macros import parse
@ -41,9 +41,6 @@ DEVICE_CREATED = 1
FAILED = 2
DEVICE_SKIPPED = 3
# offset between xkb and linux keycodes. linux keycodes are lower
KEYCODE_OFFSET = 8
def is_numlock_on():
"""Get the current state of the numlock."""
@ -112,6 +109,10 @@ class KeycodeInjector:
self.mapping = mapping
self._process = None
def __del__(self):
if self._process is not None:
self._process.terminate()
def start_injecting(self):
"""Start injecting keycodes."""
self._process = multiprocessing.Process(target=self._start_injecting)
@ -124,13 +125,16 @@ class KeycodeInjector:
if device is None:
return None
capabilities = device.capabilities(absinfo=False)[evdev.ecodes.EV_KEY]
capabilities = device.capabilities(absinfo=False)
needed = False
for keycode, _ in self.mapping:
if keycode - KEYCODE_OFFSET in capabilities:
if keycode - KEYCODE_OFFSET in capabilities[evdev.ecodes.EV_KEY]:
needed = True
break
# TODO only if map ABS to REL keep ABS devics
if capabilities.get(evdev.ecodes.EV_REL) is not None:
needed = True
if not needed:
# skipping reading and checking on events from those devices
@ -162,6 +166,10 @@ class KeycodeInjector:
return device
def map_abs_to_rel(self):
# TODO offer configuration via the UI if a gamepad is elected
return True
def _modify_capabilities(self, input_device):
"""Adds all keycode into a copy of a devices capabilities.
@ -169,19 +177,32 @@ class KeycodeInjector:
---------
input_device : evdev.InputDevice
"""
ecodes = evdev.ecodes
# copy the capabilities because the keymapper_device is going
# to act like the device.
capabilities = input_device.capabilities(absinfo=False)
# However, make sure that it supports all keycodes, not just some
# random ones, because the mapping could contain anything.
# That's why I avoid from_device for this
capabilities[evdev.ecodes.EV_KEY] = list(evdev.ecodes.keys.keys())
# Furthermore, support all injected keycodes
for _, character in self.mapping:
keycode = system_mapping.get(character)
if keycode is not None:
capabilities[ecodes.EV_KEY].append(keycode - KEYCODE_OFFSET)
if self.map_abs_to_rel():
if capabilities.get(ecodes.EV_ABS):
del capabilities[ecodes.EV_ABS]
capabilities[ecodes.EV_REL] = [
evdev.ecodes.REL_X,
evdev.ecodes.REL_Y,
# for my system to recognize it as mouse, WHEEL is also needed:
evdev.ecodes.REL_WHEEL,
]
# just like what python-evdev does in from_device
if evdev.ecodes.EV_SYN in capabilities:
del capabilities[evdev.ecodes.EV_SYN]
if evdev.ecodes.EV_FF in capabilities:
del capabilities[evdev.ecodes.EV_FF]
if ecodes.EV_SYN in capabilities:
del capabilities[ecodes.EV_SYN]
if ecodes.EV_FF in capabilities:
del capabilities[ecodes.EV_FF]
return capabilities
@ -191,24 +212,29 @@ class KeycodeInjector:
Stuff is non-blocking by using asyncio in order to do multiple things
somewhat concurrently.
"""
# TODO do select.select insted of async_read_loop
loop = asyncio.get_event_loop()
coroutines = []
paths = get_devices()[self.device]['paths']
logger.info('Starting injecting the mapping for %s', self.device)
paths = get_devices()[self.device]['paths']
devices = [self._prepare_device(path) for path in paths]
# Watch over each one of the potentially multiple devices per hardware
for path in paths:
input_device = self._prepare_device(path)
for input_device in devices:
if input_device is None:
continue
# certain capabilities can have side effects apparently. with an
# EV_ABS capability, EV_REL won't move the mouse pointer anymore.
# so don't merge all InputDevices into one UInput device.
uinput = evdev.UInput(
name=f'key-mapper {input_device.name}',
name=f'key-mapper {self.device}',
phys='key-mapper',
events=self._modify_capabilities(input_device)
)
coroutine = self._injection_loop(input_device, uinput)
coroutines.append(coroutine)
@ -217,19 +243,24 @@ class KeycodeInjector:
loop.run_until_complete(asyncio.gather(*coroutines))
def _write(self, device, keycode, value):
def _write(self, device, type, keycode, value):
"""Actually inject."""
device.write(evdev.ecodes.EV_KEY, keycode - KEYCODE_OFFSET, value)
device.write(type, keycode, value)
device.syn()
def _macro_write(self, character, value, keymapper_device):
"""Handler for macros."""
keycode = system_mapping.get_keycode(character)
keycode = system_mapping[character]
logger.spam(
'macro writes code:%s value:%d char:%s',
keycode, value, character
)
self._write(keymapper_device, keycode, value)
self._write(
keymapper_device,
evdev.ecodes.EV_KEY - KEYCODE_OFFSET,
keycode,
value
)
async def _injection_loop(self, device, keymapper_device):
"""Inject keycodes for one of the virtual devices.
@ -241,6 +272,7 @@ class KeycodeInjector:
keymapper_device : evdev.UInput
where to write keycodes to
"""
# TODO this function is too long
# Parse all macros beforehand
logger.debug('Parsing macros')
macros = {}
@ -258,6 +290,29 @@ class KeycodeInjector:
)
async for event in device.async_read_loop():
if self.map_abs_to_rel() and event.type == evdev.ecodes.EV_ABS:
if event.code not in [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y]:
continue
# TODO somehow the injector has to keep injecting EV_REL
# codes to keep the mouse moving
# code 0:X, 1:Y
# TODO get absinfo beforehand
value = event.value // 2000
if value == 0:
continue
print(
evdev.ecodes.EV_REL,
event.code,
value
)
self._write(
keymapper_device,
evdev.ecodes.EV_REL,
event.code,
value
)
continue
if event.type != evdev.ecodes.EV_KEY:
keymapper_device.write(event.type, event.code, event.value)
# this already includes SYN events, so need to syn here again
@ -289,7 +344,9 @@ class KeycodeInjector:
asyncio.ensure_future(macro.run())
continue
else:
target_keycode = system_mapping.get_keycode(character)
# TODO compile int-int mapping instead of going this route.
# I think that makes the reverse mapping obsolete.
target_keycode = system_mapping[character]
if target_keycode is None:
logger.error(
'Cannot find character %s in the internal mapping',
@ -305,7 +362,12 @@ class KeycodeInjector:
character
)
self._write(keymapper_device, target_keycode, event.value)
self._write(
keymapper_device,
event.type,
target_keycode - KEYCODE_OFFSET,
event.value
)
# this should only ever happen in tests to avoid blocking them
# forever, as soon as all events are consumed. In normal operation

View File

@ -28,10 +28,7 @@ import multiprocessing
from keymapper.logger import logger
from keymapper.getdevices import get_devices, refresh_devices
# offset between xkb and linux keycodes. linux keycodes are lower
KEYCODE_OFFSET = 8
from keymapper.state import KEYCODE_OFFSET
class _KeycodeReader:

View File

@ -25,22 +25,35 @@
import stat
import re
import subprocess
import evdev
from keymapper.mapping import Mapping
def parse_xmodmap(mapping):
"""Read the output of xmodmap into a mapping."""
# offset between xkb and linux keycodes. linux keycodes are lower
KEYCODE_OFFSET = 8
def populate_system_mapping():
"""Get a mapping of all available names to their keycodes."""
mapping = {}
xmodmap = subprocess.check_output(['xmodmap', '-pke']).decode() + '\n'
mappings = re.findall(r'(\d+) = (.+)\n', xmodmap)
for keycode, characters in mappings:
# this is the "array" format needed for symbols files
character = ', '.join(characters.split())
mapping.change(
previous_keycode=None,
new_keycode=int(keycode),
character=character
)
for keycode, names in mappings:
for name in names.split():
mapping[name] = int(keycode)
for name, ecode in evdev.ecodes.ecodes.items():
mapping[name] = ecode + KEYCODE_OFFSET
return mapping
def clear_system_mapping():
"""Remove all mapped keys. Only needed for tests."""
for key in system_mapping:
del system_mapping[key]
# one mapping object for the whole application that holds all
@ -48,8 +61,7 @@ def parse_xmodmap(mapping):
custom_mapping = Mapping()
# this mapping represents the xmodmap output, which stays constant
system_mapping = Mapping()
parse_xmodmap(system_mapping)
system_mapping = populate_system_mapping()
# permissions for files created in /usr
_PERMISSIONS = stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IROTH

View File

@ -34,7 +34,8 @@ import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from keymapper.state import custom_mapping, system_mapping
from keymapper.state import custom_mapping, system_mapping, \
clear_system_mapping
from keymapper.config import config
from keymapper.daemon import Daemon, get_dbus_interface
@ -89,8 +90,8 @@ class TestDaemon(unittest.TestCase):
keycode_to = 100
custom_mapping.change(keycode_from, 'a')
system_mapping.empty()
system_mapping.change(keycode_to, 'a')
clear_system_mapping()
system_mapping['a'] = keycode_to
custom_mapping.save('device 2', 'foo')
config.set_autoload_preset('device 2', 'foo')

View File

@ -24,8 +24,9 @@ import unittest
import evdev
from keymapper.dev.injector import is_numlock_on, toggle_numlock,\
ensure_numlock, KeycodeInjector, KEYCODE_OFFSET
from keymapper.state import custom_mapping, system_mapping
ensure_numlock, KeycodeInjector
from keymapper.state import custom_mapping, system_mapping, \
clear_system_mapping, KEYCODE_OFFSET
from keymapper.mapping import Mapping
from test import uinput_write_history, Event, pending_events, fixtures, \
@ -134,13 +135,13 @@ class TestInjector(unittest.TestCase):
# one mapping that is unknown in the system_mapping on purpose
custom_mapping.change(10, 'b')
system_mapping.empty()
clear_system_mapping()
code_a = 100
code_q = 101
code_w = 102
system_mapping.change(code_a, 'a')
system_mapping.change(code_q, 'q')
system_mapping.change(code_w, 'w')
system_mapping['a'] = code_a
system_mapping['q'] = code_q
system_mapping['w'] = code_w
# the second arg of those event objects is 8 lower than the
# keycode used in X and in the mappings

View File

@ -34,7 +34,8 @@ import shutil
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from keymapper.state import custom_mapping, system_mapping
from keymapper.state import custom_mapping, system_mapping, \
clear_system_mapping
from keymapper.paths import CONFIG, get_config_path
from keymapper.config import config
@ -395,8 +396,8 @@ class TestIntegration(unittest.TestCase):
keycode_to = 200
self.change_empty_row(keycode_from, 'a')
system_mapping.empty()
system_mapping.change(keycode_to, 'a')
clear_system_mapping()
system_mapping['a'] = keycode_to
pending_events['device 2'] = [
Event(evdev.events.EV_KEY, keycode_from - 8, 1),
@ -432,8 +433,8 @@ class TestIntegration(unittest.TestCase):
keycode_to = 90
self.change_empty_row(keycode_from, 't')
system_mapping.empty()
system_mapping.change(keycode_to, 't')
clear_system_mapping()
system_mapping['t'] = keycode_to
# not all of those events should be processed, since that takes some
# time due to time.sleep in the fakes and the injection is stopped.

View File

@ -22,7 +22,7 @@
import unittest
from keymapper.mapping import Mapping
from keymapper.state import parse_xmodmap
from keymapper.state import populate_system_mapping
class TestMapping(unittest.TestCase):
@ -30,8 +30,8 @@ class TestMapping(unittest.TestCase):
self.mapping = Mapping()
self.assertFalse(self.mapping.changed)
def test_parse_xmodmap(self):
parse_xmodmap(self.mapping)
def test_populate_system_mapping(self):
populate_system_mapping(self.mapping)
self.assertGreater(len(self.mapping), 100)
# keycode 10 is typically mapped to '1'
self.assertEqual(self.mapping.get_keycode('1'), 10)