56

I know it is possible to consistently rewrite the last line displayed in the terminal with "\r", but I am having trouble figuring out if there is a way to go back and edit previous lines printed in the console.

What I would like to do is reprint multiple lines for a text-based RPG, however, a friend was also wondering about this for an application which had one line dedicated to a progress bar, and another describing the download.

i.e. the console would print:

Moving file: NameOfFile.txt  
Total Progress: [########              ] 40%

and then update appropriately (to both lines) as the program was running.

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
JRJurman
  • 1,615
  • 2
  • 19
  • 30

7 Answers7

56

On Unix, use the curses module.

On Windows, there are several options:

Simple example using curses (I am a total curses n00b):

import curses
import time

def report_progress(filename, progress):
    """progress: 0-10"""
    stdscr.addstr(0, 0, "Moving file: {0}".format(filename))
    stdscr.addstr(1, 0, "Total progress: [{1:10}] {0}%".format(progress * 10, "#" * progress))
    stdscr.refresh()

if __name__ == "__main__":
    stdscr = curses.initscr()
    curses.noecho()
    curses.cbreak()

    try:
        for i in range(10):
            report_progress("file_{0}.txt".format(i), i+1)
            time.sleep(0.5)
    finally:
        curses.echo()
        curses.nocbreak()
        curses.endwin()
Galigator
  • 8,957
  • 2
  • 25
  • 39
codeape
  • 97,830
  • 24
  • 159
  • 188
  • Thanks, I added the link in the answer. Is the API just like Unix curses? – codeape Jul 27 '11 at 07:30
  • 1
    There's actually a module to do what you want: [progressbar][http://code.google.com/p/python-progressbar/] – Andrew Jaffe Sep 25 '11 at 22:03
  • Well, but how to display status rows not at the top of screen, but at the bottom (on the next line after previous output). I want an effect like with multiple `pv --name` in one pipeline – Alex Dvoretsky Aug 01 '16 at 07:58
16

Like this:

#!/usr/bin/env python

import sys
import time
from collections import deque

queue = deque([], 3)
for t in range(20):
    time.sleep(0.5)
    s = "update %d" % t
    for _ in range(len(queue)):
        sys.stdout.write("\x1b[1A\x1b[2K") # move up cursor and delete whole line
    queue.append(s)
    for i in range(len(queue)):
        sys.stdout.write(queue[i] + "\n") # reprint the lines

I discovered this in the Jiri project, written in Go.

Even better: erase all lines after done:

#!/usr/bin/env python

import sys
import time
from collections import deque

queue = deque([], 3)
t = 0
while True:
    time.sleep(0.5)
    if t <= 20:
        s = "update %d" % t
        t += 1
    else:
        s = None
    for _ in range(len(queue)):
        sys.stdout.write("\x1b[1A\x1b[2K") # move up cursor and delete whole line
    if s != None:
        queue.append(s)
    else:
        queue.popleft()
    if len(queue) == 0:
        break
    for i in range(len(queue)):
        sys.stdout.write(queue[i] + "\n") # reprint the lines
Leedehai
  • 3,660
  • 3
  • 21
  • 44
8

Ultimately, if you want to manipulate the screen, you need to use the underlying OS libraries, which will typically be:

  • curses (or the underlying terminal control codes as tracked by the terminfo/termcap database) on Linux or OSX
  • the win32 console API on Windows.

The answer from @codeape already gives you some of the many options if you don't mind sticking to one OS or are happy to install third party libraries on Windows.

However, if you want a cross-platform solution that you can simply pip install, you could use asciimatics. As part of developing this package, I've had to resolve the differences between environments to provide a single API that works on Linux, OSX and Windows.

For progress bars, you could use the BarChart object as shown in this demo using this code.

Peter Brittain
  • 13,489
  • 3
  • 41
  • 57
  • 1
    I'm somewhat amazed that something as useful as a cross platform console manipulation isn't shipped with python. (Esp. given the existence of Tkinter) – PythonNut Jun 08 '16 at 15:43
  • 1
    @PythonNut - I don't know the full history, but AFAIK no one was prepared to write an support a compatible API for Windows and so the [recommendation](https://docs.python.org/3/howto/curses.html) was to use the various glue code options already mentioned. – Peter Brittain Jun 08 '16 at 16:18
8

Here is a Python module for both Python 2/3, which can simply solve such situation with a few line of code ;D

reprint - A simple module for Python 2/3 to print and refresh multi line output contents in terminal

You can simply treat that output instance as a normal dict or list(depend on which mode you use). When you modify that content in the output instance, the output in terminal will automatically refresh :D

For your need, here is the code:

from reprint import output
import time

if __name__ == "__main__":
    with output(output_type='dict') as output_lines:
        for i in range(10):
            output_lines['Moving file'] = "File_{}".format(i)
            for progress in range(100):
                output_lines['Total Progress'] = "[{done}{padding}] {percent}%".format(
                    done = "#" * int(progress/10),
                    padding = " " * (10 - int(progress/10)),
                    percent = progress
                    )
                time.sleep(0.05)
Yinzo
  • 165
  • 2
  • 8
  • When I try `output_lines['one'] = 'abcd'`, after setting it up like you did, I get a zero division error. I believe there is something wrong here. Do you have any insight into that? – cat40 Feb 27 '17 at 20:21
  • @cat40 I think that's maybe something make `get_terminal_size()` returning a value of (0,0). May I ask for more detail for that? You can post it at Github issue :D – Yinzo Mar 01 '17 at 00:18
5

Carriage return can be used to go to the beginning of line, and ANSI code ESC A ("\033[A") can bring you up a line. This works on Linux. It can work on Windows by using the colorama package to enable ANSI codes:

import time
import sys
import colorama

colorama.init()

print("Line 1")
time.sleep(1)
print("Line 2")
time.sleep(1)
print("Line 3 (no eol)", end="")
sys.stdout.flush()
time.sleep(1)
print("\rLine 3 the sequel")
time.sleep(1)
print("\033[ALine 3 the second sequel")
time.sleep(1)
print("\033[A\033[A\033[ALine 1 the sequel")
time.sleep(1)
print()  # skip two lines so that lines 2 and 3 don't get overwritten by the next console prompt
print()

Output:

> python3 multiline.py
Line 1 the sequel
Line 2
Line 3 the second sequel
>

Under the hood, colorama presumably enables Console Virtual Terminal Sequences using SetConsoleMode.

(also posted here: https://stackoverflow.com/a/64360937/461834)

jtpereyda
  • 6,987
  • 10
  • 51
  • 80
0

You can try tqdm.

from time import sleep
from tqdm import tqdm
from tqdm import trange

files = [f'file_{i}' for i in range(10)]
desc_bar = tqdm(files, bar_format='{desc}')
prog_bar = trange(len(files), desc='Total Progress', ncols=50, ascii=' #',
                  bar_format='{desc}: [{bar}] {percentage:3.0f}%')

for f in desc_bar:
    desc_bar.set_description_str(f'Moving file: {f}')
    prog_bar.update(1)
    sleep(0.25)

enter image description here

There is also nested progress bars feature of tqdm

from tqdm.auto import trange
from time import sleep

for i in trange(4, desc='1st loop'):
    for k in trange(50, desc='2rd loop', leave=False):
        sleep(0.01)

enter image description here

Note that nested progress bars in tqdm have some Known Issues:

  • Consoles in general: require support for moving cursors up to the previous line. For example, IDLE, ConEmu and PyCharm (also here, here, and here) lack full support.
  • Windows: additionally may require the Python module colorama to ensure nested bars stay within their respective lines.

For nested progress bar in Python, Double Progress Bar in Python - Stack Overflow has more info.

Ynjxsjmh
  • 28,441
  • 6
  • 34
  • 52
-1

I found simple solution with a "magic_char".

magic_char = '\033[F'
multi_line = 'First\nSecond\nThird'
ret_depth = magic_char * multi_line.count('\n')
print('{}{}'.format(ret_depth, multi_line), end='', flush = True)