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)