Keyboard input (via pynput) and threads in python

Viewed 1193

I'm trying to make a kind of text-based game in Python 3. For the game I will need to listen for keyboard input, in particular measuring how long a key is held down, while printing things to the screen. I'm trying to start by making a working minimal example.

First, the following code, using pynput, appears to successfully measures the length time for which the user holds down a key:

from pynput import keyboard 
import time

print("Press and hold any key to measure duration of keypress. Esc ends program")

# A dictionary of keys pressed down right now and the time each was pressed down at
keys_currently_pressed = {} 

def on_press(key):
    global keys_currently_pressed 
    # Record the key and the time it was pressed only if we don't already have it
    if key not in keys_currently_pressed:
        keys_currently_pressed[key] = time.time()

def on_release(key):
    global keys_currently_pressed
    if key in keys_currently_pressed:
        animate = False
        duration = time.time() - keys_currently_pressed[key]
        print("The key",key," was pressed for",str(duration)[0:5],"seconds")
        del keys_currently_pressed[key]
    if key == keyboard.Key.esc:
        # Stop the listener
        return False

with keyboard.Listener(on_press = on_press, on_release=on_release, suppress=True) as listener: 
    listener.join()

Now what I'd like to do is, only while a key is pressed down by the user, print a text-based "animation" to the screen. In the following example my "animation" is simply printing "*" every half second. So far I've tried to have the "animation" handled by a second thread but I am a total novice when it comes to multithreading. The following code will start the animation at the correct time but won't stop it.

from pynput import keyboard 
import sys
import time
import threading

print("Press and hold any key to measure duration of keypress. Esc ends program")

# A dictionary of keys pressed down right now and the time each was pressed down at
keys_currently_pressed = {} 

def my_animation():
    # A simple "animation" that prints a new "*" every half second
    limit = 60 # just in case, don't do more than this many iterations
    j = 0
    while j<limit:
        j += 1
        sys.stdout.write("*")
        time.sleep(0.5)

anim = threading.Thread(target=my_animation)

def on_press(key):
    global keys_currently_pressed 
    # Record the key and the time it was pressed only if we don't already have it
    if key not in keys_currently_pressed:
        keys_currently_pressed[key] = time.time()
        anim.start()

def on_release(key):
    global keys_currently_pressed
    if key in keys_currently_pressed:
        animate = False
        duration = time.time() - keys_currently_pressed[key]
        print("The key",key," was pressed for",str(duration)[0:5],"seconds")
        del keys_currently_pressed[key]
    if key == keyboard.Key.esc:
        # Stop the listener
        return False

with keyboard.Listener(on_press = on_press, on_release=on_release, suppress=True) as listener:         listener.join()

Here's an approach (following @furas's comment) where the animation is coded after the with statement, however I cannot get this to work for me:

from pynput import keyboard 
import time

print("Press and hold any key to measure duration of keypress. Esc ends program")

# A dictionary of keys pressed down right now and the time each was pressed down at
keys_currently_pressed = {} 

# animation flag
anim_allowed = False

def on_press(key):
    global keys_currently_pressed 
    global anim_allowed
    # Record the key and the time it was pressed only if we don't already have it
    if key not in keys_currently_pressed:
        keys_currently_pressed[key] = time.time()
        anim_allowed = True

def on_release(key):
    global keys_currently_pressed
    global anim_allowed
    if key in keys_currently_pressed:
        animate = False
        duration = time.time() - keys_currently_pressed[key]
        print("The key",key," was pressed for",str(duration)[0:5],"seconds")
        del keys_currently_pressed[key]
        anim_allowed = False
    if key == keyboard.Key.esc:
        # Stop the listener
        return False

with keyboard.Listener(on_press = on_press, on_release=on_release, suppress=True) as listener:
    while anim_allowed:
        sys.stdout.write("*")
        time.sleep(0.5)
    listener.join()

Ultimately I want to be able to do this with more complex animations. For example

def mysquare(delay):
    print("@"*10)
    time.sleep(delay)
    for i in range(8):
        print("@" + " "*8 + "@")
        time.sleep(delay)
    print("@"*10)

What's the right way to approach this? Many thanks!

1 Answers

Listener already uses thread so there is no need to run animation in separated thread. You can run it in current tread in

with keyboard.Listener(on_press = on_press, on_release=on_release, suppress=True) as listener:         

    #... your code ...

    listener.join()

or without with ... as ...

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

#... your code ...

#listener.wait()
listener.join()

You can run there even long runing code - ie. endless while loop which will check if variable animate is True and write new *.

I had to add sys.stdout.flush() on my Linux to see * on screen.


My version:

It runs animation all time when you press any button but there is also code with variable counter to limit animation to 6 moves. If you press new key when it runs animation then it reset this counter and animation will longer.

This loop has to run all time to check if there is new animation - you can't finish this loop when animation is finished

from pynput import keyboard 
import sys
import time

# --- functions ---

def on_press(key):
    global keys_currently_pressed 
    global animate
    #global counter

    # Record the key and the time it was pressed only if we don't already have it
    if key not in keys_currently_pressed and key != keyboard.Key.esc:
        keys_currently_pressed[key] = time.time()
        animate = True
        #counter = 0 # reset counter on new key

def on_release(key):
    global keys_currently_pressed
    global animate

    if key in keys_currently_pressed:
        duration = time.time() - keys_currently_pressed[key]
        print("The key", key, "was pressed for", str(duration)[0:5], "seconds")
        del keys_currently_pressed[key]

        if not keys_currently_pressed: 
            animate = False

    if key == keyboard.Key.esc:
        # Stop the listener
        return False

# --- main ---

print("Press and hold any key to measure duration of keypress. Esc ends program")

# A dictionary of keys pressed down right now and the time each was pressed down at
keys_currently_pressed = {} 

animate = False # default value at start (to use in `while` loop)
#limit = 6 # limit animation to 6 moves
#counter = 0 # count animation moves

with keyboard.Listener(on_press = on_press, on_release=on_release, suppress=True) as listener:         

    while listener.is_alive(): # infinite loop which runs all time

        if animate:
            #sys.stdout.write("\b *")  # animation with removing previous `*` 
            sys.stdout.write("*")  # normal animation
            sys.stdout.flush()  # send buffer on screen
            #counter += 1
            #if counter >= limit:
            #    counter = 0
            #    animate = False
            
        time.sleep(0.5)


    listener.join()
Related