Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Wayland support with keyboard emulation and capture using uinput #1679

Merged
merged 16 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading