refactor(progress): extract format_stage_line with themed styling

Pull the line-formatting logic out of finish_stage() into a standalone
public format_stage_line() so that sync.rs can build stage lines without
needing a live ProgressBar (e.g. for static multi-line blocks printed
after the spinner is cleared).

The new function applies Theme::info().bold() to the label and
Theme::timing() to the elapsed column, giving every stage line
consistent color treatment. finish_stage() now delegates to it.

Includes a unit test asserting the formatted output contains the
expected icon, label, summary, and elapsed components.
This commit is contained in:
teernisse
2026-02-14 11:36:48 -05:00
parent eef73decb5
commit a570327a6b

View File

@@ -4,7 +4,7 @@ use std::sync::LazyLock;
use std::time::Duration; use std::time::Duration;
use tracing_subscriber::fmt::MakeWriter; use tracing_subscriber::fmt::MakeWriter;
use crate::cli::render::Icons; use crate::cli::render::{Icons, Theme};
static MULTI: LazyLock<MultiProgress> = LazyLock::new(MultiProgress::new); static MULTI: LazyLock<MultiProgress> = LazyLock::new(MultiProgress::new);
@@ -56,12 +56,21 @@ pub fn nested_progress(msg: &str, len: u64, robot_mode: bool) -> ProgressBar {
/// ///
/// Output: ` ✓ Label summary elapsed` /// Output: ` ✓ Label summary elapsed`
pub fn finish_stage(pb: &ProgressBar, icon: &str, label: &str, summary: &str, elapsed: Duration) { pub fn finish_stage(pb: &ProgressBar, icon: &str, label: &str, summary: &str, elapsed: Duration) {
let elapsed_str = format_elapsed(elapsed); let line = format_stage_line(icon, label, summary, elapsed);
let line = format!(" {icon} {label:<12}{summary:>40} {elapsed_str:>8}",);
pb.set_style(ProgressStyle::with_template("{msg}").expect("valid template")); pb.set_style(ProgressStyle::with_template("{msg}").expect("valid template"));
pb.finish_with_message(line); pb.finish_with_message(line);
} }
/// Build a static stage line showing icon, label, summary, and elapsed.
///
/// Output: ` ✓ Label summary elapsed`
pub fn format_stage_line(icon: &str, label: &str, summary: &str, elapsed: Duration) -> String {
let elapsed_str = format_elapsed(elapsed);
let styled_label = Theme::info().bold().render(&format!("{label:<12}"));
let styled_elapsed = Theme::timing().render(&format!("{elapsed_str:>8}"));
format!(" {icon} {styled_label}{summary:>40} {styled_elapsed}")
}
/// Format a Duration as a compact human string (e.g. "1.2s", "42ms", "1m 5s"). /// Format a Duration as a compact human string (e.g. "1.2s", "42ms", "1m 5s").
fn format_elapsed(d: Duration) -> String { fn format_elapsed(d: Duration) -> String {
let ms = d.as_millis(); let ms = d.as_millis();
@@ -203,4 +212,13 @@ mod tests {
assert_eq!(format_elapsed(Duration::from_secs(65)), "1m 5s"); assert_eq!(format_elapsed(Duration::from_secs(65)), "1m 5s");
assert_eq!(format_elapsed(Duration::from_secs(120)), "2m 0s"); assert_eq!(format_elapsed(Duration::from_secs(120)), "2m 0s");
} }
#[test]
fn format_stage_line_includes_label_summary_and_elapsed() {
let line = format_stage_line("", "Issues", "10 issues", Duration::from_millis(4200));
assert!(line.contains(""));
assert!(line.contains("Issues"));
assert!(line.contains("10 issues"));
assert!(line.contains("4.2s"));
}
} }