use std::borrow::Cow;
use std::time::{Duration, Instant};
use indicatif::HumanBytes;
const BAR_TEMPLATE: &str = "{msg} [{bar:24.cyan/blue}] {percent:>3}% · {bytes}/{total_bytes} · {bytes_per_sec} · eta {eta}";
fn is_terminal() -> bool {
use std::io::IsTerminal;
use std::sync::OnceLock;
static TTY: OnceLock<bool> = OnceLock::new();
*TTY.get_or_init(|| std::io::stdout().is_terminal())
}
pub fn intro(title: impl Into<Cow<'static, str>>) {
let title = title.into();
if is_terminal() {
let _ = cliclack::intro(title);
} else {
println!("{title}");
}
}
pub fn outro(message: impl Into<Cow<'static, str>>) {
let message = message.into();
if is_terminal() {
let _ = cliclack::outro(message);
} else {
println!("{message}");
}
}
pub fn outro_error(message: impl Into<Cow<'static, str>>) {
let message = message.into();
if is_terminal() {
let _ = cliclack::outro_cancel(message);
} else {
eprintln!("{message}");
}
}
pub fn info(message: &str) {
if is_terminal() {
let _ = cliclack::log::info(message);
} else {
println!("{message}");
}
}
pub fn warn(message: &str) {
if is_terminal() {
let _ = cliclack::log::warning(message);
} else {
eprintln!("note: {message}");
}
}
pub struct Step {
bar: Option<cliclack::ProgressBar>,
pending: Option<Cow<'static, str>>,
}
impl Step {
pub fn start(message: impl Into<Cow<'static, str>>) -> Self {
let message = message.into();
if !is_terminal() {
return Self {
bar: None,
pending: Some(message),
};
}
let bar = cliclack::spinner();
bar.start(message);
Self { bar: Some(bar), pending: None }
}
pub fn update(&self, message: impl Into<Cow<'static, str>>) {
if let Some(bar) = &self.bar {
bar.set_message(message.into());
}
}
pub fn done(self, message: impl Into<Cow<'static, str>>) {
let message = message.into();
match self.bar {
Some(bar) => bar.stop(message),
None => println!("{message}"),
}
}
pub fn clear(self) {
match self.bar {
Some(bar) => bar.clear(),
None => {
if let Some(message) = &self.pending {
println!("{message}");
}
}
}
}
}
pub struct Transfer {
bar: Option<cliclack::ProgressBar>,
started: Instant,
}
impl Transfer {
pub fn start(files: u64, bytes: u64) -> Self {
let started = Instant::now();
if !is_terminal() {
let plural = if files == 1 { "" } else { "s" };
println!("Uploading {files} file{plural} ({}) ...", human_bytes(bytes));
return Self { bar: None, started };
}
let bar = cliclack::progress_bar(bytes).with_template(BAR_TEMPLATE);
bar.start("Uploading");
Self { bar: Some(bar), started }
}
pub fn advance(&self, bytes: u64) {
if let Some(bar) = &self.bar {
bar.inc(bytes);
}
}
pub fn elapsed(&self) -> Duration {
self.started.elapsed()
}
pub fn done(self, message: impl Into<Cow<'static, str>>) {
let message = message.into();
match self.bar {
Some(bar) => bar.stop(message),
None => println!("{message}"),
}
}
}
pub fn human_bytes(bytes: u64) -> String {
HumanBytes(bytes).to_string()
}
pub fn human_duration(elapsed: Duration) -> String {
let secs = elapsed.as_secs_f64();
if secs < 10.0 {
return format!("{secs:.1}s");
}
let whole = secs.round() as u64;
if whole < 60 {
format!("{whole}s")
} else {
format!("{}m {:02}s", whole / 60, whole % 60)
}
}
pub fn rate(bytes: u64, elapsed: Duration) -> String {
let secs = elapsed.as_secs_f64();
if secs <= f64::EPSILON {
return format!("{}/s", HumanBytes(bytes));
}
format!("{}/s", HumanBytes((bytes as f64 / secs) as u64))
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::{human_bytes, human_duration, rate};
#[test]
fn formats_byte_counts_like_the_bar() {
assert_eq!(human_bytes(0), "0 B");
assert_eq!(human_bytes(93), "93 B");
assert!(human_bytes(831_488).ends_with("KiB"), "got {}", human_bytes(831_488));
assert!(human_bytes(6_400_000).ends_with("MiB"), "got {}", human_bytes(6_400_000));
assert!(human_bytes(3_221_225_472).ends_with("GiB"), "got {}", human_bytes(3_221_225_472));
}
#[test]
fn formats_durations_at_a_human_scale() {
assert_eq!(human_duration(Duration::from_millis(400)), "0.4s");
assert_eq!(human_duration(Duration::from_millis(4_100)), "4.1s");
assert_eq!(human_duration(Duration::from_secs(12)), "12s");
assert_eq!(human_duration(Duration::from_secs(103)), "1m 43s");
assert_eq!(human_duration(Duration::from_millis(59_700)), "1m 00s");
}
#[test]
fn rates_use_the_bar_units_and_survive_zero_time() {
let mib = rate(5 * 1024 * 1024, Duration::from_secs(4));
assert_eq!(mib, "1.25 MiB/s");
assert!(rate(1000, Duration::ZERO).ends_with("/s"));
}
}