use super::ci::Snap;
use clx::style;
use std::sync::atomic::{AtomicBool, Ordering};
const TARBALL_COMPRESSION_RATIO: f64 = 0.30;
pub(super) fn progress_line(snap: Snap, term_width: usize, bar_width: usize) -> String {
if snap.phase == 0 {
return String::new();
}
let completed = if snap.phase == 1 {
0
} else {
clamped_completed(snap)
};
let label = label_for(snap, completed);
if label.is_empty() {
return String::new();
}
let bar = bar_only(snap, bar_width, completed);
let _ = term_width; format!("{bar} {label}")
}
pub(super) fn bar_only(snap: Snap, width: usize, completed: usize) -> String {
let (numerator, denominator) = if snap.phase == 1 {
(0, 1)
} else {
let denom = snap.resolved.max(1);
(completed, denom)
};
let filled = numerator
.checked_mul(width)
.and_then(|v| v.checked_div(denominator))
.unwrap_or(0)
.min(width);
let empty = width - filled;
let fill = "█".repeat(filled);
let empty = "░".repeat(empty);
format!("{}{}", style::egreen(fill), style::edim(empty))
}
fn label_for(snap: Snap, completed: usize) -> String {
match snap.phase {
1 => {
format!(
"{} pkgs · {} · {}",
style::ebold(snap.resolved),
style::eyellow("resolving"),
style::edim("ETA …"),
)
}
2 => {
let mut parts = Vec::with_capacity(4);
parts.push(format!(
"{}/{} pkgs",
style::ebold(completed),
style::ebold(snap.resolved),
));
let seg = bytes_segment(snap);
if !seg.is_empty() {
parts.push(seg);
}
if let Some(rate) = transfer_rate(snap) {
parts.push(style::edim(format!("{}/s", format_bytes(rate))).to_string());
}
parts.push(eta_segment(snap, completed));
parts.join(&format!(" {} ", style::edim("·")))
}
3 => {
let mut parts = vec![format!(
"{}/{} pkgs",
style::ebold(completed),
style::ebold(snap.resolved),
)];
if snap.bytes > 0 {
parts.push(style::edim(format_bytes(snap.bytes)).to_string());
}
parts.push(style::ecyan("linking").to_string());
parts.join(&format!(" {} ", style::edim("·")))
}
_ => String::new(),
}
}
fn bytes_segment(snap: Snap) -> String {
let estimated_download = estimated_download_bytes(snap.estimated);
if estimated_download > snap.bytes && snap.bytes > 0 {
format!(
"{} / ~{}",
style::ebold(format_bytes(snap.bytes)),
style::edim(format_bytes(estimated_download)),
)
} else if snap.bytes > 0 {
style::ebold(format_bytes(snap.bytes)).to_string()
} else if estimated_download > 0 {
format!("~{}", style::edim(format_bytes(estimated_download)),)
} else {
String::new()
}
}
pub(super) fn estimated_download_bytes(unpacked: u64) -> u64 {
if unpacked == 0 {
return 0;
}
(unpacked as f64 * TARBALL_COMPRESSION_RATIO) as u64
}
fn eta_segment(snap: Snap, completed: usize) -> String {
if completed >= snap.resolved {
return style::edim("ETA …").to_string();
}
let Some(baseline) = snap.completed_at_fetch_start else {
return style::edim("ETA …").to_string();
};
let fetch_completed = completed.saturating_sub(baseline);
if fetch_completed == 0 || snap.fetch_elapsed_ms == 0 {
return style::edim("ETA …").to_string();
}
let remaining = snap.resolved - completed;
let eta_ms = snap.fetch_elapsed_ms.saturating_mul(remaining as u64) / fetch_completed as u64;
style::edim(format!(
"ETA {}",
format_duration(std::time::Duration::from_millis(eta_ms))
))
.to_string()
}
fn transfer_rate(snap: Snap) -> Option<u64> {
if snap.bytes == 0 || snap.fetch_elapsed_ms == 0 {
return None;
}
Some(snap.bytes.saturating_mul(1000) / snap.fetch_elapsed_ms)
}
static OVERFLOW_WARNED: AtomicBool = AtomicBool::new(false);
fn clamped_completed(snap: Snap) -> usize {
let raw = snap.reused + snap.downloaded;
if raw > snap.resolved && snap.resolved > 0 && !OVERFLOW_WARNED.swap(true, Ordering::Relaxed) {
tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_PROGRESS_OVERFLOW,
raw_completed = raw,
resolved = snap.resolved,
"progress numerator exceeded resolved-package denominator; clamping display"
);
}
raw.min(snap.resolved)
}
pub(super) fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1_000;
const MB: u64 = 1_000_000;
const GB: u64 = 1_000_000_000;
if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.0} kB", bytes as f64 / KB as f64)
} else {
format!("{bytes} B")
}
}
pub(super) fn format_duration(d: std::time::Duration) -> String {
super::ci::format_duration(d)
}
#[cfg(test)]
mod tests {
use super::*;
fn snap(phase: usize, resolved: usize, completed: usize, bytes: u64, estimated: u64) -> Snap {
Snap {
phase,
resolved,
reused: completed,
downloaded: 0,
bytes,
estimated,
fetch_elapsed_ms: 3_000,
completed_at_fetch_start: Some(0),
}
}
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' && chars.peek() == Some(&'[') {
chars.next();
for esc_c in chars.by_ref() {
if esc_c.is_ascii_alphabetic() {
break;
}
}
continue;
}
out.push(c);
}
out
}
#[test]
fn resolving_phase_shows_count_and_eta_placeholder() {
let line = strip_ansi(&progress_line(snap(1, 89, 0, 0, 0), 80, 15));
assert!(line.contains("89 pkgs"), "got: {line}");
assert!(line.contains("resolving"), "got: {line}");
assert!(line.contains("ETA …"), "got: {line}");
}
#[test]
fn fetching_phase_shows_bytes_and_estimate() {
let line = strip_ansi(&progress_line(
snap(2, 142, 23, 4_200_000, 46_000_000),
80,
15,
));
assert!(line.contains("23/142 pkgs"), "got: {line}");
assert!(line.contains("4.2 MB"), "got: {line}");
assert!(line.contains("~13.8 MB"), "got: {line}");
}
#[test]
fn fetching_phase_drops_estimate_when_running_exceeds_it() {
let line = strip_ansi(&progress_line(
snap(2, 142, 23, 4_200_000, 13_800_000),
80,
15,
));
assert!(line.contains("4.2 MB"), "got: {line}");
assert!(!line.contains("~"), "estimate should drop: {line}");
}
#[test]
fn linking_phase_drops_rate_and_eta() {
let line = strip_ansi(&progress_line(
snap(3, 142, 142, 13_800_000, 13_800_000),
80,
15,
));
assert!(line.contains("142/142"), "got: {line}");
assert!(line.contains("linking"), "got: {line}");
assert!(!line.contains("MB/s"), "rate must drop in linking: {line}");
assert!(!line.contains("ETA"), "eta must drop in linking: {line}");
}
#[test]
fn clamps_overflow_to_resolved() {
let mut s = snap(2, 5, 7, 0, 0);
s.reused = 7;
let line = strip_ansi(&progress_line(s, 80, 15));
assert!(line.contains("5/5 pkgs"), "got: {line}");
}
}