use kdam::Animation;
use std::fmt::Display;
use std::num::NonZeroU16;
use std::time::Duration;
use super::{cyan, err_line, faint, green, is_quiet, line, magenta, red};
pub fn progress(
label: &str,
current: usize,
total: usize,
detail: impl Display,
elapsed: Duration,
) {
if is_quiet() {
return;
}
line(progress_line(
label,
current,
total,
&detail.to_string(),
elapsed,
));
}
fn progress_line(
label: &str,
current: usize,
total: usize,
detail: &str,
elapsed: Duration,
) -> String {
let total = total.max(1);
let current = current.min(total);
let head = format!(
" {} {current}/{total} {}",
progress_bar(current, total, 18),
cyan(label)
);
if detail.trim().is_empty() {
format!("{head} · {}", faint(format_duration(elapsed)))
} else {
format!("{head} · {detail} · {}", faint(format_duration(elapsed)))
}
}
fn progress_bar(current: usize, total: usize, width: u16) -> String {
let total = total.max(1);
let current = current.min(total);
let percentage = current as f32 / total as f32;
Animation::FillUp.fmt_render(
NonZeroU16::new(width.max(1)).expect("progress bar width is non-zero"),
percentage,
&None,
)
}
pub fn tool_batch(round: usize, count: usize) {
if is_quiet() {
return;
}
err_line(tool_batch_line(round, count));
}
pub fn tool_start(name: &str, detail: &str) {
if is_quiet() {
return;
}
err_line(tool_start_line(name, detail));
}
pub fn tool_result(name: &str, elapsed: Duration, preview: &str) {
if is_quiet() {
return;
}
let preview = preview.trim_end();
let head = tool_result_head(name, elapsed);
let Some((first, rest)) = preview.split_once('\n') else {
if preview.is_empty() {
err_line(head);
} else {
err_line(format_args!("{head} · {first}", first = preview));
}
return;
};
err_line(format_args!("{head} · {first}"));
for line in rest.lines() {
err_line(format_args!(" {line}"));
}
}
pub fn tool_error(name: &str, elapsed: Duration, err: impl Display) {
if is_quiet() {
return;
}
err_line(format_args!(
" {} {name} {} · {err:#}",
red("✗"),
format_duration(elapsed)
));
}
pub fn format_duration(elapsed: Duration) -> String {
if elapsed.as_millis() < 1000 {
format!("{}ms", elapsed.as_millis())
} else {
format!("{:.1}s", elapsed.as_secs_f64())
}
}
fn tool_batch_line(round: usize, count: usize) -> String {
format!("{} tools r{round} ×{count}", magenta("↻"))
}
fn tool_start_line(name: &str, detail: &str) -> String {
if detail.is_empty() {
format!(" {} {name}", cyan("→"))
} else {
format!(" {} {name} · {detail}", cyan("→"))
}
}
fn tool_result_head(name: &str, elapsed: Duration) -> String {
format!(" {} {name} {}", green("✓"), format_duration(elapsed))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::{OutputMode, set_output_mode};
#[test]
fn elapsed_format_is_compact() {
assert_eq!(format_duration(Duration::from_millis(42)), "42ms");
assert_eq!(format_duration(Duration::from_millis(1250)), "1.2s");
}
#[test]
fn progress_line_shows_bar_count_detail_and_elapsed() {
set_output_mode(OutputMode::Normal);
assert_eq!(progress_bar(2, 4, 8), "|████▂ |");
assert_eq!(
progress_line("review", 2, 4, "chunk 3", Duration::from_millis(1250)),
" |█████████▂ | 2/4 review · chunk 3 · 1.2s"
);
}
#[test]
fn tool_progress_lines_are_dense() {
set_output_mode(OutputMode::Normal);
assert_eq!(tool_batch_line(2, 3), "↻ tools r2 ×3");
assert_eq!(
tool_start_line("read", "path=src/main.rs"),
" → read · path=src/main.rs"
);
assert_eq!(
tool_result_head("read", Duration::from_millis(42)),
" ✓ read 42ms"
);
}
}