parsing libinput_list

xkb
sezanzeb 4 years ago committed by sezanzeb
parent 26081ac612
commit fecfa01bc6

@ -10,9 +10,23 @@ work.
<img src="data/screenshot.png"/> <img src="data/screenshot.png"/>
</p> </p>
# Running
```
sudo python3 setup.py install && sudo key-mapper-gtk -d
```
# Dependencies # 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 # Roadmap

@ -49,11 +49,59 @@ def load_keymapping():
key_mapping[search[0]] = search[1] key_mapping[search[0]] = search[1]
def find_devices(): def parse_libinput_list():
"""Return a mapping of {name: [ids]} for each input device. """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 # It asks for a device afterwads, so just insert garbage into it
p = subprocess.Popen( p = subprocess.Popen(
@ -64,52 +112,45 @@ def find_devices():
) )
# the list we are looking for is in stderr # the list we are looking for is in stderr
_, evtest = p.communicate() _, evtest = p.communicate()
evtest = [ evtest = [
line line
for line in evtest.decode().split('\n') for line in evtest.decode().split('\n')
if line.startswith('/dev') if line.startswith('/dev')
] ]
logger.debug('evtest devices: \n%s', '\n'.join(evtest)) logger.debug('evtest devices: \n%s', '\n'.join(evtest))
# evtest also returns a bunch of other devices, like some audio devices, # evtest also returns a bunch of other devices, like some audio devices,
# so check this list against `xinput list` to get keyboards and mice # so check this list against `xinput list` to get keyboards and mice
xinput = get_xinput_list() xinput = get_xinput_list()
logger.debug('xinput devices: \n%s', '\n'.join(xinput)) logger.debug('xinput devices: \n%s', '\n'.join(xinput))
devices = {} result = {}
# 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: 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: if match is None:
continue continue
# the id refers to a file in /dev/input, it is different from # the path refers to a file in /dev/input/event#. Note, that this is
# the id that `xinput list` can return. # different from the id that `xinput list` can return.
id = match[1] path = match[1]
name = match[2] name = match[2]
if name not in xinput: if name not in xinput:
continue continue
# there can be if not result.get(name):
# 'Logitech USB Keyboard' and result[name] = []
# 'Logitech USB Keyboard Consumer Control' result[name].append(path)
if not devices.get(name): return result
devices[name] = []
devices[name].append(id)
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(): 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']) xinput = subprocess.check_output(['xinput', 'list', f'--name-only'])
return [line for line in xinput.decode().split('\n') if line != ''] return [line for line in xinput.decode().split('\n') if line != '']

@ -0,0 +1,49 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2020 sezanzeb <proxima@hip70890b.de>
#
# 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 <https://www.gnu.org/licenses/>.
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()
Loading…
Cancel
Save