Detecting if a key is HELD down - python
Asked Answered
L

4

6

My use case

I need to know when a (specific) key is pressed and held down. The use case after detection is fairly easy. When the key is released, send a signal to stop the callback (which I know already).

Desired behavior

Here is a rough scheme of how the algo looks like:

def the_callback():
    if key_held == the_hotkey:
        someObj.start()  # this class Obj works totally well so no issues here on
    elif key_released == the_hotkey:
        someObj.stop()
    else:
        # we don't care. continue looking for Keyboard events

# here any kinda listener or just a loop which passes events to the callback

I should mention that any kinda listener which blocks the execution is okay as it will run in its own thread (already running pynput.keyboard.Listener in a thread so not a problem)

What I've tried

I used pynput and its pynput.keyboard.Listener to detect key-presses and invoke callbacks accordingly but I couldn't make that work to detect when a key is HELD down.

the current solution looks roughly like:

# not real code. just rough scheme
def on_pressed(key):
    if key == my_hotkey:
        if running_already:  # this part works well already
            obj.stop()
        else:
            obj.start()
    else:
        # we don't care

with pynput.keyboard.Listener(on_press=on_pressed) as listener:
    listener.join()  # blocking call until SystemExit, `return False` from callback or `listener.stop()` 
    

I have a very strong feeling that I can make this work by adding on_release=another_callback_that_handles_releases (available within pynput.keyboard.listener).

Perhaps by storing the last known pressed keystroke, and checking if the key released was the same as the hotkey which was pressed earlier but I'm not sure how would I go about it and can that even work?

Then I decided to give keyboard (different lib) a go. I wrote the below code for the same which can detect keys being held down. This below code achieves almost nearly what I want:

import keyboard as kb, time

while 1:
    while kb.is_pressed('q'):
        print('Key is held')
        time.sleep(0.5)  # sleep added just to stop it from spamming the stdout
        
    else:
        print('No it\'s Not')
        time.sleep(0.5)

The issue with this solution is, it's not very well suited for OSX and Ubuntu. And it has some issues working with special keys. Moreover, I have the hotkey stored as pynput.keyboard.Key.f7 (for eg) or pynput.keyboard.KeyCode(char='s') # for character keys and these enums have different values than what keyboard uses to scan key IDs (using keyboard.hook()).

The final question

How should I go about detecting a key being HELD down. I'd prefer to achieve this using pynput as the rest of the code base uses it but 'keyboard is fine too. Again I have a feeling that using on_press=a_callback and on_release=another_callback this might be achieved but I'm not entirely sure about it. Lastly, the solution is preferred to be cross platform (I'm fine with using three different functions depending on value of platform.system()).

How would you go about achieving it?

EDIT-1

HERE is what I wrote as an attempt (and MCVE) after suggestion by Isak. This works almost perfectly with just 1 flaw. And that is that it doesn't listen to keypresses right from the program start.

It takes some time for some unknown reason before it starts to actually detect any keypress. Good thing is that once it detects the keypress for the first time, it works flawlessly.

What am I missing there?

Linoleum answered 20/4, 2021 at 13:0 Comment(0)
S
2

Try to check for the key_pressed event on the specific key until the event becomes key_released. So when you detect a click on the key you execute your code and when it detects the release of that key the code stops

Similarity answered 20/4, 2021 at 13:23 Comment(3)
HERE is what I wrote as an attempt (and MCVE). This works almost perfectly with just 1 flaw. And that is that it doesn't listen to keypresses right from the program start. It takes some time for some unknown reason before it starts to actually detects any keypress. Good thing is that once it detects the keypress for the first time, it works flawlessly. What am I missing there?Linoleum
@PSSolanki the lag is probably because of the time it takes to import the keyboard module.Gut
Appreciate the response, Sid! But I don't import keyboard in the script actually. And about the pynput.keyboard.Listener instance, I have a much bigger script which imports a lot more stuff including the ones in this mcve, and that works instantly. @GutLinoleum
L
1

