diff --git a/README.md b/README.md
index fb47aca5..41becfd2 100644
--- a/README.md
+++ b/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
diff --git a/data/key-mapper.glade b/data/key-mapper.glade
index ec53d623..d0e5f141 100644
--- a/data/key-mapper.glade
+++ b/data/key-mapper.glade
@@ -688,7 +688,8 @@
"KP_0" - "KP_9"
"Shift_L", "Shift_R"
"Alt_L", "Alt_R"
-"Control_R"
+"Control_R"
+"Mouse_1" - "Mouse_5"
5
5
Mapping
diff --git a/keymapper/dev/injector.py b/keymapper/dev/injector.py
index 8a54e6b4..c004303b 100644
--- a/keymapper/dev/injector.py
+++ b/keymapper/dev/injector.py
@@ -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
diff --git a/keymapper/dev/reader.py b/keymapper/dev/reader.py
index be8dfcff..062d794c 100644
--- a/keymapper/dev/reader.py
+++ b/keymapper/dev/reader.py
@@ -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:
diff --git a/keymapper/state.py b/keymapper/state.py
index dc851c1b..2700bc46 100644
--- a/keymapper/state.py
+++ b/keymapper/state.py
@@ -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
diff --git a/tests/testcases/daemon.py b/tests/testcases/daemon.py
index 4b66deeb..4361ef3b 100644
--- a/tests/testcases/daemon.py
+++ b/tests/testcases/daemon.py
@@ -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')
diff --git a/tests/testcases/injector.py b/tests/testcases/injector.py
index 296c0a4d..17738195 100644
--- a/tests/testcases/injector.py
+++ b/tests/testcases/injector.py
@@ -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
diff --git a/tests/testcases/integration.py b/tests/testcases/integration.py
index 754c34da..09a0c218 100644
--- a/tests/testcases/integration.py
+++ b/tests/testcases/integration.py
@@ -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.
diff --git a/tests/testcases/mapping.py b/tests/testcases/mapping.py
index 1c6a390b..ac8dfec7 100644
--- a/tests/testcases/mapping.py
+++ b/tests/testcases/mapping.py
@@ -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)