This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import selectors
|
||||
import threading
|
||||
from typing import Callable
|
||||
|
||||
from evdev import InputDevice, categorize, ecodes, list_devices
|
||||
|
||||
from .input_devices import is_keyboard
|
||||
|
||||
_CANCEL_KEY = "KEY_ESC"
|
||||
|
||||
|
||||
def capture_next_combo(on_captured: Callable[[tuple[str, ...] | None], None]) -> None:
|
||||
"""Wait in a background thread for the user to press (and release) a
|
||||
key combo and report it as a sorted tuple of evdev key names - Escape
|
||||
pressed alone reports None (cancelled) instead.
|
||||
|
||||
Tracks every key held down until they're *all* released again, using
|
||||
the peak simultaneously-held set as the captured combo - so e.g.
|
||||
holding Ctrl then F1 then releasing both captures ("KEY_F1",
|
||||
"KEY_LEFTCTRL"), not just whichever key happened to complete the chord.
|
||||
|
||||
Used by the settings window's hotkey "click to record" buttons: since
|
||||
hotkeys are matched by this same evdev keycode naming (see hotkeys.py/
|
||||
state.py), capturing it directly avoids the user needing to know
|
||||
evdev's naming (KEY_F1, KEY_KP1, ...) to type it in by hand.
|
||||
|
||||
`on_captured` runs on this background thread, not the GTK main thread -
|
||||
callers touching GTK widgets from it must marshal via GLib.idle_add.
|
||||
"""
|
||||
def _run() -> None:
|
||||
keyboards = [d for d in (InputDevice(p) for p in list_devices()) if is_keyboard(d)]
|
||||
if not keyboards:
|
||||
on_captured(None)
|
||||
return
|
||||
sel = selectors.DefaultSelector()
|
||||
for dev in keyboards:
|
||||
sel.register(dev, selectors.EVENT_READ)
|
||||
held: set[str] = set()
|
||||
peak: set[str] = set()
|
||||
try:
|
||||
while True:
|
||||
for key, _mask in sel.select():
|
||||
dev = key.fileobj
|
||||
for raw_event in dev.read():
|
||||
if raw_event.type != ecodes.EV_KEY:
|
||||
continue
|
||||
key_event = categorize(raw_event)
|
||||
keycode = key_event.keycode
|
||||
name = keycode[0] if isinstance(keycode, list) else keycode
|
||||
if key_event.keystate == key_event.key_down:
|
||||
held.add(name)
|
||||
peak |= held
|
||||
elif key_event.keystate == key_event.key_up:
|
||||
held.discard(name)
|
||||
if not held and peak:
|
||||
if peak == {_CANCEL_KEY}:
|
||||
on_captured(None)
|
||||
else:
|
||||
on_captured(tuple(sorted(peak)))
|
||||
return
|
||||
finally:
|
||||
sel.close()
|
||||
for dev in keyboards:
|
||||
dev.close()
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
Reference in New Issue
Block a user