mirror of
https://github.com/sezanzeb/input-remapper
synced 2024-11-16 06:12:58 +00:00
support for linux kernel keycode constants
This commit is contained in:
parent
5edf376562
commit
d338ebe7f8
10
README.md
10
README.md
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user