forked from NationTech/harmony
The multiprogress wasn't used properly and leading to conflicting progress bars (within our own progress bars, as well as the log wrapper). This PR introduce a layer on top of `indicatif::MultiProgress` to properly handle sections of progress bars, where we can dynamically add/update/remove progress bars from any sections. We can see in the demo that new sections + progress bars are added on the fly and that extra logs (e.g. info logs) are appended on top of the progress bars. Progress are also grouped together based on their parent score. Co-authored-by: Ian Letourneau <letourneau.ian@gmail.com> Co-authored-by: johnride <jg@nationtech.io> Reviewed-on: NationTech/harmony#101
164 lines
4.9 KiB
Rust
164 lines
4.9 KiB
Rust
use indicatif::{MultiProgress, ProgressBar};
|
|
use std::collections::HashMap;
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::Duration;
|
|
|
|
pub trait ProgressTracker: Send + Sync {
|
|
fn contains_section(&self, id: &str) -> bool;
|
|
fn add_section(&self, id: &str, message: &str);
|
|
fn add_task(&self, section_id: &str, task_id: &str, message: &str);
|
|
fn finish_task(&self, id: &str, message: &str);
|
|
fn fail_task(&self, id: &str, message: &str);
|
|
fn skip_task(&self, id: &str, message: &str);
|
|
fn clear(&self);
|
|
}
|
|
|
|
struct Section {
|
|
header_index: usize,
|
|
task_count: usize,
|
|
pb: ProgressBar,
|
|
}
|
|
|
|
struct IndicatifProgressTrackerState {
|
|
sections: HashMap<String, Section>,
|
|
tasks: HashMap<String, ProgressBar>,
|
|
pb_count: usize,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct IndicatifProgressTracker {
|
|
mp: MultiProgress,
|
|
state: Arc<Mutex<IndicatifProgressTrackerState>>,
|
|
}
|
|
|
|
impl IndicatifProgressTracker {
|
|
pub fn new(base: MultiProgress) -> Self {
|
|
// The indicatif log bridge will insert a progress bar at the top.
|
|
// To prevent our first section from being erased, we need to create
|
|
// a dummy progress bar as our first progress bar.
|
|
let _ = base.clear();
|
|
let log_pb = base.add(ProgressBar::new(1));
|
|
|
|
let mut sections = HashMap::new();
|
|
sections.insert(
|
|
"__log__".into(),
|
|
Section {
|
|
header_index: 0,
|
|
task_count: 0,
|
|
pb: log_pb.clone(),
|
|
},
|
|
);
|
|
|
|
let mut tasks = HashMap::new();
|
|
tasks.insert("__log__".into(), log_pb);
|
|
|
|
let state = Arc::new(Mutex::new(IndicatifProgressTrackerState {
|
|
sections,
|
|
tasks,
|
|
pb_count: 1,
|
|
}));
|
|
|
|
Self { mp: base, state }
|
|
}
|
|
}
|
|
|
|
impl ProgressTracker for IndicatifProgressTracker {
|
|
fn add_section(&self, id: &str, message: &str) {
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
let header_pb = self
|
|
.mp
|
|
.add(ProgressBar::new(1).with_style(crate::theme::SECTION_STYLE.clone()));
|
|
header_pb.finish_with_message(message.to_string());
|
|
|
|
let header_index = state.pb_count;
|
|
state.pb_count += 1;
|
|
|
|
state.sections.insert(
|
|
id.to_string(),
|
|
Section {
|
|
header_index,
|
|
task_count: 0,
|
|
pb: header_pb,
|
|
},
|
|
);
|
|
}
|
|
|
|
fn add_task(&self, section_id: &str, task_id: &str, message: &str) {
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
let insertion_index = {
|
|
let current_section = state
|
|
.sections
|
|
.get(section_id)
|
|
.expect("Section ID not found");
|
|
current_section.header_index + current_section.task_count + 1 // +1 to insert after header
|
|
};
|
|
|
|
let pb = self.mp.insert(insertion_index, ProgressBar::new_spinner());
|
|
pb.set_style(crate::theme::SPINNER_STYLE.clone());
|
|
pb.set_prefix(" ");
|
|
pb.set_message(message.to_string());
|
|
pb.enable_steady_tick(Duration::from_millis(80));
|
|
|
|
state.pb_count += 1;
|
|
|
|
let section = state
|
|
.sections
|
|
.get_mut(section_id)
|
|
.expect("Section ID not found");
|
|
section.task_count += 1;
|
|
|
|
// We inserted a new progress bar, so we must update the header_index
|
|
// for all subsequent sections.
|
|
for (id, s) in state.sections.iter_mut() {
|
|
if id != section_id && s.header_index >= insertion_index {
|
|
s.header_index += 1;
|
|
}
|
|
}
|
|
|
|
state.tasks.insert(task_id.to_string(), pb);
|
|
}
|
|
|
|
fn finish_task(&self, id: &str, message: &str) {
|
|
let state = self.state.lock().unwrap();
|
|
if let Some(pb) = state.tasks.get(id) {
|
|
pb.set_style(crate::theme::SUCCESS_SPINNER_STYLE.clone());
|
|
pb.finish_with_message(message.to_string());
|
|
}
|
|
}
|
|
|
|
fn fail_task(&self, id: &str, message: &str) {
|
|
let state = self.state.lock().unwrap();
|
|
if let Some(pb) = state.tasks.get(id) {
|
|
pb.set_style(crate::theme::ERROR_SPINNER_STYLE.clone());
|
|
pb.finish_with_message(message.to_string());
|
|
}
|
|
}
|
|
|
|
fn skip_task(&self, id: &str, message: &str) {
|
|
let state = self.state.lock().unwrap();
|
|
if let Some(pb) = state.tasks.get(id) {
|
|
pb.set_style(crate::theme::SKIP_SPINNER_STYLE.clone());
|
|
pb.finish_with_message(message.to_string());
|
|
}
|
|
}
|
|
|
|
fn contains_section(&self, id: &str) -> bool {
|
|
let state = self.state.lock().unwrap();
|
|
state.sections.contains_key(id)
|
|
}
|
|
|
|
fn clear(&self) {
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
state.tasks.values().for_each(|p| self.mp.remove(p));
|
|
state.tasks.clear();
|
|
state.sections.values().for_each(|s| self.mp.remove(&s.pb));
|
|
state.sections.clear();
|
|
state.pb_count = 0;
|
|
|
|
let _ = self.mp.clear();
|
|
}
|
|
}
|