Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
MoGuGuai-hzr opened this issue May 28, 2023 · 9 comments
Open

Comments

@MoGuGuai-hzr
Copy link

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
Copy link
Collaborator

djc commented May 28, 2023

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

@MoGuGuai-hzr
Copy link
Author

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
Copy link
Contributor

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:

  • 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.

@alejandrodnm
Copy link

alejandrodnm commented May 31, 2023

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
Copy link
Author

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
Copy link
Author

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
Copy link
Contributor

azriel91 commented Jun 1, 2023

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
Copy link
Author

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
Copy link
Collaborator

djc commented Jun 1, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants