progress.rs raw

use std::fmt::Display;
use std::io::{self, Write};

/// Says what is happening, for work slow enough that silence looks like a hang.
///
/// Everything goes to stderr, leaving stdout to the report a caller might pipe
/// somewhere.
///
/// The interesting part is [`start`] and [`finish`]: the name of a thing is
/// written, and flushed, *before* the work begins, and its outcome lands on the
/// same line afterwards. So a run that is still going says which item it is
/// stuck on rather than merely that it has not finished — and a boot log read
/// the next morning ends on the name of whatever was in progress when the
/// machine gave up.
///
/// [`start`]: Reporter::start
/// [`finish`]: Reporter::finish
pub struct Reporter {
    enabled: bool,
}

impl Reporter {
    pub fn to_stderr() -> Self {
        Self { enabled: true }
    }

    /// A reporter that says nothing.
    ///
    /// Only tests want this. Every command has a user in front of it, or a log
    /// behind it, and both are better off being told what is going on.
    #[cfg(test)]
    pub fn silent() -> Self {
        Self { enabled: false }
    }

    /// A heading for a stretch of work.
    pub fn phase(&self, message: impl Display) {
        if self.enabled {
            let _ = writeln!(io::stderr(), "{message}");
        }
    }

    /// A whole line at once.
    ///
    /// One `writeln!` holds the lock for its whole call, so this stays legible
    /// when several threads report finishing at once — unlike [`start`], which
    /// leaves a line open and must not be interleaved.
    ///
    /// [`start`]: Reporter::start
    pub fn line(&self, message: impl Display) {
        if self.enabled {
            let _ = writeln!(io::stderr(), "  {message}");
        }
    }

    /// Name the thing about to be worked on, leaving the line open.
    ///
    /// Flushed, because the whole point is to appear before the waiting starts.
    /// Only one thread may have a line open at a time.
    pub fn start(&self, what: impl Display) {
        if self.enabled {
            let mut stderr = io::stderr();
            let _ = write!(stderr, "  {what} ... ");
            let _ = stderr.flush();
        }
    }

    /// Close the line opened by [`start`].
    ///
    /// [`start`]: Reporter::start
    pub fn finish(&self, outcome: impl Display) {
        if self.enabled {
            let _ = writeln!(io::stderr(), "{outcome}");
        }
    }
}

/// Pick a word for a count, so reports do not say "1 directories".
pub fn plural(count: usize, one: &'static str, many: &'static str) -> &'static str {
    if count == 1 { one } else { many }
}