input-remapper/keymapper/injection/xkb.py
2021-11-27 12:12:12 +01:00

173 lines
5.8 KiB
Python

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2021 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/>.
"""Handles xkb config files.
This is optional and can be disabled via the configuration. If disabled,
outputting keys that are unknown to the system layout is impossible.
It is optional because broken xkb configs can crash the X session or screw
up the injection, and in ttys xkb configs don't have any effect. setxkbmap
is hard to work with, and xkb configs are horrible.
It uses setxkbmap to tell the window manager to do stuff differently for
the injected keycodes.
workflow:
1. injector preparation sees that "a" maps to "b"
2. check which keycode "b" would usually be
2.a if not in the system_mapping, this keycode is unknown to the
window manager and therefore cannot be used. To fix that,
find an integer code that is not present in system_mapping yet.
2.b if in the system_mapping, use the existing int code.
3. in the symbols file map that code to "b"
4. the "key-mapper ... mapped" uinput is created by the daemon. Since the
daemon doesn't know anything about the users X session, the GUI calls
setxkbmap instead with the appropriate path generated by helper functions.
injection:
1. running injection sees that "a" was clicked on the keyboard
2. the injector knows based on the mapping that this maps to
e.g. 48 and injects it
3. the window manager sees code 48 and writes a "b" into the focused
application, because the xkb config tells it to do so
now it is possible to map "ö" on an US keyboard
Resources:
[1] https://wiki.archlinux.org/index.php/Keyboard_input
[2] http://people.uleth.ca/~daniel.odonnell/Blog/custom-keyboard-in-linuxx11
[3] https://www.x.org/releases/X11R7.7/doc/xorg-docs/input/XKB-Enhancing.html
Mapping code 10 to a on device_1 and 10 to shift on device_2 may cause issues
when pressing both at the same time, More information can be found in
readme/history.md. That's why the resulting symbols file should match
the existing keyboard layout as much as possible, so that shift stays on the
code that it would usually be.
"""
import os
from keymapper.logger import logger
from keymapper.paths import touch
from keymapper.system_mapping import system_mapping, XKB_KEYCODE_OFFSET
SYMBOLS_TEMPLATE = """default xkb_symbols "key-mapper" {
%s
};
"""
LINE_TEMPLATE = "key <%d> { [ %s ] };"
def get_xkb_symbols_name(device):
"""Get the name that can be used for -symbols argument of setxkbmap."""
return f"key-mapper/{device}".replace(" ", "_")
def generate_symbols_lines(context):
"""Generate lines to put in symbols files.
Returns
-------
string[]
list of 'key <...> {[...]};' strings
"""
symbols = []
# because tricky problems appeared during development, add this to
# have some assertion that it works correctly
used_codes = set()
for name, code in system_mapping.xmodmap_dict.items():
# TODO if name is a, how to get modified versions of it (A)
# into the symbols file?
code = int(code)
if not context.is_written(code):
# don't include any codes in the symbols file that are
# not used anyway
continue
logger.spam('"%s" (%s) from xmodmap is used', name, code)
symbols.append(LINE_TEMPLATE % (code + XKB_KEYCODE_OFFSET, name))
assert code not in used_codes
used_codes.add(code)
# TODO store unknown mappings in the context to clarify that it is
# unique per injection
for name, code in system_mapping.get_unknown_mappings().items():
logger.spam('"%s" (%s) is allocated', name, code)
symbols.append(LINE_TEMPLATE % (code + XKB_KEYCODE_OFFSET, name))
assert code not in used_codes
used_codes.add(code)
return symbols
def generate_xkb_config(context, device):
"""Generate the needed config file for apply_xkb_config.
If it is not needed, it will remove any existing xkb symbols config for
that device.
Parameters
----------
context : Context
device : string
Used to name the file in /usr/share/X11/xkb/symbols/key-mapper.
Existing configs with that name will be overwritten.
As indexed in get_devices
"""
# TODO test
name = get_xkb_symbols_name(device)
path = f"/usr/share/X11/xkb/symbols/{name}"
# remove the old xkb config. If there is no need to apply it then
# there is no need to keep it. The ui will apply it if it is there.
if os.path.exists(path):
os.remove(path)
if len(context.macros) == 0 and len(context.key_to_code) == 0:
return None
if len(system_mapping.get_unknown_mappings()) == 0:
# there is no need to change the layout of the device
return None
logger.info("Unknown characters found, creating xkb configs")
symbols = generate_symbols_lines(context)
if len(symbols) == 0:
logger.error("Failed to populate symbols with anything")
return
touch(path)
with open(path, "w") as f:
logger.info('Writing xkb symbols "%s"', path)
contents = SYMBOLS_TEMPLATE % "\n ".join(symbols)
logger.spam('"%s":\n%s', path, contents.strip())
f.write(contents)