From fee78b094435ae6050f07bd3b97498ecb8a746d2 Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Thu, 6 Aug 2026 14:09:30 -0400 Subject: [PATCH] fix: preserve completed progress output --- src/main.rs | 4 +- src/ui.rs | 126 +++++++++++++++++++++++++----------------------- src/workflow.rs | 8 +-- 3 files changed, 71 insertions(+), 67 deletions(-) diff --git a/src/main.rs b/src/main.rs index b9b03c8..00d5355 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,11 +37,11 @@ async fn main() -> anyhow::Result<()> { let jobs = match eval_result { Ok(result) => { - ui.finish_section_success(evaluation); + ui.finish_section_success(&evaluation); result } Err(error) => { - ui.finish_section_failure(evaluation); + ui.finish_section_failure(&evaluation); return Err(error); } }; diff --git a/src/ui.rs b/src/ui.rs index 9cf4b09..01e1660 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::fmt::Display; use std::io::{self, IsTerminal}; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; use std::time::Duration; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; @@ -9,23 +9,28 @@ use owo_colors::{OwoColorize, Stream}; pub struct Ui { progress: MultiProgress, - is_interactive: bool, - retained_progress: Mutex>, + uses_live_progress: bool, needs_section_separator: Mutex, } pub struct SectionProgress { name: String, progress: ProgressBar, - jobs: HashMap, + jobs: HashMap, + job_order: Vec, +} + +#[derive(Clone)] +pub struct JobProgress { + progress: ProgressBar, + result: Arc>>, } impl Ui { pub fn new() -> Self { Self { progress: MultiProgress::new(), - is_interactive: io::stderr().is_terminal(), - retained_progress: Mutex::new(Vec::new()), + uses_live_progress: io::stderr().is_terminal(), needs_section_separator: Mutex::new(false), } } @@ -38,7 +43,7 @@ impl Ui { self.add_section_separator(); let heading = phase_heading(name); - if !self.is_interactive { + if !self.uses_live_progress { eprintln!("⠋ {heading}"); } @@ -46,6 +51,7 @@ impl Ui { name: heading.clone(), progress: self.new_section_spinner(heading), jobs: HashMap::new(), + job_order: Vec::new(), } } @@ -54,8 +60,12 @@ impl Ui { return; } - let progress = self.new_job_spinner(display_name); + let progress = JobProgress { + progress: self.new_job_spinner(display_name), + result: Arc::new(Mutex::new(None)), + }; section.jobs.insert(name.to_owned(), progress); + section.job_order.push(name.to_owned()); } pub fn start_job( @@ -63,56 +73,51 @@ impl Ui { section: &SectionProgress, name: &str, display_name: &str, - ) -> ProgressBar { + ) -> JobProgress { let progress = section .jobs .get(name) .expect("job progress must be registered") .clone(); - progress.enable_steady_tick(Duration::from_millis(100)); - progress.tick(); + progress + .progress + .enable_steady_tick(Duration::from_millis(100)); + progress.progress.tick(); - if !self.is_interactive { + if !self.uses_live_progress { eprintln!(" ⠋ {display_name}"); } progress } - pub fn finish_job_success(&self, progress: &ProgressBar, name: &str) { + pub fn finish_job_success(&self, progress: &JobProgress, name: &str) { self.finish_job(progress, format!(" {} {name}", success_marker())); } - pub fn finish_job_failure(&self, progress: &ProgressBar, name: &str, error: impl Display) { + pub fn finish_job_failure(&self, progress: &JobProgress, name: &str, error: impl Display) { self.finish_job(progress, format!(" {} {name}: {error}", failure_marker())); } - pub fn job_progress<'a>(section: &'a SectionProgress, name: &str) -> &'a ProgressBar { + pub fn job_progress<'a>(section: &'a SectionProgress, name: &str) -> &'a JobProgress { section .jobs .get(name) .expect("job progress must be registered") } - pub fn finish_section_success(&self, section: SectionProgress) { + pub fn finish_section_success(&self, section: &SectionProgress) { self.finish_section(section, true); } - pub fn finish_section_failure(&self, section: SectionProgress) { + pub fn finish_section_failure(&self, section: &SectionProgress) { self.finish_section(section, false); } pub fn print_summary(&self, message: &str) { self.add_section_separator(); let message = format!("{} {}", success_marker(), phase_heading(message)); - if self.is_interactive { - let progress = self.progress.add(ProgressBar::new_spinner()); - progress.set_style(completed_style()); - progress.finish_with_message(message); - self.retain_progress(progress); - } else { - eprintln!("{message}"); - } + eprintln!("{message}"); } fn new_section_spinner(&self, message: impl Into) -> ProgressBar { @@ -139,17 +144,22 @@ impl Ui { progress } - fn finish_job(&self, progress: &ProgressBar, message: String) { - if self.is_interactive { - progress.set_style(completed_style()); - progress.finish_with_message(message); + fn finish_job(&self, progress: &JobProgress, message: String) { + *progress + .result + .lock() + .expect("job result mutex is not poisoned") = Some(message.clone()); + + if self.uses_live_progress { + progress.progress.set_style(completed_style()); + progress.progress.finish_with_message(message); } else { eprintln!("{message}"); - progress.finish_and_clear(); + progress.progress.finish_and_clear(); } } - fn finish_section(&self, section: SectionProgress, is_success: bool) { + fn finish_section(&self, section: &SectionProgress, is_success: bool) { let message = if is_success { format!("{} {}", success_marker(), section.name) } else { @@ -158,21 +168,38 @@ impl Ui { if !is_success { for progress in section.jobs.values() { - if !progress.is_finished() { - progress.finish_and_clear(); + if !progress.progress.is_finished() { + progress.progress.finish_and_clear(); } } } - if self.is_interactive { - section.progress.set_style(completed_style()); - section.progress.finish_with_message(message); + if self.uses_live_progress { + section.progress.finish_and_clear(); + self.progress + .clear() + .expect("clearing completed progress must succeed"); + eprintln!("{message}"); + for name in §ion.job_order { + let progress = §ion.jobs[name]; + if let Some(result) = progress + .result + .lock() + .expect("job result mutex is not poisoned") + .as_deref() + { + eprintln!("{result}"); + } + } } else { eprintln!("{message}"); section.progress.finish_and_clear(); } - self.retain(section); + self.progress.remove(§ion.progress); + for progress in section.jobs.values() { + self.progress.remove(&progress.progress); + } *self .needs_section_separator .lock() @@ -190,30 +217,7 @@ impl Ui { *needs_separator = false; drop(needs_separator); - if self.is_interactive { - let separator = self.progress.add(ProgressBar::new_spinner()); - separator.set_style(completed_style()); - separator.finish_with_message(" ".to_owned()); - self.retain_progress(separator); - } else { - eprintln!(); - } - } - - fn retain(&self, section: SectionProgress) { - let mut retained = self - .retained_progress - .lock() - .expect("progress retention mutex is not poisoned"); - retained.push(section.progress); - retained.extend(section.jobs.into_values()); - } - - fn retain_progress(&self, progress: ProgressBar) { - self.retained_progress - .lock() - .expect("progress retention mutex is not poisoned") - .push(progress); + eprintln!(); } } diff --git a/src/workflow.rs b/src/workflow.rs index 50d3de7..6e156fa 100644 --- a/src/workflow.rs +++ b/src/workflow.rs @@ -102,13 +102,13 @@ async fn build_closures( } Err(error) => { ui.finish_job_failure(&job_progress, &name, &error); - ui.finish_section_failure(progress); + ui.finish_section_failure(&progress); return Err(error).context(format!("building {name}")); } } } - ui.finish_section_success(progress); + ui.finish_section_success(&progress); Ok(closures) } @@ -146,11 +146,11 @@ async fn deploy_closures( .await; if results.iter().any(Result::is_err) { - ui.finish_section_failure(progress); + ui.finish_section_failure(&progress); anyhow::bail!("deployment failed"); } - ui.finish_section_success(progress); + ui.finish_section_success(&progress); ui.print_summary("Deployment complete"); Ok(()) } -- 2.51.2