calepin 0.0.17

A Rust CLI for preprocessing Typst documents with executable code chunks
use std::io::{self, IsTerminal};
use std::time::Duration;

use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};

#[derive(Clone)]
pub struct ProgressManager {
    quiet: bool,
    interactive: bool,
    multi: Option<MultiProgress>,
}

impl ProgressManager {
    pub fn new(quiet: bool) -> Self {
        if quiet {
            return Self {
                quiet,
                interactive: false,
                multi: None,
            };
        }
        let interactive = io::stderr().is_terminal();
        let multi = interactive
            .then(|| MultiProgress::with_draw_target(ProgressDrawTarget::stderr_with_hz(12)));
        Self {
            quiet,
            interactive,
            multi,
        }
    }

    pub fn spinner(&self, message: impl Into<String>) -> Progress {
        Progress::from_manager(self, ProgressKind::Spinner, message.into(), 0)
    }

    pub fn bar(&self, message: impl Into<String>, len: u64) -> Progress {
        Progress::from_manager(self, ProgressKind::Bar, message.into(), len)
    }
}

pub struct Progress {
    quiet: bool,
    finished: bool,
    bar: Option<ProgressBar>,
    _multi: Option<MultiProgress>,
}

enum ProgressKind {
    Spinner,
    Bar,
}

impl Progress {
    pub fn spinner(message: impl Into<String>, quiet: bool) -> Self {
        ProgressManager::new(quiet).spinner(message)
    }

    pub fn bar(message: impl Into<String>, len: u64, quiet: bool) -> Self {
        ProgressManager::new(quiet).bar(message, len)
    }

    fn from_manager(
        manager: &ProgressManager,
        kind: ProgressKind,
        message: String,
        len: u64,
    ) -> Self {
        if manager.quiet {
            return Self::hidden(manager.quiet);
        }
        if !manager.interactive {
            eprintln!("{message}");
            return Self::hidden(manager.quiet);
        }

        let Some(multi) = manager.multi.clone() else {
            return Self::hidden(manager.quiet);
        };
        let bar = match kind {
            ProgressKind::Spinner => {
                let bar = multi.add(ProgressBar::new_spinner());
                let style = ProgressStyle::with_template("{spinner} {msg}")
                    .unwrap_or_else(|_| ProgressStyle::default_spinner())
                    .tick_chars("|/-\\");
                bar.set_style(style);
                bar
            }
            ProgressKind::Bar => {
                let bar = multi.add(ProgressBar::new(len));
                let style = ProgressStyle::with_template(
                    "{spinner} [{elapsed_precise}] [{bar:32.cyan/blue}] {pos}/{len} {msg}",
                )
                .unwrap_or_else(|_| ProgressStyle::default_bar())
                .progress_chars("#>-")
                .tick_chars("|/-\\");
                bar.set_style(style);
                bar
            }
        };
        bar.set_message(message);
        bar.enable_steady_tick(Duration::from_millis(120));

        Self {
            quiet: manager.quiet,
            finished: false,
            bar: Some(bar),
            _multi: Some(multi),
        }
    }

    pub fn set_message(&self, message: impl Into<String>) {
        if let Some(bar) = &self.bar {
            bar.set_message(message.into());
        }
    }

    pub fn inc(&self, delta: u64) {
        if let Some(bar) = &self.bar {
            bar.inc(delta);
        }
    }

    pub fn finish(mut self, message: impl AsRef<str>) {
        if let Some(bar) = &self.bar {
            bar.finish_and_clear();
        }
        self.finished = true;
        if !self.quiet && self.bar.is_none() {
            eprintln!("{}", message.as_ref());
        }
    }

    fn hidden(quiet: bool) -> Self {
        Self {
            quiet,
            finished: false,
            bar: None,
            _multi: None,
        }
    }
}

impl Drop for Progress {
    fn drop(&mut self) {
        if !self.finished {
            if let Some(bar) = &self.bar {
                bar.finish_and_clear();
            }
        }
    }
}