overthrowing the functionality completely

pull/14/head
sezanzeb 4 years ago
parent 9bf255c42b
commit 151a1f3173

@ -1,7 +1,7 @@
# The problems with overwriting keys # The problems with overwriting keys
Branches for all that stuff exist to archive it instead of loosing it forever. Branches for some of that stuff exist to archive it instead of loosing it
Look for branches called "fifth", "fourth", etc. forever.
**Initial target** You write a symbols file based on your specified mapping, **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 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. pressing that mouse button and that keyboard button at the same time.
Keycodes may not clash. Keycodes may not clash.
This was quite mature, pretty much finished.
**The second idea** was to write special keycodes known only to key-mapper **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 (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 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 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 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, 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 **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 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 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 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 **Fourth idea**: Based on the first idea, instead of using keycodes greater
than 255, use unused keycodes starting from 255, going down. Issues existed 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 **Fifth idea**: Instead of writing xkb symbol files, just disable all
mouse buttons with a single symbol file. Key-mapper listens for key events 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 in /dev and then writes the mapped keycode into a new device in /dev. For
should be mapped to Shift_L, xkb configs would disable key 10 and key-mapper example, if 10 should be mapped to Shift_L, xkb configs would disable
would write 50 into /dev, which is Shift_L in xmodmaps output. This sounds key 10 and key-mapper would write 50 into /dev, which is Shift_L in xmodmaps
incredibly simple and makes me throw away tons of code. 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.
# How I would have liked it to be # How I would have liked it to be

@ -19,11 +19,9 @@
# along with key-mapper. If not, see <https://www.gnu.org/licenses/>. # along with key-mapper. If not, see <https://www.gnu.org/licenses/>.
"""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 Currently it is not needed to create symbols files in xkb. Very sad.
display server should be supported. Or does wayland use the same
config files?
Resources: Resources:
[1] https://wiki.archlinux.org/index.php/Keyboard_input [1] https://wiki.archlinux.org/index.php/Keyboard_input
@ -35,19 +33,19 @@ Resources:
import os import os
import re import re
import stat import stat
import subprocess
from keymapper.paths import get_usr_path, KEYCODES_PATH, DEFAULT_SYMBOLS, \ from keymapper.paths import get_usr_path, KEYCODES_PATH, DEFAULT_SYMBOLS, \
X11_SYMBOLS X11_SYMBOLS
from keymapper.logger import logger, is_debug from keymapper.logger import logger
from keymapper.data import get_data_path from keymapper.data import get_data_path
from keymapper.linux import get_devices from keymapper.mapping import custom_mapping, system_mapping, Mapping
from keymapper.mapping import custom_mapping, system_mapping, \
Mapping, MIN_KEYCODE, MAX_KEYCODE
permissions = stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IROTH 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): def create_preset(device, name=None):
"""Create an empty preset and return the potentially incremented name. """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') DEFAULT_SYMBOLS_NAME = get_preset_name('default')
EMPTY_SYMBOLS_NAME = get_preset_name('empty')
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()))
def create_identity_mapping(): def create_identity_mapping():
@ -197,15 +127,15 @@ def create_identity_mapping():
This has the added benefit that keycodes reported by xev can be This has the added benefit that keycodes reported by xev can be
identified in the symbols file. identified in the symbols file.
The identity mapping is provided to '-keycodes' of setxkbmap.
""" """
if os.path.exists(KEYCODES_PATH): if os.path.exists(KEYCODES_PATH):
logger.debug('Found the keycodes file at %s', KEYCODES_PATH) logger.debug('Found the keycodes file at %s', KEYCODES_PATH)
return return
xkb_keycodes = [] xkb_keycodes = []
maximum = MAX_KEYCODE for keycode in range(MIN_KEYCODE, MAX_KEYCODE + 1):
minimum = MIN_KEYCODE
for keycode in range(minimum, maximum + 1):
xkb_keycodes.append(f'<{keycode}> = {keycode};') xkb_keycodes.append(f'<{keycode}> = {keycode};')
template_path = get_data_path('xkb_keycodes_template') template_path = get_data_path('xkb_keycodes_template')
@ -213,8 +143,8 @@ def create_identity_mapping():
template = template_file.read() template = template_file.read()
result = template.format( result = template.format(
minimum=minimum, minimum=MIN_KEYCODE,
maximum=maximum, maximum=MAX_KEYCODE,
xkb_keycodes='\n '.join(xkb_keycodes) xkb_keycodes='\n '.join(xkb_keycodes)
) )
@ -229,7 +159,7 @@ def create_identity_mapping():
def generate_symbols( 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. """Create config contents to be placed in /usr/share/X11/xkb/symbols.
@ -304,24 +234,6 @@ def generate_symbols(
return result 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): def parse_symbols_file(device, preset):
"""Parse a symbols file populate the mapping. """Parse a symbols file populate the mapping.
@ -344,7 +256,8 @@ def parse_symbols_file(device, preset):
# avoid lines that start with special characters # avoid lines that start with special characters
# (might be comments) # (might be comments)
# And only find those lines that have a system-keycode written # 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() content = f.read()
result = re.findall( result = re.findall(
r'\n\s+?key <(.+?)>.+?\[\s+(.+?)\s+\]\s+?}; // (\d+)', r'\n\s+?key <(.+?)>.+?\[\s+(.+?)\s+\]\s+?}; // (\d+)',
@ -355,31 +268,11 @@ def parse_symbols_file(device, preset):
custom_mapping.change( custom_mapping.change(
previous_keycode=None, previous_keycode=None,
new_keycode=system_keycode, new_keycode=system_keycode,
character=character, character=character
target_keycode=int(target_keycode)
) )
custom_mapping.changed = False 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(): def create_default_symbols():
"""Parse the output of xmodmap and create a default symbols file. """Parse the output of xmodmap and create a default symbols file.

@ -0,0 +1,145 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2020 sezanzeb <proxima@hip70890b.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""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()

@ -0,0 +1,106 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2020 sezanzeb <proxima@hip70890b.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""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

@ -27,7 +27,7 @@ gi.require_version('Gtk', '3.0')
gi.require_version('GLib', '2.0') gi.require_version('GLib', '2.0')
from gi.repository import Gtk from gi.repository import Gtk
from keymapper.mapping import custom_mapping, DONTMAP, GENERATE from keymapper.mapping import custom_mapping
from keymapper.logger import logger from keymapper.logger import logger
@ -72,8 +72,7 @@ class Row(Gtk.ListBoxRow):
custom_mapping.change( custom_mapping.change(
previous_keycode=None, previous_keycode=None,
new_keycode=keycode, new_keycode=keycode,
character=character, character=character
target_keycode=GENERATE
) )
def on_key_pressed(self, button, event): def on_key_pressed(self, button, event):
@ -113,8 +112,7 @@ class Row(Gtk.ListBoxRow):
custom_mapping.change( custom_mapping.change(
previous_keycode=previous_keycode, previous_keycode=previous_keycode,
new_keycode=new_keycode, new_keycode=new_keycode,
character=character, character=character
target_keycode=GENERATE
) )
def put_together(self, keycode, character): def put_together(self, keycode, character):

@ -28,13 +28,13 @@ gi.require_version('GLib', '2.0')
from gi.repository import Gtk, Gdk, GLib from gi.repository import Gtk, Gdk, GLib
from keymapper.data import get_data_path from keymapper.data import get_data_path
from keymapper.X import create_setxkbmap_config, apply_preset, \ from keymapper.mapping import custom_mapping
create_preset, custom_mapping, system_mapping, parse_symbols_file, \
setxkbmap
from keymapper.presets import get_presets, find_newest_preset, \ from keymapper.presets import get_presets, find_newest_preset, \
delete_preset, rename_preset delete_preset, rename_preset
from keymapper.logger import logger 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.row import Row
from keymapper.gtk.unsaved import unsaved_changes_dialog, GO_BACK from keymapper.gtk.unsaved import unsaved_changes_dialog, GO_BACK
@ -164,7 +164,9 @@ class Window:
presets = get_presets(device) presets = get_presets(device)
self.get('preset_name_input').set_text('') self.get('preset_name_input').set_text('')
if len(presets) == 0: if len(presets) == 0:
presets = [create_preset(device)] # presets = [create_preset(device)]
# TODO create one empty preset
presets = []
else: else:
logger.debug('Presets for "%s": %s', device, ', '.join(presets)) logger.debug('Presets for "%s": %s', device, ', '.join(presets))
preset_selection = self.get('preset_selection') preset_selection = self.get('preset_selection')
@ -243,12 +245,11 @@ class Window:
self.selected_preset, self.selected_preset,
self.selected_device self.selected_device
) )
apply_preset(self.selected_device, self.selected_preset)
self.get('status_bar').push( self.get('status_bar').push(
CTX_APPLY, CTX_APPLY,
f'Applied "{self.selected_preset}"' f'Applied "{self.selected_preset}"'
) )
keycode_reader = KeycodeReader(self.selected_device) KeycodeReader(self.selected_device)
def on_select_device(self, dropdown): def on_select_device(self, dropdown):
"""List all presets, create one if none exist yet.""" """List all presets, create one if none exist yet."""
@ -275,7 +276,10 @@ class Window:
return return
try: 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').append(new_preset, new_preset)
self.get('preset_selection').set_active_id(new_preset) self.get('preset_selection').set_active_id(new_preset)
except PermissionError as e: except PermissionError as e:
@ -300,7 +304,7 @@ class Window:
logger.debug('Selecting preset "%s"', preset) logger.debug('Selecting preset "%s"', preset)
self.selected_preset = 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') key_list = self.get('key_list')
for keycode, output in custom_mapping: for keycode, output in custom_mapping:
@ -345,10 +349,7 @@ class Window:
self.selected_preset self.selected_preset
) )
create_setxkbmap_config( # TODO tell the mapping to dump itself as JSON somewhere
self.selected_device,
self.selected_preset
)
custom_mapping.changed = False custom_mapping.changed = False
self.unhighlight_all_rows() self.unhighlight_all_rows()

