nishikaze 0.2.0

Zephyr build system companion.
Documentation
//! Logging helpers for user-facing messages.

use std::io::Write;
use std::sync::atomic::{AtomicU8, Ordering};

use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};

/// Current verbosity level for user-facing logs.
static VERBOSITY: AtomicU8 = AtomicU8::new(2);

/// Sets the global verbosity level for logging.
pub fn set_verbosity(level: u8) {
    VERBOSITY.store(level, Ordering::Relaxed);
}

/// Returns true when logs should be emitted for the current verbosity level.
#[must_use]
pub const fn is_enabled_for(level: u8) -> bool {
    level >= 2
}

/// Returns true when logs should be emitted.
#[must_use]
pub fn enabled() -> bool {
    if !is_enabled_for(VERBOSITY.load(Ordering::Relaxed)) {
        return false;
    }
    if std::env::var_os("KAZE_TESTING").is_some() && std::env::var_os("KAZE_LOGS").is_none() {
        return false;
    }
    true
}

/// Logs a user-facing message with a kaze prefix.
pub fn info(msg: impl AsRef<str>) {
    if !enabled() {
        return;
    }
    let mut stream = StandardStream::stdout(ColorChoice::Auto);
    if write_info(&mut stream, msg.as_ref()).is_err()
        && writeln!(stream, "kaze: {}", msg.as_ref()).is_err()
    {
        drop(stream);
    }
}

/// Logs a user-facing error message in red.
pub fn error(msg: impl AsRef<str>) {
    let mut stream = StandardStream::stderr(ColorChoice::Auto);
    if write_error(&mut stream, msg.as_ref()).is_err()
        && writeln!(stream, "{}", msg.as_ref()).is_err()
    {
        drop(stream);
    }
}

/// Writes a green log line to the provided colored output stream.
fn write_info(out: &mut dyn WriteColor, msg: &str) -> std::io::Result<()> {
    let mut spec = ColorSpec::new();
    spec.set_fg(Some(Color::Green));
    out.set_color(&spec)?;
    writeln!(out, "kaze: {msg}")?;
    out.reset()?;
    Ok(())
}

/// Writes a red error line to the provided colored output stream.
fn write_error(out: &mut dyn WriteColor, msg: &str) -> std::io::Result<()> {
    let mut spec = ColorSpec::new();
    spec.set_fg(Some(Color::Red));
    out.set_color(&spec)?;
    writeln!(out, "{msg}")?;
    out.reset()?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn write_info_includes_color_when_ansi_enabled() {
        let mut buf = termcolor::Buffer::ansi();
        write_info(&mut buf, "hello").expect("write info");
        let output = std::str::from_utf8(buf.as_slice()).expect("utf8");
        assert!(output.contains("kaze: hello"));
        assert!(output.contains("\u{1b}["));
    }

    #[test]
    fn write_info_no_color_when_disabled() {
        let mut buf = termcolor::Buffer::no_color();
        write_info(&mut buf, "hello").expect("write info");
        let output = std::str::from_utf8(buf.as_slice()).expect("utf8");
        assert_eq!(output, "kaze: hello\n");
    }

    #[test]
    fn write_error_includes_color_when_ansi_enabled() {
        let mut buf = termcolor::Buffer::ansi();
        write_error(&mut buf, "error: boom").expect("write error");
        let output = std::str::from_utf8(buf.as_slice()).expect("utf8");
        assert!(output.contains("error: boom"));
        assert!(output.contains("\u{1b}["));
    }

    #[test]
    fn write_error_no_color_when_disabled() {
        let mut buf = termcolor::Buffer::no_color();
        write_error(&mut buf, "error: boom").expect("write error");
        let output = std::str::from_utf8(buf.as_slice()).expect("utf8");
        assert_eq!(output, "error: boom\n");
    }
}