2

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!

2
  • listener already runs in separated thread so there is no need to use another thread for animation. You can run your code in current thread between with ... as listener and listener.join() - and it can be long running loop which check variables and update screen. It will works similar to PyGame which also runs loops which all time check there is something to move and redraws elements Commented Dec 9, 2020 at 20:01
  • @furas thanks for the tip, I have tried to do what you suggested, now shown in latest edit of question -- but I can't seem to get it to work Commented Dec 9, 2020 at 20:16

1 Answer 1

3

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()
Sign up to request clarification or add additional context in comments.

4 Comments

Thank you that's super helpful and this works for me. But supposing the animation is more complex than just printing the same character over and over--like suppose it is drawing some kind of shape via ascii art. I edited the question to have an example of something like this. Can this approach be adapted to do that?
if you will need more complex animation then you will need more complex code inside while loop. And again this animation it should draw only one frame in every loop - don't run internal loops.
if you want to draw mysquare with sleep then it is wrong idea - you should have only one loop - ` while listener.is_alive()` - and it should control time and draw new elements.
OK, thanks for the advice. I guess I can track animation state with a kind of counter that gets reset upon starting a new animation. I'll accept this as the answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.