In Python, is it possible to request user input with input() in the console while simultaneously printing out text in the line BEFORE the prompt?
It should look something like this:
Text 1
Text 2
Text 3
Please enter something: abc
Whenever new text is printed, it should be printed after the previous text and before the input() prompt. Also, it should not interrupt the user entering the text.
Therefore, after printing "Text 4" the console should look like this:
Text 1
Text 2
Text 3
Text 4
Please enter something: abc
Is that possible to do in Python without using any external libraries?
I have already tried using \r, \b and similar codes as well as threading.
I also know that I will probably need to have one thread printing out the text while I have another one requesting the user input.
Here's another approach using ANSI/VT100 terminal control escape sequences.
We tell the terminal to only scroll the upper region of the terminal, where the output data is printed, so that the lower region, where the input prompt is printed, stays fixed in place. When we exit the program (using Ctrl C) we need to restore the default scrolling settings.
This program first clears the screen and then sits in a loop prompting the user for a number n
and then prints the numbers in range(n)
, one per line, with a small time delay to make it easier to see what's happening. The output for each range follows on from the previous range. If the user enters a non-integer the prompt is re-printed. The readline
module is imported so that editing and history are available at the input prompt; this module may not be available on all platforms.
First, a Python 2 version.
''' Print text in a scrolling region of the terminal above a fixed line for input
Written by PM 2Ring 2016.05.29
Python 2 version
'''
from __future__ import print_function
from time import sleep
import readline
# Some ANSI/VT100 Terminal Control Escape Sequences
CSI = '\x1b['
CLEAR = CSI + '2J'
CLEAR_LINE = CSI + '2K'
SAVE_CURSOR = CSI + 's'
UNSAVE_CURSOR = CSI + 'u'
def emit(*args):
print(*args, sep='', end='')
def set_scroll(n):
return CSI + '0;%dr' % n
# Height of scrolling region
height = 40
GOTO_INPUT = CSI + '%d;0H' % (height + 1)
emit(CLEAR, set_scroll(height))
try:
while True:
#Get input
emit(SAVE_CURSOR, GOTO_INPUT, CLEAR_LINE)
try:
n = int(raw_input('Number: '))
except ValueError:
continue
finally:
emit(UNSAVE_CURSOR)
#Display some output
for i in range(n):
print(i)
sleep(0.1)
except KeyboardInterrupt:
#Disable scrolling, but leave cursor below the input row
emit(set_scroll(0), GOTO_INPUT, '\n')
And here's a version that runs on Python 2 and Python 3. When run on Python 3 this script calls shutil.get_terminal_size()
to set the height of the scrolling region. It is possible to get the terminal size in Python 2, but the code is rather messy, so I opted for a fixed height.
I should mention that both versions of this script don't cope well if the terminal size is changed while they are running; handling that properly is left as an exercise for the reader. :)
''' Print text in a scrolling region of the terminal above a fixed line for input
Written by PM 2Ring 2016.05.29
Python 2 / 3 version
'''
from __future__ import print_function
import sys
import readline
from time import sleep
if sys.version_info > (3,):
# Get the (current) number of lines in the terminal
import shutil
height = shutil.get_terminal_size().lines - 1
stdout_write_bytes = sys.stdout.buffer.write
else:
height = 40
input = raw_input
stdout_write_bytes = sys.stdout.write
# Some ANSI/VT100 Terminal Control Escape Sequences
CSI = b'\x1b['
CLEAR = CSI + b'2J'
CLEAR_LINE = CSI + b'2K'
SAVE_CURSOR = CSI + b's'
UNSAVE_CURSOR = CSI + b'u'
GOTO_INPUT = CSI + b'%d;0H' % (height + 1)
def emit(*args):
stdout_write_bytes(b''.join(args))
def set_scroll(n):
return CSI + b'0;%dr' % n
emit(CLEAR, set_scroll(height))
try:
while True:
#Get input
emit(SAVE_CURSOR, GOTO_INPUT, CLEAR_LINE)
try:
n = int(input('Number: '))
except ValueError:
continue
finally:
emit(UNSAVE_CURSOR)
#Display some output
for i in range(n):
print(i)
sleep(0.1)
except KeyboardInterrupt:
#Disable scrolling, but leave cursor below the input row
emit(set_scroll(0), GOTO_INPUT, b'\n')
On Linux you can do it this way:
#!/usr/bin/python
CURSOR_UP_ONE = '\x1b[1A'
ERASE_LINE = '\x1b[2K'
print('Text 1\nText 2\nText 3')
inp = ''
while inp != 'END':
inp = raw_input('Please enter something:')
print(CURSOR_UP_ONE + ERASE_LINE + inp)
The point is in moving the cursor one line up every time and erasing the line Please enter something:
. Then you can write the input on its place and write the notice about input again, if needed.
Special strings:
CURSOR_UP_ONE
and ERASE_LINE
are strings with special meaning. The complete list if those Terminal escape control sequences you can find HERE.
Use of ANSI Terminal escape control sequences on Windows:
To some extent it's possible to use this sequences on Windows too. E.g. those questions deals with this topic:
- https://superuser.com/questions/413073/windows-console-with-ansi-colors-handling/1050078#1050078
- How to make win32 console recognize ANSI/VT100 escape sequences?
Workflow of this script
You could mimic what you want by doing something like this:
import os
def clear(): os.system('cls')
screen = ""
prompt = ""
def printToScreen(s):
global screen, prompt
clear()
screen += (s + '\n')
print(screen)
print(prompt)
def promptToScreen(p):
global screen, prompt
clear()
print(screen)
s = input(p)
prompt = p + s
return s
#test:
while True:
s = promptToScreen("Enter anything -- or press 'Enter' to quit: ")
if len(s) > 0:
printToScreen(s)
else:
break
The following two screen shots shows how it works:
then:
This is a Windows solution. In other OS's -- use their command to clear the screen. You could make it more portable by writing clear()
so that it issues different commands depending on the OS. The above is mostly proof of concept. The key idea is to maintain a string which represent how you want the terminal to look and then write your own print function which modifies the string, clears the terminal, then prints that screen to the command prompt. Unless you are doing something strange, the amount of overhead is negligible and the clear/update is invisible to the user.