I figured out why My Approach was taking a lot of time to initialize before starting the Listener. It was because of the while loop which didn't have any time.sleep() calls and it was probably messing with the system (although I wouldn't expect that to happen as it runs in its own thread but probably the while loop doesn't release the GIL as it's just in the loop doing literally nothing without any sort of delay).

I just added time.sleep(0.2) inside the while loop (the outer one). Any delay would do as that would release the GIL for some time and the Listener thread would be processed and made active.

Edit: Accepting the answer by Isak as accepted as that is the correct approach.

Linoleum answered 22/4, 2021 at 13:33 Comment(0)
S
1

I have written a script in pynput that detects if a key is pressed and held. I used a seperate variable last_pressed_key to track the key hold.

from pynput.keyboard import Key , Listener
from pynput import keyboard

last_pressed_key = None

def on_press(key):
    global last_pressed_key
    if last_pressed_key == None and key == Key.f8:
        last_pressed_key = key
        print(f"Pressed: {key}")
    elif key == last_pressed_key:
        pass

def on_release(key):
    global last_pressed_key
    if last_pressed_key is not None and key == last_pressed_key:
        print(f"Released: {key}")
        last_pressed_key = None  # Reset after release
    else:
        pass

listener = keyboard.Listener(on_press=on_press,on_release=on_release)
listener.start()

while True:
    pass
Swifter answered 11/5 at 11:47 Comment(0)
P
1

You can use set to detect if a key is pressed to process your code and reset when the key is released.

To give you a better understanding of pynput here is a tidbit from their documentation.
Under the subsection of pynput Package Documentation -> Monitoring the keyboard

Synchronous event listening for the keyboard listener
To simplify scripting, synchronous event listening is supported through the utility class pynput.keyboard.Events. This class supports reading single events in a non-blocking fashion, as well as iterating over all events.

Copied from PyCharm's Documentation Popup Box.

pynput.keyboard.Events
A keyboard event listener supporting synchronous iteration over the events.
Possible events are:
:class: Events.Press A key was pressed.
:class: Events.Release A key was released.

pynput.keyboard.Events will block the main thread until a key is pressed and will continue blocking when the key is released.

from pynput.keyboard import Events, Key, KeyCode

class Break(Exception):
    pass

hotkey_sets = [
    {Key.ctrl_l, Key.alt_l, 65},  # LEFT CTRL + LEFT ALT + A
    {Key.esc}
]
pressed = set()
with Events() as events:
    for event in events:
        if type(event.key) is Key:
            key = event.key
            print(type(event.key).__name__, type(event).__name__, event.key.name, event.key.value.char, event.key.value.vk, sep = ', ')
        if type(event.key) is KeyCode:
            key = event.key.vk
            print(type(event.key).__name__, type(event).__name__, event.key.char, event.key.vk, sep = ', ')

        if type(event) is Events.Press:
            if key not in pressed:
                pressed.add(key)
                if key == Key.f8:
                    # Execute Your Code
                    continue

                """ To further explain how you can use set to detect if a key is pressed, I've added this. """
                # try:
                #     for hotkeys in hotkey_sets:
                #         if all([_key in pressed for _key in hotkeys]):
                #             for _key in hotkeys:
                #                 if _key == key:
                #                     if hotkeys == hotkey_sets[0]:
                #                         print('LEFT CTRL + LEFT ALT + A')  # Execute Your Code
                #                         raise Break
                #                     elif hotkeys == hotkey_sets[1]:
                #                         print('ESC')  # Execute Your Code
                #                         raise Break
                # except Break:
                #     pass
        elif type(event) is Events.Release:
            if key in pressed:
                pressed.remove(key)
                if key == Key.f8:
                    # Reset After Release
                    pass
                elif key == Key.esc:
                    break
Protostele answered 10/10 at 1:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.