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

how to show progress bar and inquire select at the same time? #564

Open
Daniel-Xu opened this issue Jul 24, 2023 · 13 comments
Open

how to show progress bar and inquire select at the same time? #564

Daniel-Xu opened this issue Jul 24, 2023 · 13 comments

Comments

@Daniel-Xu
Copy link

Hi, I have a simple program that the user can pick a song, play it and go back to pick song again.

The initial state:
2023-07-25 at 1 31 AM
I'm using inquire's select:

            let ans = Select::new("choose a song: ", songs_info.clone()).prompt();

When the user finishes picking the song, the loop goes back to the Select again.

At the same time, there's another thread that will send back playing state via channel to the main thread.

what I want to achieve is that I want to show the progressing bar below the select options. However, the bar kept clashing with the first line of Select UI. (the original line is: "? choose a song:", and now is " >>>> Connecting...")

2023-07-25 at 1 35 AM

I've tried to set the target to stdout, it doesn't work either.

Any ideas?

@chris-laplante
Copy link
Collaborator

Hi Daniel,

You should do the Select prompt within a ProgressBar::suspend. See: https://docs.rs/indicatif/latest/indicatif/struct.ProgressBar.html#method.suspend

@Daniel-Xu
Copy link
Author

Daniel-Xu commented Jul 24, 2023

Hi Daniel,

You should do the Select prompt within a ProgressBar::suspend. See: https://docs.rs/indicatif/latest/indicatif/struct.ProgressBar.html#method.suspend

Hi, Chris:
Thanks for the tips.
I tried suspend, unfortunately it wil introduce happens-after relationship with the Select.
If I don't choose an option, the PBar will never show with suspend

                let ans = pb
                    .suspend(|| Select::new("choose a song again: ", songs_info.clone()).prompt());

@chris-laplante
Copy link
Collaborator

Are you able to share the code so I can give it a try myself? Or at least the UI code to reproduce the problem.

@Daniel-Xu
Copy link
Author

Are you able to share the code so I can give it a try myself? Or at least the UI code to reproduce the problem.
here is a runnable demo, what I want to do is displaying the pbar all the time, but the following code won't work.

use std::sync::Arc;
use std::thread;
use std::time::Duration;

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

fn main() {
    let pb = Arc::new(ProgressBar::new_spinner());
    pb.set_style(
        ProgressStyle::with_template("{spinner:.blue} {msg}")
            .unwrap()
            // For more spinners check out the cli-spinners project:
            // https://github.com/sindresorhus/cli-spinners/blob/master/spinners.json
            .tick_strings(&[
                "▹▹▹▹▹",
                "▸▹▹▹▹",
                "▹▸▹▹▹",
                "▹▹▸▹▹",
                "▹▹▹▸▹",
                "▹▹▹▹▸",
                "▪▪▪▪▪",
            ]),
    );
    // pb.enable_steady_tick(Duration::from_millis(120));
    pb.set_message("Playing Connecting...");
    let pb_clone = pb.clone();

    let h1 = thread::spawn(move || {
        for i in 0..128 {
            thread::sleep(Duration::from_millis(15));
            pb.set_message(format!("item #{}", i + 1));
            pb.inc(1);
        }
        pb.finish_with_message("done");
    });

    let songs_info = vec!["a", "b", "c"];

    let ans = pb_clone.suspend(|| Select::new("choose a song: ", songs_info).prompt());

    match ans {
        Ok(choice) => {
            pb_clone.suspend(|| {
                println!("{choice}! Enjoy!");
            });
        }

        Err(_) => {}
    }

    let _ = h1.join();
}

I guess I have to change my workflow:

  1. show the Select menu
  2. choose an option
  3. show the pgbar
  4. handle ctrl_c signal
  5. clear pgbar and show the menu again

@chris-laplante
Copy link
Collaborator

chris-laplante commented Jul 24, 2023

The code as written seems to work OK for me - what different behavior are you expecting?

Also one thing that I notice is you don't need to put ProgressBar in an Arc. ProgressBar is already an Arc around its internal state.

@Daniel-Xu
Copy link
Author

The code as written seems to work OK for me - what different behavior are you expecting?

Also one thing that I notice is you don't need to put ProgressBar in an Arc. ProgressBar is already an Arc around its internal state.

I want the progress bar and Select menu to show on the screen at the same time.
Imagine you can select new songs while listening to the current one.

thanks for the Arc reminder.

@Daniel-Xu
Copy link
Author

Daniel-Xu commented Jul 25, 2023

@chris-laplante hi, another unrelated question:
how to reset the front color's position to 0? I've tried bar.set_position(0), it doesn't work.
2023-07-26 at 2 40 AM

@Daniel-Xu
Copy link
Author

@chris-laplante hi, another unrelated question: how to reset the front color's position to 0? I've tried bar.set_position(0), it doesn't work. 2023-07-26 at 2 40 AM

reset or set_position won't work properly in multiple threads. If so I don't think the progressbar should be Sync.

@chris-laplante
Copy link
Collaborator

I want the progress bar and Select menu to show on the screen at the same time. Imagine you can select new songs while listening to the current one.

