console-rs / indicatif

A command line progress reporting library for Rust
MIT License
4.22k stars 238 forks source link

Can I implement a progress bar for this kind of screen scrolling? #543

Open MoGuGuai-hzr opened 1 year ago

MoGuGuai-hzr commented 1 year ago

I am using Docker and I find its progress bar very cool. Can Indicatif implement this feature?

Maybe I can use MultiProgress to achieve it: use one as the main progress bar and the others as secondary progress bars which only displaying personalized messages.

However, to implement the scrolling function, I need to continuously scroll the message of the secondary progress bar, so clone the message every time. Is this a good implementation approach?

progress_bar

djc commented 1 year ago

I don't have an opinion off the top of my head. Just try to implement it?

MoGuGuai-hzr commented 1 year ago

I don't have an opinion off the top of my head. Just try to implement it?

Sorry~, perhaps I didn't express myself well. Using MultiProgress, it seems that I need to reset each sub-progress bar line by line in order to achieve the scrolling effect. And in order to pass the new message to each sub-progress bar, I need to save the previous message and manually clone a copy for them.

Is there a more elegant way to achieve this scrolling effect? It's like every time I use println, the information in the terminal automatically moves up.

This may not be feasible, as you know, I lack knowledge of terminal display.

azriel91 commented 1 year ago

What you could do to achieve that effect is, for keeping track of the message, use VecDeque, where you:

  1. log_lines.push_back(line) whenever you receive a new line of log message.
  2. log_lines.pop_front() when it exceeds the maximum number of lines to scroll.
  3. Use let = log_lines.join("\n") (from Join::join to create the message to set on the progress bar, which means the progress bar template will have a \n in it (to put the message below the bar).

    ProgressBar::println places the message above the progress bar, not below, so that would also mean a change to indicatif if "below the progress bar" printing is to be supported.

Note that for 1., it would mean either:

I can't think of a way to avoid allocations for the message when setting it on the progress bar with the current API.

alejandrodnm commented 1 year ago

I did something kind off similar on a project, that's not open-sourced so I can't share the code, I had a MultiProgress, and I added 2 ProgressBars that I used for reference, and had to juggle insert_after and insert_before.

MoGuGuai-hzr commented 1 year ago

What you could do to achieve that effect is, for keeping track of the message, use VecDeque, where you:

1. [`log_lines.push_back(line)`](https://doc.rust-lang.org/std/collections/struct.VecDeque.html#method.push_back) whenever you receive a new line of log message.

2. [`log_lines.pop_front()`](https://doc.rust-lang.org/std/collections/struct.VecDeque.html#method.pop_front) when it exceeds the maximum number of lines to scroll.

3. Use `let  = log_lines.join("\n")` (from [`Join::join`](https://doc.rust-lang.org/std/slice/trait.Join.html#tymethod.join) to create the message to set on the progress bar, which means the progress bar template will have a `\n` in it (to put the message below the bar).
   [`ProgressBar::println`](https://docs.rs/indicatif/latest/indicatif/struct.ProgressBar.html#method.println) places the message above the progress bar, not below, so that would also mean a change to `indicatif` if "below the progress bar" printing is to be supported.

Note that for 1., it would mean either:

* all log lines must be one line (no `\n` in them)

* splitting log messages into lines before adding them to the `vec_deque`

I can't think of a way to avoid allocations for the message when setting it on the progress bar with the current API.

I apologize for the delayed response as it appears my email forgot about me.

As for what was previously mentioned, I implemented the function using an unsightly piece of code as follows.

use std::{thread, time};

use indicatif::{MultiProgress, ProgressBar, ProgressStyle};

const N: usize = 3;

struct MyMultiProgress {
    m: MultiProgress,
    main: ProgressBar,
    pbs: Vec<ProgressBar>,
    msgs: Vec<String>,
    cnt: usize,
}

impl MyMultiProgress {
    fn new(len: u64) -> Self {
        let spinner_style = ProgressStyle::with_template("{wide_msg}").unwrap();

        let m = MultiProgress::new();
        let mut pbs = Vec::with_capacity(N);

        for _ in 0..N {
            let pb = m.add(ProgressBar::new(0));
            pb.set_style(spinner_style.clone());
            pbs.push(pb);
        }
        let main = m.add(ProgressBar::new(len));

        Self {
            m,
            main,
            pbs,
            msgs: Vec::with_capacity(N),
            cnt: 0,
        }
    }

    fn add_msg(&mut self, msg: String) {
        if self.msgs.len() < N {
            self.msgs.push(msg);
        } else {
            self.msgs[self.cnt % N] = msg;
        }

        self.cnt += 1;
        self.show();
    }

    fn show(&mut self) {
        if self.msgs.len() < N {
            for i in 0..self.msgs.len() {
                self.pbs[i].set_message(self.msgs[i].clone());
            }
        } else {
            for i in 0..N {
                self.pbs[i].set_message(self.msgs[(self.cnt + i) % N].clone());
            }
        }
    }

    fn inc(&mut self, n: u64) {
        self.main.inc(n);
    }

    fn clean(&mut self) {
        for pb in &mut self.pbs {
            pb.finish();
        }
        self.main.finish();
        self.m.clear().unwrap();
    }
}

#[test]
fn multiple_bar() {
    let interval = time::Duration::from_millis(1000);
    let mut m = MyMultiProgress::new(100);

    for i in 0..100 {
        m.add_msg(format!("hello: {}", i));
        m.inc(1);
        thread::sleep(interval);
    }

    m.clean();
}

However, besides the inconvenience of having to copy strings every time, there are some unexpected issues, such as messages not immediately displaying on the terminal every time they are set.

Specifically, if the interval is set to 1000ms, it works perfectly. interval_1000ms

However, if it is set to 100ms or lower, there may be some overlapping. interval _100ms

MoGuGuai-hzr commented 1 year ago

I did something kind off similar on a project, that's not open-sourced so I can't share the code, I had a MultiProgress, and I added 2 ProgressBars that I used for reference, and had to juggle insert_after and insert_before.

Does this resemble what I showed above?

azriel91 commented 1 year ago

heya, I'm not sure why the numbers overlap

are you using steady_tick? (can't see it in the code above, but just in case) if you are, try not using it, and calling .tick() inside the show() method after updating the progress bars

otherwise it needs some investigation into indicatif's inner state and rendering, which I won't be able to get to today

MoGuGuai-hzr commented 1 year ago

I did not use steady_tick, I simply put it to sleep.

I try to use .tick() for each progress bar after set_message, but there was no noticeable improvement.

djc commented 1 year ago

This probably happens because of indicatif's drawing rate limits.