diff --git a/README.md b/README.md
index 98aa4488..1d29f053 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,10 @@ work.
+# Dependencies
+
+`evtest`
+
# Roadmap
- [x] show a dropdown to select an arbitrary device from `xinput list`
diff --git a/bin/key-mapper-gtk b/bin/key-mapper-gtk
index 3b241292..7272705a 100755
--- a/bin/key-mapper-gtk
+++ b/bin/key-mapper-gtk
@@ -114,7 +114,8 @@ class Window:
"""Make the devices selectable."""
devices = find_devices()
device_selection = self.get('device_selection')
- for (id, device) in devices:
+ for device in devices:
+ ids = devices[device]
device_selection.append(device, device)
def populate_presets(self):
@@ -132,7 +133,6 @@ class Window:
if isinstance(device, Gtk.ComboBoxText):
device = device.get_active_text()
- device = device.get_active_text()
presets = get_presets(device)
if len(presets) == 0:
create_preset(device)
diff --git a/keymapper/X.py b/keymapper/X.py
index afbf71e2..2d142692 100644
--- a/keymapper/X.py
+++ b/keymapper/X.py
@@ -19,16 +19,23 @@
# along with key-mapper. If not, see .
-"""Stuff that interacts with the X Server"""
+"""Stuff that interacts with the X Server
+
+Resources:
+https://wiki.archlinux.org/index.php/Keyboard_input
+http://people.uleth.ca/~daniel.odonnell/Blog/custom-keyboard-in-linuxx11
+"""
import re
import subprocess
+from keymapper.logger import logger
-# mapping of keycode to character, e.g. 38 to the 'A' key of the keyboard.
+
+# mapping of key to character
# This depends on the configured keyboard layout.
-# example: 38: "a A a A ae AE ae"
+# example: AC01: "a A a A ae AE ae".
key_mapping = {}
@@ -42,30 +49,67 @@ def load_keymapping():
key_mapping[search[0]] = search[1]
-def get_xinput_list(type):
- """Run xinput and get the result as list.
-
- Parameters
- ----------
- type : string
- Ine of 'id' or 'name'
- """
- output = subprocess.check_output(['xinput', 'list', f'--{type}-only'])
- return [line for line in output.decode().split('\n') if line != '']
-
-
def find_devices():
- """Get a list of (id, name) for each input device."""
- # `xinput list`
- ids = get_xinput_list('id')
- names = get_xinput_list('name')
+ """Return a mapping of {name: [ids]} for each input device.
- # names contains duplicates and "Virtual"-somethings, filter those
- known_names = []
- # TODO remember all IDS? try each one of them for setxkbmap until success?
- result = []
- for (id, name) in zip(ids, names):
- if name not in known_names and not name.startswith('Virtual'):
- known_names.append(name)
- result.append((id, name))
- return result
+ Evtest listing is really slow, query this only once when the
+ program starts.
+ """
+ # It asks for a device afterwads, so just insert garbage into it
+ p = subprocess.Popen(
+ 'echo a | sudo evtest',
+ shell=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE
+ )
+ # the list we are looking for is in stderr
+ _, evtest = p.communicate()
+
+ evtest = [
+ line
+ for line in evtest.decode().split('\n')
+ if line.startswith('/dev')
+ ]
+
+ logger.debug('evtest devices: \n%s', '\n'.join(evtest))
+
+ # evtest also returns a bunch of other devices, like some audio devices,
+ # so check this list against `xinput list` to get keyboards and mice
+ xinput = get_xinput_list()
+
+ logger.debug('xinput devices: \n%s', '\n'.join(xinput))
+
+ devices = {}
+ # there may be multiple entries per device in /dev, because one handles
+ # movement while the other handles extra buttons. Remember all of the
+ # device ids, so that the input mapping can be applied to all matching
+ # ids, one of them is going to be the right one.
+ for line in evtest:
+ match = re.search(r'event(\d+):\s+(.+)', line)
+ if match is None:
+ continue
+
+ # the id refers to a file in /dev/input, it is different from
+ # the id that `xinput list` can return.
+ id = match[1]
+ name = match[2]
+
+ if name not in xinput:
+ continue
+
+ # there can be
+ # 'Logitech USB Keyboard' and
+ # 'Logitech USB Keyboard Consumer Control'
+ if not devices.get(name):
+ devices[name] = []
+ devices[name].append(id)
+
+ logger.info('Devices: %s', ', '.join(list(devices.keys())))
+
+ return devices
+
+
+def get_xinput_list():
+ """Run xinput and get the result as list."""
+ xinput = subprocess.check_output(['xinput', 'list', f'--name-only'])
+ return [line for line in xinput.decode().split('\n') if line != '']