From f18b0135131ea591521cad3d556416fa2eb70a75 Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Wed, 18 Nov 2020 22:06:54 +0100 Subject: [PATCH] works --- bin/key-mapper-gtk | 3 - keymapper/archive/__init__.py | 0 keymapper/archive/xkb.py | 222 ---------------------------------- keymapper/cli.py | 146 ---------------------- keymapper/getdevices.py | 11 +- keymapper/gtk/window.py | 4 - keymapper/injector.py | 77 ++++-------- keymapper/logger.py | 16 +-- keymapper/mapping.py | 1 - keymapper/state.py | 207 +++---------------------------- 10 files changed, 60 insertions(+), 627 deletions(-) delete mode 100644 keymapper/archive/__init__.py delete mode 100644 keymapper/archive/xkb.py delete mode 100644 keymapper/cli.py diff --git a/bin/key-mapper-gtk b/bin/key-mapper-gtk index b12132cf..3198ba27 100755 --- a/bin/key-mapper-gtk +++ b/bin/key-mapper-gtk @@ -35,7 +35,6 @@ from gi.repository import Gtk from keymapper.logger import logger, update_verbosity, log_info from keymapper.gtk.error import ErrorDialog from keymapper.gtk.window import Window -from keymapper.state import initialize if __name__ == '__main__': @@ -50,8 +49,6 @@ if __name__ == '__main__': update_verbosity(options.debug) log_info() - initialize() - window = Window() def stop_injecting(): diff --git a/keymapper/archive/__init__.py b/keymapper/archive/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/keymapper/archive/xkb.py b/keymapper/archive/xkb.py deleted file mode 100644 index 86995cc8..00000000 --- a/keymapper/archive/xkb.py +++ /dev/null @@ -1,222 +0,0 @@ -#!/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 . - - -"""Code that is not used anymore, but might be in the future. - -Currently it is not needed to create symbols files in xkb. Which is a pity -considering all the work put into this. This stuff is even unittested. - -Resources: -[1] https://wiki.archlinux.org/index.php/Keyboard_input -[2] http://people.uleth.ca/~daniel.odonnell/Blog/custom-keyboard-in-linuxx11 -[3] https://www.x.org/releases/X11R7.7/doc/xorg-docs/input/XKB-Enhancing.html -""" - - -import os -import re -import stat - -from keymapper.logger import logger -from keymapper.data import get_data_path -from keymapper.state import custom_mapping, internal_mapping -from keymapper.paths import KEYCODES_PATH - - -permissions = stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IROTH - -MAX_KEYCODE = 255 -MIN_KEYCODE = 8 - -# the path that contains ALL symbols, not just ours -X11_SYMBOLS = '/usr/share/X11/xkb/symbols' - -# should not contain spaces -# getlogin gets the user who ran sudo -USERS_SYMBOLS = os.path.join( - '/usr/share/X11/xkb/symbols/key-mapper', - os.getlogin().replace(' ', '_') -) - - -def get_usr_path(device=None, preset=None): - """Get the path to the config file in /usr. - - This folder is a symlink and the files are in ~/.config/key-mapper - - If preset is omitted, returns the folder for the device. - """ - if device is None: - return USERS_SYMBOLS - - device = device.strip() - - if preset is not None: - preset = preset.strip() - return os.path.join(USERS_SYMBOLS, device, preset).replace(' ', '_') - - if device is not None: - return os.path.join(USERS_SYMBOLS, device.replace(' ', '_')) - - -DEFAULT_SYMBOLS = get_usr_path('default') - - -def create_preset(device, name=None): - """Create an empty preset and return the potentially incremented name. - - Automatically avoids file conflicts by adding a number to the name - if needed. - """ - if name is None: - name = 'new preset' - - # find a name that is not already taken - if os.path.exists(get_usr_path(device, name)): - i = 2 - while os.path.exists(get_usr_path(device, f'{name} {i}')): - i += 1 - name = f'{name} {i}' - - path = get_usr_path(device, name) - if not os.path.exists(path): - logger.info('Creating new file %s', path) - os.makedirs(os.path.dirname(path), exist_ok=True) - os.mknod(path) - - # add the same permissions as other symbol files, only root may write. - os.chmod(path, permissions) - - return name - - -def get_preset_name(device, preset=None): - """Get the name for that preset that is used for the setxkbmap command.""" - # It's the relative path starting from X11/xkb/symbols and must not - # contain spaces - name = get_usr_path(device, preset)[len(X11_SYMBOLS) + 1:] - assert ' ' not in name - return name - - -DEFAULT_SYMBOLS_NAME = get_preset_name('default') -EMPTY_SYMBOLS_NAME = get_preset_name('empty') - - -def create_setxkbmap_config(device, preset): - """Generate a config file for setxkbmap. - - Parameters - ---------- - device : string - preset : string - """ - if len(custom_mapping) == 0: - logger.debug('Got empty mappings') - return None - - create_identity_mapping() - create_default_symbols() - - device_path = get_usr_path(device) - if not os.path.exists(device_path): - logger.info('Creating directory "%s"', device_path) - os.makedirs(device_path, exist_ok=True) - - preset_path = get_usr_path(device, preset) - if not os.path.exists(preset_path): - logger.info('Creating config file "%s"', preset_path) - os.mknod(preset_path) - - logger.info('Writing key mappings to %s', preset_path) - with open(preset_path, 'w') as f: - contents = generate_symbols(get_preset_name(device, preset)) - if contents is not None: - f.write(contents) - - -def parse_symbols_file(device, preset): - """Parse a symbols file populate the mapping. - - Existing mappings are overwritten if there are conflicts. - """ - path = get_usr_path(device, preset) - - if not os.path.exists(path): - logger.debug( - 'Tried to load non existing preset "%s" for %s', - preset, device - ) - custom_mapping.empty() - custom_mapping.changed = False - return - - with open(path, 'r') as f: - # from "key <12> { [ 1 ] };" extract 12 and 1, - # from "key <12> { [ a, A ] };" extract 12 and [a, A] - # avoid lines that start with special characters - # (might be comments) - # And only find those lines that have a system-keycode written - # after them, because I need that one to show in the ui. (Might - # be deprecated.) - content = f.read() - result = re.findall( - r'\n\s+?key <(.+?)>.+?\[\s+(.+?)\s+\]\s+?}; // (\d+)', - content - ) - logger.debug('Found %d mappings in preset "%s"', len(result), preset) - for target_keycode, character, system_keycode in result: - custom_mapping.change( - previous_keycode=None, - new_keycode=system_keycode, - character=character - ) - custom_mapping.changed = False - - -def create_default_symbols(): - """Parse the output of xmodmap and create a default symbols file. - - Since xmodmap may print mappings that have already been modified by - key-mapper, this should be done only once after the installation. - - This is needed because all our keycode aliases in the symbols files - are "", whereas the others are and such, so they are not - compatible. - """ - if os.path.exists(DEFAULT_SYMBOLS): - logger.debug('Found the default mapping at %s', DEFAULT_SYMBOLS) - return - - contents = generate_symbols(DEFAULT_SYMBOLS_NAME, None, system_mapping) - - if not os.path.exists(DEFAULT_SYMBOLS): - logger.info('Creating %s', DEFAULT_SYMBOLS) - os.makedirs(os.path.dirname(DEFAULT_SYMBOLS), exist_ok=True) - os.mknod(DEFAULT_SYMBOLS) - os.chmod(DEFAULT_SYMBOLS, permissions) - - with open(DEFAULT_SYMBOLS, 'w') as f: - if contents is not None: - logger.info('Updating default mappings') - f.write(contents) - else: - logger.error('Failed to write default mappings') diff --git a/keymapper/cli.py b/keymapper/cli.py deleted file mode 100644 index 2e16471e..00000000 --- a/keymapper/cli.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/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 . - - -"""Parsing and running CLI tools.""" - - -import os -import re -import subprocess - -from keymapper.logger import logger, is_debug -from keymapper.getdevices import get_devices - - -def get_system_layout_locale(): - """Get the system wide configured default keyboard layout locale.""" - localectl = subprocess.check_output( - ['localectl', 'status'] - ).decode().split('\n') - # example: - # System Locale: LANG=en_GB.UTF-8 - # VC Keymap: tmp - # X11 Layout: de - # X11 Model: pc105 - return [ - line for line in localectl - if 'X11 Layout' in line - ][0].split(': ')[-1] - - -def apply_symbols(device, name=None, keycodes=None): - """Apply a symbols configuration to the device. - - Parameters - ---------- - device : string - A device, should be a key of get_devices - name : string - This is the name of the symbols to apply. For example "de", - "key-mapper-empty" or "key-mapper-dev" - keycodes : string - This is the name of the keycodes file needed for that. If you don't - provide the correct one, X will crash. For example "key-mapper", - which is the "identity mapping", or "de" - """ - if get_devices().get(device) is None: - # maybe you should run refresh_devices - logger.error('Tried to apply symbols on unknown device "%s"', device) - return - - if name is None: - name = get_system_layout_locale() - - logger.debug('Applying symbols "%s" to device "%s"', name, device) - - # sanity check one - symbols_path = os.path.join('/usr/share/X11/xkb/symbols', name) - if not os.path.exists(symbols_path): - logger.error('Symbols file "%s" doesn\'t exist', symbols_path) - return - with open(symbols_path, 'r') as f: - if f.read() == '': - logger.error('Tried to load empty symbols %s', symbols_path) - return - - if keycodes is not None: - # sanity check two - keycodes_path = os.path.join('/usr/share/X11/xkb/keycodes', keycodes) - if not os.path.exists(keycodes_path): - logger.error('keycodes "%s" don\'t exist', keycodes_path) - return - with open(keycodes_path, 'r') as f: - if f.read() == '': - logger.error('Found empty keycodes "%s"', keycodes_path) - return - - cmd = ['setxkbmap', '-layout', name] - if keycodes is not None: - cmd += ['-keycodes', keycodes] - - # 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. - group = get_devices()[device] - for xinput_name, xinput_id in get_xinput_id_mapping(): - if xinput_name not in group['devices']: - # only all virtual devices of the same hardware device - continue - - device_cmd = cmd + ['-device', str(xinput_id)] - logger.debug('Running `%s`', ' '.join(device_cmd)) - output = subprocess.run(device_cmd, capture_output=True) - output = output.stderr.decode().strip() - if output != '': - logger.debug2(output) - - -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) - - -def parse_xmodmap(mapping): - """Read the output of xmodmap into a mapping.""" - xmodmap = subprocess.check_output(['xmodmap', '-pke']).decode() + '\n' - mappings = re.findall(r'(\d+) = (.+)\n', xmodmap) - # TODO is this tested? - for keycode, characters in mappings: - # this is the "array" format needed for symbols files - character = ', '.join(characters.split()) - mapping.change( - previous_keycode=None, - new_keycode=int(keycode), - character=character - ) diff --git a/keymapper/getdevices.py b/keymapper/getdevices.py index 7b6dc1fa..07f88419 100644 --- a/keymapper/getdevices.py +++ b/keymapper/getdevices.py @@ -54,6 +54,7 @@ class _GetDevicesProcess(multiprocessing.Process): def run(self): """Do what get_devices describes.""" + logger.debug('Discovering device paths') devices = [evdev.InputDevice(path) for path in evdev.list_devices()] # group them together by usb device because there could be stuff like @@ -67,12 +68,20 @@ class _GetDevicesProcess(multiprocessing.Process): continue if evdev.ecodes.EV_REL in capabilities: - # skip devices that control movement + # skip devices that control movement, because I need to + # grab the device and + logger.debug( + 'Skipping %s to avoid impairing mouse movement', + device.path + ) continue usb = device.phys.split('/')[0] if grouped.get(usb) is None: grouped[usb] = [] + + logger.debug('Adding %s', device.path) + grouped[usb].append((device.name, device.path)) # now write down all the paths of that group diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py index 89ec630f..9ad6e8bb 100755 --- a/keymapper/gtk/window.py +++ b/keymapper/gtk/window.py @@ -22,9 +22,7 @@ """User Interface.""" -import sys import gi -import time gi.require_version('Gtk', '3.0') gi.require_version('GLib', '2.0') from gi.repository import Gtk, Gdk, GLib @@ -35,7 +33,6 @@ from keymapper.presets import get_presets, find_newest_preset, \ delete_preset, rename_preset, get_available_preset_name from keymapper.logger import logger from keymapper.injector import KeycodeInjector -from keymapper.cli import apply_symbols from keymapper.getdevices import get_devices from keymapper.gtk.row import Row from keymapper.gtk.unsaved import unsaved_changes_dialog, GO_BACK @@ -208,7 +205,6 @@ class Window: """Load the mapping.""" if self.keycode_reader is not None: self.keycode_reader.stop_injecting() - apply_symbols(self.selected_device) self.get('status_bar').push( CTX_APPLY, f'Applied the system default' diff --git a/keymapper/injector.py b/keymapper/injector.py index 2187f882..2e74a386 100644 --- a/keymapper/injector.py +++ b/keymapper/injector.py @@ -30,13 +30,12 @@ import asyncio import evdev from keymapper.logger import logger -from keymapper.cli import apply_symbols -from keymapper.getdevices import get_devices, refresh_devices -from keymapper.state import custom_mapping, internal_mapping, \ - system_mapping, capabilities +from keymapper.getdevices import get_devices +from keymapper.state import custom_mapping, system_mapping DEV_NAME = 'key-mapper' +DEVICE_CREATED = 1 def can_grab(path): @@ -71,23 +70,19 @@ class KeycodeInjector: ', '.join(paths) ) - apply_symbols(self.device, name='key-mapper-empty') - # Watch over each one of the potentially multiple devices per hardware for path in paths: + pipe = multiprocessing.Pipe() worker = multiprocessing.Process( target=self._start_injecting_worker, - args=(path, custom_mapping) + args=(path, custom_mapping, pipe[1]) ) worker.start() + # wait for the process to notify creation of the new injection + # device, to keep the logs in order. + pipe[0].recv() self.processes.append(worker) - # it takes a little time for the key-mapper devices to appear - time.sleep(0.1) - - refresh_devices() - apply_symbols(DEV_NAME, name='key-mapper-dev', keycodes='key-mapper') - def stop_injecting(self): """Stop injecting keycodes.""" # TODO test @@ -100,30 +95,24 @@ class KeycodeInjector: process.terminate() self.processes[i] = None - # apply the default layout back - apply_symbols(self.device) - - def _start_injecting_worker(self, path, mapping): + def _start_injecting_worker(self, path, mapping, pipe): """Inject keycodes for one of the virtual devices.""" # TODO test loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) device = evdev.InputDevice(path) - """try: + try: # grab to avoid e.g. the disabled keycode of 10 to confuse X, # especially when one of the buttons of your mouse also uses 10 device.grab() except IOError: - logger.error('Cannot grab %s', path)""" + logger.error('Cannot grab %s', path) - # foo = evdev.InputDevice('/dev/input/event2') - keymapper_device = evdev.UInput( - name=DEV_NAME, - phys='key-mapper-uinput', - events={ - evdev.ecodes.EV_KEY: [c - 8 for c in capabilities] - } - ) + # copy the capabilities because the keymapper_device is going + # to act like the mouse + keymapper_device = evdev.UInput.from_device(device) + + pipe.send(DEVICE_CREATED) logger.debug( 'Started injecting into %s, fd %s', @@ -132,6 +121,12 @@ class KeycodeInjector: for event in device.read_loop(): if event.type != evdev.ecodes.EV_KEY: + logger.spam( + 'got type:%s code:%s value:%s, forward', + event.type, event.code, event.value + ) + keymapper_device.write(event.type, event.code, event.value) + keymapper_device.syn() continue if event.value == 2: @@ -155,20 +150,8 @@ class KeycodeInjector: character ) continue - # turns out, if I don't sleep here X/Linux gets confused. Lets - # assume a mapping of 10 to z. Without sleep it would always - # result in 1z 1z 1z. Even though the empty xkb symbols file - # was applied on the mouse! And I really made sure `write` was - # not called twice. '1' just somewhow sneaks past the symbols. - # 0.0005 has many errors. 0.001 has them super rare. - # 5ms is still faster than anything on the planet so that's. - # fine. I came up with that after randomly poking around in, - # frustration. I don't know of any helpful resource that - # explains this - # TODO still needed? if yes, add to HELP.md - time.sleep(0.005) - logger.debug2( + logger.spam( 'got code:%s value:%s, maps to code:%s char:%s', event.code + 8, event.value, target_keycode, character ) @@ -180,18 +163,4 @@ class KeycodeInjector: event.value ) - # the second device that starts writing an event.value of 2 will - # take ownership of what is happening. Following example: - # (KB = keyboard, example devices) - # hold a on KB1: - # a-1, a-2, a-2, a-2, ... - # hold shift on KB2: - # shift-2, shift-2, shift-2, ... - # No a-2 on KB1 happening anymore. The xkb symbols of KB2 will - # be used! So if KB2 maps shift+a to b, it will write b, even - # though KB1 maps shift+a to c! And if you reverse this, hold - # shift on KB2 first and then a on KB1, the xkb mapping of KB1 - # will take effect and write c! - - # foo.write(evdev.ecodes.EV_SYN, evdev.ecodes.SYN_REPORT, 0) keymapper_device.syn() diff --git a/keymapper/logger.py b/keymapper/logger.py index b5e74ed6..e20dfc0e 100644 --- a/keymapper/logger.py +++ b/keymapper/logger.py @@ -27,15 +27,15 @@ import logging import pkg_resources -def debug2(self, message, *args, **kws): - if self.isEnabledFor(DEBUG2): +def spam(self, message, *args, **kws): + if self.isEnabledFor(SPAM): # https://stackoverflow.com/a/13638084 - self._log(DEBUG2, message, args, **kws) + self._log(SPAM, message, args, **kws) -DEBUG2 = 5 -logging.addLevelName(DEBUG2, "DEBUG2") -logging.Logger.debug2 = debug2 +SPAM = 5 +logging.addLevelName(SPAM, "SPAM") +logging.Logger.spam = spam class Formatter(logging.Formatter): @@ -53,7 +53,7 @@ class Formatter(logging.Formatter): logging.ERROR: 31, logging.FATAL: 31, logging.DEBUG: 36, - DEBUG2: 34, + SPAM: 34, logging.INFO: 32, }.get(record.levelno, 0) if debug: @@ -96,7 +96,7 @@ def log_info(): def update_verbosity(debug): """Set the logging verbosity according to the settings object.""" if debug: - logger.setLevel(DEBUG2) + logger.setLevel(SPAM) else: logger.setLevel(logging.INFO) diff --git a/keymapper/mapping.py b/keymapper/mapping.py index 2246dcc4..4d998807 100644 --- a/keymapper/mapping.py +++ b/keymapper/mapping.py @@ -28,7 +28,6 @@ import shutil from keymapper.logger import logger from keymapper.paths import get_config_path -from keymapper.cli import parse_xmodmap class Mapping: diff --git a/keymapper/state.py b/keymapper/state.py index 477ecd45..7142071e 100644 --- a/keymapper/state.py +++ b/keymapper/state.py @@ -22,204 +22,35 @@ """Create some files and objects that are needed for the app to work.""" -import os -import re import stat - -import evdev +import re +import subprocess from keymapper.mapping import Mapping -from keymapper.cli import parse_xmodmap, apply_symbols -from keymapper.logger import logger -from keymapper.paths import KEYCODES_PATH, SYMBOLS_PATH -from keymapper.data import get_data_path + + +def parse_xmodmap(mapping): + """Read the output of xmodmap into a mapping.""" + xmodmap = subprocess.check_output(['xmodmap', '-pke']).decode() + '\n' + mappings = re.findall(r'(\d+) = (.+)\n', xmodmap) + # TODO is this tested? + for keycode, characters in mappings: + # this is the "array" format needed for symbols files + character = ', '.join(characters.split()) + mapping.change( + previous_keycode=None, + new_keycode=int(keycode), + character=character + ) # one mapping object for the whole application that holds all # customizations, as shown in the UI custom_mapping = Mapping() -# this mapping is for the custom key-mapper /dev device. The keycode -# injector injects those keys to trigger the wanted character -internal_mapping = Mapping() - +# this mapping represents the xmodmap output, which stays constant system_mapping = Mapping() - -capabilities = [] +parse_xmodmap(system_mapping) # permissions for files created in /usr _PERMISSIONS = stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IROTH - - -def create_identity_mapping(keycodes=None): - """Because the concept of "reasonable symbolic names" [3] doesn't apply - when mouse buttons are all over the place. Create an identity mapping - to make generating "symbols" files easier. Keycode 10 -> "<10>" - - The identity mapping is provided to '-keycodes' of setxkbmap. - """ - logger.debug2('Available keycodes: %s', keycodes) - min_keycode = min(keycodes) - max_keycode = max(keycodes) - logger.debug( - 'Creating the identity mapping. min: %s, max: %s', - min_keycode, - max_keycode - ) - - xkb_keycodes = [] - if keycodes is None: - keycodes = range(min_keycode, max_keycode + 1) - - for keycode in keycodes: - xkb_keycodes.append(f'<{keycode}> = {keycode};') - - template_path = get_data_path('xkb_keycodes_template') - with open(template_path, 'r') as template_file: - template = template_file.read() - - result = template.format( - minimum=min_keycode, - maximum=max_keycode, - xkb_keycodes='\n '.join(xkb_keycodes) - ) - - if not os.path.exists(KEYCODES_PATH): - logger.debug('Creating "%s"', KEYCODES_PATH) - os.makedirs(os.path.dirname(KEYCODES_PATH), exist_ok=True) - os.mknod(KEYCODES_PATH) - os.chmod(KEYCODES_PATH, _PERMISSIONS) - - with open(KEYCODES_PATH, 'w') as f: - f.write(result) - - -def generate_symbols(mapping): - """Create config contents to be placed in /usr/share/X11/xkb/symbols. - - It's the mapping of the preset as expected by X. This function does not - create the file. - - Parameters - ---------- - mapping : Mapping - If you need to create a symbols file for some other mapping you can - pass it to this parameter. By default the custom mapping will be - used that is also displayed in the user interface. - """ - if len(mapping) == 0: - raise ValueError('Mapping is empty') - - # 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 FileNotFoundError('Expected the keycodes file to exist') - - with open(KEYCODES_PATH, 'r') as f: - keycodes = re.findall(r'<.+?>', f.read()) - - xkb_symbols = [] - for keycode, character in mapping: - if f'<{keycode}>' not in keycodes: - logger.error(f'Unknown code <{keycode}> for "{character}"') - # don't append that one, otherwise X would crash when loading - continue - - xkb_symbols.append( - f'key <{keycode}> {{ [ {character} ] }}; ' - ) - - if len(xkb_symbols) == 0: - logger.error('Failed to populate xkb_symbols') - return None - - template_path = get_data_path('xkb_symbols_template') - with open(template_path, 'r') as template_file: - template = template_file.read() - - result = template.format( - name='key-mapper', - xkb_symbols='\n '.join(xkb_symbols) - ) - - return result - - -def find_all_used_capabilities(): - """Find all capabilities of all devices that are already in use.""" - base = '/dev/input' - inputs = os.listdir(base) - result = [] - for input in inputs: - try: - device = evdev.InputDevice(os.path.join(base, input)) - for codes in device.capabilities().values(): - for code in codes: - if not isinstance(code, int): - continue - code += 8 - if code not in result: - result.append(code) - except OSError: - pass - return result - - -find_all_used_capabilities() - - -def initialize(): - """Prepare all files and objects that are needed.""" - # this mapping represents the xmodmap output, which stays constant - # TODO verify that this is the system default and not changed when I - # setxkbmap my mouse - parse_xmodmap(system_mapping) - - # find keycodes that are unused in xmodmap. - i = 8 - # used_codes = find_all_used_capabilities() - while len(capabilities) < len(system_mapping): - if system_mapping.get_character(i) is None: - capabilities.append(i) - # if i not in used_codes: - # capabilities.append(i) - i += 1 - - # basically copy the xmodmap system mapping into another one, but - # with keycodes that don't conflict, so that I'm free to use them - # whenever I want without worrying about my keyboards "1" and my - # mouses "whatever" to clash. - for i, (_, character) in enumerate(system_mapping): - internal_mapping.change( - previous_keycode=None, - new_keycode=capabilities[i], - character=character - ) - - """# now take all holes between 8 and the maximum keycode in internal_mapping - # and fill them with none - for i in range(8, capabilities[-1]): - if i in capabilities: - continue - capabilities.append(i) - internal_mapping.change( - previous_keycode=None, - new_keycode=i, - character='none' - )""" - - # assert len(system_mapping) == len(internal_mapping) - - logger.debug('Prepared the internal mapping') - - # Specify "keycode 300 belongs to mapping <300>", which is then used - # to map keycode 300 to a character. - create_identity_mapping(capabilities) - - # now put the internal_mapping into a symbols file, which is applied - # on key-mappers own /dev input. - with open(SYMBOLS_PATH, 'w') as f: - contents = generate_symbols(internal_mapping) - if contents is not None: - f.write(contents) - logger.debug('Wrote symbols file %s', SYMBOLS_PATH)