Skip to content
Open
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
1 change: 1 addition & 0 deletions news.d/feature/1761.linux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Passthrough keyboard shortcuts
88 changes: 79 additions & 9 deletions plover/oslayer/linux/keyboardcontrol_uinput.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
from evdev import UInput, ecodes as e, util, InputDevice, list_devices
from evdev import (
UInput,
ecodes as e,
util,
InputDevice,
list_devices,
InputEvent,
KeyEvent,
)
import threading
import os
import selectors
Expand Down Expand Up @@ -322,6 +330,17 @@
zip(LAYOUTS[DEFAULT_LAYOUT].values(), LAYOUTS[DEFAULT_LAYOUT].keys())
)

MODIFIER_KEY_CODES: set[int] = {
e.KEY_LEFTSHIFT,
e.KEY_RIGHTSHIFT,
e.KEY_LEFTCTRL,
e.KEY_RIGHTCTRL,
e.KEY_LEFTALT,
e.KEY_RIGHTALT,
e.KEY_LEFTMETA,
e.KEY_RIGHTMETA,
}


class KeyboardEmulation(GenericKeyboardEmulation):
def __init__(self):
Expand Down Expand Up @@ -544,6 +563,46 @@ def suppress(self, suppressed_keys=()):
self._suppressed_keys = set(suppressed_keys)

def _run(self):
keys_pressed_with_modifier: set[int] = set()
down_modifier_keys: set[int] = set()

def _process_key_event(event: InputEvent) -> tuple[str | None, bool]:
"""
Processes an InputEvent to determine which key Plover should receive
and whether the event should be suppressed.
Considers pressed modifiers and Plover's suppressed keys.
Returns a tuple of (key_to_send_to_plover, suppress)
"""
if not self._suppressed_keys:
# No keys are suppressed
# Always send to Plover so that it can handle global shortcuts like PLOVER_TOGGLE (PHROLG)
return KEYCODE_TO_KEY.get(event.code, None), False
if event.code in MODIFIER_KEY_CODES:
# Can't use if-else because there is a third case: key_hold
if event.value == KeyEvent.key_down:
down_modifier_keys.add(event.code)
elif event.value == KeyEvent.key_up:
down_modifier_keys.discard(event.code)
return None, False
key = KEYCODE_TO_KEY.get(event.code, None)
if key is None:
# Key is unhandled. Passthrough
return None, False
if event.value == KeyEvent.key_down and down_modifier_keys:
keys_pressed_with_modifier.add(event.code)
return None, False
if (
event.value == KeyEvent.key_up
and event.code in keys_pressed_with_modifier
):
# Must pass through key up event if key was pressed with modifier
# or else it will stay pressed down and start repeating.
# Must release even if modifier key was released first
keys_pressed_with_modifier.discard(event.code)
return None, False
suppress = key in self._suppressed_keys
return key, suppress

try:
while True:
for key, events in self._selector.select():
Expand All @@ -553,14 +612,25 @@ def _run(self):
assert isinstance(key.fileobj, InputDevice)
device: InputDevice = key.fileobj
for event in device.read():
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()
if event.type == e.EV_KEY:
key_to_send_to_plover, suppress = _process_key_event(event)
if key_to_send_to_plover is not None:
# Always send keys to Plover when no keys suppressed.
# This is required for global shortcuts like
# Plover toggle (PHROLG) when Plover is disabled.
# Note: Must explicitly check key_up or key_down
# because there is a third case: key_hold
if event.value == KeyEvent.key_down:
self.key_down(key_to_send_to_plover)
elif event.value == KeyEvent.key_up:
self.key_up(key_to_send_to_plover)
if suppress:
# Skip rest of loop to prevent event from
# being passed through
continue

# Passthrough event
self._ui.write_event(event)
except:
log.error("keyboard capture error", exc_info=True)
finally:
Expand Down
Loading