pythonterminalncursesansi-escape

How to move the cursor above the first line without overighting it


Here is my code

print("1")
print("2")

My desired output

2
1

I tried using ansi characters but the second line is overighting the first line


print("1")
print("\033[3A2")  

output

2

How can I achieve my desired output using other means apart from loop to reverse the order of the print statement

I prefer ansi characters, terminal(), but I will also use other methods


Solution

  • To achieve the visual effect of printing a new line at the top while moving previous lines downwards, you can keep the previous lines in a list and print them in reverse order when printing a new line.

    Before printing a new line though, you need to move the cursor up for however many previous lines there are by using the <ESC>[{count}A control code. Use the <ESC>[K control code to clear from the current cursor position to the end of the current line to erase any leftover characters from a longer previous line:

    def reverse_print_factory(print=print):
        def _print(line):
            if lines:
                print(f'\033[{len(lines)}A', end='')
            lines.append(line)
            for line in reversed(lines):
                print(f'{line}\033[K')
        lines = []
        return _print
    
    print = reverse_print_factory()
    

    so that:

    from time import sleep
    
    for text in 'first', 'second', 'third':
        print(text)
        sleep(1)
    

    would produce the following output on a VT100/ANSI-compatible terminal:

    first
    

    and then after a second:

    second
    first
    

    and then after another second:

    third
    second
    first
    

    EDIT: A more efficient (but slightly less portable because of the need to control the standard input) approach would be to scroll up the region of the previous lines so you don't have to store previous lines in a list.

    This can be done by using the <ESC>[{start row};{end row}r control code to set the scrollable region, move the cursor to the top of the region with the <ESC>[{row};{column}f control code, and then using the <ESC>M control code to scroll up, effectively moving the lines in the region downwards.

    To obtain the starting row, you can use the <ESC>[6n control code to query the cursor position and then read the response from standard input in the <ESC>[{row};{column}R format. This has been helpfully written into the cursorPos function in this answer, which I'm using in the following example:

    def reverse_print_factory(print=print):
        def _print(text):
            nonlocal count
            if count:
                print(f'\033[{start_row};{start_row + count}r\033[{start_row};1f\033M', end='')
            count += 1
            print(f'{text}\033[K\033[{start_row + count};1f', flush=True, end='')    
        start_row = int(cursorPos()[1])
        count = 0
        return _print
    
    print = reverse_print_factory()
    

    To further account for lines that reach the height of the terminal (obtainable with shutil.get_terminal_size), you can move the lines before the starting row upwards (with the <ESC>D control code) to make way for the new line instead of moving the previous lines downwards, until the entire screen is filled with output, at which point move the entire screen downwards again to make way for the new line at the top.

    Also use atexist.register to register an exit handler that resets the scrollable region to the entire screen and outputs an additional newline if the lines have reached the terminal height to prevent the command prompt from unnecessarily overwriting the last line of output:

    import shutil
    import atexit
    
    def reverse_print_factory(print=print):
        def _print(text):
            nonlocal start_row, count
            if count:
                if 1 < start_row > height - count:
                    print(f'\033[1;{start_row - 1}r\033[{start_row - 1};1f\033D\033[{start_row - 1};1f', end='')
                    start_row -= 1
                else:
                    print(f'\033[{start_row};{start_row + count}r\033[{start_row};1f\033M', end='')
            count += 1
            print(f'{text}\033[K\033[{start_row + count};1f', flush=True, end='')
        def _exit():
            print(f'\033[r\033[{start_row + count};1f', end='')
            if 1 < start_row > height - count:
                print()
        height = shutil.get_terminal_size()[1]
        start_row = int(cursorPos()[1])
        count = 0
        atexit.register(_exit)
        return _print
    
    print = reverse_print_factory()
    

    Note that a complete set of VT100 control sequences can be found at: https://vt100.net/docs/vt100-ug/chapter3.html