use std::io::IsTerminal as _;
use std::io::Write as _;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use pdfboss_aio::{AsyncDocument, CachedBackend, HttpBackend};
const BAR_WIDTH: usize = 30;
fn human_size(bytes: u64) -> String {
const UNITS: [(&str, u64); 3] = [
("GiB", 1024 * 1024 * 1024),
("MiB", 1024 * 1024),
("KiB", 1024),
];
for (unit, scale) in UNITS {
if bytes >= scale {
return format!("{:.1} {unit}", bytes as f64 / scale as f64);
}
}
format!("{bytes} B")
}
fn percent_of(collected: u64, total: u64) -> u64 {
(collected.min(total) * 100)
.checked_div(total)
.unwrap_or(100)
}
fn progress_line(collected: u64, total: u64) -> String {
let collected = collected.min(total);
let percent = percent_of(collected, total);
let filled = (percent as usize * BAR_WIDTH) / 100;
let bar = if filled == BAR_WIDTH {
"=".repeat(BAR_WIDTH)
} else {
format!(
"{}>{}",
"=".repeat(filled),
" ".repeat(BAR_WIDTH - filled - 1)
)
};
format!(
"[{bar}] {percent:>3}% {}/{}",
human_size(collected),
human_size(total)
)
}
struct DownloadBar {
started: AtomicBool,
last_percent: AtomicU64,
}
impl DownloadBar {
fn new() -> DownloadBar {
DownloadBar {
started: AtomicBool::new(false),
last_percent: AtomicU64::new(u64::MAX),
}
}
fn tick(&self, collected: u64, total: u64) {
if !self.started.swap(true, Ordering::SeqCst) {
eprintln!(
"pdfboss: server ignores Range requests, downloading the whole file ({})",
human_size(total)
);
}
let percent = percent_of(collected, total);
if self.last_percent.swap(percent, Ordering::SeqCst) == percent {
return;
}
eprint!("\r{}", progress_line(collected, total));
let _ = std::io::stderr().flush();
}
fn finish(&self) {
if !self.started.load(Ordering::SeqCst) {
return;
}
eprint!("\r\x1b[K");
let _ = std::io::stderr().flush();
}
}
pub async fn open_url_with_progress(
url: &str,
password: &str,
) -> pdfboss_aio::Result<AsyncDocument> {
let backend = HttpBackend::new(url).await?;
if !std::io::stderr().is_terminal() {
return AsyncDocument::with_backend_with_password(CachedBackend::new(backend), password)
.await;
}
let bar = Arc::new(DownloadBar::new());
let ticker = Arc::clone(&bar);
let backend =
backend.on_fallback_progress(move |collected, total| ticker.tick(collected, total));
let doc =
AsyncDocument::with_backend_with_password(CachedBackend::new(backend), password).await;
bar.finish();
doc
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn human_size_picks_the_unit() {
assert_eq!(human_size(0), "0 B");
assert_eq!(human_size(1023), "1023 B");
assert_eq!(human_size(1024), "1.0 KiB");
assert_eq!(human_size(110_175), "107.6 KiB");
assert_eq!(human_size(11_165_345), "10.6 MiB");
assert_eq!(human_size(3 * 1024 * 1024 * 1024), "3.0 GiB");
}
#[test]
fn progress_line_renders_the_bar_states() {
assert_eq!(
progress_line(0, 110_175),
"[> ] 0% 0 B/107.6 KiB"
);
assert_eq!(
progress_line(55_000, 110_175),
"[==============> ] 49% 53.7 KiB/107.6 KiB"
);
assert_eq!(
progress_line(110_175, 110_175),
"[==============================] 100% 107.6 KiB/107.6 KiB"
);
}
#[test]
fn progress_line_clamps_overrun_and_zero_total() {
assert_eq!(
progress_line(2000, 1000),
"[==============================] 100% 1000 B/1000 B"
);
assert!(progress_line(0, 0).contains("100%"));
}
}