diff --git a/.pylintrc b/.pylintrc
index e865c706..bc7d52ec 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -2,8 +2,8 @@
disable=
# that is the standard way to import GTK afaik
- wrong-import-position
+ wrong-import-position,
# using """ for comments highlights them in green for me and makes it
# a great way to separate stuff into multiple sections
- pointless-string-statement
\ No newline at end of file
+ pointless-string-statement
diff --git a/keymapper/config.py b/keymapper/config.py
index 7af9cadd..d2a683bc 100644
--- a/keymapper/config.py
+++ b/keymapper/config.py
@@ -23,9 +23,7 @@
import os
-import sys
import json
-import shutil
import copy
from keymapper.paths import CONFIG_PATH, USER, touch
diff --git a/keymapper/getdevices.py b/keymapper/getdevices.py
index afbba21a..abfdc31b 100644
--- a/keymapper/getdevices.py
+++ b/keymapper/getdevices.py
@@ -28,8 +28,7 @@ import time
import asyncio
import evdev
-from evdev.ecodes import EV_KEY, EV_ABS, KEY_CAMERA, EV_REL, ABS_PRESSURE, \
- BTN_STYLUS, BTN_A
+from evdev.ecodes import EV_KEY, EV_ABS, KEY_CAMERA, EV_REL, BTN_STYLUS, BTN_A
from keymapper.logger import logger
diff --git a/keymapper/gui/reader.py b/keymapper/gui/reader.py
index e31c1fcb..a772ce43 100644
--- a/keymapper/gui/reader.py
+++ b/keymapper/gui/reader.py
@@ -34,7 +34,7 @@ from evdev.ecodes import EV_KEY, EV_ABS, ABS_MISC, EV_REL
from keymapper.logger import logger
from keymapper.key import Key
from keymapper.state import custom_mapping
-from keymapper.getdevices import get_devices
+from keymapper.getdevices import get_devices, is_gamepad
from keymapper import utils
CLOSE = 1
@@ -119,6 +119,11 @@ class _KeycodeReader:
If read is called without prior start_reading, no keycodes
will be available.
+
+ Parameters
+ ----------
+ device_name : string
+ As indexed in get_devices()
"""
if self._pipe is not None:
self.stop_reading()
@@ -126,33 +131,39 @@ class _KeycodeReader:
self.virtual_devices = []
- for name, group in get_devices().items():
- if device_name not in name:
+ group = get_devices()[device_name]
+
+ # Watch over each one of the potentially multiple devices per hardware
+ for path in group['paths']:
+ try:
+ device = evdev.InputDevice(path)
+ except FileNotFoundError:
continue
- # Watch over each one of the potentially multiple devices per
- # hardware
- for path in group['paths']:
- try:
- device = evdev.InputDevice(path)
- except FileNotFoundError:
- continue
+ if evdev.ecodes.EV_KEY in device.capabilities():
+ self.virtual_devices.append(device)
- if evdev.ecodes.EV_KEY in device.capabilities():
- self.virtual_devices.append(device)
-
- logger.debug(
- 'Starting reading keycodes from "%s"',
- '", "'.join([device.name for device in self.virtual_devices])
- )
+ logger.debug(
+ 'Starting reading keycodes from "%s"',
+ '", "'.join([device.name for device in self.virtual_devices])
+ )
pipe = multiprocessing.Pipe()
self._pipe = pipe
self._process = threading.Thread(target=self._read_worker)
self._process.start()
- def _pipe_event(self, event, device):
- """Write the event into the pipe to the main process."""
+ def _pipe_event(self, event, device, gamepad):
+ """Write the event into the pipe to the main process.
+
+ Parameters
+ ----------
+ event : evdev.InputEvent
+ device : evdev.InputDevice
+ gamepad : bool
+ If true, ABS_X and ABS_Y might be mapped to buttons as well
+ depending on the purpose configuration
+ """
# value: 1 for down, 0 for up, 2 for hold.
if self._pipe is None or self._pipe[1].closed:
logger.debug('Pipe closed, reader stops.')
@@ -173,7 +184,7 @@ class _KeycodeReader:
# which breaks the current workflow.
return
- if not utils.should_map_event_as_btn(event, custom_mapping):
+ if not utils.should_map_event_as_btn(event, custom_mapping, gamepad):
return
max_abs = utils.get_max_abs(device)
@@ -186,13 +197,18 @@ class _KeycodeReader:
# using a thread that blocks instead of read_one made it easier
# to debug via the logs, because the UI was not polling properly
# at some point which caused logs for events not to be written.
- rlist = {device.fd: device for device in self.virtual_devices}
+ rlist = {}
+ gamepad = {}
+ for device in self.virtual_devices:
+ rlist[device.fd] = device
+ gamepad[device.fd] = is_gamepad(device)
+
rlist[self._pipe[1]] = self._pipe[1]
while True:
ready = select.select(rlist, [], [])[0]
for fd in ready:
- readable = rlist[fd] # a device or a pipe
+ readable = rlist[fd] # an InputDevice or a pipe
if isinstance(readable, multiprocessing.connection.Connection):
msg = readable.recv()
if msg == CLOSE:
@@ -202,7 +218,11 @@ class _KeycodeReader:
try:
for event in rlist[fd].read():
- self._pipe_event(event, readable)
+ self._pipe_event(
+ event,
+ readable,
+ gamepad.get(fd, False)
+ )
except OSError:
logger.debug(
'Device "%s" disappeared from the reader',
diff --git a/keymapper/injection/context.py b/keymapper/injection/context.py
index 7da0cf89..e8ba2495 100644
--- a/keymapper/injection/context.py
+++ b/keymapper/injection/context.py
@@ -59,6 +59,9 @@ class Context:
macros : dict
mapping of ((type, code, value),) to _Macro objects.
Combinations work similar as in key_to_code
+ is_gamepad : bool
+ if key-mapper considers this device to be a gamepad. If yes, ABS_X
+ and ABS_Y events can be treated as buttons.
"""
def __init__(self, mapping):
self.mapping = mapping
@@ -67,6 +70,7 @@ class Context:
# might be a bit expensive
self.key_to_code = self._map_keys_to_codes()
self.macros = self._parse_macros()
+
self.left_purpose = None
self.right_purpose = None
self.update_purposes()
diff --git a/keymapper/injection/injector.py b/keymapper/injection/injector.py
index 90b1b93f..108001d4 100644
--- a/keymapper/injection/injector.py
+++ b/keymapper/injection/injector.py
@@ -406,6 +406,8 @@ class Injector(multiprocessing.Process):
source.path, source.fd
)
+ gamepad = is_gamepad(source)
+
keycode_handler = KeycodeMapper(self.context, source, uinput)
async for event in source.async_read_loop():
@@ -415,7 +417,7 @@ class Injector(multiprocessing.Process):
continue
# for mapped stuff
- if utils.should_map_event_as_btn(event, self.context.mapping):
+ if utils.should_map_event_as_btn(event, self.context.mapping, gamepad):
will_report_key_up = utils.will_report_key_up(event)
keycode_handler.handle_keycode(event)
diff --git a/keymapper/logger.py b/keymapper/logger.py
index ab18ae9e..bd6ea56c 100644
--- a/keymapper/logger.py
+++ b/keymapper/logger.py
@@ -54,6 +54,7 @@ def key_spam(self, key, msg, *args):
anything that can be string formatted, but usually a tuple of
(type, code, value) tuples
"""
+ # pylint: disable=protected-access
if not self.isEnabledFor(SPAM):
return
diff --git a/keymapper/utils.py b/keymapper/utils.py
index e9579ecb..49db786f 100644
--- a/keymapper/utils.py
+++ b/keymapper/utils.py
@@ -95,7 +95,7 @@ def will_report_key_up(event):
return not is_wheel(event)
-def should_map_event_as_btn(event, mapping):
+def should_map_event_as_btn(event, mapping, gamepad):
"""Does this event describe a button.
If it does, this function will make sure its value is one of [-1, 0, 1],
@@ -106,6 +106,13 @@ def should_map_event_as_btn(event, mapping):
Especially important for gamepad events, some of the buttons
require special rules.
+
+ Parameters
+ ----------
+ event : evdev.InputEvent
+ mapping : Mapping
+ gamepad : bool
+ If the device is treated as gamepad
"""
if (event.type, event.code) in STYLUS:
return False
@@ -121,6 +128,9 @@ def should_map_event_as_btn(event, mapping):
return False
if event.code in JOYSTICK:
+ if not gamepad:
+ return False
+
l_purpose = mapping.get('gamepad.joystick.left_purpose')
r_purpose = mapping.get('gamepad.joystick.right_purpose')
@@ -130,6 +140,8 @@ def should_map_event_as_btn(event, mapping):
if event.code in [ABS_RX, ABS_RY] and r_purpose == BUTTONS:
return True
else:
+ # for non-joystick buttons just always offer mapping them to
+ # buttons
return True
if is_wheel(event):
diff --git a/readme/development.md b/readme/development.md
index 29357376..1f04520d 100644
--- a/readme/development.md
+++ b/readme/development.md
@@ -33,7 +33,7 @@ requests.
- [x] map keys using a `modifier + modifier + ... + key` syntax
- [ ] injecting keys that aren't available in the systems keyboard layout
- [ ] injecting keys while abs capabilities are present. e.g. stylus buttons
-- [ ] ship with a list of all keys known to xkb and validate input in gui
+- [ ] ship with a list of all keys known to xkb and validate input in the gui
## Tests
diff --git a/readme/pylint.svg b/readme/pylint.svg
index a8977660..5ff69b25 100644
--- a/readme/pylint.svg
+++ b/readme/pylint.svg
@@ -17,7 +17,7 @@
pylint
- 9.81
- 9.81
+ 9.84
+ 9.84
\ No newline at end of file
diff --git a/tests/testcases/test_dev_utils.py b/tests/testcases/test_dev_utils.py
index 0993d139..bdc17137 100644
--- a/tests/testcases/test_dev_utils.py
+++ b/tests/testcases/test_dev_utils.py
@@ -56,57 +56,76 @@ class TestDevUtils(unittest.TestCase):
mapping = Mapping()
# the function name is so horribly long
- def do(event):
- return utils.should_map_event_as_btn(event, mapping)
+ def do(gamepad, event):
+ return utils.should_map_event_as_btn(event, mapping, gamepad)
"""D-Pad"""
- self.assertTrue(do(new_event(EV_ABS, ABS_HAT0X, 1)))
- self.assertTrue(do(new_event(EV_ABS, ABS_HAT0X, -1)))
+ self.assertTrue(do(1, new_event(EV_ABS, ABS_HAT0X, 1)))
+ self.assertTrue(do(0, new_event(EV_ABS, ABS_HAT0X, -1)))
"""Mouse movements"""
- self.assertTrue(do(new_event(EV_REL, REL_WHEEL, 1)))
- self.assertTrue(do(new_event(EV_REL, REL_WHEEL, -1)))
- self.assertTrue(do(new_event(EV_REL, REL_HWHEEL, 1)))
- self.assertTrue(do(new_event(EV_REL, REL_HWHEEL, -1)))
- self.assertFalse(do(new_event(EV_REL, REL_X, -1)))
+ self.assertTrue(do(1, new_event(EV_REL, REL_WHEEL, 1)))
+ self.assertTrue(do(0, new_event(EV_REL, REL_WHEEL, -1)))
+ self.assertTrue(do(1, new_event(EV_REL, REL_HWHEEL, 1)))
+ self.assertTrue(do(0, new_event(EV_REL, REL_HWHEEL, -1)))
+ self.assertFalse(do(1, new_event(EV_REL, REL_X, -1)))
"""regular keys and buttons"""
- self.assertTrue(do(new_event(EV_KEY, KEY_A, 1)))
- self.assertTrue(do(new_event(EV_ABS, ABS_HAT0X, -1)))
+ self.assertTrue(do(1, new_event(EV_KEY, KEY_A, 1)))
+ self.assertTrue(do(0, new_event(EV_KEY, KEY_A, 1)))
+ self.assertTrue(do(1, new_event(EV_ABS, ABS_HAT0X, -1)))
+ self.assertTrue(do(0, new_event(EV_ABS, ABS_HAT0X, -1)))
"""mousepad events"""
- self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_MT_SLOT, 1)))
- self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_MT_TOOL_Y, 1)))
- self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_MT_POSITION_X, 1)))
- self.assertFalse(do(new_event(EV_KEY, ecodes.BTN_TOUCH, 1)))
+ self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_MT_SLOT, 1)))
+ self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_MT_SLOT, 1)))
+ self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_MT_TOOL_Y, 1)))
+ self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_MT_TOOL_Y, 1)))
+ self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_MT_POSITION_X, 1)))
+ self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_MT_POSITION_X, 1)))
+ self.assertFalse(do(1, new_event(EV_KEY, ecodes.BTN_TOUCH, 1)))
+ self.assertFalse(do(0, new_event(EV_KEY, ecodes.BTN_TOUCH, 1)))
"""stylus movements"""
- self.assertFalse(do(new_event(EV_KEY, ecodes.BTN_DIGI, 1)))
- self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_TILT_X, 1)))
- self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_TILT_Y, 1)))
- self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_DISTANCE, 1)))
- self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_PRESSURE, 1)))
+ self.assertFalse(do(0, new_event(EV_KEY, ecodes.BTN_DIGI, 1)))
+ self.assertFalse(do(1, new_event(EV_KEY, ecodes.BTN_DIGI, 1)))
+ self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_TILT_X, 1)))
+ self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_TILT_X, 1)))
+ self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_TILT_Y, 1)))
+ self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_TILT_Y, 1)))
+ self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_DISTANCE, 1)))
+ self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_DISTANCE, 1)))
+ self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_PRESSURE, 1)))
+ self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_PRESSURE, 1)))
"""joysticks"""
- self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_RX, 1234)))
- self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_Y, -1)))
- self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_RY, -1)))
-
- """weird events"""
-
- self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_MISC, -1)))
+ # without a purpose of BUTTONS it won't map any button, even for
+ # gamepads
+ self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_RX, 1234)))
+ self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_RX, 1234)))
+ self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_Y, -1)))
+ self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_Y, -1)))
+ self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_RY, -1)))
+ self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_RY, -1)))
mapping.set('gamepad.joystick.right_purpose', BUTTONS)
config.set('gamepad.joystick.left_purpose', BUTTONS)
+ # but only for gamepads
+ self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_Y, -1)))
+ self.assertTrue(do(1, new_event(EV_ABS, ecodes.ABS_Y, -1)))
+ self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_RY, -1)))
+ self.assertTrue(do(1, new_event(EV_ABS, ecodes.ABS_RY, -1)))
- self.assertTrue(do(new_event(EV_ABS, ecodes.ABS_Y, -1)))
- self.assertTrue(do(new_event(EV_ABS, ecodes.ABS_RY, -1)))
+ """weird events"""
+
+ self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_MISC, -1)))
+ self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_MISC, -1)))
def test_normalize_value(self):
def do(event):