use std::cell::RefCell;
use std::io::{IsTerminal, Write};
#[derive(Debug, Clone, Copy)]
pub enum Event {
ObjectsPacked(usize),
PackUploaded(u64),
ObjectsUnpacked(usize),
}
const REPORT_INTERVAL: usize = 8;
struct Reporter {
label: &'static str,
total: Option<usize>,
done: usize,
bytes: u64,
last_emit_done: usize,
emitted: bool,
}
impl Reporter {
fn new(label: &'static str, total: Option<usize>) -> Self {
Self {
label,
total,
done: 0,
bytes: 0,
last_emit_done: 0,
emitted: false,
}
}
fn record(&mut self, event: Event) {
match event {
Event::ObjectsPacked(n) | Event::ObjectsUnpacked(n) => {
self.done += n;
if self.done.saturating_sub(self.last_emit_done) >= REPORT_INTERVAL {
self.emit();
}
}
Event::PackUploaded(bytes) => {
self.bytes = self.bytes.saturating_add(bytes);
self.emit();
}
}
}
fn emit(&mut self) {
self.last_emit_done = self.done;
self.emitted = true;
let mut stderr = std::io::stderr().lock();
let _ = match (self.total, self.bytes) {
(Some(total), 0) => write!(stderr, "\r{}: {}/{} objects", self.label, self.done, total),
(Some(total), bytes) => write!(
stderr,
"\r{}: {}/{} objects, {} bytes",
self.label, self.done, total, bytes
),
(None, 0) => write!(stderr, "\r{}: {} objects", self.label, self.done),
(None, bytes) => write!(
stderr,
"\r{}: {} objects, {} bytes",
self.label, self.done, bytes
),
};
let _ = stderr.flush();
}
fn finish(&mut self) {
if self.done == 0 && self.bytes == 0 {
return;
}
self.emit();
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, ", done.");
}
}
thread_local! {
static REPORTER: RefCell<Option<Reporter>> = const { RefCell::new(None) };
}
#[derive(Debug)]
#[must_use = "dropping this immediately ends progress reporting"]
pub struct Guard {
_private: (),
}
impl Drop for Guard {
fn drop(&mut self) {
REPORTER.with(|r| {
if let Some(mut rep) = r.borrow_mut().take() {
rep.finish();
}
});
}
}
pub fn start(label: &'static str, total: Option<usize>, enabled: bool) -> Guard {
REPORTER.with(|r| {
*r.borrow_mut() = if enabled {
Some(Reporter::new(label, total))
} else {
None
};
});
Guard { _private: () }
}
pub fn report(event: Event) {
REPORTER.with(|r| {
if let Ok(mut slot) = r.try_borrow_mut()
&& let Some(rep) = slot.as_mut()
{
rep.record(event);
}
});
}
#[must_use]
pub fn should_report(quiet: bool) -> bool {
if quiet {
return false;
}
match std::env::var("MKIT_PROGRESS").ok().as_deref() {
Some("always") => true,
Some("never") => false,
_ => std::io::stderr().is_terminal(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn report_without_guard_is_a_silent_no_op() {
report(Event::ObjectsPacked(1));
report(Event::PackUploaded(128));
report(Event::ObjectsUnpacked(3));
}
#[test]
fn pack_uploaded_accumulates_across_multiple_packs() {
let mut rep = Reporter::new("Writing objects", None);
rep.record(Event::PackUploaded(100));
assert_eq!(rep.bytes, 100);
rep.record(Event::PackUploaded(50));
assert_eq!(rep.bytes, 150, "second pack's bytes must add, not replace");
rep.record(Event::PackUploaded(25));
assert_eq!(rep.bytes, 175);
}
#[test]
fn disabled_guard_installs_no_reporter() {
let guard = start("Writing objects", Some(4), false);
report(Event::ObjectsPacked(4));
drop(guard);
}
#[test]
fn should_report_quiet_always_wins() {
assert!(!should_report(true));
}
}