Skip to content

Commit

Permalink
Wayland support with keyboard emulation and capture using uinput (#1679)
Browse files Browse the repository at this point in the history
Co-authored-by: Martin Körner <[email protected]>
  • Loading branch information
LilleAila and mkrnr authored Feb 6, 2025
1 parent 99ec10b commit b12c617
Show file tree
Hide file tree
Showing 11 changed files with 495 additions and 3 deletions.
36 changes: 36 additions & 0 deletions linux/appimage/apprun.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 "$@"
}

Expand Down
1 change: 1 addition & 0 deletions news.d/feature/1679.linux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added keyboard emulation and capture using uinput, compatible with X11, Wayland and anything else on linux and bsd.
1 change: 1 addition & 0 deletions plover/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,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),
choice_option("keyboard_layout", ("qwerty", "qwertz", "colemak", "colemak-dh"), 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),
Expand Down
3 changes: 3 additions & 0 deletions plover/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 a QWERTY keyboard.
if hasattr(self._keyboard_emulation, '_update_layout'):
self._keyboard_emulation._update_layout(config["keyboard_layout"])
# Update system.
system_name = config['system_name']
if system.NAME != system_name:
Expand Down
9 changes: 9 additions & 0 deletions plover/gui_qt/config_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,15 @@ 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(_("Linux keyboard layout:"), "keyboard_layout",
partial(ChoiceOption, choices={
"qwerty": "qwerty",
"qwertz": "qwertz",
"colemak": "colemak",
"colemak-dh": "colemak-dh",
}),
_("Set the keyboard layout configurad in your system.\n"
"This only applies when using Linux/BSD and not using X11."))
)),
# i18n: Widget: “ConfigWindow”.
(_('Plugins'), (
Expand Down
12 changes: 10 additions & 2 deletions plover/gui_qt/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from PyQt5.QtWidgets import (
QMainWindow,
QMenu,
QApplication,
)

from plover import _, log
Expand Down Expand Up @@ -151,6 +152,9 @@ def set_visible(self, visible):
else:
self.showMinimized()

def _is_wayland(self):
return "wayland" in QApplication.platformName().lower()

def _activate_dialog(self, name, args=(), manage_windows=False):
if manage_windows:
previous_window = wmctrl.GetForegroundWindow()
Expand All @@ -166,7 +170,10 @@ def on_finished():
wmctrl.SetForegroundWindow(previous_window)
dialog.finished.connect(on_finished)
dialog.showNormal()
dialog.activateWindow()
if not self._is_wayland():
# Otherwise gives this warning:
# Qt: Wayland does not support QWindow::requestActivate()
dialog.activateWindow()
dialog.raise_()

def _add_translation(self, dictionary=None, manage_windows=False):
Expand All @@ -177,7 +184,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):
Expand Down
9 changes: 9 additions & 0 deletions plover/oslayer/linux/display_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import os

"""
This value should be one of:
- x11
- wayland
- tty
"""
DISPLAY_SERVER = os.environ.get("XDG_SESSION_TYPE", None)
7 changes: 6 additions & 1 deletion plover/oslayer/linux/keyboardcontrol.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit b12c617

Please sign in to comment.