-
Notifications
You must be signed in to change notification settings - Fork 283
uinput backend for pynput.keyboard incorrectly identifies keyboard device in some circumstances #658
Description
This is the function, in pynput._util.uinput.py, which determines which device on the system is the "keyboard":
def _device(self, paths):
"""Attempts to load a readable keyboard device.
:param paths: A list of paths.
:return: a compatible device
"""
dev, count = None, 0
for path in paths:
# Open the device
try:
next_dev = evdev.InputDevice(path)
except OSError:
continue
# Does this device provide more handled event codes?
capabilities = next_dev.capabilities()
next_count = sum(
len(codes)
for event, codes in capabilities.items()
if event in self._EVENTS)
if next_count > count:
dev = next_dev
count = next_count
else:
next_dev.close()
if dev is None:
raise OSError('no keyboard device available')
else:
return devThis seems reasonable - it just returns whichever device on the system has the most capabilities, since every output a keyboard can give counts as its own capability.
It failed on my system, though: As it turns out, this function was picking my mouse rather than my keyboard - because my mouse reports 294 capabilities while my keyboard reports 153. The reason seems to be that mice with buttons that are programmable report themselves to the computer as keyboards - and any key they're capable of mapping to those buttons get reported as a capability.
I got around this by having the function check for the word 'keyboard' in next_dev.name.lower(), and only consider those devices. I had it fall back to the old behavior if that failed to find a valid device. Here's the code I used:
def _device(self, paths, first_try = True):
"""Attempts to load a readable keyboard device.
:param paths: A list of paths.
:return: a compatible device
"""
dev, count = None, 0
for path in paths:
# Open the device
try:
next_dev = evdev.InputDevice(path)
except OSError:
continue
if first_try and 'keyboard' not in next_dev.name.lower():
next_dev.close()
continue
# Does this device provide more handled event codes?
capabilities = next_dev.capabilities()
next_count = sum(
len(codes)
for event, codes in capabilities.items()
if event in self._EVENTS)
if next_count > count:
dev = next_dev
count = next_count
else:
next_dev.close()
if dev is None:
if first_try:
return self._device(paths, first_try = False)
raise OSError('no keyboard device available')
else:
return devand that does correctly identify my keyboard. I originally tried detecting the presence of specific capabilities, like evdev.ecodes.KEY_A, but they were all present in my mouse's capabilities. This solution does seem fragile - I could easily see other mice including the word 'keyboard' in their virtual keyboard - but it's at least better. I do wonder if there's a way to tell with more certainty.
I've seen a lot of other people saying that pynput doesn't work for them on Wayland, even with sudo and the uinput backend - I suspect this is what's happening to most of those people.
Also: I saw saw on this page that you say this:
When running under Wayland, the X server emulator Xwayland will usually run, providing limited functionality. Notably, you will only receive input events from applications running under this emulator.
I just want to mention that, at least on my system, that is not true: With this change, as well as the changes to the Controller mentioned in my other bug report (#657), I seem to get 100% of pynput functionality in Wayland, in any window or even just on the desktop. I'm pretty sure it is running on XWayland but I've yet to find anything it doesn't see inputs in.
Platform and pynput version
Fedora Linux 42
KDE 6.4.1 (Wayland)
Pynput 1.8.1