#[cfg(feature = "progress")]
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use std::io::IsTerminal;
use std::time::{Duration, Instant};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) enum ProgressDisplay {
Off,
Bar,
Lines,
}
pub(crate) struct ProgressReporter {
mode: ProgressDisplay,
#[cfg(feature = "progress")]
bar: Option<ProgressBar>,
}
impl ProgressReporter {
pub fn from_verbose(verbose: bool) -> Self {
let mode = if !verbose {
ProgressDisplay::Off
} else if std::io::stderr().is_terminal() && cfg!(feature = "progress") {
ProgressDisplay::Bar
} else {
ProgressDisplay::Lines
};
Self {
mode,
#[cfg(feature = "progress")]
bar: None,
}
}
#[cfg(test)]
pub fn from_display(mode: ProgressDisplay) -> Self {
Self {
mode,
#[cfg(feature = "progress")]
bar: None,
}
}
#[cfg(test)]
pub fn display_mode(&self) -> ProgressDisplay {
self.mode
}
pub fn log(&self, msg: &str) {
if self.mode == ProgressDisplay::Off {
return;
}
#[cfg(feature = "progress")]
if let Some(bar) = &self.bar {
bar.println(format!("napparent: {msg}"));
return;
}
eprintln!("napparent: {msg}");
}
#[allow(unused_variables)]
pub fn pass_start(&mut self, pass: u8, total_passes: u8, label: &str, batch_count: usize) {
self.finish_bar();
match self.mode {
ProgressDisplay::Off => {}
ProgressDisplay::Lines => {
eprintln!("napparent: pass {pass}/{total_passes}: {label}");
}
ProgressDisplay::Bar => {
#[cfg(feature = "progress")]
{
let bar = ProgressBar::new(batch_count as u64);
bar.set_draw_target(ProgressDrawTarget::stderr());
bar.set_style(
ProgressStyle::with_template(
"napparent pass {prefix:.bold} [{bar:40.cyan/blue}] {pos}/{len} ({percent}%) {msg}",
)
.expect("progress template")
.progress_chars("█▓░"),
);
bar.set_prefix(format!("{pass}/{total_passes} {label}"));
self.bar = Some(bar);
}
#[cfg(not(feature = "progress"))]
{
eprintln!("napparent: pass {pass}/{total_passes}: {label}");
}
}
}
}
#[allow(unused_variables)]
pub fn batch_tick(&mut self, index: usize, total: usize, rows: usize, line_msg: &str) {
match self.mode {
ProgressDisplay::Off => {}
ProgressDisplay::Bar =>
{
#[cfg(feature = "progress")]
if let Some(bar) = &self.bar {
bar.set_position((index + 1) as u64);
bar.set_message(format!("batch {}/{} ({} rows)", index + 1, total, rows));
}
}
ProgressDisplay::Lines => {
if should_log_batch(index, total) {
eprintln!("napparent: {line_msg}");
}
}
}
}
pub fn pass_finish(&mut self, msg: &str) {
self.finish_bar();
if self.mode != ProgressDisplay::Off {
eprintln!("napparent: {msg}");
}
}
pub fn finish(&mut self, msg: &str) {
self.finish_bar();
if self.mode != ProgressDisplay::Off {
eprintln!("napparent: {msg}");
}
}
pub fn abandon(&mut self) {
#[cfg(feature = "progress")]
if let Some(bar) = self.bar.take() {
bar.abandon_with_message("interrupted");
}
}
fn finish_bar(&mut self) {
#[cfg(feature = "progress")]
if let Some(bar) = self.bar.take() {
bar.finish_and_clear();
}
}
}
pub(crate) fn should_log_batch(index: usize, total: usize) -> bool {
if total <= 50 {
return true;
}
let step = (total / 20).max(1);
index.is_multiple_of(step) || index + 1 == total
}
pub(crate) struct ProgressTimer {
start: Instant,
}
impl ProgressTimer {
pub fn start() -> Self {
Self {
start: Instant::now(),
}
}
pub fn elapsed(&self) -> Duration {
self.start.elapsed()
}
pub fn elapsed_secs(&self) -> f64 {
self.elapsed().as_secs_f64()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verbose_off_is_off_mode() {
let r = ProgressReporter::from_verbose(false);
assert_eq!(r.display_mode(), ProgressDisplay::Off);
}
#[test]
fn from_display_lines() {
let r = ProgressReporter::from_display(ProgressDisplay::Lines);
assert_eq!(r.display_mode(), ProgressDisplay::Lines);
}
#[test]
fn throttle_small_runs_all_batches() {
assert!(should_log_batch(0, 10));
assert!(should_log_batch(9, 10));
}
#[test]
fn throttle_large_runs_sparse() {
let total = 162;
let step = (total / 20).max(1);
assert!(should_log_batch(0, total));
assert!(should_log_batch(total - 1, total));
assert!(!should_log_batch(1, total) || 1_usize.is_multiple_of(step));
}
}