diff --git a/README.md b/README.md index 1d29f053..bb0a37dc 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,23 @@ work.

+# Running + +``` +sudo python3 setup.py install && sudo key-mapper-gtk -d +``` + # Dependencies -`evtest` +`evtest`, `libinput` + +# Tests + +sudo is required because some tests actually read /dev stuff. + +``` +sudo python3 setup.py install && sudo python3 tests/test.py +``` # Roadmap diff --git a/keymapper/X.py b/keymapper/X.py index 2d142692..17a1a6ca 100644 --- a/keymapper/X.py +++ b/keymapper/X.py @@ -49,11 +49,59 @@ def load_keymapping(): key_mapping[search[0]] = search[1] -def find_devices(): - """Return a mapping of {name: [ids]} for each input device. +def parse_libinput_list(): + """Get a mapping of {name: [paths]} for `libinput list-devices` devices. + + This is grouped by group, so the "Logitech USB Keyboard" and + "Logitech USB Keyboard Consumer Control" are one key (the shorter one), + and the paths array for that is therefore 2 entries large. + """ + stdout = subprocess.check_output(['libinput', 'list-devices']) + devices = [ + device for device in stdout.decode().split('\n\n') + if device != '' + ] + + grouped = {} + for device in devices: + info = {} + for line in device.split('\n'): + # example: + # "Kernel: /dev/input/event0" + match = re.match(r'(\w+):\s+(.+)', line) + if match is None: + continue + info[match[1]] = match[2] + + name = info['Device'] + group = info['Group'] # int + dev = info['Kernel'] # /dev/input/event# + + if grouped.get(group) is None: + grouped[group] = [] + grouped[group].append((name, dev)) + + result = {} + for i in grouped: + group = grouped[i] + names = [entry[0] for entry in group] + devs = [entry[1] for entry in group] + shortest_name = sorted(names, key=len)[0] + result[shortest_name] = devs + + return result - Evtest listing is really slow, query this only once when the - program starts. + +def parse_evtest(): + """Get a mapping of {name: [paths]} for each evtest device. + + evtest is quite slow. + + This is grouped by name, so "Logitech USB Keyboard" and + "Logitech USB Keyboard Consumer Control" are two keys in result. Some + devices have the same name for each of those entries. + + Use parse_libinput_list instead, which properly groups all of them. """ # It asks for a device afterwads, so just insert garbage into it p = subprocess.Popen( @@ -64,52 +112,45 @@ def find_devices(): ) # 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. + result = {} for line in evtest: - match = re.search(r'event(\d+):\s+(.+)', line) + match = re.search(r'(/dev/input/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] + # the path refers to a file in /dev/input/event#. Note, that this is + # different from the id that `xinput list` can return. + path = 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) + if not result.get(name): + result[name] = [] + result[name].append(path) + return result - logger.info('Devices: %s', ', '.join(list(devices.keys()))) - return devices +def find_devices(): + """Return a mapping of {name: [paths]} for each input device.""" + result = parse_libinput_list() + logger.info('Found %s', ', '.join([f'"{name}"' for name in result])) + return result def get_xinput_list(): - """Run xinput and get the result as list.""" + """Run xinput and get the resulting device names as list.""" xinput = subprocess.check_output(['xinput', 'list', f'--name-only']) return [line for line in xinput.decode().split('\n') if line != ''] diff --git a/tests/testcases/X.py b/tests/testcases/X.py new file mode 100644 index 00000000..c460c339 --- /dev/null +++ b/tests/testcases/X.py @@ -0,0 +1,49 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# key-mapper - GUI for device specific keyboard mappings +# Copyright (C) 2020 sezanzeb +# +# This file is part of key-mapper. +# +# key-mapper is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# key-mapper is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with key-mapper. If not, see . + + +import os +import unittest + +from keymapper.logger import update_verbosity +from keymapper.X import parse_libinput_list, parse_evtest + + +class TestX(unittest.TestCase): + def check_result(self, result): + count = 0 + for name, paths in result.items(): + self.assertIsInstance(name, str) + self.assertIsInstance(paths, list) + for path in paths: + self.assertIsInstance(path, str) + self.assertTrue(path.startswith('/dev/input/event')) + count += 1 + self.assertGreater(count, 0) + + def test_libinput(self): + self.check_result(parse_libinput_list()) + + def test_evtest(self): + self.check_result(parse_evtest()) + + +if __name__ == "__main__": + unittest.main()