none stop while loop responsive to user key press

Question:

I have a while that do something in an infinite, nonstop loop.

It should not be stopped or wait for user input.
But I need user can stop while with specific key press.
For example if user press f do someting new or p something else.

How should I get user key press in a nonstop while?

n = 1
while True:
    # do somthing 
     n += 1
    if <press p >
        # do task 1
    if <press f >
        # exit while


## do somthing else

I can’t use keyboard library because need sudo privilege on Linux

Asked By: JACK

||

Answers:

As @Kris said in comment, threading + queue can do a trick. Here is just an example, it needs a few fixes (messes up terminal look) but should be a great start for you (this won’t work on windows in this form, but just to give you example how to use it). Also, before using threading please read docs of threading, especially parts about global interpreter lock

import sys
import tty
import threading
import queue
import termios

def watcher(q: queue.Queue):
    # To bring back terminal look. More info here https://docs.python.org/3/library/termios.html
    fd = sys.stdin.fileno()
    old_Settings = termios.tcgetattr(fd) 

    while True:
        # Reads 1 char form sdin without need of newline
        tty.setraw(sys.stdin.fileno())
        i = sys.stdin.read(1)
        if i == "p":
            q.put_nowait("p")
            termios.tcsetattr(fd,termios.TCSADRAIN,old_Settings)
        elif i == "f":
            q.put_nowait("f")
            termios.tcsetattr(fd,termios.TCSADRAIN,old_Settings)
            break


def runner(q: queue.Queue):
    n = 1
    while True:
        n += 1
        # You need to check if q is empty, or It will throw an empty err
        if not q.empty():
            key = q.get_nowait()
            if  key == "f":
                print("Got exit event after {} loops".format(n))
                break
            if key == "p":
                print("Got p after {} loops".format(n))

if __name__ == "__main__":
    # Queue setup and 2 threads.
    q = queue.Queue()
    x = threading.Thread(target=watcher, args=(q,))
    x.start()
    y = threading.Thread(target=runner, args=(q,))
    y.start()

Output afrer running:

python3 ../../test.py
Got p after 1055953 loops
                         Got exit event after 4369605 loops
Answered By: How about nope

As stated on "How about nope"’s answer, that is not straightforward.
One can either go into the low level of the input system and change some settings to allow for non-blocking keyboard reading, or makeuse of a thirdy party library that will do that and provide some friendly interface.

terminedia is one such 3rdy party project – among other niceties, it implements the inkey() call which is similar to old-time BASIC function with the same name: it returns the key currently pressed, if there is one, or an empty string.

You just have to call it inside a context block using the terminedia keyboard: with terminedia.keyboard:

So, you have to first install terminedia in your Python environment with pip install terminedia. Then your code could be like this:

import terminedia as TM

n = 1
with TM.keyboard:
    while True:
        # do something 
        n += 1
        if (pressed:=TM.inkey()) == "p":
            # do task 1
        if pressed == "f":
            # exit while
            break

Another advantage over writing the code to set stdin settings is that
terminedia keyboard input is multiplatform and will work in windows (though much of the other functionalities in the lib will be Unix only)

(disclaimer: I am the project author)

Answered By: jsbueno
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.