63 lines
2.8 KiB
Python
63 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from evdev import InputDevice, ecodes, list_devices
|
|
|
|
|
|
def is_mouse(dev: InputDevice) -> bool:
|
|
keys = dev.capabilities().get(ecodes.EV_KEY, [])
|
|
rels = dev.capabilities().get(ecodes.EV_REL, [])
|
|
return ecodes.BTN_LEFT in keys and ecodes.REL_X in rels and ecodes.REL_Y in rels
|
|
|
|
|
|
def is_keyboard(dev: InputDevice) -> bool:
|
|
caps = dev.capabilities().get(ecodes.EV_KEY, [])
|
|
# Gaming mice (e.g. Logitech G502) commonly also advertise KEY_A/
|
|
# KEY_SPACE (programmable buttons / media-key emulation) and would
|
|
# otherwise get opened redundantly as both "keyboard" and "mouse" by
|
|
# separate listeners, which was observed to make one of the duplicate
|
|
# opens randomly drop ("Input device disconnected").
|
|
return ecodes.KEY_A in caps and ecodes.KEY_SPACE in caps and not is_mouse(dev)
|
|
|
|
|
|
def inaccessible_keyboard_names() -> list[str]:
|
|
"""Best-effort list of device names the kernel flags as keyboards (a
|
|
"kbd" handler in /proc/bus/input/devices) but that this process can't
|
|
actually open.
|
|
|
|
evdev.list_devices() silently drops any /dev/input/eventN this process
|
|
fails an os.access(R_OK | W_OK) check on - no exception, no gap in the
|
|
returned list, so a real keyboard blocked by permissions just vanishes
|
|
without a trace and whatever else happens to qualify as "keyboard-like"
|
|
(e.g. a gaming mouse's macro-key HID sub-interface) silently becomes
|
|
the only thing listened on. Observed on a Bazzite/GNOME machine: the
|
|
physical keyboard's device node wasn't covered by the desktop
|
|
session's usual per-session uaccess ACL (likely stripped by a
|
|
Steam-Input udev rule for recognized gaming-keyboard hardware IDs),
|
|
leaving it gated by the static 'input' group alone - which only takes
|
|
effect for the process's *next* login session, not a new terminal in
|
|
an already-running one. /proc/bus/input/devices is a plain readable
|
|
text file regardless of any of that, so it's used here purely to
|
|
produce an actionable diagnostic, never to open anything.
|
|
"""
|
|
try:
|
|
text = Path("/proc/bus/input/devices").read_text()
|
|
except OSError:
|
|
return []
|
|
|
|
accessible = {Path(p).name for p in list_devices()}
|
|
missing: list[str] = []
|
|
name: str | None = None
|
|
for line in text.splitlines():
|
|
if line.startswith("N: Name="):
|
|
name = line.split("=", 1)[1].strip('"')
|
|
elif line.startswith("H: Handlers="):
|
|
handlers = line.split("=", 1)[1].split()
|
|
if "kbd" not in handlers:
|
|
continue
|
|
event_handlers = [h for h in handlers if h.startswith("event")]
|
|
if event_handlers and event_handlers[0] not in accessible:
|
|
missing.append(name or event_handlers[0])
|
|
return missing
|