From 5c7f46fe7ce582430c70aa66973030a2d72af66e Mon Sep 17 00:00:00 2001 From: LilleAila Date: Thu, 11 Jul 2024 17:06:36 +0200 Subject: [PATCH 1/9] added uinput keyboard emulation --- plover/oslayer/linux/display_server.py | 9 + plover/oslayer/linux/keyboardcontrol.py | 7 +- .../oslayer/linux/keyboardcontrol_uinput.py | 260 ++++++++++++++++++ plover/oslayer/linux/xkb_symbols.py | 59 ++++ reqs/dist.txt | 2 + 5 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 plover/oslayer/linux/display_server.py create mode 100644 plover/oslayer/linux/keyboardcontrol_uinput.py create mode 100644 plover/oslayer/linux/xkb_symbols.py diff --git a/plover/oslayer/linux/display_server.py b/plover/oslayer/linux/display_server.py new file mode 100644 index 000000000..04d5fb0ca --- /dev/null +++ b/plover/oslayer/linux/display_server.py @@ -0,0 +1,9 @@ +import os + +""" +This value should be one of: + - x11 + - wayland + - tty +""" +DISPLAY_SERVER = os.environ.get("XDG_SESSION_TYPE", None) diff --git a/plover/oslayer/linux/keyboardcontrol.py b/plover/oslayer/linux/keyboardcontrol.py index bb1356144..cf5e019ab 100644 --- a/plover/oslayer/linux/keyboardcontrol.py +++ b/plover/oslayer/linux/keyboardcontrol.py @@ -1 +1,6 @@ -from .keyboardcontrol_x11 import KeyboardCapture, KeyboardEmulation # pylint: disable=unused-import +from .display_server import DISPLAY_SERVER + +if DISPLAY_SERVER == "x11": + from .keyboardcontrol_x11 import KeyboardCapture, KeyboardEmulation # pylint: disable=unused-import +else: + from .keyboardcontrol_uinput import KeyboardCapture, KeyboardEmulation #pylint: disable=unused-import diff --git a/plover/oslayer/linux/keyboardcontrol_uinput.py b/plover/oslayer/linux/keyboardcontrol_uinput.py new file mode 100644 index 000000000..9ad92f63e --- /dev/null +++ b/plover/oslayer/linux/keyboardcontrol_uinput.py @@ -0,0 +1,260 @@ +# TODO: get a stop() function to run, calling `self._ui.close()` +# TODO: figure out a better way to do this: +import os # To read the layout variable + +# TODO: find out how to add evdev as a dependency +# TODO: somehow get the requires udev rule installed +from evdev import UInput, ecodes as e, util + +# TODO: add xkbcommon as a dependency, *or* figure out how to do it using xlib instead, as it is already a dependency for the x11 keyboard emulation +from .xkb_symbols import generate_symbols + +from plover.output.keyboard import GenericKeyboardEmulation +from plover.machine.keyboard_capture import Capture +from plover.key_combo import parse_key_combo +from plover import log + + +modifiers = { + "alt_l": e.KEY_LEFTALT, + "alt_r": e.KEY_RIGHTALT, + "alt": e.KEY_LEFTALT, + "control_l": e.KEY_LEFTCTRL, + "control_r": e.KEY_RIGHTCTRL, + "control": e.KEY_LEFTCTRL, + "shift_l": e.KEY_LEFTSHIFT, + "shift_r": e.KEY_RIGHTSHIFT, + "shift": e.KEY_LEFTSHIFT, + "super_l": e.KEY_LEFTMETA, + "super_r": e.KEY_RIGHTMETA, + "super": e.KEY_LEFTMETA, +} + + +keys = { + # Lowercase + "a": e.KEY_A, + "b": e.KEY_B, + "c": e.KEY_C, + "d": e.KEY_D, + "e": e.KEY_E, + "f": e.KEY_F, + "g": e.KEY_G, + "h": e.KEY_H, + "i": e.KEY_I, + "j": e.KEY_J, + "k": e.KEY_K, + "l": e.KEY_L, + "m": e.KEY_M, + "n": e.KEY_N, + "o": e.KEY_O, + "p": e.KEY_P, + "q": e.KEY_Q, + "r": e.KEY_R, + "s": e.KEY_S, + "t": e.KEY_T, + "u": e.KEY_U, + "v": e.KEY_V, + "w": e.KEY_W, + "x": e.KEY_X, + "y": e.KEY_Y, + "z": e.KEY_Z, + # Numbers + "1": e.KEY_1, + "2": e.KEY_2, + "3": e.KEY_3, + "4": e.KEY_4, + "5": e.KEY_5, + "6": e.KEY_6, + "7": e.KEY_7, + "8": e.KEY_8, + "9": e.KEY_9, + "0": e.KEY_0, + # Symbols + "-": e.KEY_MINUS, + "=": e.KEY_EQUAL, + " ": e.KEY_SPACE, + "[": e.KEY_LEFTBRACE, + "]": e.KEY_RIGHTBRACE, + ";": e.KEY_SEMICOLON, + "'": e.KEY_APOSTROPHE, + "`": e.KEY_GRAVE, + "\\": e.KEY_BACKSLASH, + ".": e.KEY_DOT, + ",": e.KEY_COMMA, + "/": e.KEY_SLASH, + "\b": e.KEY_BACKSPACE, + "\n": e.KEY_ENTER, + # Other keys (for send_key_combination()) + # https://github.com/openstenoproject/plover/blob/9b5a357f1fb57cb0a9a8596ae12cd1e84fcff6c4/plover/oslayer/osx/keyboardcontrol.py#L75 + # https://gist.github.com/jfortin42/68a1fcbf7738a1819eb4b2eef298f4f8 + "return": e.KEY_ENTER, + "tab": e.KEY_TAB, + "backspace": e.KEY_BACKSPACE, + "delete": e.KEY_DELETE, + "escape": e.KEY_ESC, + "clear": e.KEY_CLEAR, + # Navigation + "up": e.KEY_UP, + "down": e.KEY_DOWN, + "left": e.KEY_LEFT, + "right": e.KEY_RIGHT, + "page_up": e.KEY_PAGEUP, + "page_down": e.KEY_PAGEDOWN, + "home": e.KEY_HOME, + "insert": e.KEY_INSERT, + "end": e.KEY_END, + "space": e.KEY_SPACE, + "print": e.KEY_PRINT, + # Function keys + "fn": e.KEY_FN, + "f1": e.KEY_F1, + "f2": e.KEY_F2, + "f3": e.KEY_F3, + "f4": e.KEY_F4, + "f5": e.KEY_F5, + "f6": e.KEY_F6, + "f7": e.KEY_F7, + "f8": e.KEY_F8, + "f9": e.KEY_F9, + "f10": e.KEY_F10, + "f11": e.KEY_F11, + "f12": e.KEY_F12, + "f13": e.KEY_F13, + "f14": e.KEY_F14, + "f15": e.KEY_F15, + "f16": e.KEY_F16, + "f17": e.KEY_F17, + "f18": e.KEY_F18, + "f19": e.KEY_F19, + "f20": e.KEY_F20, + "f21": e.KEY_F21, + "f22": e.KEY_F22, + "f23": e.KEY_F23, + "f24": e.KEY_F24, + # Numpad + "kp_1": e.KEY_KP1, + "kp_2": e.KEY_KP2, + "kp_3": e.KEY_KP3, + "kp_4": e.KEY_KP4, + "kp_5": e.KEY_KP5, + "kp_6": e.KEY_KP6, + "kp_7": e.KEY_KP7, + "kp_8": e.KEY_KP8, + "kp_9": e.KEY_KP9, + "kp_0": e.KEY_KP0, + "kp_add": e.KEY_KPPLUS, + "kp_decimal": e.KEY_KPDOT, + "kp_delete": e.KEY_DELETE, # There is no KPDELETE + "kp_divide": e.KEY_KPSLASH, + "kp_enter": e.KEY_KPENTER, + "kp_equal": e.KEY_KPEQUAL, + "kp_multiply": e.KEY_KPASTERISK, + "kp_subtract": e.KEY_KPMINUS, + # Media keys + "audioraisevolume": e.KEY_VOLUMEUP, + "audiolowervolume": e.KEY_VOLUMEDOWN, + "monbrightnessup": e.KEY_BRIGHTNESSUP, + "monbrightnessdown": e.KEY_BRIGHTNESSDOWN, + "audiomute": e.KEY_MUTE, + "num_lock": e.KEY_NUMLOCK, + "eject": e.KEY_EJECTCD, + "audiopause": e.KEY_PAUSE, + "audioplay": e.KEY_PLAY, + "audionext": e.KEY_NEXT, # Maybe VIDEO_NEXT or NEXTSONG + "audiorewind": e.KEY_REWIND, + "kbdbrightnessup": e.KEY_KBDILLUMUP, + "kbdbrightnessdown": e.KEY_KBDILLUMDOWN, +} + + +class KeyboardEmulation(GenericKeyboardEmulation): + def __init__(self): + super().__init__() + self._res = util.find_ecodes_by_regex(r"KEY_.*") + self._ui = UInput(self._res) + # Set the keyboard layout from an environment variable + kb_env = "PLOVER_UINPUT_LAYOUT" + kb_layout = os.environ[kb_env] if kb_env in os.environ else "us" + self._set_layout(kb_layout) + + def _set_layout(self, layout): + log.info("Using keyboard layout " + layout + " for keyboard emulation.") + symbols = generate_symbols(layout) + # Remove unwanted symbols from the table + # Includes symbols such as numpad-star - use unicode instead + # There has to be a cleaner way to do this + syms_to_remove = [] + for sym in symbols: + (base, _) = symbols[sym] + if base not in keys: + syms_to_remove.append(sym) + for sym in syms_to_remove: + symbols.pop(sym) + self._symbols = symbols + + def _press_key(self, key, state): + self._ui.write(e.EV_KEY, key, 1 if state else 0) + self._ui.syn() + + def _send_key(self, key): + self._press_key(key, True) + self._press_key(key, False) + self._ui.syn() + + # Send unicode character (through an IME such as iBus or fcitx5) + # This assumes you are using the default keybinding to input unicode hex chanracters + # TODO: avoid hard-coded keys and delays here? + # TODO: at least for the "u" key, as it can vary with different keyboard layouts - maybe use the already existing generated symbols table? + def _send_unicode(self, hex): + self._press_key(modifiers["control_l"], True) + self._press_key(modifiers["shift_r"], True) + self.delay() + self._send_key(keys["u"]) + self.delay() + self._press_key(modifiers["control_l"], False) + self._press_key(modifiers["shift_r"], False) + self.delay() + self.send_string(hex) + self._send_key(keys["\n"]) + + def _send_char(self, char): + # Key can be sent with a key combination + if char in self._symbols: + (base, mods) = self._symbols[char] + for mod in mods: + self._press_key(modifiers[mod], True) + self.delay() + self._send_key(keys[base]) + for mod in mods: + self._press_key(modifiers[mod], False) + + # Key press can not be emulated - send unicode symbol instead + else: + # Convert to hex and remove leading "0x" + unicode_hex = hex(ord(char))[2:] + self._send_unicode(unicode_hex) + + def send_string(self, string): + for key in self.with_delay(list(string)): + self._send_char(key) + + def send_backspaces(self, count): + for _ in range(count): + self._send_key(keys["\b"]) + + def send_key_combination(self, combo): + key_events = parse_key_combo(combo) + + for key, pressed in self.with_delay(key_events): + k = ( + modifiers[key.lower()] + if key.lower() in modifiers + else keys[key.lower()] + ) + self._press_key(k, pressed) + + +class KeyboardCapture(Capture): + def __init__(self): + raise NotImplementedError() # TODO diff --git a/plover/oslayer/linux/xkb_symbols.py b/plover/oslayer/linux/xkb_symbols.py new file mode 100644 index 000000000..72b863aae --- /dev/null +++ b/plover/oslayer/linux/xkb_symbols.py @@ -0,0 +1,59 @@ +""" +Simple loop to go through all keys in a keymap, and find the possible keys to type using modifiers, +then return a list in the format [(base, modifiers)], that can be used to emulate the key presses +This code is a bit messy, and could be improved on a lot, but from my testing, it seems to work with different layouts +""" + +from xkbcommon import xkb + + +# layout can be "no", "us", "gb", "fr" or any other xkb layout +def generate_symbols(layout="us"): + ctx = xkb.Context() + keymap = ctx.keymap_new_from_names(layout=layout) + # The keymaps have to be "translated" to a US layout keyboard for evdev + keymap_us = ctx.keymap_new_from_names(layout="us") + + # Modifier names from xkb, converted to lists of modifiers + level_mapping = { + 0: [], + 1: ["shift_l"], + 2: ["alt_r"], + 3: ["shift_l", "alt_r"], + } + + symbols = {} + + for key in iter(keymap): + try: + # Levels are different outputs from the same key with modifiers pressed + levels = keymap.num_levels_for_key(key, 1) + if levels == 0: # Key has no output + continue + + # === Base key symbol === + base_key_syms = keymap_us.key_get_syms_by_level(key, 1, 0) + if len(base_key_syms) == 0: # There are no symbols for this key + continue + base_key = xkb.keysym_to_string(base_key_syms[0]) + if base_key is None: + continue + + # === Key variations === + if levels < 1: # There are no variations (Check maybe not needed) + continue + for level in range(0, levels + 1): # Ignoring the first (base) one + level_key_syms = keymap.key_get_syms_by_level(key, 1, level) + if len(level_key_syms) == 0: + continue + level_key = xkb.keysym_to_string(level_key_syms[0]) + if level_key is None: + continue + modifiers = level_mapping.get(level, "") + if not level_key in symbols: + symbols[level_key] = (base_key, modifiers) + except xkb.XKBInvalidKeycode: + # Iter *should* return only valid, but still returns some invalid... + pass + + return symbols diff --git a/reqs/dist.txt b/reqs/dist.txt index 43102086a..4dc8c98b2 100644 --- a/reqs/dist.txt +++ b/reqs/dist.txt @@ -7,6 +7,8 @@ pyobjc-framework-Quartz>=4.0; "darwin" in sys_platform pyserial>=2.7 python-xlib>=0.16; ("linux" in sys_platform or "bsd" in sys_platform) and python_version < "3.9" python-xlib>=0.29; ("linux" in sys_platform or "bsd" in sys_platform) and python_version >= "3.9" +evdev; "linux" in sys_platform or "bsd" in sys_platform +xkbcommon; "linux" in sys_platform or "bsd" in sys_platform rtf_tokenize setuptools wcwidth From 4e146c97946f1765fa9ae6ce83d18d80194c8e71 Mon Sep 17 00:00:00 2001 From: LilleAila Date: Thu, 11 Jul 2024 18:58:31 +0200 Subject: [PATCH 2/9] implemented uinput keyboard capture and added comments about remaining things that have to be done --- .../oslayer/linux/keyboardcontrol_uinput.py | 74 +++++++++++++++++-- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/plover/oslayer/linux/keyboardcontrol_uinput.py b/plover/oslayer/linux/keyboardcontrol_uinput.py index 9ad92f63e..0e85b0112 100644 --- a/plover/oslayer/linux/keyboardcontrol_uinput.py +++ b/plover/oslayer/linux/keyboardcontrol_uinput.py @@ -1,12 +1,14 @@ +# TODO: add some logging + # TODO: get a stop() function to run, calling `self._ui.close()` # TODO: figure out a better way to do this: import os # To read the layout variable -# TODO: find out how to add evdev as a dependency # TODO: somehow get the requires udev rule installed -from evdev import UInput, ecodes as e, util +from evdev import UInput, ecodes as e, util, InputDevice, list_devices +import threading +from select import select -# TODO: add xkbcommon as a dependency, *or* figure out how to do it using xlib instead, as it is already a dependency for the x11 keyboard emulation from .xkb_symbols import generate_symbols from plover.output.keyboard import GenericKeyboardEmulation @@ -166,7 +168,7 @@ "kbdbrightnessup": e.KEY_KBDILLUMUP, "kbdbrightnessdown": e.KEY_KBDILLUMDOWN, } - +KEYCODE_TO_KEY = dict(zip(keys.values(), keys.keys())) class KeyboardEmulation(GenericKeyboardEmulation): def __init__(self): @@ -257,4 +259,66 @@ def send_key_combination(self, combo): class KeyboardCapture(Capture): def __init__(self): - raise NotImplementedError() # TODO + super().__init__() + # This is based on the example from the python-evdev documentation, using the first of the three alternative methods: https://python-evdev.readthedocs.io/en/latest/tutorial.html#reading-events-from-multiple-devices-using-select + self._devices = self._get_devices() + self._running = False + self._thread = None + + def _get_devices(self): + input_devices = [InputDevice(path) for path in list_devices()] + keyboard_devices = [dev for dev in input_devices if self._filter_devices(dev)] + return {dev.fd: dev for dev in keyboard_devices} + + def _filter_devices(self, device): + is_uinput = device.name == "py-evdev-uinput" or device.phys == "py-evdev-uinput" + capabilities = device.capabilities() + is_mouse = e.EV_REL in capabilities or e.EV_ABS in capabilities + return not is_uinput and not is_mouse + + def start(self): + self._running = True + self._thread = threading.Thread(target=self._run) + self._thread.start() + + def cancel(self): + self._running = False + [dev.ungrab() for dev in self._devices.values()] + if self._thread is not None: + self._thread.join() + + def suppress(self, suppressed_keys=()): + """ + TODO: instead of grabbing the entire keyboard, only select keys should be suppressed + Unfortunately, that is not possible using python-evdev. To circumvent this, i think + i have to emulate the keyboard and send the keys through KeyboardEmulation if they + are not present in the list of suppressed keys + """ + pass + + def _run(self): + [dev.grab() for dev in self._devices.values()] + while self._running: + """ + The select() call blocks the loop until it gets an input, which meant that the keyboard + had to be pressed once after executing `cancel()`. Now, there is a 1 second delay instead + FIXME: maybe use one of the other options to avoid the timeout + https://python-evdev.readthedocs.io/en/latest/tutorial.html#reading-events-from-multiple-devices-using-select + """ + r, _, _ = select(self._devices, [], [], 1) + for fd in r: + for event in self._devices[fd].read(): + if event.type == e.EV_KEY: + try: + key_code = event.code + # TODO: support different layouts through the xkb_symbols thing + if key_code in KEYCODE_TO_KEY: + key_name = KEYCODE_TO_KEY[key_code] + pressed = event.value == 1 + if pressed: + log.info(f"Key {key_name} pressed") + else: + log.info(f"Key {key_name} released") + (self.key_down if pressed else self.key_up)(key_name) + except KeyError: + pass From b886bdcfd71243ba33059fc098774700d0c59b8f Mon Sep 17 00:00:00 2001 From: LilleAila Date: Fri, 12 Jul 2024 09:08:24 +0200 Subject: [PATCH 3/9] finished keyboard capture added suppressing keys and pass through non-suppressed keys --- .../oslayer/linux/keyboardcontrol_uinput.py | 83 +++++++++++-------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/plover/oslayer/linux/keyboardcontrol_uinput.py b/plover/oslayer/linux/keyboardcontrol_uinput.py index 0e85b0112..b293b0de8 100644 --- a/plover/oslayer/linux/keyboardcontrol_uinput.py +++ b/plover/oslayer/linux/keyboardcontrol_uinput.py @@ -1,6 +1,6 @@ # TODO: add some logging -# TODO: get a stop() function to run, calling `self._ui.close()` +# TODO: get a stop() function to run when KeyboardEmulation stops, calling `self._ui.close()` # TODO: figure out a better way to do this: import os # To read the layout variable @@ -168,7 +168,8 @@ "kbdbrightnessup": e.KEY_KBDILLUMUP, "kbdbrightnessdown": e.KEY_KBDILLUMDOWN, } -KEYCODE_TO_KEY = dict(zip(keys.values(), keys.keys())) +KEY_TO_KEYCODE = {**keys, **modifiers} +KEYCODE_TO_KEY = dict(zip(KEY_TO_KEYCODE.values(), KEY_TO_KEYCODE.keys())) class KeyboardEmulation(GenericKeyboardEmulation): def __init__(self): @@ -189,7 +190,7 @@ def _set_layout(self, layout): syms_to_remove = [] for sym in symbols: (base, _) = symbols[sym] - if base not in keys: + if base not in KEY_TO_KEYCODE: syms_to_remove.append(sym) for sym in syms_to_remove: symbols.pop(sym) @@ -204,32 +205,35 @@ def _send_key(self, key): self._press_key(key, False) self._ui.syn() - # Send unicode character (through an IME such as iBus or fcitx5) - # This assumes you are using the default keybinding to input unicode hex chanracters - # TODO: avoid hard-coded keys and delays here? + """ + Send a unicode character. + This depends on an IME such as iBus or fcitx5. iBus is used by GNOME, and fcitx5 by KDE. + It assumes the default keybinding ctrl-shift-u, enter hex, enter is used, which is the default in both. + """ + # TODO: avoid hard-coded keys and delays here, maybe call the send key combination function? # TODO: at least for the "u" key, as it can vary with different keyboard layouts - maybe use the already existing generated symbols table? def _send_unicode(self, hex): - self._press_key(modifiers["control_l"], True) - self._press_key(modifiers["shift_r"], True) + self._press_key(KEY_TO_KEYCODE["control_l"], True) + self._press_key(KEY_TO_KEYCODE["shift_r"], True) self.delay() - self._send_key(keys["u"]) + self._send_key(KEY_TO_KEYCODE["u"]) self.delay() - self._press_key(modifiers["control_l"], False) - self._press_key(modifiers["shift_r"], False) + self._press_key(KEY_TO_KEYCODE["control_l"], False) + self._press_key(KEY_TO_KEYCODE["shift_r"], False) self.delay() self.send_string(hex) - self._send_key(keys["\n"]) + self._send_key(KEY_TO_KEYCODE["\n"]) def _send_char(self, char): # Key can be sent with a key combination if char in self._symbols: (base, mods) = self._symbols[char] for mod in mods: - self._press_key(modifiers[mod], True) + self._press_key(KEY_TO_KEYCODE[mod], True) self.delay() - self._send_key(keys[base]) + self._send_key(KEY_TO_KEYCODE[base]) for mod in mods: - self._press_key(modifiers[mod], False) + self._press_key(KEY_TO_KEYCODE[mod], False) # Key press can not be emulated - send unicode symbol instead else: @@ -243,17 +247,13 @@ def send_string(self, string): def send_backspaces(self, count): for _ in range(count): - self._send_key(keys["\b"]) + self._send_key(KEY_TO_KEYCODE["\b"]) def send_key_combination(self, combo): key_events = parse_key_combo(combo) for key, pressed in self.with_delay(key_events): - k = ( - modifiers[key.lower()] - if key.lower() in modifiers - else keys[key.lower()] - ) + k = KEY_TO_KEYCODE[key.lower()] self._press_key(k, pressed) @@ -264,6 +264,9 @@ def __init__(self): self._devices = self._get_devices() self._running = False self._thread = None + self._res = util.find_ecodes_by_regex(r"KEY_.*") + self._ui = UInput(self._res) + self._suppressed_keys = [] def _get_devices(self): input_devices = [InputDevice(path) for path in list_devices()] @@ -286,15 +289,15 @@ def cancel(self): [dev.ungrab() for dev in self._devices.values()] if self._thread is not None: self._thread.join() + self._ui.close() def suppress(self, suppressed_keys=()): """ - TODO: instead of grabbing the entire keyboard, only select keys should be suppressed - Unfortunately, that is not possible using python-evdev. To circumvent this, i think - i have to emulate the keyboard and send the keys through KeyboardEmulation if they - are not present in the list of suppressed keys + UInput is not capable of suppressing only specific keys. To get around this, non-suppressed keys + are passed through to a UInput device and emulated, while keys in this list get sent to plover. + It does add a little bit of delay, but that is not noticeable. """ - pass + self._suppressed_keys = suppressed_keys def _run(self): [dev.grab() for dev in self._devices.values()] @@ -310,15 +313,23 @@ def _run(self): for event in self._devices[fd].read(): if event.type == e.EV_KEY: try: - key_code = event.code - # TODO: support different layouts through the xkb_symbols thing - if key_code in KEYCODE_TO_KEY: - key_name = KEYCODE_TO_KEY[key_code] - pressed = event.value == 1 - if pressed: - log.info(f"Key {key_name} pressed") - else: - log.info(f"Key {key_name} released") - (self.key_down if pressed else self.key_up)(key_name) + """ + It could be possible to support different layouts through the xkb_symbols script + , however, i think having it like this is actually better, because the keys + are supposed to be in these locations, and reduces the amount of work done + configuring it afterwards. + """ + if event.code in KEYCODE_TO_KEY: + key_name = KEYCODE_TO_KEY[event.code] + if key_name in self._suppressed_keys: + pressed = event.value == 1 + if pressed: + log.info(f"Key {key_name} pressed") + else: + log.info(f"Key {key_name} released") + (self.key_down if pressed else self.key_up)(key_name) + continue # Go to the next iteration, skipping the below code: + self._ui.write(e.EV_KEY, event.code, event.value) + self._ui.syn() except KeyError: pass From cb49aa0756208ff56a23781782beb5fc10949a55 Mon Sep 17 00:00:00 2001 From: LilleAila Date: Fri, 12 Jul 2024 09:41:43 +0200 Subject: [PATCH 4/9] uinput: added news and improved various functions --- news.d/feature/1679.linux.md | 1 + .../oslayer/linux/keyboardcontrol_uinput.py | 72 +++++++++---------- 2 files changed, 37 insertions(+), 36 deletions(-) create mode 100644 news.d/feature/1679.linux.md diff --git a/news.d/feature/1679.linux.md b/news.d/feature/1679.linux.md new file mode 100644 index 000000000..9ca0b7b36 --- /dev/null +++ b/news.d/feature/1679.linux.md @@ -0,0 +1 @@ +Added keyboard emulation and capture using uinput, compatible with X11, Wayland and anything else on linux and bsd. diff --git a/plover/oslayer/linux/keyboardcontrol_uinput.py b/plover/oslayer/linux/keyboardcontrol_uinput.py index b293b0de8..253b26e9c 100644 --- a/plover/oslayer/linux/keyboardcontrol_uinput.py +++ b/plover/oslayer/linux/keyboardcontrol_uinput.py @@ -1,5 +1,3 @@ -# TODO: add some logging - # TODO: get a stop() function to run when KeyboardEmulation stops, calling `self._ui.close()` # TODO: figure out a better way to do this: import os # To read the layout variable @@ -21,6 +19,9 @@ "alt_l": e.KEY_LEFTALT, "alt_r": e.KEY_RIGHTALT, "alt": e.KEY_LEFTALT, + "ctrl_l": e.KEY_LEFTCTRL, + "ctrl_r": e.KEY_RIGHTCTRL, + "ctrl": e.KEY_LEFTCTRL, "control_l": e.KEY_LEFTCTRL, "control_r": e.KEY_RIGHTCTRL, "control": e.KEY_LEFTCTRL, @@ -87,7 +88,6 @@ "/": e.KEY_SLASH, "\b": e.KEY_BACKSPACE, "\n": e.KEY_ENTER, - # Other keys (for send_key_combination()) # https://github.com/openstenoproject/plover/blob/9b5a357f1fb57cb0a9a8596ae12cd1e84fcff6c4/plover/oslayer/osx/keyboardcontrol.py#L75 # https://gist.github.com/jfortin42/68a1fcbf7738a1819eb4b2eef298f4f8 "return": e.KEY_ENTER, @@ -209,20 +209,16 @@ def _send_key(self, key): Send a unicode character. This depends on an IME such as iBus or fcitx5. iBus is used by GNOME, and fcitx5 by KDE. It assumes the default keybinding ctrl-shift-u, enter hex, enter is used, which is the default in both. + From my testing, it works fine in using iBus and fcitx5, but in kitty terminal emulator, which uses + the same keybinding, it's too fast for it to handle and ends up writing random stuff. I don't + think there is a way to fix that other than increasing the delay. """ - # TODO: avoid hard-coded keys and delays here, maybe call the send key combination function? - # TODO: at least for the "u" key, as it can vary with different keyboard layouts - maybe use the already existing generated symbols table? def _send_unicode(self, hex): - self._press_key(KEY_TO_KEYCODE["control_l"], True) - self._press_key(KEY_TO_KEYCODE["shift_r"], True) - self.delay() - self._send_key(KEY_TO_KEYCODE["u"]) - self.delay() - self._press_key(KEY_TO_KEYCODE["control_l"], False) - self._press_key(KEY_TO_KEYCODE["shift_r"], False) + self.send_key_combination("ctrl_l(shift(u))") self.delay() self.send_string(hex) - self._send_key(KEY_TO_KEYCODE["\n"]) + self.delay() + self._send_char("\n") def _send_char(self, char): # Key can be sent with a key combination @@ -253,8 +249,16 @@ def send_key_combination(self, combo): key_events = parse_key_combo(combo) for key, pressed in self.with_delay(key_events): - k = KEY_TO_KEYCODE[key.lower()] - self._press_key(k, pressed) + try: + if key.lower() in self._symbols: + # Ignore modifier layers for this one, there may be better ways to do this logic + (base, _) = self._symbols[key.lower()] + k = KEY_TO_KEYCODE[base] + else: + k = KEY_TO_KEYCODE[key.lower()] + self._press_key(k, pressed) + except KeyError: # In case the key does not exist + log.warning("Key " + key + " is not valid!") class KeyboardCapture(Capture): @@ -274,6 +278,9 @@ def _get_devices(self): return {dev.fd: dev for dev in keyboard_devices} def _filter_devices(self, device): + """ + Filter out devices that should not be grabbed and suppressed, to avoid output feeding into itself. + """ is_uinput = device.name == "py-evdev-uinput" or device.phys == "py-evdev-uinput" capabilities = device.capabilities() is_mouse = e.EV_REL in capabilities or e.EV_ABS in capabilities @@ -312,24 +319,17 @@ def _run(self): for fd in r: for event in self._devices[fd].read(): if event.type == e.EV_KEY: - try: - """ - It could be possible to support different layouts through the xkb_symbols script - , however, i think having it like this is actually better, because the keys - are supposed to be in these locations, and reduces the amount of work done - configuring it afterwards. - """ - if event.code in KEYCODE_TO_KEY: - key_name = KEYCODE_TO_KEY[event.code] - if key_name in self._suppressed_keys: - pressed = event.value == 1 - if pressed: - log.info(f"Key {key_name} pressed") - else: - log.info(f"Key {key_name} released") - (self.key_down if pressed else self.key_up)(key_name) - continue # Go to the next iteration, skipping the below code: - self._ui.write(e.EV_KEY, event.code, event.value) - self._ui.syn() - except KeyError: - pass + """ + It could be possible to support different layouts through the xkb_symbols script + , however, i think having it like this is actually better, because the keys + are supposed to be in these locations, and reduces the amount of work done + configuring it afterwards. + """ + if event.code in KEYCODE_TO_KEY: + key_name = KEYCODE_TO_KEY[event.code] + if key_name in self._suppressed_keys: + pressed = event.value == 1 + (self.key_down if pressed else self.key_up)(key_name) + continue # Go to the next iteration, skipping the below code: + self._ui.write(e.EV_KEY, event.code, event.value) + self._ui.syn() From 949854a00fffc2df6f500bf5217abc2c8de46395 Mon Sep 17 00:00:00 2001 From: LilleAila Date: Fri, 12 Jul 2024 09:59:44 +0200 Subject: [PATCH 5/9] don't run activateWindow() on wayland that function is not supported on wayland, and gave this warning: Qt: Wayland does not support QWindow::requestActivate() --- plover/gui_qt/main_window.py | 15 +++++++++++++-- plover/oslayer/linux/keyboardcontrol_uinput.py | 3 +++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/plover/gui_qt/main_window.py b/plover/gui_qt/main_window.py index cc6de0df4..4a9bc414f 100644 --- a/plover/gui_qt/main_window.py +++ b/plover/gui_qt/main_window.py @@ -9,6 +9,7 @@ from PyQt5.QtWidgets import ( QMainWindow, QMenu, + QApplication, ) from plover import _, log @@ -151,6 +152,14 @@ def set_visible(self, visible): else: self.showMinimized() + def _is_wayland(self): + """ + Calling .activateWindow() on a window results in this error: + Qt: Wayland does not support QWindow::requestActivate() + This function returns a boolean used to determine whether to run activateWindow() + """ + return "wayland" in QApplication.platformName().lower() + def _activate_dialog(self, name, args=(), manage_windows=False): if manage_windows: previous_window = wmctrl.GetForegroundWindow() @@ -166,7 +175,8 @@ def on_finished(): wmctrl.SetForegroundWindow(previous_window) dialog.finished.connect(on_finished) dialog.showNormal() - dialog.activateWindow() + if not self._is_wayland(): + dialog.activateWindow() dialog.raise_() def _add_translation(self, dictionary=None, manage_windows=False): @@ -177,7 +187,8 @@ def _add_translation(self, dictionary=None, manage_windows=False): def _focus(self): self.showNormal() - self.activateWindow() + if not self._is_wayland(): + self.activateWindow() self.raise_() def _configure(self, manage_windows=False): diff --git a/plover/oslayer/linux/keyboardcontrol_uinput.py b/plover/oslayer/linux/keyboardcontrol_uinput.py index 253b26e9c..60f054177 100644 --- a/plover/oslayer/linux/keyboardcontrol_uinput.py +++ b/plover/oslayer/linux/keyboardcontrol_uinput.py @@ -174,6 +174,7 @@ class KeyboardEmulation(GenericKeyboardEmulation): def __init__(self): super().__init__() + # Initialize UInput with all keys available self._res = util.find_ecodes_by_regex(r"KEY_.*") self._ui = UInput(self._res) # Set the keyboard layout from an environment variable @@ -182,6 +183,7 @@ def __init__(self): self._set_layout(kb_layout) def _set_layout(self, layout): + # Get the system keyboard layout so that emulation works on different layouts log.info("Using keyboard layout " + layout + " for keyboard emulation.") symbols = generate_symbols(layout) # Remove unwanted symbols from the table @@ -246,6 +248,7 @@ def send_backspaces(self, count): self._send_key(KEY_TO_KEYCODE["\b"]) def send_key_combination(self, combo): + # https://plover.readthedocs.io/en/latest/api/key_combo.html#module-plover.key_combo key_events = parse_key_combo(combo) for key, pressed in self.with_delay(key_events): From 2d294e3c99bde0f19368b9db3c87bf5a374d81d6 Mon Sep 17 00:00:00 2001 From: LilleAila Date: Fri, 12 Jul 2024 15:22:23 +0200 Subject: [PATCH 6/9] specify version of xkbcommon, as recommended by the docs --- linux/README.md | 1 + reqs/dist.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/linux/README.md b/linux/README.md index 17f90702b..7973a57b6 100644 --- a/linux/README.md +++ b/linux/README.md @@ -8,5 +8,6 @@ distribution corresponding packages): - Treal support: `libusb` (1.0) and `libudev` are needed by the [`hidapi` package](https://pypi.org/project/hidapi/). - log / notifications support: `libdbus` is needed. +- Uinput support: `libxkbcommon` are needed by the [`xkbcommon` package](https://pypi.org/project/xkbcommon) For the rest of the steps, follow the [developer guide](../doc/developer_guide.md). diff --git a/reqs/dist.txt b/reqs/dist.txt index 4dc8c98b2..af671e290 100644 --- a/reqs/dist.txt +++ b/reqs/dist.txt @@ -8,7 +8,7 @@ pyserial>=2.7 python-xlib>=0.16; ("linux" in sys_platform or "bsd" in sys_platform) and python_version < "3.9" python-xlib>=0.29; ("linux" in sys_platform or "bsd" in sys_platform) and python_version >= "3.9" evdev; "linux" in sys_platform or "bsd" in sys_platform -xkbcommon; "linux" in sys_platform or "bsd" in sys_platform +xkbcommon<1.1; "linux" in sys_platform or "bsd" in sys_platform rtf_tokenize setuptools wcwidth From a5a50a4f88d394f7d9618c44940cf2fac29d8024 Mon Sep 17 00:00:00 2001 From: LilleAila Date: Sat, 13 Jul 2024 09:40:27 +0200 Subject: [PATCH 7/9] Install udev rule in AppRun on linux --- See | 0 [36 | 0 linux/appimage/apprun.sh | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 See create mode 100644 [36 diff --git a/See b/See new file mode 100644 index 000000000..e69de29bb diff --git a/[36 b/[36 new file mode 100644 index 000000000..e69de29bb diff --git a/linux/appimage/apprun.sh b/linux/appimage/apprun.sh index d610dfc52..f31068c2f 100755 --- a/linux/appimage/apprun.sh +++ b/linux/appimage/apprun.sh @@ -62,8 +62,44 @@ appimage_python() exec "${APPDIR}/usr/bin/python" "$@" } +install_udev_rule() +{ + # Pass through variables because pkexec doesn't pass through env + local UDEV_RULE_FILE="$1" + local USER="$2" + if ! grep -q "^plover:" /etc/group; then + groupadd plover + fi + # NOTE: this requires a reboot! + if ! groups "$USER" | grep -qw "plover"; then + usermod -aG plover "$USER" + fi + if [ ! -f "$UDEV_RULE_FILE" ]; then + echo 'KERNEL=="uinput", GROUP="plover", MODE="0660", OPTIONS+="static_node=uinput"' > "$UDEV_RULE_FILE" + chmod 644 "$UDEV_RULE_FILE" + udevadm control --reload-rules + udevadm trigger + # Temporarily give the current user access + # This is done because the groupadd does not take effect until next reboot + # And this temporary solution works *until* the next reboot + # FIXME if someone can find a better solution + chown "${USER}:plover" /dev/uinput + chmod 660 /dev/uinput + fi +} + appimage_launch() { + # Install the udev rule required for uinput + UDEV_RULE_FILE="/etc/udev/rules.d/99-plover-uinput.rules" + # It's done like this to have the lowest possible number of pkexec calls + # Each time it's called, the user gets shown a new password input dialog + # FIXME if there is an easier way to do it + if [ ! -f "$UDEV_RULE_FILE" ] || ! grep -q "^plover:" /etc/group || ! groups | grep -qw "plover"; then + notify-send -t 10000 "Installing udev rules" "You will be prompted for your password" + pkexec bash -c "$(declare -f install_udev_rule); install_udev_rule '$UDEV_RULE_FILE' '$USER'" + notify-send -t 10000 "Successfully installed udev rules" "A reboot may be required for output to work" + fi appimage_python -s -m plover.scripts.dist_main "$@" } @@ -73,6 +109,7 @@ APPDIR="$(dirname "$(readlink -e "$0")")" appimage_setenv + # Handle AppImage specific options. [ -n "$APPIMAGE" ] && case "$1" in --install|--uninstall) From abcd919d055b3591b90a07b0675ac764abca0bac Mon Sep 17 00:00:00 2001 From: LilleAila Date: Sat, 13 Jul 2024 16:43:27 +0200 Subject: [PATCH 8/9] added config option to set keyboard layout --- See | 0 [36 | 0 linux/appimage/apprun.sh | 1 - plover/config.py | 15 +++++++++++++ plover/engine.py | 3 +++ plover/gui_qt/config_window.py | 22 +++++++++++++++++++ .../oslayer/linux/keyboardcontrol_uinput.py | 18 +++------------ 7 files changed, 43 insertions(+), 16 deletions(-) delete mode 100644 See delete mode 100644 [36 diff --git a/See b/See deleted file mode 100644 index e69de29bb..000000000 diff --git a/[36 b/[36 deleted file mode 100644 index e69de29bb..000000000 diff --git a/linux/appimage/apprun.sh b/linux/appimage/apprun.sh index f31068c2f..1af393555 100755 --- a/linux/appimage/apprun.sh +++ b/linux/appimage/apprun.sh @@ -109,7 +109,6 @@ APPDIR="$(dirname "$(readlink -e "$0")")" appimage_setenv - # Handle AppImage specific options. [ -n "$APPIMAGE" ] && case "$1" in --install|--uninstall) diff --git a/plover/config.py b/plover/config.py index 7ae6c3330..e9861ae5f 100644 --- a/plover/config.py +++ b/plover/config.py @@ -122,6 +122,20 @@ def validate(config, key, value): return value return ConfigOption(name, lambda c, k: default, getter, setter, validate, None) +def str_option(name, default, section, option=None): + option = option or name + def getter(config, key): + return config._config[section][option] + def setter(config, key, value): + config._set(section, option, str(value)) + def validate(config, key, value): + try: + value = str(value) + except ValueError as e: + raise InvalidConfigOption(value, default) from e + return value + return ConfigOption(name, lambda c, k: default, getter, setter, validate, None) + def boolean_option(name, default, section, option=None): option = option or name def getter(config, key): @@ -338,6 +352,7 @@ def _set(self, section, option, value): boolean_option('start_capitalized', False, OUTPUT_CONFIG_SECTION), int_option('undo_levels', DEFAULT_UNDO_LEVELS, MINIMUM_UNDO_LEVELS, None, OUTPUT_CONFIG_SECTION), int_option('time_between_key_presses', DEFAULT_TIME_BETWEEN_KEY_PRESSES, MINIMUM_TIME_BETWEEN_KEY_PRESSES, None, OUTPUT_CONFIG_SECTION), + str_option('xkb_layout', "us", OUTPUT_CONFIG_SECTION), # Logging. path_option('log_file_name', expand_path('strokes.log'), LOGGING_CONFIG_SECTION, 'log_file'), boolean_option('enable_stroke_logging', False, LOGGING_CONFIG_SECTION), diff --git a/plover/engine.py b/plover/engine.py index cc83c5259..c1c5b8336 100644 --- a/plover/engine.py +++ b/plover/engine.py @@ -212,6 +212,9 @@ def _update(self, config_update=None, full=False, reset_machine=False): self._formatter.start_capitalized = config['start_capitalized'] self._translator.set_min_undo_length(config['undo_levels']) self._keyboard_emulation.set_key_press_delay(config['time_between_key_presses']) + # This only applies to UInput, because it emulates a physical keyboard and follows the layout set in software. Because there is no standard of defining it, the user has to do so manually if not using an US keyboard if not using an US keyboard. + if hasattr(self._keyboard_emulation, '_update_layout'): + self._keyboard_emulation._update_layout(config['xkb_layout']) # Update system. system_name = config['system_name'] if system.NAME != system_name: diff --git a/plover/gui_qt/config_window.py b/plover/gui_qt/config_window.py index a200a10d4..c24818f75 100644 --- a/plover/gui_qt/config_window.py +++ b/plover/gui_qt/config_window.py @@ -20,6 +20,7 @@ QLabel, QScrollArea, QSpinBox, + QLineEdit, QStyledItemDelegate, QTableWidget, QTableWidgetItem, @@ -71,6 +72,17 @@ def __init__(self, maximum=None, minimum=None): self.setMinimum(minimum) +class StrOption(QLineEdit): + + valueChanged = pyqtSignal(str) + + def __init__(self): + super().__init__() + self.textChanged.connect(self.valueChanged.emit) + + def setValue(self, value): + self.setText(value) + class ChoiceOption(QComboBox): valueChanged = pyqtSignal(str) @@ -392,6 +404,16 @@ def __init__(self, engine): 'programs time to process each key press.\n' 'Setting the delay too high will negatively impact the\n' 'performance of key stroke output.')), + ConfigOption(_('Keyboard Layout:'), 'xkb_layout', StrOption, + _('Set the keyboard layout configured in your system.\n' + 'Examples: "us", "gb", "fr", "no"\n' + '\n' + 'This only applies when using Linux/BSD and not using X11.\n' + 'If you\'re unsure, you probably don\'t need to change it.\n' + 'If you need to configure more options about your layout,\n' + 'such as setting the variant to a different layout like colemak,\n' + 'you can set environment variables starting with XKB_DEFAULT_\n' + 'for the RULES, MODEL, VARIANT and OPTIONS')), )), # i18n: Widget: “ConfigWindow”. (_('Plugins'), ( diff --git a/plover/oslayer/linux/keyboardcontrol_uinput.py b/plover/oslayer/linux/keyboardcontrol_uinput.py index 60f054177..071138131 100644 --- a/plover/oslayer/linux/keyboardcontrol_uinput.py +++ b/plover/oslayer/linux/keyboardcontrol_uinput.py @@ -1,8 +1,3 @@ -# TODO: get a stop() function to run when KeyboardEmulation stops, calling `self._ui.close()` -# TODO: figure out a better way to do this: -import os # To read the layout variable - -# TODO: somehow get the requires udev rule installed from evdev import UInput, ecodes as e, util, InputDevice, list_devices import threading from select import select @@ -163,7 +158,7 @@ "eject": e.KEY_EJECTCD, "audiopause": e.KEY_PAUSE, "audioplay": e.KEY_PLAY, - "audionext": e.KEY_NEXT, # Maybe VIDEO_NEXT or NEXTSONG + "audionext": e.KEY_NEXT, "audiorewind": e.KEY_REWIND, "kbdbrightnessup": e.KEY_KBDILLUMUP, "kbdbrightnessdown": e.KEY_KBDILLUMDOWN, @@ -177,18 +172,11 @@ def __init__(self): # Initialize UInput with all keys available self._res = util.find_ecodes_by_regex(r"KEY_.*") self._ui = UInput(self._res) - # Set the keyboard layout from an environment variable - kb_env = "PLOVER_UINPUT_LAYOUT" - kb_layout = os.environ[kb_env] if kb_env in os.environ else "us" - self._set_layout(kb_layout) - def _set_layout(self, layout): - # Get the system keyboard layout so that emulation works on different layouts + def _update_layout(self, layout): log.info("Using keyboard layout " + layout + " for keyboard emulation.") symbols = generate_symbols(layout) - # Remove unwanted symbols from the table - # Includes symbols such as numpad-star - use unicode instead - # There has to be a cleaner way to do this + # Remove symbols not in KEY_TO_KEYCODE syms_to_remove = [] for sym in symbols: (base, _) = symbols[sym] From 9fbe6b2bd43ce613eb9ce5cae253b38df636e7ea Mon Sep 17 00:00:00 2001 From: LilleAila Date: Sat, 13 Jul 2024 16:47:43 +0200 Subject: [PATCH 9/9] added ability to change layout of emulated keyboard, for example colemak --- linux/README.md | 2 +- plover/gui_qt/config_window.py | 6 ++---- plover/oslayer/linux/keyboardcontrol_uinput.py | 10 +++++++--- plover/oslayer/linux/xkb_symbols.py | 4 ++-- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/linux/README.md b/linux/README.md index 7973a57b6..e94faf491 100644 --- a/linux/README.md +++ b/linux/README.md @@ -8,6 +8,6 @@ distribution corresponding packages): - Treal support: `libusb` (1.0) and `libudev` are needed by the [`hidapi` package](https://pypi.org/project/hidapi/). - log / notifications support: `libdbus` is needed. -- Uinput support: `libxkbcommon` are needed by the [`xkbcommon` package](https://pypi.org/project/xkbcommon) +- Uinput support: `libxkbcommon` is needed by the [`xkbcommon` package](https://pypi.org/project/xkbcommon) For the rest of the steps, follow the [developer guide](../doc/developer_guide.md). diff --git a/plover/gui_qt/config_window.py b/plover/gui_qt/config_window.py index c24818f75..bc045f276 100644 --- a/plover/gui_qt/config_window.py +++ b/plover/gui_qt/config_window.py @@ -410,10 +410,8 @@ def __init__(self, engine): '\n' 'This only applies when using Linux/BSD and not using X11.\n' 'If you\'re unsure, you probably don\'t need to change it.\n' - 'If you need to configure more options about your layout,\n' - 'such as setting the variant to a different layout like colemak,\n' - 'you can set environment variables starting with XKB_DEFAULT_\n' - 'for the RULES, MODEL, VARIANT and OPTIONS')), + 'If you use a different layout variant, format it as\n' + '"language:layout", for example "us:colemak"')), )), # i18n: Widget: “ConfigWindow”. (_('Plugins'), ( diff --git a/plover/oslayer/linux/keyboardcontrol_uinput.py b/plover/oslayer/linux/keyboardcontrol_uinput.py index 071138131..04b0cdcb6 100644 --- a/plover/oslayer/linux/keyboardcontrol_uinput.py +++ b/plover/oslayer/linux/keyboardcontrol_uinput.py @@ -173,9 +173,13 @@ def __init__(self): self._res = util.find_ecodes_by_regex(r"KEY_.*") self._ui = UInput(self._res) - def _update_layout(self, layout): - log.info("Using keyboard layout " + layout + " for keyboard emulation.") - symbols = generate_symbols(layout) + def _update_layout(self, _layout): + log.info("Using keyboard layout " + + _layout + " for keyboard emulation.") + _layout_options = _layout.split(":") + layout = _layout_options[0] + variant = _layout_options[1] if len(_layout_options) > 1 else "" + symbols = generate_symbols(layout, variant) # Remove symbols not in KEY_TO_KEYCODE syms_to_remove = [] for sym in symbols: diff --git a/plover/oslayer/linux/xkb_symbols.py b/plover/oslayer/linux/xkb_symbols.py index 72b863aae..c192c6cd3 100644 --- a/plover/oslayer/linux/xkb_symbols.py +++ b/plover/oslayer/linux/xkb_symbols.py @@ -8,9 +8,9 @@ # layout can be "no", "us", "gb", "fr" or any other xkb layout -def generate_symbols(layout="us"): +def generate_symbols(layout="us", variant = ""): ctx = xkb.Context() - keymap = ctx.keymap_new_from_names(layout=layout) + keymap = ctx.keymap_new_from_names(layout=layout, variant=variant) # The keymaps have to be "translated" to a US layout keyboard for evdev keymap_us = ctx.keymap_new_from_names(layout="us")