From dc74b5ddb6eb00773059715d0e67d4dd4a9ed0b3 Mon Sep 17 00:00:00 2001 From: sezanzeb Date: Wed, 18 Nov 2020 13:17:49 +0100 Subject: [PATCH] wip --- HELP.md | 74 ++++++++++++------------- bin/key-mapper-gtk | 7 +++ data/key-mapper.glade | 1 - keymapper/cli.py | 8 ++- keymapper/gtk/window.py | 15 ++++- keymapper/linux.py | 120 ++++++++++++++++++++++++++-------------- keymapper/mapping.py | 10 +++- tests/test.py | 4 +- 8 files changed, 150 insertions(+), 89 deletions(-) diff --git a/HELP.md b/HELP.md index c416d682..60bfa2b4 100644 --- a/HELP.md +++ b/HELP.md @@ -11,17 +11,16 @@ symbol files in xkb. However, if you had one keyboard layout for your mouse that writes SHIFT keys on keycode 10, and one for your keyboard that is normal 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. +This was quite mature, pretty much finished and tested. **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. +existed to prevent the original keycode 10 from writing a 1. But Linux seems +to ignore anything greater than 255 for regular keyboard events (EDIT: I think +this is because of the device capabilities), or even crash X in some cases when +something is wrong with the configuration. **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 @@ -52,6 +51,8 @@ mapped to Shift_L. It is impossible to write "!" using this mapped button and a second keyboard, except if pressing key 10 triggers key-mapper to write key 253 into the /dev device, while mapping key 10 to nothing. Unfortunately linux just completely ignores some keycodes. 140 works, 145 won't, 150 works. +EDIT: I think this is because of "capabilities", however, injecting keycodes +won't work at all anyway, see the fifth idea. **Fifth idea**: Instead of writing xkb symbol files, just disable all mouse buttons with a single symbol file. Key-mapper listens for key events @@ -59,6 +60,35 @@ 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. +This conflicts with the original keycodes though, writing custom keycodes +into /dev/uinput makes the original keycode not mapped by xbk symbol files, +and therefore leak through. In the previous example, it would still write '1', +and then after that the other key. By adding a timeout single keys work, but +holding down a button that is mapped to shift will (would usually have +a keycode of 10, now triggers writing 50) write "!!!!!!!!!". Even though +no symbols are loaded for that button. + +This is because the second device that starts writing an event.value of 2 will +take control of what is happening. Following example: (KB = keyboard, +example devices) +1. hold a on KB1: `a-1`, `a-2`, `a-2`, `a-2`, ... +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! + +Which means in order to prevent "!!!!!!" being written while holding down +keycode 10 on the mouse, which is supposed to be shift, the 10 of the +key-mapper /dev node has to be mapped to none as well. But that would +prevent a key that is mapped to "1", which translates to 10, from working. + +That means instead of using the output from xmodmap to determine the correct +keycode, use a custom mapping that starts at 255 and just offsets xmodmap +by 255. The correct capabilities need to exist this time. Everything below +255 is disabled. This mapping is applied to key-mappers custom /dev node. + # How I would have liked it to be @@ -73,35 +103,3 @@ config looks like: done. Without crashing X. Without printing generic useless errors. Without colliding with other devices using the same keycodes. If it was that easy, an app to map keys would have already existed. - -# Folder Structure of Key Mapper in /usr - -Stuff has to be placed in `/usr/share/X11/xkb` to my knowledge. - -Every user gets a path within that `/usr/...` directory which is very -unconventional, but it works. This way the presets of multiple users -don't clash. - -**Presets** - -- `/usr/share/X11/xkb/symbols/key-mapper///` - -This is how a single preset is stored. - -**Defaults** - -- `/usr/share/X11/xkb/symbols/key-mapper//default` - -This is where key-mapper stores the defaults. They are generated from the -parsed output of `xmodmap` and used to keep the unmapped keys at their system -defaults. - -**Keycodes** - -- `/usr/share/X11/xkb/keycodes/key-mapper` - -Because the concept of "reasonable symbolic names" ([www.x.org](https://www.x.org/releases/X11R7.7/doc/xorg-docs/input/XKB-Enhancing.html)) -doesn't apply when mouse buttons are all over the place, an identity mapping -to make generating "symbols" files easier/possible exists. A keycode of -10 will be known as "<10>" in symbols configs. This has the added benefit -that keycodes reported by xev can be identified in the symbols file. diff --git a/bin/key-mapper-gtk b/bin/key-mapper-gtk index 2857beec..3198ba27 100755 --- a/bin/key-mapper-gtk +++ b/bin/key-mapper-gtk @@ -23,6 +23,7 @@ import sys +import atexit import getpass from argparse import ArgumentParser @@ -50,6 +51,12 @@ if __name__ == '__main__': window = Window() + def stop_injecting(): + if window.keycode_reader is not None: + window.keycode_reader.stop_injecting() + + atexit.register(stop_injecting) + if getpass.getuser() != 'root' and 'unittest' not in sys.modules.keys(): logger.error('Needs to run with sudo') ErrorDialog( diff --git a/data/key-mapper.glade b/data/key-mapper.glade index 561b79a6..208a4cd1 100644 --- a/data/key-mapper.glade +++ b/data/key-mapper.glade @@ -335,7 +335,6 @@ True True True - In order to apply any changes, save the preset first. True diff --git a/keymapper/cli.py b/keymapper/cli.py index 5d1a7910..9f717355 100644 --- a/keymapper/cli.py +++ b/keymapper/cli.py @@ -47,7 +47,7 @@ def get_system_layout_locale(): ][0].split(': ')[-1] -def setxkbmap(device, layout): +def setxkbmap(device, layout=None): """Apply a preset to the device. Parameters @@ -93,7 +93,11 @@ def apply_empty_symbols(device): logger.debug('Applying the empty symbols to %s', device) group = get_devices()[device] - cmd = ['setxkbmap', '-layout', 'key-mapper/empty'] + cmd = [ + 'setxkbmap', + '-layout', 'key-mapper/empty', + # '-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 diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py index e19fd339..9c3f8261 100755 --- a/keymapper/gtk/window.py +++ b/keymapper/gtk/window.py @@ -22,7 +22,9 @@ """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 @@ -32,7 +34,7 @@ from keymapper.mapping import custom_mapping from keymapper.presets import get_presets, find_newest_preset, \ delete_preset, rename_preset, get_available_preset_name from keymapper.logger import logger -from keymapper.linux import KeycodeReader +from keymapper.linux import KeycodeInjector from keymapper.cli import setxkbmap from keymapper.getdevices import get_devices from keymapper.gtk.row import Row @@ -76,6 +78,7 @@ class Window: def __init__(self): self.selected_device = None self.selected_preset = None + self.keycode_reader = None css_provider = Gtk.CssProvider() with open(get_data_path('style.css'), 'r') as f: @@ -124,6 +127,8 @@ class Window: def on_close(self, *_): """Safely close the application.""" + if self.keycode_reader is not None: + self.keycode_reader.stop_injecting() GLib.source_remove(self.timeout) Gtk.main_quit() @@ -201,7 +206,9 @@ class Window: def on_apply_system_layout_clicked(self, button): """Load the mapping.""" - setxkbmap(self.selected_device, None) + if self.keycode_reader is not None: + self.keycode_reader.stop_injecting() + setxkbmap(self.selected_device) self.get('status_bar').push( CTX_APPLY, f'Applied the system default' @@ -249,7 +256,9 @@ class Window: CTX_APPLY, f'Applied "{self.selected_preset}"' ) - KeycodeReader(self.selected_device) + if self.keycode_reader is not None: + self.keycode_reader.stop_injecting() + self.keycode_reader = KeycodeInjector(self.selected_device) def on_select_device(self, dropdown): """List all presets, create one if none exist yet.""" diff --git a/keymapper/linux.py b/keymapper/linux.py index 13c60cb8..1822b7d8 100644 --- a/keymapper/linux.py +++ b/keymapper/linux.py @@ -24,13 +24,13 @@ import subprocess import time -import threading +import multiprocessing import asyncio import evdev from keymapper.logger import logger -from keymapper.cli import apply_empty_symbols +from keymapper.cli import apply_empty_symbols, setxkbmap from keymapper.getdevices import get_devices from keymapper.mapping import custom_mapping, system_mapping @@ -47,47 +47,47 @@ def can_grab(path): return p.returncode == 1 -class KeycodeReader: - """Keeps reading keycodes in the background for the UI to use. - - When a button was pressed, the newest keycode can be obtained from this - object. - """ +class KeycodeInjector: + """Keeps injecting keycodes in the background based on the mapping.""" def __init__(self, device): self.device = device self.virtual_devices = [] + self.processes = [] self.start_injecting() - def clear(self): - """Next time when reading don't return the previous keycode.""" - # read all of them to clear the buffer or whatever - for virtual_device in self.virtual_devices: - while virtual_device.read_one(): - pass - - def start_injecting_worker(self, path): + def _start_injecting_worker(self, path, mapping): """Inject keycodes for one of the virtual devices.""" + # TODO test loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) device = evdev.InputDevice(path) + # foo = evdev.InputDevice('/dev/input/event2') keymapper_device = evdev.UInput( name='key-mapper', phys='key-mapper-uinput' ) + logger.debug( + 'Started injecting into %s, fd %s', + device.path, keymapper_device.fd + ) + for event in device.read_loop(): if event.type != evdev.ecodes.EV_KEY: continue + print('got', event.code, event.value, 'from device') + # this happens to report key codes that are 8 lower # than the ones reported by xev and that X expects input_keycode = event.code + 8 - character = custom_mapping.get_character(input_keycode) + character = mapping.get_character(input_keycode) if character is None: # unknown keycode, forward it target_keycode = input_keycode + continue else: target_keycode = system_mapping.get_keycode(character) if target_keycode is None: @@ -99,24 +99,60 @@ class KeycodeReader: # 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. + # 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) + time.sleep(0.01) + """if event.value == 2: + print('device simulated up', event.value, 0) + device.write( + evdev.ecodes.EV_KEY, + event.code, + 0 + ) + device.write(evdev.ecodes.EV_SYN, evdev.ecodes.SYN_REPORT, 0)""" # TODO test for the stuff put into write - keymapper_device.write(evdev.ecodes.EV_KEY, target_keycode - 8, event.value) + """logger.debug( + 'Injecting %s -> %s -> %s', + input_keycode, + character, + target_keycode, + )""" + print('km write', target_keycode - 8, event.value) + keymapper_device.write( + evdev.ecodes.EV_KEY, + target_keycode - 8, + 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() def start_injecting(self): """Read keycodes and inject the mapped character forever.""" + self.stop_injecting() + paths = get_devices()[self.device]['paths'] - logger.debug( + logger.info( 'Starting injecting the mapping for %s on %s', self.device, ', '.join(paths) @@ -126,22 +162,24 @@ class KeycodeReader: # Watch over each one of the potentially multiple devices per hardware for path in paths: - threading.Thread( - target=self.start_injecting_worker, - args=(path,) - ).start() - - def read(self): - """Get the newest key or None if none was pressed.""" - newest_keycode = None - for virtual_device in self.virtual_devices: - while True: - event = virtual_device.read_one() - if event is None: - break - if event.type == evdev.ecodes.EV_KEY and event.value == 1: - # value: 1 for down, 0 for up, 2 for hold. - # this happens to report key codes that are 8 lower - # than the ones reported by xev - newest_keycode = event.code + 8 - return newest_keycode + worker = multiprocessing.Process( + target=self._start_injecting_worker, + args=(path, custom_mapping) + ) + worker.start() + self.processes.append(worker) + + def stop_injecting(self): + """Stop injecting keycodes.""" + # TODO test + logger.info('Stopping injecting keycodes') + for i, process in enumerate(self.processes): + if process is None: + continue + + if process.is_alive(): + process.terminate() + self.processes[i] = None + + # apply the default layout back + setxkbmap(self.device) diff --git a/keymapper/mapping.py b/keymapper/mapping.py index 433bc935..c21ab371 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.presets import get_available_preset_name class Mapping: @@ -117,7 +116,14 @@ class Mapping: return with open(path, 'r') as f: - self._mapping = json.load(f) + mapping = json.load(f) + for keycode, character in mapping.items(): + try: + keycode = int(keycode) + except ValueError: + logger.error('Found non-int keycode: %s', keycode) + continue + self._mapping[keycode] = character self.changed = False diff --git a/tests/test.py b/tests/test.py index 19171ea3..b47394e1 100644 --- a/tests/test.py +++ b/tests/test.py @@ -41,8 +41,8 @@ def patch_paths(): def patch_linux(): from keymapper import linux - linux.KeycodeReader.start_reading = lambda *args: None - linux.KeycodeReader.read = lambda *args: None + linux.KeycodeInjector.start_reading = lambda *args: None + linux.KeycodeInjector.read = lambda *args: None def patch_evdev():