1

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?

1 Answer 1

3

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

Comments

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.