@ -23,17 +23,16 @@
import subprocess import subprocess
import multiprocessing import time
import threading import threading
import asyncio import asyncio
import evdev import evdev
from keymapper.logger import logger from keymapper.logger import logger
from keymapper.mapping import custom_mapping, MAX_KEYCODE, MIN_KEYCODE from keymapper.cli import apply_empty_symbols
from keymapper.getdevices import get_devices
from keymapper.mapping import custom_mapping, system_mapping
DEVNODE = '/dev/keymapper'
def can_grab(path): def can_grab(path):
@ -67,15 +66,14 @@ class KeycodeReader:
pass pass
def start_injecting_worker(self, path): def start_injecting_worker(self, path):
"""Inject keycodes for one of the virtual devices. """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.
"""
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
device = evdev.InputDevice(path) 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(): for event in device.read_loop():
if event.type != evdev.ecodes.EV_KEY: if event.type != evdev.ecodes.EV_KEY:
@ -85,24 +83,38 @@ class KeycodeReader:
# than the ones reported by xev and that X expects # than the ones reported by xev and that X expects
input_keycode = event.code + 8 input_keycode = event.code + 8
if custom_mapping.get_keycode(input_keycode) is None: character = custom_mapping.get_character(input_keycode)
# unknown keycode, skip
continue if character is None:
# unknown keycode, forward it
target_keycode = custom_mapping.get_keycode(input_keycode) target_keycode = input_keycode
else:
if target_keycode > MAX_KEYCODE or target_keycode < MIN_KEYCODE: target_keycode = system_mapping.get_keycode(character)
continue if target_keycode is None:
logger.error(
print('read', input_keycode, 'write', target_keycode, path) '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 # 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() keymapper_device.syn()
def start_injecting(self): def start_injecting(self):
"""Read keycodes and inject the mapped character forever.""" """Read keycodes and inject the mapped character forever."""
paths = _devices[self.device]['paths'] paths = get_devices()[self.device]['paths']
logger.debug( logger.debug(
'Starting injecting the mapping for %s on %s', 'Starting injecting the mapping for %s on %s',
@ -110,6 +122,8 @@ class KeycodeReader:
', '.join(paths) ', '.join(paths)
) )
apply_empty_symbols(self.device)
# Watch over each one of the potentially multiple devices per hardware # Watch over each one of the potentially multiple devices per hardware
for path in paths: for path in paths:
threading.Thread( threading.Thread(
@ -131,80 +145,3 @@ class KeycodeReader:
# than the ones reported by xev # than the ones reported by xev
newest_keycode = event.code + 8 newest_keycode = event.code + 8
return newest_keycode 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

@ -25,43 +25,6 @@
from keymapper.logger import logger 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: class Mapping:
"""Contains and manages mappings. """Contains and manages mappings.
@ -69,9 +32,6 @@ class Mapping:
character. character.
""" """
def __init__(self): 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._mapping = {}
self.changed = False self.changed = False
@ -82,22 +42,7 @@ class Mapping:
def __len__(self): def __len__(self):
return len(self._mapping) return len(self._mapping)
def find_keycode(self, character, case=False): def change(self, previous_keycode, new_keycode, character):
"""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):
"""Replace the mapping of a keycode with a different one. """Replace the mapping of a keycode with a different one.
Return True on success. Return True on success.
@ -112,31 +57,20 @@ class Mapping:
The source keycode, what the mouse would report without any The source keycode, what the mouse would report without any
modification. modification.
character : string or string[] character : string or string[]
If an array of strings, will put something like { [ a, A ] }; A single character known to xkb, Examples: KP_1, Shift_L, a, B.
into the symbols file. Can also be an array, which is used for reading the xkbmap output
target_keycode : int or None completely.
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.
""" """
try: try:
new_keycode = int(new_keycode) new_keycode = int(new_keycode)
if target_keycode is not None:
target_keycode = int(target_keycode)
if previous_keycode is not None: if previous_keycode is not None:
previous_keycode = int(previous_keycode) previous_keycode = int(previous_keycode)
except ValueError: except ValueError:
logger.error('Can only use numbers as keycodes') logger.error('Can only use numbers as keycodes')
return False return False
# TODO test
if target_keycode == GENERATE:
target_keycode = get_target_keycode()
if new_keycode and character: if new_keycode and character:
self._mapping[new_keycode] = (target_keycode, str(character)) self._mapping[new_keycode] = character
if new_keycode != previous_keycode: if new_keycode != previous_keycode:
# clear previous mapping of that code, because the line # clear previous mapping of that code, because the line
# representing that one will now represent a different one. # representing that one will now represent a different one.
@ -162,9 +96,18 @@ class Mapping:
self._mapping = {} self._mapping = {}
self.changed = True self.changed = True
def get_keycode(self, keycode): def get_keycode(self, character):
"""Read the output keycode that is mapped to this input keycode.""" """Get the keycode for that character."""
return self._mapping.get(keycode, (None, None))[0] # 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): def get_character(self, keycode):
"""Read the character that is mapped to this keycode. """Read the character that is mapped to this keycode.
@ -173,22 +116,13 @@ class Mapping:
---------- ----------
keycode : int keycode : int
""" """
return self._mapping.get(keycode, (None, None))[1] return self._mapping.get(keycode)
def has(self, keycode): def has(self, keycode):
"""Check if this keycode is going to be a line in the symbols file.""" """Check if this keycode is going to be a line in the symbols file.
# TODO test TODO no symbols files anymore ^
if self._mapping.get(keycode) is not None: """
# the keycode that is disabled, because it is mapped to return self._mapping.get(keycode) is not None
# 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
# one mapping object for the whole application that holds all # one mapping object for the whole application that holds all

