Search code examples
pythonmultithreadingraw-input

Reading input from raw_input() without having the prompt overwritten by other threads in Python


I'm trying to let the user input commands at a console using raw_input(), this works fine. The problem is I have background threads that occasionally output log-information to the screen and when they do they mess up the input prompt (since the output go wherever the cursor happens to be at the moment).

This is a small Python program that illustrate what i mean.

#!/usr/bin/env python
import threading
import time

def message_loop():
    while True:
        time.sleep(1)
        print "Hello World"

thread = threading.Thread(target = message_loop)
thread.start()

while True:
    input = raw_input("Prompt> ")
    print "You typed", input

This is an example of what it could look like when I run it:

Prompt> Hello World
Hello World
Hello World
Hello World
test
You typed test
Prompt> Hello World
Hello World
Hello World
hellHello World
o
You typed hello
Prompt> Hello World
Hello World
Hello World
Hello World

What I want is for the prompt to move along with the output from the thread. Like so:

Hello World
Hello World
Prompt> test
You typed test
Hello World
Hello World
Hello World
Hello World
Hello World
Prompt> hello
You typed hello
Hello World
Hello World
Hello World
Hello World
Prompt> 

Any ideas on how to achieve this without resorting to ugly hacks? :)


Solution

  • I recently encountered this problem, and would like to leave this solution here for future reference. These solutions clear the pending raw_input (readline) text from the terminal, print the new text, then reprint to the terminal what was in the raw_input buffer.

    This first program is pretty simple, but only works correctly when there is only 1 line of text waiting for raw_input:

    #!/usr/bin/python
    
    import time,readline,thread,sys
    
    def noisy_thread():
        while True:
            time.sleep(3)
            sys.stdout.write('\r'+' '*(len(readline.get_line_buffer())+2)+'\r')
            print 'Interrupting text!'
            sys.stdout.write('> ' + readline.get_line_buffer())
            sys.stdout.flush()
    
    thread.start_new_thread(noisy_thread, ())
    while True:
        s = raw_input('> ')
    

    Output:

    $ ./threads_input.py
    Interrupting text!
    Interrupting text!
    Interrupting text!
    > WELL, PRINCE, Genoa and Lucca are now no more than private estates of the Bo
    Interrupting text!
    > WELL, PRINCE, Genoa and Lucca are now no more than private estates of the Bo
    naparte family. No, I warn you, that if you do not tell me we are at war,
    

    The second correctly handles 2 or more buffered lines, but has more (standard) module dependencies and requires a wee bit of terminal hackery:

    #!/usr/bin/python
    
    import time,readline,thread
    import sys,struct,fcntl,termios
    
    def blank_current_readline():
        # Next line said to be reasonably portable for various Unixes
        (rows,cols) = struct.unpack('hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ,'1234'))
    
        text_len = len(readline.get_line_buffer())+2
    
        # ANSI escape sequences (All VT100 except ESC[0G)
        sys.stdout.write('\x1b[2K')                         # Clear current line
        sys.stdout.write('\x1b[1A\x1b[2K'*(text_len/cols))  # Move cursor up and clear line
        sys.stdout.write('\x1b[0G')                         # Move to start of line
    
    
    def noisy_thread():
        while True:
            time.sleep(3)
            blank_current_readline()
            print 'Interrupting text!'
            sys.stdout.write('> ' + readline.get_line_buffer())
            sys.stdout.flush()          # Needed or text doesn't show until a key is pressed
    
    
    if __name__ == '__main__':
        thread.start_new_thread(noisy_thread, ())
        while True:
            s = raw_input('> ')
    

    Output. Previous readline lines cleared properly:

    $ ./threads_input2.py
    Interrupting text!
    Interrupting text!
    Interrupting text!
    Interrupting text!
    > WELL, PRINCE, Genoa and Lucca are now no more than private estates of the Bo
    naparte family. No, I warn you, that if you do not tell me we are at war,
    

    Useful sources:

    How to get Linux console window width in Python

    apt like column output - python library (This code sample shows how to get terminal width for either Unix or Windows)

    http://en.wikipedia.org/wiki/ANSI_escape_code