Skip to content

uinput backend for pynput.keyboard incorrectly identifies keyboard device in some circumstances #658

@ntczkjfg

Description

@ntczkjfg

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 dev

This 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 dev

and 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions