pull/14/head
sezanzeb 4 years ago
parent 0792880ae6
commit dc74b5ddb6

@ -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/<user>/<device>/<preset>`
This is how a single preset is stored.
**Defaults**
- `/usr/share/X11/xkb/symbols/key-mapper/<user>/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.

@ -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(

@ -335,7 +335,6 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">In order to apply any changes, save the preset first.</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_apply_preset_clicked" swapped="no"/>
</object>

@ -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

@ -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."""

@ -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)

@ -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

@ -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():

Loading…
Cancel
Save