diff --git a/HELP.md b/HELP.md index 665df0d7..c416d682 100644 --- a/HELP.md +++ b/HELP.md @@ -1,7 +1,7 @@ # The problems with overwriting keys -Branches for all that stuff exist to archive it instead of loosing it forever. -Look for branches called "fifth", "fourth", etc. +Branches for some of that stuff exist to archive it instead of loosing it +forever. **Initial target** You write a symbols file based on your specified mapping, and that's pretty much it. There were two mappings: The first one is in the @@ -13,13 +13,15 @@ and writes 1/! on keycode 10, then you would not be able to write ! by pressing that mouse button and that keyboard button at the same time. Keycodes may not clash. +This was quite mature, pretty much finished. + **The second idea** was to write special keycodes known only to key-mapper (256 - 511) into the input device of your mouse in /dev/input, and map those to SHIFT and such, whenever a button is clicked. A mapping would have existed to prevent the original keycode 10 from writing a 1. But X/Linux seem to ignore anything greater than 255 for regular keyboard events, or even crash in some cases. Mouse click buttons can use those high keycodes though, -but they cannot be remapped, which I guess is another indicator of that. +but they cannot be remapped. **The third idea** is to create a new input device that uses 8 - 255, just like other layouts, and key-mapper always tries to use the same keycodes for @@ -39,7 +41,8 @@ SHIFT as already used in the system default. The pipeline is like this: But this is a rather complicated approach. The mapping of 10 -> 50 would have to be stored somewhere as well. It would make the mess of configuration -files already needed for xkb even worse. +files already needed for xkb even worse. This idea was not considered for +long, so no "third" branch exists. **Fourth idea**: Based on the first idea, instead of using keycodes greater than 255, use unused keycodes starting from 255, going down. Issues existed @@ -52,22 +55,10 @@ linux just completely ignores some keycodes. 140 works, 145 won't, 150 works. **Fifth idea**: Instead of writing xkb symbol files, just disable all mouse buttons with a single symbol file. Key-mapper listens for key events -in /dev and then writes the mapped keycode into /dev. For example, if 10 -should be mapped to Shift_L, xkb configs would disable key 10 and key-mapper -would write 50 into /dev, which is Shift_L in xmodmaps output. This sounds -incredibly simple and makes me throw away tons of code. - - -# The various mappings - -There were two mappings: The first one is in the keycodes file and contains -"<10> = 10", which is super redundant but needed for xkb. The second one -mapped "<10>" to characters, modifiers, etc. using symbol files in xkb. - - -The third mapping reads the input keycodes from your mouse (also known as -system_keycode here) and writes a different one into /dev (also known as -target_keycode here). It is explained above why. +in /dev and then writes the mapped keycode into a new device in /dev. For +example, if 10 should be mapped to Shift_L, xkb configs would disable +key 10 and key-mapper would write 50 into /dev, which is Shift_L in xmodmaps +output. This sounds incredibly simple and makes me throw away tons of code. # How I would have liked it to be diff --git a/keymapper/X.py b/keymapper/archive.py similarity index 70% rename from keymapper/X.py rename to keymapper/archive.py index 4b6b90cb..0a4912ca 100644 --- a/keymapper/X.py +++ b/keymapper/archive.py @@ -19,11 +19,9 @@ # along with key-mapper. If not, see . -"""Stuff that interacts with the X Server, be it commands or config files. +"""Quite some code that is not used anymore, but might be in the future. -TODO create a base class to standardize the interface if a different - display server should be supported. Or does wayland use the same - config files? +Currently it is not needed to create symbols files in xkb. Very sad. Resources: [1] https://wiki.archlinux.org/index.php/Keyboard_input @@ -35,19 +33,19 @@ Resources: import os import re import stat -import subprocess from keymapper.paths import get_usr_path, KEYCODES_PATH, DEFAULT_SYMBOLS, \ X11_SYMBOLS -from keymapper.logger import logger, is_debug +from keymapper.logger import logger from keymapper.data import get_data_path -from keymapper.linux import get_devices -from keymapper.mapping import custom_mapping, system_mapping, \ - Mapping, MIN_KEYCODE, MAX_KEYCODE +from keymapper.mapping import custom_mapping, system_mapping, Mapping permissions = stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IROTH +MAX_KEYCODE = 255 +MIN_KEYCODE = 8 + def create_preset(device, name=None): """Create an empty preset and return the potentially incremented name. @@ -119,75 +117,7 @@ def get_preset_name(device, preset=None): DEFAULT_SYMBOLS_NAME = get_preset_name('default') - - -def apply_preset(device, preset): - """Apply a preset to the device. - - Parameters - ---------- - device : string - preset : string - """ - layout = get_preset_name(device, preset) - setxkbmap(device, layout) - - -def get_system_layout(): - """Get the system wide configured default keyboard layout.""" - 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 setxkbmap(device, layout): - """Apply a preset to the device. - - Parameters - ---------- - device : string - layout : string or None - For example 'de', passed to setxkbmap unmodified. If None, will - load the system default - """ - if layout is not None: - path = os.path.join(X11_SYMBOLS, layout) - if not os.path.exists(path): - logger.error('Symbols %s don\'t exist', path) - return - with open(path, 'r') as f: - if f.read() == '': - logger.error('Tried to load empty symbols %s', path) - return - - logger.info('Applying layout "%s" on device %s', layout, device) - group = get_devices()[device] - - if layout is None: - cmd = ['setxkbmap', '-layout', get_system_layout()] - else: - cmd = ['setxkbmap', '-layout', layout, '-keycodes', 'key-mapper'] - - # 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. - 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)) - subprocess.run(device_cmd, capture_output=(not is_debug())) +EMPTY_SYMBOLS_NAME = get_preset_name('empty') def create_identity_mapping(): @@ -197,15 +127,15 @@ def create_identity_mapping(): This has the added benefit that keycodes reported by xev can be identified in the symbols file. + + The identity mapping is provided to '-keycodes' of setxkbmap. """ if os.path.exists(KEYCODES_PATH): logger.debug('Found the keycodes file at %s', KEYCODES_PATH) return xkb_keycodes = [] - maximum = MAX_KEYCODE - minimum = MIN_KEYCODE - for keycode in range(minimum, maximum + 1): + for keycode in range(MIN_KEYCODE, MAX_KEYCODE + 1): xkb_keycodes.append(f'<{keycode}> = {keycode};') template_path = get_data_path('xkb_keycodes_template') @@ -213,8 +143,8 @@ def create_identity_mapping(): template = template_file.read() result = template.format( - minimum=minimum, - maximum=maximum, + minimum=MIN_KEYCODE, + maximum=MAX_KEYCODE, xkb_keycodes='\n '.join(xkb_keycodes) ) @@ -229,7 +159,7 @@ def create_identity_mapping(): def generate_symbols( - name, include=DEFAULT_SYMBOLS_NAME, mapping=custom_mapping + name, include=DEFAULT_SYMBOLS_NAME, mapping=custom_mapping ): """Create config contents to be placed in /usr/share/X11/xkb/symbols. @@ -304,24 +234,6 @@ def generate_symbols( return result -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_symbols_file(device, preset): """Parse a symbols file populate the mapping. @@ -344,7 +256,8 @@ def parse_symbols_file(device, preset): # 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. + # 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+)', @@ -355,31 +268,11 @@ def parse_symbols_file(device, preset): custom_mapping.change( previous_keycode=None, new_keycode=system_keycode, - character=character, - target_keycode=int(target_keycode) + character=character ) custom_mapping.changed = False -def parse_xmodmap(): - """Read the output of xmodmap as a Mapping object.""" - xmodmap = subprocess.check_output(['xmodmap', '-pke']).decode() + '\n' - mappings = re.findall(r'(\d+) = (.+)\n', xmodmap) - # TODO is this tested? - for keycode, characters in mappings: - system_mapping.change( - previous_keycode=None, - new_keycode=int(keycode), - character=', '.join(characters.split()), - target_keycode=None - ) - - -# TODO verify that this is the system default and not changed when I -# setxkbmap my mouse -parse_xmodmap() - - def create_default_symbols(): """Parse the output of xmodmap and create a default symbols file. diff --git a/keymapper/cli.py b/keymapper/cli.py new file mode 100644 index 00000000..f7a5ae95 --- /dev/null +++ b/keymapper/cli.py @@ -0,0 +1,145 @@ +#!/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.paths import X11_SYMBOLS +from keymapper.logger import logger, is_debug +from keymapper.getdevices import get_devices +from keymapper.mapping import system_mapping + + +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 setxkbmap(device, layout): + """Apply a preset to the device. + + Parameters + ---------- + device : string + layout : string or None + For example 'de', passed to setxkbmap unmodified. If None, will + load the system default + """ + if layout is not None: + path = os.path.join(X11_SYMBOLS, layout) + if not os.path.exists(path): + logger.error('Symbols %s don\'t exist', path) + return + with open(path, 'r') as f: + if f.read() == '': + logger.error('Tried to load empty symbols %s', path) + return + + logger.info('Applying layout "%s" on device %s', layout, device) + group = get_devices()[device] + + if layout is None: + cmd = ['setxkbmap', '-layout', get_system_layout_locale()] + else: + cmd = ['setxkbmap', '-layout', layout, '-keycodes', 'key-mapper'] + + # 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. + 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)) + subprocess.run(device_cmd, capture_output=(not is_debug())) + + +def apply_empty_symbols(device): + """Make the device not write any character anymore.""" + logger.debug('Applying the empty symbols to %s', device) + group = get_devices()[device] + + cmd = ['setxkbmap', '-layout', 'key-mapper/empty'] + + # 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. + 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)) + subprocess.run(device_cmd, capture_output=(not is_debug())) + + +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(): + """Read the output of xmodmap as a Mapping object.""" + xmodmap = subprocess.check_output(['xmodmap', '-pke']).decode() + '\n' + mappings = re.findall(r'(\d+) = (.+)\n', xmodmap) + # TODO is this tested? + for keycode, characters in mappings: + system_mapping.change( + previous_keycode=None, + new_keycode=int(keycode), + character=characters.split() + ) + + +# TODO verify that this is the system default and not changed when I +# setxkbmap my mouse +parse_xmodmap() diff --git a/keymapper/getdevices.py b/keymapper/getdevices.py new file mode 100644 index 00000000..a92a2c6b --- /dev/null +++ b/keymapper/getdevices.py @@ -0,0 +1,106 @@ +#!/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 and evdev stuff that is independent from the display server.""" + + +import multiprocessing + +import evdev + +from keymapper.logger import logger + + +_devices = None + + +class GetDevicesProcess(multiprocessing.Process): + """Process to get the devices that can be worked with. + + Since InputDevice destructors take quite some time, do this + asynchronously so that they can take as much time as they want without + slowing down the initialization. To avoid evdevs asyncio stuff spamming + errors, do this with multiprocessing and not multithreading. + TODO to threading, make eventloop + """ + def __init__(self, pipe): + """Construct the process. + + Parameters + ---------- + pipe : multiprocessing.Pipe + used to communicate the result + """ + self.pipe = pipe + super().__init__() + + def run(self): + """Do what get_devices describes.""" + 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 evdev.ecodes.EV_KEY not 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 + } + + self.pipe.send(result) + + +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. + """ + global _devices + if _devices is None: + pipe = multiprocessing.Pipe() + GetDevicesProcess(pipe[1]).start() + # block until devices are available + _devices = pipe[0].recv() + logger.info('Found %s', ', '.join([f'"{name}"' for name in _devices])) + return _devices diff --git a/keymapper/gtk/row.py b/keymapper/gtk/row.py index 6c367ce6..28d04c25 100644 --- a/keymapper/gtk/row.py +++ b/keymapper/gtk/row.py @@ -27,7 +27,7 @@ gi.require_version('Gtk', '3.0') gi.require_version('GLib', '2.0') from gi.repository import Gtk -from keymapper.mapping import custom_mapping, DONTMAP, GENERATE +from keymapper.mapping import custom_mapping from keymapper.logger import logger @@ -72,8 +72,7 @@ class Row(Gtk.ListBoxRow): custom_mapping.change( previous_keycode=None, new_keycode=keycode, - character=character, - target_keycode=GENERATE + character=character ) def on_key_pressed(self, button, event): @@ -113,8 +112,7 @@ class Row(Gtk.ListBoxRow): custom_mapping.change( previous_keycode=previous_keycode, new_keycode=new_keycode, - character=character, - target_keycode=GENERATE + character=character ) def put_together(self, keycode, character): diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py index ee8cb1fb..cb573d5e 100755 --- a/keymapper/gtk/window.py +++ b/keymapper/gtk/window.py @@ -28,13 +28,13 @@ gi.require_version('GLib', '2.0') from gi.repository import Gtk, Gdk, GLib from keymapper.data import get_data_path -from keymapper.X import create_setxkbmap_config, apply_preset, \ - create_preset, custom_mapping, system_mapping, parse_symbols_file, \ - setxkbmap +from keymapper.mapping import custom_mapping from keymapper.presets import get_presets, find_newest_preset, \ delete_preset, rename_preset from keymapper.logger import logger -from keymapper.linux import get_devices, KeycodeReader +from keymapper.linux import KeycodeReader +from keymapper.cli import setxkbmap +from keymapper.getdevices import get_devices from keymapper.gtk.row import Row from keymapper.gtk.unsaved import unsaved_changes_dialog, GO_BACK @@ -164,7 +164,9 @@ class Window: presets = get_presets(device) self.get('preset_name_input').set_text('') if len(presets) == 0: - presets = [create_preset(device)] + # presets = [create_preset(device)] + # TODO create one empty preset + presets = [] else: logger.debug('Presets for "%s": %s', device, ', '.join(presets)) preset_selection = self.get('preset_selection') @@ -243,12 +245,11 @@ class Window: self.selected_preset, self.selected_device ) - apply_preset(self.selected_device, self.selected_preset) self.get('status_bar').push( CTX_APPLY, f'Applied "{self.selected_preset}"' ) - keycode_reader = KeycodeReader(self.selected_device) + KeycodeReader(self.selected_device) def on_select_device(self, dropdown): """List all presets, create one if none exist yet.""" @@ -275,7 +276,10 @@ class Window: return try: - new_preset = create_preset(self.selected_device) + # new_preset = create_preset(self.selected_device) + # TODO create a preset file, tell custom_mapping to do so + # or something + new_preset = 'new_preset' self.get('preset_selection').append(new_preset, new_preset) self.get('preset_selection').set_active_id(new_preset) except PermissionError as e: @@ -300,7 +304,7 @@ class Window: logger.debug('Selecting preset "%s"', preset) self.selected_preset = preset - parse_symbols_file(self.selected_device, self.selected_preset) + # TODO load config into custom_mapping key_list = self.get('key_list') for keycode, output in custom_mapping: @@ -345,10 +349,7 @@ class Window: self.selected_preset ) - create_setxkbmap_config( - self.selected_device, - self.selected_preset - ) + # TODO tell the mapping to dump itself as JSON somewhere custom_mapping.changed = False self.unhighlight_all_rows() diff --git a/keymapper/linux.py b/keymapper/linux.py index 8e42305a..13c60cb8 100644 --- a/keymapper/linux.py +++ b/keymapper/linux.py @@ -23,17 +23,16 @@ import subprocess -import multiprocessing +import time import threading import asyncio import evdev from keymapper.logger import logger -from keymapper.mapping import custom_mapping, MAX_KEYCODE, MIN_KEYCODE - - -DEVNODE = '/dev/keymapper' +from keymapper.cli import apply_empty_symbols +from keymapper.getdevices import get_devices +from keymapper.mapping import custom_mapping, system_mapping def can_grab(path): @@ -67,15 +66,14 @@ class KeycodeReader: pass def start_injecting_worker(self, path): - """Inject keycodes for one of the virtual devices. - - This depends on a setxkbmap-loaded symbol file that contains - the mappings for keycodes in the range of MIN and MAX_KEYCODE. - """ + """Inject keycodes for one of the virtual devices.""" loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) device = evdev.InputDevice(path) - keymapper_device = evdev.UInput(devnode=DEVNODE) + keymapper_device = evdev.UInput( + name='key-mapper', + phys='key-mapper-uinput' + ) for event in device.read_loop(): if event.type != evdev.ecodes.EV_KEY: @@ -85,24 +83,38 @@ class KeycodeReader: # than the ones reported by xev and that X expects input_keycode = event.code + 8 - if custom_mapping.get_keycode(input_keycode) is None: - # unknown keycode, skip - continue - - target_keycode = custom_mapping.get_keycode(input_keycode) - - if target_keycode > MAX_KEYCODE or target_keycode < MIN_KEYCODE: - continue - - print('read', input_keycode, 'write', target_keycode, path) + character = custom_mapping.get_character(input_keycode) + + if character is None: + # unknown keycode, forward it + target_keycode = input_keycode + else: + target_keycode = system_mapping.get_keycode(character) + if target_keycode is None: + logger.error( + 'Cannot find character %s in xmodmap', + 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 + time.sleep(0.005) # TODO test for the stuff put into write - keymapper_device.write(evdev.ecodes.EV_KEY, target_keycode, event.value) + keymapper_device.write(evdev.ecodes.EV_KEY, target_keycode - 8, event.value) keymapper_device.syn() def start_injecting(self): """Read keycodes and inject the mapped character forever.""" - paths = _devices[self.device]['paths'] + paths = get_devices()[self.device]['paths'] logger.debug( 'Starting injecting the mapping for %s on %s', @@ -110,6 +122,8 @@ class KeycodeReader: ', '.join(paths) ) + apply_empty_symbols(self.device) + # Watch over each one of the potentially multiple devices per hardware for path in paths: threading.Thread( @@ -131,80 +145,3 @@ class KeycodeReader: # than the ones reported by xev newest_keycode = event.code + 8 return newest_keycode - - -_devices = None - - -class GetDevicesProcess(multiprocessing.Process): - """Process to get the devices that can be worked with. - - Since InputDevice destructors take quite some time, do this - asynchronously so that they can take as much time as they want without - slowing down the initialization. To avoid evdevs asyncio stuff spamming - errors, do this with multiprocessing and not multithreading. - TODO to threading, make eventloop - """ - def __init__(self, pipe): - """Construct the process. - - Parameters - ---------- - pipe : multiprocessing.Pipe - used to communicate the result - """ - self.pipe = pipe - super().__init__() - - def run(self): - """Do what get_devices describes.""" - 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 evdev.ecodes.EV_KEY not 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 - } - - self.pipe.send(result) - - -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. - """ - global _devices - if _devices is None: - pipe = multiprocessing.Pipe() - GetDevicesProcess(pipe[1]).start() - # block until devices are available - _devices = pipe[0].recv() - logger.info('Found %s', ', '.join([f'"{name}"' for name in _devices])) - return _devices diff --git a/keymapper/mapping.py b/keymapper/mapping.py index 2f48196a..abbfb8d5 100644 --- a/keymapper/mapping.py +++ b/keymapper/mapping.py @@ -25,43 +25,6 @@ from keymapper.logger import logger -# if MIN_KEYCODE < 255 and MAX_KEYCODE > 255: X crashes -# the maximum specified in /usr/share/X11/xkb/keycodes is usually 255 -# and the minimum 8 -MAX_KEYCODE = 255 -MIN_KEYCODE = 8 - - -# modes for change: -GENERATE = -1 -DONTMAP = None - - -def get_input_keycode(keycode): - """Same as get_output_keycode, but vice versa.""" - return keycode - MIN_KEYCODE - - -def get_target_keycode(): - # see HELP.md - for keycode in range(MAX_KEYCODE, MIN_KEYCODE - 1, -1): - # starting from the MAX_KEYCODE, find the first keycode that is - # unused in both custom_mapping and system_mapping. - if not (custom_mapping.has(keycode) or system_mapping.has(keycode)): - return keycode - - # no unused keycode found, take the highest keycode that is unused - # in the current custom_mapping. - for keycode in range(MAX_KEYCODE, MIN_KEYCODE - 1, -1): - # starting from the MAX_KEYCODE, find the first keycode that is - # unused in both custom_mapping and system_mapping. - if not (custom_mapping.has(keycode)): - return keycode - - logger.error('All %s keycodes are mapped!', MAX_KEYCODE - MIN_KEYCODE) - return None - - class Mapping: """Contains and manages mappings. @@ -69,9 +32,6 @@ class Mapping: character. """ def __init__(self): - # TODO this is a stupid data structure if there are two keys - # that should be unique individually. system_keycode and - # target_keycode. two _mapping objects maybe? self._mapping = {} self.changed = False @@ -82,22 +42,7 @@ class Mapping: def __len__(self): return len(self._mapping) - def find_keycode(self, character, case=False): - """For a given character, find the used keycodes in the mapping.""" - # TODO test - if not case: - character = character.lower() - for keycode, (mapped_keycode, mapped_character) in self._mapping: - # keycode is what the system would use for that key, - # mapped_keycode is what we use instead by writing into /dev, - # and mapped_character is what we expect to appear. - # mapped_character might be multiple things, like "a, A" - if not case: - mapped_character = mapped_character.lower() - if character in [c.strip() for c in mapped_character.split(',')]: - return keycode, mapped_keycode - - def change(self, previous_keycode, new_keycode, character, target_keycode): + def change(self, previous_keycode, new_keycode, character): """Replace the mapping of a keycode with a different one. Return True on success. @@ -112,31 +57,20 @@ class Mapping: The source keycode, what the mouse would report without any modification. character : string or string[] - If an array of strings, will put something like { [ a, A ] }; - into the symbols file. - target_keycode : int or None - Which keycode should be used for that key instead. If -1, - will figure out a new one. This is for stuff that happens - under the hood and the user won't see this unless they open - config files. If None, will only map new_keycode to character - without any in-between step. + A single character known to xkb, Examples: KP_1, Shift_L, a, B. + Can also be an array, which is used for reading the xkbmap output + completely. """ try: new_keycode = int(new_keycode) - if target_keycode is not None: - target_keycode = int(target_keycode) if previous_keycode is not None: previous_keycode = int(previous_keycode) except ValueError: logger.error('Can only use numbers as keycodes') return False - # TODO test - if target_keycode == GENERATE: - target_keycode = get_target_keycode() - if new_keycode and character: - self._mapping[new_keycode] = (target_keycode, str(character)) + self._mapping[new_keycode] = character if new_keycode != previous_keycode: # clear previous mapping of that code, because the line # representing that one will now represent a different one. @@ -162,9 +96,18 @@ class Mapping: self._mapping = {} self.changed = True - def get_keycode(self, keycode): - """Read the output keycode that is mapped to this input keycode.""" - return self._mapping.get(keycode, (None, None))[0] + def get_keycode(self, character): + """Get the keycode for that character.""" + # TODO prepare this with .lower() instead to make it faster + character = character.lower() + for keycode, mapping in self._mapping.items(): + if isinstance(mapping, list): + if character in [c.lower() for c in mapping]: + return keycode + elif mapping.lower() == character: + return int(keycode) + + return None def get_character(self, keycode): """Read the character that is mapped to this keycode. @@ -173,22 +116,13 @@ class Mapping: ---------- keycode : int """ - return self._mapping.get(keycode, (None, None))[1] + return self._mapping.get(keycode) def has(self, keycode): - """Check if this keycode is going to be a line in the symbols file.""" - # TODO test - if self._mapping.get(keycode) is not None: - # the keycode that is disabled, because it is mapped to - # something else - return True - - for _, (target_keycode, _) in self._mapping.items(): - if target_keycode == keycode: - # the keycode that is actually being mapped - return True - - return False + """Check if this keycode is going to be a line in the symbols file. + TODO no symbols files anymore ^ + """ + return self._mapping.get(keycode) is not None # one mapping object for the whole application that holds all diff --git a/keymapper/paths.py b/keymapper/paths.py index 2e23274d..af858592 100644 --- a/keymapper/paths.py +++ b/keymapper/paths.py @@ -60,3 +60,4 @@ def get_usr_path(device=None, preset=None): DEFAULT_SYMBOLS = get_usr_path('default') +EMPTY_SYMBOLS = get_usr_path('empty') diff --git a/keymapper/presets.py b/keymapper/presets.py index 15ca36db..2161ea6c 100644 --- a/keymapper/presets.py +++ b/keymapper/presets.py @@ -28,7 +28,7 @@ import glob from keymapper.paths import get_usr_path, USERS_SYMBOLS from keymapper.logger import logger -from keymapper.linux import get_devices +from keymapper.getdevices import get_devices def get_presets(device): diff --git a/tests/testcases/X.py b/tests/testcases/X.py index 91ba198a..97167010 100644 --- a/tests/testcases/X.py +++ b/tests/testcases/X.py @@ -22,12 +22,12 @@ import re import unittest -from keymapper.X import get_system_layout +from keymapper.cli import get_system_layout_locale class Test(unittest.TestCase): - def test_get_system_layout(self): - layout = get_system_layout() + def test_get_system_layout_locale(self): + layout = get_system_layout_locale() self.assertGreater(len(layout), 0) # should be all alphanumeric match = re.findall(r'\w+', layout) diff --git a/tests/testcases/config.py b/tests/testcases/config.py index 0e10f0c1..7d3e7c26 100644 --- a/tests/testcases/config.py +++ b/tests/testcases/config.py @@ -23,7 +23,7 @@ import os import unittest import shutil -from keymapper.X import custom_mapping, generate_symbols, \ +from keymapper.xkb import custom_mapping, generate_symbols, \ create_identity_mapping, create_setxkbmap_config, \ get_preset_name, create_default_symbols, parse_symbols_file from keymapper.paths import get_usr_path, KEYCODES_PATH, USERS_SYMBOLS diff --git a/tests/testcases/presets.py b/tests/testcases/presets.py index 4bd1d0d1..5d92e7c4 100644 --- a/tests/testcases/presets.py +++ b/tests/testcases/presets.py @@ -26,7 +26,7 @@ import time from keymapper.presets import find_newest_preset, rename_preset, \ get_any_preset, delete_preset -from keymapper.X import create_preset +from keymapper.xkb import create_preset from keymapper.paths import USERS_SYMBOLS from test import tmp diff --git a/tests/testcases/test.py b/tests/testcases/test.py index fad8775f..935a8bc9 100644 --- a/tests/testcases/test.py +++ b/tests/testcases/test.py @@ -21,7 +21,7 @@ import unittest -from keymapper.linux import get_devices +from keymapper.getdevices import get_devices from keymapper.paths import USERS_SYMBOLS, X11_SYMBOLS, \ DEFAULT_SYMBOLS