Ah, gotcha. For the progress bar to be running at the same time as the user is making a selection, we'd need the ProgressBar and the Select to coordinate somehow. (Since to redraw the progress bar at each step, we'd first need to clear the screen of the Select). That's certainly not implemented for a '3rd party' crate like inquire, and I'm 99% sure it's also not implemented for dialoguer which is a similar crate in our family: https://docs.rs/dialoguer/latest/dialoguer/.

It wouldn't be impossible to implement the coordination between indicatif and dialoguer, but IMHO it is a bit outside the scope of indicatif currently. @djc what do you think?

You might want to consider using a curses-based crate: https://crates.io/keywords/curses

@chris-laplante
Copy link
Collaborator

@chris-laplante hi, another unrelated question: how to reset the front color's position to 0? I've tried bar.set_position(0), it doesn't work. 2023-07-26 at 2 40 AM

reset or set_position won't work properly in multiple threads. If so I don't think the progressbar should be Sync.

Can you show multi-threaded code you are trying? It should work fine from multiple threads.

@djc
Copy link
Member

djc commented Jul 27, 2023

It wouldn't be impossible to implement the coordination between indicatif and dialoguer, but IMHO it is a bit outside the scope of indicatif currently. @djc what do you think?

Yeah, I'm personally not motivated to spend time on that.

@Daniel-Xu
Copy link
Author

Jul 26, 2023

@chris-laplante hi, another unrelated question: how to reset the front color's position to 0? I've tried bar.set_position(0), it doesn't work. 2023-07-26 at 2 40 AM

reset or set_position won't work properly in multiple threads. If so I don't think the progressbar should be Sync.

Can you show multi-threaded code you are trying? It should work fine from multiple threads.

use async_trait::async_trait;
use std::sync::Arc;
use std::thread;
use std::time::Duration;

use inquire::{InquireError, Select};

use indicatif::{HumanDuration, ProgressBar};

async fn tick(p: ProgressBar) {
    let mut interval = tokio::time::interval(Duration::from_secs(1));
    loop {
        p.inc(50);
        interval.tick().await;
    }
}

use crossbeam::channel::{self, Receiver, Sender};
pub struct Player {
    //send message to sink thread
    sender: Sender<PlayerMessage>,
    // receive message from sink
    pub receiver: Receiver<SinkMessage>,
}

enum PlayerMessage {
    Play { listen_url: String },
}

#[derive(Debug)]
pub enum SinkMessage {
    Initialized,
    Playing,
    Done,
}

use anyhow::{anyhow, Context, Result};

impl Player {
    /// Creating a `Player` might be time consuming. It might take several seconds on first run.
    pub fn try_new() -> Result<Self> {
        let (sender, receiver) = channel::unbounded();
        let (sink_sender, sink_receiver) = channel::unbounded();

        thread::spawn(move || loop {
            while let Ok(message) = receiver.recv() {
                match message {
                    PlayerMessage::Play { listen_url } => {
                        break;
                    }
                }
            }

            sink_sender.send(SinkMessage::Initialized).unwrap();
        });

        Ok(Self {
            sender,
            receiver: sink_receiver,
        })
    }

    pub fn play(&self, listen_url: &str) {
        self.sender
            .send(PlayerMessage::Play {
                listen_url: listen_url.to_owned(),
            })
            .unwrap();
    }
}

fn process_sink_message(player: Arc<Player>, pb: ProgressBar) {
    while let Ok(msg) = player.receiver.recv() {
        match msg {
            SinkMessage::Initialized => {
                pb.suspend(|| {
                     println!("initialized 1");
                });

                pb.reset();
            }

            SinkMessage::Playing => {}

            SinkMessage::Done => pb.suspend(|| {
                println!("done");
            }),
        }
    }
}

#[tokio::main]
async fn main() {
    let pb = ProgressBar::new(1024);

    let player = Arc::new(Player::try_new().unwrap());

    let pb_clone = pb.clone();
    tokio::spawn(tick(pb_clone));

    let playerd = player.clone();
    let pb_cloned = pb.clone();

    let notification_handle = thread::spawn(move || process_sink_message(playerd, pb_cloned));

    let ans = pb.suspend(|| Select::new("choose a song: ", vec!["ab", "b", "c"]).prompt());
    match ans {
        Ok(choice) => {
            pb.suspend(|| {
                println!("hello world choose {}", choice);
            });
            pb.set_message(HumanDuration(Duration::from_secs(1000)).to_string());
        }

        Err(_) => {
            std::process::exit(0);
        }
    }

    player.play("abc");

    notification_handle.join().unwrap();
}

this will reproduce the issue:

  1. when you are at the Select menu, wait for a few seconds (let the tick run a few seconds)

2023-07-27 at 9 46 PM

  1. you will see that the initialized message is printed, so the pb.reset() will be executed. However, the progress is not reset as expected.

2023-07-27 at 9 47 PM

@chris-laplante
Copy link
Collaborator

this will reproduce the issue:

  1. when you are at the Select menu, wait for a few seconds (let the tick run a few seconds)

2023-07-27 at 9 46 PM

  1. you will see that the initialized message is printed, so the pb.reset() will be executed. However, the progress is not reset as expected.

2023-07-27 at 9 47 PM

I can reproduce that, and it's weird. I'll look into it later this week (can't do it right now unfortunately).

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

3 participants