Merged xkb prototype with new main
parent
f5bdafa682
commit
cd625a1257
@ -0,0 +1,265 @@
|
||||
// keycodes configuration for key-mapper presets
|
||||
|
||||
// The concept of "reasonable symbolic names" [3] doesn't apply
|
||||
// when mouse buttons are all over the place. Furthermore this file has
|
||||
// to work for all devices, which place their keys in different places.
|
||||
// So create an identity mapping instead to make generating "symbols" files
|
||||
// easier. Keycode 10 -> "<10>"
|
||||
// This has the added benefit that keycodes reported by xev can be
|
||||
// identified in the symbols file.
|
||||
|
||||
// Keycodes reported by evdev are 8 lower than those reported by xev,
|
||||
// so 10 in this file means 2 in key-mapper.
|
||||
|
||||
default xkb_keycodes "key-mapper" {
|
||||
minimum = 8;
|
||||
maximum = 255;
|
||||
<8> = 8;
|
||||
<9> = 9;
|
||||
<10> = 10;
|
||||
<11> = 11;
|
||||
<12> = 12;
|
||||
<13> = 13;
|
||||
<14> = 14;
|
||||
<15> = 15;
|
||||
<16> = 16;
|
||||
<17> = 17;
|
||||
<18> = 18;
|
||||
<19> = 19;
|
||||
<20> = 20;
|
||||
<21> = 21;
|
||||
<22> = 22;
|
||||
<23> = 23;
|
||||
<24> = 24;
|
||||
<25> = 25;
|
||||
<26> = 26;
|
||||
<27> = 27;
|
||||
<28> = 28;
|
||||
<29> = 29;
|
||||
<30> = 30;
|
||||
<31> = 31;
|
||||
<32> = 32;
|
||||
<33> = 33;
|
||||
<34> = 34;
|
||||
<35> = 35;
|
||||
<36> = 36;
|
||||
<37> = 37;
|
||||
<38> = 38;
|
||||
<39> = 39;
|
||||
<40> = 40;
|
||||
<41> = 41;
|
||||
<42> = 42;
|
||||
<43> = 43;
|
||||
<44> = 44;
|
||||
<45> = 45;
|
||||
<46> = 46;
|
||||
<47> = 47;
|
||||
<48> = 48;
|
||||
<49> = 49;
|
||||
<50> = 50;
|
||||
<51> = 51;
|
||||
<52> = 52;
|
||||
<53> = 53;
|
||||
<54> = 54;
|
||||
<55> = 55;
|
||||
<56> = 56;
|
||||
<57> = 57;
|
||||
<58> = 58;
|
||||
<59> = 59;
|
||||
<60> = 60;
|
||||
<61> = 61;
|
||||
<62> = 62;
|
||||
<63> = 63;
|
||||
<64> = 64;
|
||||
<65> = 65;
|
||||
<66> = 66;
|
||||
<67> = 67;
|
||||
<68> = 68;
|
||||
<69> = 69;
|
||||
<70> = 70;
|
||||
<71> = 71;
|
||||
<72> = 72;
|
||||
<73> = 73;
|
||||
<74> = 74;
|
||||
<75> = 75;
|
||||
<76> = 76;
|
||||
<77> = 77;
|
||||
<78> = 78;
|
||||
<79> = 79;
|
||||
<80> = 80;
|
||||
<81> = 81;
|
||||
<82> = 82;
|
||||
<83> = 83;
|
||||
<84> = 84;
|
||||
<85> = 85;
|
||||
<86> = 86;
|
||||
<87> = 87;
|
||||
<88> = 88;
|
||||
<89> = 89;
|
||||
<90> = 90;
|
||||
<91> = 91;
|
||||
<92> = 92;
|
||||
<93> = 93;
|
||||
<94> = 94;
|
||||
<95> = 95;
|
||||
<96> = 96;
|
||||
<97> = 97;
|
||||
<98> = 98;
|
||||
<99> = 99;
|
||||
<100> = 100;
|
||||
<101> = 101;
|
||||
<102> = 102;
|
||||
<103> = 103;
|
||||
<104> = 104;
|
||||
<105> = 105;
|
||||
<106> = 106;
|
||||
<107> = 107;
|
||||
<108> = 108;
|
||||
<109> = 109;
|
||||
<110> = 110;
|
||||
<111> = 111;
|
||||
<112> = 112;
|
||||
<113> = 113;
|
||||
<114> = 114;
|
||||
<115> = 115;
|
||||
<116> = 116;
|
||||
<117> = 117;
|
||||
<118> = 118;
|
||||
<119> = 119;
|
||||
<120> = 120;
|
||||
<121> = 121;
|
||||
<122> = 122;
|
||||
<123> = 123;
|
||||
<124> = 124;
|
||||
<125> = 125;
|
||||
<126> = 126;
|
||||
<127> = 127;
|
||||
<128> = 128;
|
||||
<129> = 129;
|
||||
<130> = 130;
|
||||
<131> = 131;
|
||||
<132> = 132;
|
||||
<133> = 133;
|
||||
<134> = 134;
|
||||
<135> = 135;
|
||||
<136> = 136;
|
||||
<137> = 137;
|
||||
<138> = 138;
|
||||
<139> = 139;
|
||||
<140> = 140;
|
||||
<141> = 141;
|
||||
<142> = 142;
|
||||
<143> = 143;
|
||||
<144> = 144;
|
||||
<145> = 145;
|
||||
<146> = 146;
|
||||
<147> = 147;
|
||||
<148> = 148;
|
||||
<149> = 149;
|
||||
<150> = 150;
|
||||
<151> = 151;
|
||||
<152> = 152;
|
||||
<153> = 153;
|
||||
<154> = 154;
|
||||
<155> = 155;
|
||||
<156> = 156;
|
||||
<157> = 157;
|
||||
<158> = 158;
|
||||
<159> = 159;
|
||||
<160> = 160;
|
||||
<161> = 161;
|
||||
<162> = 162;
|
||||
<163> = 163;
|
||||
<164> = 164;
|
||||
<165> = 165;
|
||||
<166> = 166;
|
||||
<167> = 167;
|
||||
<168> = 168;
|
||||
<169> = 169;
|
||||
<170> = 170;
|
||||
<171> = 171;
|
||||
<172> = 172;
|
||||
<173> = 173;
|
||||
<174> = 174;
|
||||
<175> = 175;
|
||||
<176> = 176;
|
||||
<177> = 177;
|
||||
<178> = 178;
|
||||
<179> = 179;
|
||||
<180> = 180;
|
||||
<181> = 181;
|
||||
<182> = 182;
|
||||
<183> = 183;
|
||||
<184> = 184;
|
||||
<185> = 185;
|
||||
<186> = 186;
|
||||
<187> = 187;
|
||||
<188> = 188;
|
||||
<189> = 189;
|
||||
<190> = 190;
|
||||
<191> = 191;
|
||||
<192> = 192;
|
||||
<193> = 193;
|
||||
<194> = 194;
|
||||
<195> = 195;
|
||||
<196> = 196;
|
||||
<197> = 197;
|
||||
<198> = 198;
|
||||
<199> = 199;
|
||||
<200> = 200;
|
||||
<201> = 201;
|
||||
<202> = 202;
|
||||
<203> = 203;
|
||||
<204> = 204;
|
||||
<205> = 205;
|
||||
<206> = 206;
|
||||
<207> = 207;
|
||||
<208> = 208;
|
||||
<209> = 209;
|
||||
<210> = 210;
|
||||
<211> = 211;
|
||||
<212> = 212;
|
||||
<213> = 213;
|
||||
<214> = 214;
|
||||
<215> = 215;
|
||||
<216> = 216;
|
||||
<217> = 217;
|
||||
<218> = 218;
|
||||
<219> = 219;
|
||||
<220> = 220;
|
||||
<221> = 221;
|
||||
<222> = 222;
|
||||
<223> = 223;
|
||||
<224> = 224;
|
||||
<225> = 225;
|
||||
<226> = 226;
|
||||
<227> = 227;
|
||||
<228> = 228;
|
||||
<229> = 229;
|
||||
<230> = 230;
|
||||
<231> = 231;
|
||||
<232> = 232;
|
||||
<233> = 233;
|
||||
<234> = 234;
|
||||
<235> = 235;
|
||||
<236> = 236;
|
||||
<237> = 237;
|
||||
<238> = 238;
|
||||
<239> = 239;
|
||||
<240> = 240;
|
||||
<241> = 241;
|
||||
<242> = 242;
|
||||
<243> = 243;
|
||||
<244> = 244;
|
||||
<245> = 245;
|
||||
<246> = 246;
|
||||
<247> = 247;
|
||||
<248> = 248;
|
||||
<249> = 249;
|
||||
<250> = 250;
|
||||
<251> = 251;
|
||||
<252> = 252;
|
||||
<253> = 253;
|
||||
<254> = 254;
|
||||
<255> = 255;
|
||||
};
|
@ -0,0 +1,113 @@
|
||||
#!/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 calls to setxkbmap. See injection/xkb.py for more info.
|
||||
|
||||
Since the daemon doesn't know about the X session the gui has to do it.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from keymapper.logger import logger
|
||||
from keymapper.injection.injector import get_udev_name
|
||||
from keymapper.injection.xkb import get_xkb_symbols_name
|
||||
|
||||
|
||||
def get_device_id(device):
|
||||
"""Return the device ID as known to the display server.
|
||||
|
||||
Can be used in setxkbmap.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
device : string
|
||||
Device name as found in evtest
|
||||
"""
|
||||
try:
|
||||
names = subprocess.check_output(["xinput", "list", "--name-only"])
|
||||
names = names.decode().split("\n")
|
||||
ids = subprocess.check_output(["xinput", "list", "--id-only"])
|
||||
ids = ids.decode().split("\n")
|
||||
except subprocess.CalledProcessError as error:
|
||||
# systemd services and ttys can't do that
|
||||
logger.error(str(error))
|
||||
return None
|
||||
|
||||
for name, id in zip(names, ids):
|
||||
if name == device:
|
||||
device_id = id
|
||||
break
|
||||
else:
|
||||
return None
|
||||
|
||||
return device_id
|
||||
|
||||
|
||||
def apply_xkb_config(group_key):
|
||||
"""Call setxkbmap to apply a different xkb keyboard layout to a device.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
group_key : string
|
||||
"""
|
||||
# TODO test
|
||||
# needs at least 0.2 seconds for me until the mapping device
|
||||
# is visible in xinput
|
||||
mapped_name = get_udev_name(group_key, "mapped")
|
||||
|
||||
for _ in range(5):
|
||||
time.sleep(0.2)
|
||||
device_id = get_device_id(mapped_name)
|
||||
if device_id is not None:
|
||||
break
|
||||
else:
|
||||
logger.error('Failed to get device ID for "%s"', mapped_name)
|
||||
return
|
||||
|
||||
name = get_xkb_symbols_name(group_key)
|
||||
path = f"/usr/share/X11/xkb/symbols/{name}"
|
||||
|
||||
if not os.path.exists(path):
|
||||
logger.debug('Symbols "%s" doen\'t exist, skipping setxkbmap', path)
|
||||
return
|
||||
|
||||
logger.info("Applying xkb configuration")
|
||||
|
||||
# XkbBadKeyboard: wrong -device id
|
||||
device_id = get_device_id(mapped_name)
|
||||
if device_id is None:
|
||||
return
|
||||
|
||||
cmd = [
|
||||
"setxkbmap",
|
||||
"-keycodes",
|
||||
"key-mapper-keycodes",
|
||||
"-symbols",
|
||||
name,
|
||||
"-device",
|
||||
str(device_id),
|
||||
]
|
||||
logger.debug('Running "%s"', " ".join(cmd))
|
||||
# TODO disable Popen for setxkbmap in tests
|
||||
subprocess.Popen(cmd)
|
@ -0,0 +1,172 @@
|
||||
#!/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)
|
File diff suppressed because it is too large
Load Diff
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Loading…
Reference in New Issue