feat: Linux-Hotkey-Record via evdev

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 21:16:21 +02:00
parent f380828309
commit 80a01903e8
2 changed files with 178 additions and 0 deletions
@@ -0,0 +1,71 @@
"""Hotkey-Aufzeichnung via evdev (Linux)."""
import selectors
import threading
from typing import Callable
import evdev
from evdev import InputDevice, ecodes
def find_all_keyboards() -> list[InputDevice]:
"""Gibt alle Input-Devices zurück, die EV_KEY-Events liefern können."""
keyboards: list[InputDevice] = []
for path in evdev.list_devices():
try:
device = InputDevice(path)
except (PermissionError, OSError):
continue
capabilities = device.capabilities()
if ecodes.EV_KEY in capabilities:
keyboards.append(device)
else:
device.close()
return keyboards
def _keycode_to_name(code: int) -> str:
"""Übersetzt evdev-Keycode zu Key-Namen. Gibt '' bei unbekanntem Code."""
name = ecodes.KEY.get(code)
if isinstance(name, list):
return name[0]
if isinstance(name, str):
return name
return ""
def record_hotkey(
on_result: Callable[[str, bool], None],
cancel_event: threading.Event,
) -> None:
"""Blockiert bis zum ersten Keydown oder bis cancel_event gesetzt wird.
Ruft on_result(evdev_key_name, has_conflict) auf. has_conflict ist auf
Linux immer False — es gibt kein Äquivalent zum Win32-RegisterHotKey-Check.
"""
devices = find_all_keyboards()
if not devices:
return
selector = selectors.DefaultSelector()
try:
for dev in devices:
selector.register(dev.fd, selectors.EVENT_READ, dev)
captured: str | None = None
while captured is None and not cancel_event.is_set():
for key, _mask in selector.select(timeout=0.1):
dev: InputDevice = key.data
for event in dev.read():
if event.type == ecodes.EV_KEY and event.value == 1:
captured = _keycode_to_name(event.code)
break
if captured:
break
if captured and not cancel_event.is_set():
on_result(captured, False)
finally:
selector.close()
for dev in devices:
dev.close()