Python curses handling stdout from another thread
Question: I'm running two threads in my python program, one thread which uses python
curses to run a menu system and waits for input, and one thread which does
analysis based on menu choices and outputs it's status via the built in
`print()` function. My problem here is that print doesn't play well with
curses, as, if `curses.echo()` is on, then it prints to the line where I am
waiting for input, and if `curses.noecho()` is used, then the output is not
displayed at all.

Since I want control over where and when the output is displayed, my solution
to this initially was to set `window.timeout(1000)` and then have the input
loop like this:

    
    
    try:
        c = window.getkey()
    except:
        c = -1 #timeout or error in input
    
    if c == -1:
        check_for_input()
    elif c == 'KEY_RESIZE':
        ...
    

This works quite well to allow me to check for output from stdout every
second, and then if need be update the menu, while still allowing user input.
The problem that I'm having is that I have no idea how to capture stdout and
choose to display it when I need to. Is this at all possible?


Answer: So I figured this one out, but as a disclaimer, I have no idea if this is
thread safe (no problems thus far though).

It's possible to capture the output of print using the python library
[io](https://docs.python.org/3/library/io.html), and more specifically
[`StringIO`](https://docs.python.org/3/library/io.html#StringIO) from that
library.

N.B. This is for Python3

Essentially, the solution was to set `sys.stdout` to an instance of
`io.StringIO` and read from that.

    
    
    external_output = None
    stdout_buff = io.StringIO()
    sys.stdout = stdout_buff
    stream_pos = 0 # lst read position of the stdout stream.
    
    while True: #input loop
        ...
        if stdout_buff.tell() > stream_pos:
            stdout_buff.seek(stream_pos)
            external_output = stdout_buff.read()
            stream_pos = stdout_buff.tell()
        ...
    

Below I've included a short example of the menu system I was using in case the
above isn't clear to anyone having this issue, in the hopes that this will
clear it up.

Cheers!

* * *

# Unmodified Version

So the menu's display and event loop used to look a lot like this: (note that
this is a simplified version of things and therefore a lot to do with
displaying the menu and displaying what a user types has been left out). This
basic example displays a menu and allows user to exit the program, enter
digits into their selection, or enter their selection, which is then printed
out.

    
    
    import sys
    import curses
    
    def menu(stdscr):
        # initial startup settings
        curses.start_color()
        curses.use_default_colors()
        stdscr.timeout(1000) #timeout the input loop every 1000 milliseconds
        user_selection = ''
        # other unrelated initial variables
    
        while True: #display loop
            stdscr.clear()
            # the following is actually in a function to handle automatically
            # taking care of fitting output to the screen and keeping
            # track of line numbers, etc. but for demonstration purposes
            # I'm using the this
            start_y = 0
            stdscr.addstr(start_y, 0, 'Menu Options:')
            stdscr.addstr(start_y+1, 0, '1) option 1')
            stdscr.addstr(start_y+2, 0, '1) option 2')
            stdscr.addstr(start_y+3, 0, '1) option 3')
            stdscr.addstr(start_y+4, 0, '1) option 4')
    
            while True: #input loop
                c = stdscr.getkey()
                if c == 'KEY_RESIZE':
                    handle_window_resize() # handle changing stored widths and height of window
                    break #break to redraw screen
                elif c.isdigit():
                    # if user typed a digit, add that to the selection string
                    # users may only select digits as their options
                    user_selection += c 
                elif c == '\n':
                    # user hit enter to submit their selection
                    if len(user_selection) > 0:
                        return user_selection
                elif c == 'q':
                    sys.exit()
    
    
    
    result = curses.wrapper(menu)
    print(result)
    

In this example the problem still occurs that any output from a thread running
simultaneously to this one will be printed at the cursor of `stdscr` where the
program is currently waiting for input from the user.

* * *

# Modified Version

    
    
    import sys
    import curses
    from io import StringIO
    
    
    def menu(stdscr):
        # initial startup settings
        curses.start_color()
        curses.use_default_colors()
        stdscr.timeout(1000) #timeout the input loop every 1000 milliseconds
        user_selection = ''
        # other unrelated initial variables
    
        # output handling variables
        external_output = None # latest output from stdout
        external_nlines = 2 # number of lines at top to leave for external output
        stdout_buff = StringIO()
        sys.stdout = stdout_buff
        stream_pos = 0 # lst read position of the stdout stream.
    
        while True: #display loop
            stdscr.clear()
            # the following is actually in a function to handle automatically
            # taking care of fitting output to the screen and keeping
            # track of line numbers, etc. but for demonstration purposes
            # I'm using the this
            if external_output is not None:
                stdscr.addstr(0, 0, "stdout: " + external_output)
    
            start_y = external_nlines
            stdscr.addstr(start_y, 0, 'Menu Options:')
            stdscr.addstr(start_y+1, 0, '1) option 1')
            stdscr.addstr(start_y+2, 0, '1) option 2')
            stdscr.addstr(start_y+3, 0, '1) option 3')
            stdscr.addstr(start_y+4, 0, '1) option 4')
    
            while True: #input loop
                try:
                    c = stdscr.getkey()
                except:
                    c = -1 # 1000ms timeout or error
    
                if c == -1:
                    if stdout_buff.tell() > stream_pos:
                        # current stdout_buff pos is greater than last read
                        # stream position, so there is unread output
                        stdout_buff.seek(stream_pos)
                        external_output = stdout_buff.read().strip() #strip whitespace
                        stream_pos = stdout_buff.tell() #set stream_pos to end of stdout_buff
                        break #redraw screen with new output
                elif c == 'KEY_RESIZE':
                    handle_window_resize() # handle changing stored widths and height of window
                    break #break to redraw screen
                elif c.isdigit():
                    # if user typed a digit, add that to the selection string
                    # users may only select digits as their options
                    user_selection += c 
                elif c == '\n':
                    # user hit enter to submit their selection
                    if len(user_selection) > 0:
                        sys.stdout = sys.__stdout__ # reset stdout to normal
                        return user_selection
                elif c == 'q':
                    sys.stdout = sys.__stdout__ # reset stdout to normal
                    sys.exit()
    
    
    
    result = curses.wrapper(menu)
    print(result)
    