@ -60,3 +60,4 @@ def get_usr_path(device=None, preset=None):
DEFAULT_SYMBOLS = get_usr_path('default') DEFAULT_SYMBOLS = get_usr_path('default')
EMPTY_SYMBOLS = get_usr_path('empty')

@ -28,7 +28,7 @@ import glob
from keymapper.paths import get_usr_path, USERS_SYMBOLS from keymapper.paths import get_usr_path, USERS_SYMBOLS
from keymapper.logger import logger from keymapper.logger import logger
from keymapper.linux import get_devices from keymapper.getdevices import get_devices
def get_presets(device): def get_presets(device):

@ -22,12 +22,12 @@
import re import re
import unittest import unittest
from keymapper.X import get_system_layout from keymapper.cli import get_system_layout_locale
class Test(unittest.TestCase): class Test(unittest.TestCase):
def test_get_system_layout(self): def test_get_system_layout_locale(self):
layout = get_system_layout() layout = get_system_layout_locale()
self.assertGreater(len(layout), 0) self.assertGreater(len(layout), 0)
# should be all alphanumeric # should be all alphanumeric
match = re.findall(r'\w+', layout) match = re.findall(r'\w+', layout)

@ -23,7 +23,7 @@ import os
import unittest import unittest
import shutil import shutil
from keymapper.X import custom_mapping, generate_symbols, \ from keymapper.xkb import custom_mapping, generate_symbols, \
create_identity_mapping, create_setxkbmap_config, \ create_identity_mapping, create_setxkbmap_config, \
get_preset_name, create_default_symbols, parse_symbols_file get_preset_name, create_default_symbols, parse_symbols_file
from keymapper.paths import get_usr_path, KEYCODES_PATH, USERS_SYMBOLS from keymapper.paths import get_usr_path, KEYCODES_PATH, USERS_SYMBOLS

@ -26,7 +26,7 @@ import time
from keymapper.presets import find_newest_preset, rename_preset, \ from keymapper.presets import find_newest_preset, rename_preset, \
get_any_preset, delete_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 keymapper.paths import USERS_SYMBOLS
from test import tmp from test import tmp

@ -21,7 +21,7 @@
import unittest import unittest
from keymapper.linux import get_devices from keymapper.getdevices import get_devices
from keymapper.paths import USERS_SYMBOLS, X11_SYMBOLS, \ from keymapper.paths import USERS_SYMBOLS, X11_SYMBOLS, \
DEFAULT_SYMBOLS DEFAULT_SYMBOLS

Loading…
Cancel
Save