mirror of
https://github.com/sezanzeb/input-remapper
synced 2024-11-04 12:00:16 +00:00
173 lines
5.8 KiB
Python
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)
|