rsalmei / alive-progress

A new kind of Progress Bar, with real-time throughput, ETA, and very cool animations!
MIT License
5.45k stars 205 forks source link

Issue with term cols and stderr progress bar when piping stdout to file #224

Closed Yomguithereal closed 1 year ago

Yomguithereal commented 1 year ago

Hello @rsalmei, thanks for your awesome library.

I have a little issue when printing the progress bar using file=sys.stderr and piping the stdout of a script into a file. Here is a script reproducing the problem (note that the terminal has to be ~130 cols wide for it to show):

import time
import os
import sys
import shutil
from alive_progress import alive_bar

import patch

total = 1000

stdout_cols = shutil.get_terminal_size()[0]
stderr_cols = os.get_terminal_size(sys.stderr.fileno())[0]

print("Cols of stdout", stdout_cols)
print("Cols of stderr", stderr_cols)

print("-" * stderr_cols)

with alive_bar(
    total,
    title="Processing range",
    file=sys.stderr,
    ctrl_c=False,
    unit="apple",
) as bar:
    for i in range(1000):
        time.sleep(0.005)
        bar()

If you run:

python example.py

everything works fine. But if you run:

python example.py > log.txt

then the progress bar will be erroneously truncated.

This happens because alive_progress always uses stdout cols to print itself, and when piped to a file, shutil.get_terminal_size always return some default desirable value (usually around 80). Related code can be found here.

Now if I monkey-patch your library using the following code:

import os
from types import SimpleNamespace

def patched_new(original):
    write = original.write
    flush = original.flush

    def cols():
        # more resilient one, although 7x slower than os' one.

        # HERE: only line changed HERE #
        return os.get_terminal_size(original.fileno())[0]

    def _ansi_escape_sequence(code, param=""):
        def inner(_available=None):  # because of jupyter.
            write(inner.sequence)

        inner.sequence = f"\x1b[{param}{code}"
        return inner

    def factory_cursor_up(num):
        return _ansi_escape_sequence("A", num)  # sends cursor up: CSI {x}A.

    clear_line = _ansi_escape_sequence(
        "2K\r"
    )  # clears the entire line: CSI n K -> with n=2.
    clear_end_line = _ansi_escape_sequence("K")  # clears line from cursor: CSI K.
    clear_end_screen = _ansi_escape_sequence("J")  # clears screen from cursor: CSI J.
    hide_cursor = _ansi_escape_sequence("?25l")  # hides the cursor: CSI ? 25 l.
    show_cursor = _ansi_escape_sequence("?25h")  # shows the cursor: CSI ? 25 h.
    carriage_return = "\r"

    return SimpleNamespace(**locals())

import alive_progress.utils.terminal.tty as m

m.new = patched_new

it fixes the problem.

Of course my code is naive and should be a little bit more subtle, especially when dealing with files that do not have a fileno etc. and I don't know if this kind of solution would break something else. But if you feel this is a good fix, I can submit a PR.

I wish you a good evening

Yomguithereal commented 1 year ago

Another solution would also be to add some kwarg letting users indicate how many cols they want because they might know better.

rsalmei commented 1 year ago

Hello @Yomguithereal! Sorry for the delay. Thanks, man! That's really a bug, I've never thought about grabbing the terminal size with a different handle. I'll fix it as soon as I can.

rsalmei commented 1 year ago

Hey @Yomguithereal, it is ready! The PR is #231, I'm glad I could find some time to work on this.

rsalmei commented 1 year ago

Released 👍

Yomguithereal commented 1 year ago

Thanks @rsalmei