From 8fa747d6a4b73a8eb2a048c945995851251bea94 Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Mon, 2 Nov 2020 21:57:28 +0100 Subject: [PATCH] can apply presets --- bin/key-mapper-gtk | 18 +++++--- data/key-mapper.glade | 3 +- keymapper/X.py | 75 +++++++++++++++++++++++++------ keymapper/linux.py | 92 ++++++++++++++++++++++++++++++++++++++ keymapper/presets.py | 59 +----------------------- tests/test.py | 18 +++++++- tests/testcases/presets.py | 3 +- 7 files changed, 188 insertions(+), 80 deletions(-) create mode 100644 keymapper/linux.py diff --git a/bin/key-mapper-gtk b/bin/key-mapper-gtk index 2cb83806..99badd19 100755 --- a/bin/key-mapper-gtk +++ b/bin/key-mapper-gtk @@ -32,11 +32,10 @@ gi.require_version('GLib', '2.0') from gi.repository import Gtk from keymapper.data import get_data_path -from keymapper.X import create_setxkbmap_config, \ - create_identity_mapping -from keymapper.presets import get_presets, get_mappings, \ - find_newest_preset, create_preset, get_devices +from keymapper.X import create_setxkbmap_config, apply_preset, create_preset +from keymapper.presets import get_presets, find_newest_preset from keymapper.logger import logger, update_verbosity, log_info +from keymapper.linux import get_devices window = None @@ -140,6 +139,14 @@ class Window: # and select the newest one (on the top) preset_selection.set_active(0) + def on_apply_preset_clicked(self, button): + logger.debug( + 'Applying preset "%s" for "%s"', + self.selected_preset, + self.selected_device + ) + apply_preset(self.selected_device, self.selected_preset) + def on_select_device(self, dropdown): """List all presets, create one if none exist yet.""" device = dropdown.get_active_text() @@ -182,6 +189,7 @@ class Window: def on_add_key_clicked(self, button=None): """Add a mapping to the list of mappings.""" + # TODO automatically add a line when no line is empty anymore single_key_mapping = SingleKeyMapping(self.on_row_removed) key_list = self.get('key_list') key_list.insert_row(1) @@ -225,7 +233,7 @@ class Window: ) # TODO use user defined mapping - self.mappings = [(10, 'z')] + self.mappings = [(10, 'f')] create_setxkbmap_config( self.selected_device, diff --git a/data/key-mapper.glade b/data/key-mapper.glade index e42f5d05..e612eb31 100644 --- a/data/key-mapper.glade +++ b/data/key-mapper.glade @@ -80,13 +80,14 @@ False 10 - + gtk-apply 80 True True True True + False diff --git a/keymapper/X.py b/keymapper/X.py index f705d573..cc578e6d 100644 --- a/keymapper/X.py +++ b/keymapper/X.py @@ -38,6 +38,8 @@ import subprocess from keymapper.paths import CONFIG_PATH, SYMBOLS_PATH, KEYCODES_PATH from keymapper.logger import logger from keymapper.data import get_data_path +from keymapper.presets import get_presets +from keymapper.linux import get_devices def get_keycode(device, letter): @@ -47,6 +49,23 @@ def get_keycode(device, letter): return '' +def create_preset(device, name=None): + """Create an empty preset.""" + existing_names = get_presets(device) + if name is None: + name = 'new preset' + + # find a name that is not already taken + if name in existing_names: + i = 2 + while f'{name} {i}' in existing_names: + i += 1 + name = f'{name} {i}' + + create_setxkbmap_config(device, name, []) + return name + + def create_setxkbmap_config(device, preset, mappings): """Generate a config file for setxkbmap. @@ -78,13 +97,31 @@ def create_setxkbmap_config(device, preset, mappings): def apply_preset(device, preset): - # setxkbmap -layout key-mapper/Razer_Razer_Naga_Trinity/new_preset -keycodes key-mapper -v 10 -device 13 - # TODO device 12 is from `xinput list` but currently there is no function - # to obtain that. And that cli tool outputs all those extra devices. - # 1. get all names in the group (similar to parse_libinput_list) - # 2. get all ids from xinput list for each name - # 3. apply preset to all of them - pass + """Apply a preset to the device.""" + # apply it to every device that hangs on the same usb port, because I + # have no idea how to figure out which one of those 3 devices that are + # all named after my mouse to use. + device_underscored = device.replace(' ', '_') + preset_underscored = preset.replace(' ', '_') + + group = get_devices()[device] + + for xinput_name, xinput_id in get_xinput_id_mapping(): + if xinput_name not in group['devices']: + continue + layout_name = ( + 'key-mapper' + f'/{device_underscored}' + f'/{preset_underscored}' + ) + cmd = [ + 'setxkbmap', + '-layout', layout_name, + '-keycodes', 'key-mapper', + '-device', str(xinput_id) + ] + logger.debug('Running `%s`', ' '.join(cmd)) + subprocess.run(cmd) def create_identity_mapping(): @@ -133,8 +170,8 @@ def generate_symbols_file_content(device, preset, mappings): """ system_default = 'us' # TODO get the system default - # WARNING if the symbols file contains key codes that are not present in - # the keycodes file, the whole X session will crash! + # If the symbols file contains key codes that are not present in + # the keycodes file, THE WHOLE X SESSION WILL CRASH! if not os.path.exists(KEYCODES_PATH): raise ValueError('Expected the keycodes file to exist.') with open(KEYCODES_PATH, 'r') as f: @@ -159,7 +196,19 @@ def generate_symbols_file_content(device, preset, mappings): return result -def get_xinput_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 != ''] +def get_xinput_id_mapping(): + """Run xinput and get a list of name, id tuplies. + + The ids are needed for setxkbmap. There might be duplicate names with + different ids. + """ + names = subprocess.check_output( + ['xinput', 'list', '--name-only'] + ).decode().split('\n') + ids = subprocess.check_output( + ['xinput', 'list', '--id-only'] + ).decode().split('\n') + + names = [name for name in names if name != ''] + ids = [int(id) for id in ids if id != ''] + return zip(names, ids) diff --git a/keymapper/linux.py b/keymapper/linux.py new file mode 100644 index 00000000..3dfedd6a --- /dev/null +++ b/keymapper/linux.py @@ -0,0 +1,92 @@ +#!/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 . + + +"""Device stuff that is independent from the display server.""" + + +import evdev +import subprocess + +from keymapper.logger import logger + + +_devices = None + + +def can_grab(path): + """Can input events from the device be read? + + Parameters + ---------- + path : string + Path in dev, for example '/dev/input/event7' + """ + p = subprocess.run(['fuser', '-v', path]) + return p.returncode == 1 + + +def get_devices(): + """Group devices and get relevant infos per group. + + Returns a list containing mappings of + {group_name: {paths: [paths], devices: [names]} for input devices. + + For example, group_name could be "Logitech USB Keyboard", devices might + contain "Logitech USB Keyboard System Control" and "Logitech USB Keyboard". + paths is a list of files in /dev/input that belong to the devices. + + They are grouped by usb port. + """ + # cache the result, this takes a second to complete + global _devices + if _devices is not None: + return _devices + + devices = [evdev.InputDevice(path) for path in evdev.list_devices()] + + # group them together by usb device because there could be stuff like + # "Logitech USB Keyboard" and "Logitech USB Keyboard Consumer Control" + grouped = {} + for device in devices: + # only keyboard devices + # https://www.kernel.org/doc/html/latest/input/event-codes.html + if not evdev.ecodes.EV_KEY in device.capabilities().keys(): + continue + + usb = device.phys.split('/')[0] + if grouped.get(usb) is None: + grouped[usb] = [] + grouped[usb].append((device.name, device.path)) + + # now write down all the paths of that group + result = {} + for group in grouped.values(): + names = [entry[0] for entry in group] + devs = [entry[1] for entry in group] + shortest_name = sorted(names, key=len)[0] + result[shortest_name] = { + 'paths': devs, + 'devices': names + } + + _devices = result + logger.info('Found %s', ', '.join([f'"{name}"' for name in result])) + return result diff --git a/keymapper/presets.py b/keymapper/presets.py index e607888c..4c303809 100644 --- a/keymapper/presets.py +++ b/keymapper/presets.py @@ -24,50 +24,10 @@ import os import glob -import evdev from keymapper.paths import CONFIG_PATH from keymapper.logger import logger -from keymapper.X import create_setxkbmap_config - - -_devices = None - - -def get_devices(): - """Get a mapping of {name: [paths]} for input devices.""" - # cache the result, this takes a second to complete - global _devices - if _devices is not None: - return _devices - - devices = [evdev.InputDevice(path) for path in evdev.list_devices()] - - # group them together by usb device because there could be stuff like - # "Logitech USB Keyboard" and "Logitech USB Keyboard Consumer Control" - grouped = {} - for device in devices: - # only keyboard devices - # https://www.kernel.org/doc/html/latest/input/event-codes.html - if not evdev.ecodes.EV_KEY in device.capabilities().keys(): - continue - - usb = device.phys.split('/')[0] - if grouped.get(usb) is None: - grouped[usb] = [] - grouped[usb].append((device.name, device.path)) - - # now write down all the paths of that group - result = {} - for group in grouped.values(): - 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 - - _devices = result - logger.info('Found %s', ', '.join([f'"{name}"' for name in result])) - return result +from keymapper.linux import get_devices def get_presets(device): @@ -92,23 +52,6 @@ def get_presets(device): return presets -def create_preset(device, name=None): - """Create an empty preset.""" - existing_names = get_presets(device) - if name is None: - name = 'new preset' - - # find a name that is not already taken - if name in existing_names: - i = 2 - while f'{name} {i}' in existing_names: - i += 1 - name = f'{name} {i}' - - create_setxkbmap_config(device, name, []) - return name - - def get_mappings(device, preset): """Get all configured buttons of the preset. diff --git a/tests/test.py b/tests/test.py index 4ccb8541..2a46c08c 100644 --- a/tests/test.py +++ b/tests/test.py @@ -32,8 +32,22 @@ paths.CONFIG_PATH = '/tmp/key-mapper-test/.config' from keymapper import presets presets.get_devices = lambda: ({ - 'device 1': ['/dev/input/event10', '/dev/input/event11'], - 'device 2': ['/dev/input/event3'] + 'device 1': { + 'paths': [ + '/dev/input/event10', + '/dev/input/event11', + '/dev/input/event13' + ], + 'names': [ + 'device 1 something', + 'device 1', + 'device 1' + ] + }, + 'device 2': { + 'paths': ['/dev/input/event3'], + 'names': ['device 2'] + } }) from keymapper.logger import update_verbosity diff --git a/tests/testcases/presets.py b/tests/testcases/presets.py index 9c900bec..e5c5bf7c 100644 --- a/tests/testcases/presets.py +++ b/tests/testcases/presets.py @@ -24,7 +24,8 @@ import unittest import shutil import time -from keymapper.presets import find_newest_preset, create_preset, get_devices +from keymapper.presets import find_newest_preset +from keymapper.X import create_preset tmp = '/tmp/key-mapper-test'