moses-palmer / pynput

Sends virtual input commands
GNU Lesser General Public License v3.0
1.79k stars 248 forks source link

GlobalHotKeys doesn't like Arrow Keys #440

Open brisingre opened 2 years ago

brisingre commented 2 years ago

Description GlobalHotKeys doesn't like <up>, <down>, <left>, or <right> , and possibly some other special keys. They just don't work. (Some special keys such as <alt> work fine, I don't have a comprehensive list of which keys this affects yet.)

Platform and pynput version Windows 10 LTSC x64 pynput version 1.7.6

To Reproduce

import time
from pynput import keyboard

def on_q():
    print("q pressed")

def on_alt():
    print("alt pressed")

def on_up():
    print("up pressed")

globalHotkeys = keyboard.GlobalHotKeys({
    'q': on_q,
    '<alt>': on_alt,
    '<up>': on_up
    # this doesn't work. It accepts <up> as a key name, but on_up isn't called when I press the up arrow key.
})
globalHotkeys.start()

should_quit = False
while not should_quit:
    time.sleep(.1)

When you run this script, you'll see pressing q calls on_q and pressing alt calls on_alt but pressing up does not call on_up.

brisingre commented 2 years ago

Quick update: I have reproduced this on Raspbian as well as Windows 10.

cltrudeau commented 2 years ago

I've reproduced on macOS. Possibly same problem as #439

btonasse commented 2 years ago

I have reproduced this issue as well. Cannot use global hotkeys with certain special keys like ESC. It doesn't throw an error, it just doesn't do anything.

EDIT: However, if, instead of , I use <53>, the ESC key works fine. So there's something wrong in the parsing method.

brisingre commented 2 years ago

EDIT: However, if, instead of , I use <53>, the ESC key works fine. So there's something wrong in the parsing method.

Is there a list of these numbers? This could be a viable workaround for me.

btonasse commented 2 years ago

The HotKey.parse() method gives you the code. My workaround is to replace the troublesome special keys before calling GlobalHotkeys.

Note that this logic is currently encapsulated in a class in my project, so not everything will make sense. All you need to know is that self._hotkeys is a dict where the hotkey string is the key and the callback is the value.

from pynput import keyboard as kb

def _replace_special_keys(self) -> HotkeyDict:
        """
        Due to a bug in pynput, special, non-modifier keys like ESC and F1 do not work.
        However, they can work if their key code is provided instead (e.g. <53> isntead of <esc> on MacOS)
        This method therefore replaces these special keys with their keycode. 
        """
        new_hotkeys: HotkeyDict = {}
        for hotkey in self._hotkeys:
            parsed_keys = kb.HotKey.parse(hotkey)
            for key in parsed_keys:
                # Check if key is nor a KeyCode and not a modifier key,
                # in which case replace it with the str representation of its KeyCode object e.g. <esc> becomes <53>
                if type(key) != KeyCode and key.name not in {'shift', 'ctrl', 'alt', 'cmd'}:
                    self._logger.debug(
                        f"Key <{key.name}> replaced with its keycode string: {key.value}")
                    new_hotkey_name = hotkey.replace(
                        f"<{key.name}>", str(key.value))
                    new_hotkeys[new_hotkey_name] = self._hotkeys[hotkey]
                else:
                    # Copy hotkey to new HotkeyDict as is
                    new_hotkeys[hotkey] = self._hotkeys[hotkey]
        return new_hotkeys
cwtravis commented 2 years ago

I made a pull request that addresses this issue. It changes the parser function to use the KeyCode values if the key is not a modifier key or character.

https://github.com/moses-palmer/pynput/pull/466

QHQIII commented 2 years ago

Another question, when I use vk value <49> instead of the key char in code, it doesn't do anything.

...
    {
        "<esc>": on_activate,    # esc   ❌
        "<27>": on_activate,    # esc   ✔
        "<f1>": on_activate,     # f1    ❌
        "<112>": on_activate,     #  f1 ✔
        "a": on_activate,             # a ✔
        "<49>": on_activate,            # a   ❌
    }
...