nih_log 0.3.1

An opinionated yet flexible logger catering to the needs of the NIH-plug plugin framework
Documentation
//! The logger's output targets.

use std::fmt::Debug;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
use termcolor::{BufferedStandardStream, Color, ColorChoice, ColorSpec, WriteColor};

#[cfg(windows)]
mod windbg;

/// The environment variable for controlling the logging behavior.
const NIH_LOG_ENV: &str = "NIH_LOG";

/// Similar to [`crate::builder::OutputTarget`], but contains the actual data needed to write to the
/// logger.
pub enum OutputTargetImpl {
    /// The default logging target on Windows. This checks whether a Windows debugger is attached
    /// before logging. If there is a debugger, then the message is written using
    /// `OutputDebugString()`. Otherwise the message is written to STDERR instead.
    #[cfg(windows)]
    StderrOrWinDbg(BufferedStandardStream, windbg::WinDbgWriter),
    /// Writes directly to STDERR. The default logging target on non-Windows platforms. May use
    /// colors colors depending on the environment.
    Stderr(BufferedStandardStream),
    /// Outputs to the Windows debugger using `OutputDebugString()`.
    #[cfg(windows)]
    WinDbg(windbg::WinDbgWriter),
    /// Writes to the file.
    File(BufWriter<File>),
}

impl Debug for OutputTargetImpl {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            #[cfg(windows)]
            OutputTargetImpl::StderrOrWinDbg(stderr, windbg) => f
                .debug_tuple("StderrOrWinDbg")
                .field(if stderr.supports_color() {
                    &"<stderr stream with color support>"
                } else {
                    &"<stderr stream>"
                })
                .field(windbg)
                .finish(),
            OutputTargetImpl::Stderr(stderr) => f
                .debug_tuple("Stderr")
                .field(if stderr.supports_color() {
                    &"<stderr stream with color support>"
                } else {
                    &"<stderr stream>"
                })
                .finish(),
            #[cfg(windows)]
            OutputTargetImpl::WinDbg(windbg) => f.debug_tuple("WinDbg").field(windbg).finish(),
            OutputTargetImpl::File(file) => f.debug_tuple("File").field(file).finish(),
        }
    }
}

/// A simple wrapper around the `Write` and `WriteColor` traits to allow coloring text when
/// supported by the logger target.
pub trait WriteExt: Write {
    /// Set the foreground text color. Doesn't do anything if the stream doesn't support colors.
    fn set_fg_color(&mut self, color: Color);

    /// Reset the foreground text color. Doesn't do anything if the stream doesn't support colors.
    fn reset_colors(&mut self);
}

impl WriteExt for BufferedStandardStream {
    fn set_fg_color(&mut self, color: Color) {
        let _ = self.set_color(ColorSpec::new().set_fg(Some(color)));
    }

    fn reset_colors(&mut self) {
        let _ = self.reset();
    }
}

#[cfg(windows)]
impl WriteExt for windbg::WinDbgWriter {
    fn set_fg_color(&mut self, _color: Color) {}

    fn reset_colors(&mut self) {}
}

impl WriteExt for BufWriter<File> {
    fn set_fg_color(&mut self, _color: Color) {}

    fn reset_colors(&mut self) {}
}

impl OutputTargetImpl {
    /// Construct an [`OutputTargetImpl`] that writes to STDERR with optional color support
    /// determined by the environment. If a Windows debugger is attached when writing debug output,
    /// then the output is sent to the Windows debugger instead.
    #[cfg(windows)]
    pub fn new_stderr_or_windbg() -> Self {
        OutputTargetImpl::StderrOrWinDbg(
            BufferedStandardStream::stderr(stderr_color_support()),
            windbg::WinDbgWriter::default(),
        )
    }

    /// Construct an [`OutputTargetImpl`] that writes to STDERR with optional color support
    /// determined by the environment.
    pub fn new_stderr() -> Self {
        OutputTargetImpl::Stderr(BufferedStandardStream::stderr(stderr_color_support()))
    }

    /// Construct an [`OutputTargetImpl`] that writes to the Windows debugger.
    #[cfg(windows)]
    pub fn new_windbg() -> Self {
        OutputTargetImpl::WinDbg(windbg::WinDbgWriter::default())
    }

    /// Construct an [`OutputTargetImpl`] for doing buffered writes to a file.
    pub fn new_file_path<P: AsRef<Path>>(path: P) -> Result<Self, std::io::Error> {
        let file = File::options().create(true).append(true).open(path)?;

        Ok(Self::File(BufWriter::new(file)))
    }

    /// Returns a writer that can be written to using the [`write!()`] and [`writeln!()`] macros.
    /// This writer can also be used to color the STDERR stream when outputting to an STDERR stream
    /// that supports colors. May perform a syscall to check whether the Windows debugger is
    /// attached so this should be reused for multiple `write!()` calls.
    ///
    /// Needs to be a single function since otherwise you'd need to borrow from this struct twice.
    pub fn writer(&mut self) -> &mut dyn WriteExt {
        match self {
            #[cfg(windows)]
            OutputTargetImpl::StderrOrWinDbg(_, ref mut windbg) if windbg::attached() => windbg,
            #[cfg(windows)]
            OutputTargetImpl::StderrOrWinDbg(ref mut stderr, _) => stderr,
            OutputTargetImpl::Stderr(ref mut stderr) => stderr,
            #[cfg(windows)]
            OutputTargetImpl::WinDbg(ref mut windbg) => windbg,
            OutputTargetImpl::File(ref mut file) => file,
        }
    }

    /// If the `NIH_LOG` environment variable is set, then parse that according to the rules defined
    /// in the project's readme. Otherwise defaults to the dynamic `StderrOrWinDbg` target. If
    /// `NIH_LOG` is set to output to a file and the file couldn't be opened, then this will write
    /// the error to STDERR and then also fall back to `StderrOrWinDbg`.
    pub fn default_from_environment() -> Self {
        let nih_log_env = std::env::var(NIH_LOG_ENV);
        let nih_log_env_str = nih_log_env.as_deref().unwrap_or("");
        if nih_log_env_str.eq_ignore_ascii_case("stderr") {
            return Self::new_stderr();
        }
        #[cfg(windows)]
        if nih_log_env_str.eq_ignore_ascii_case("windbg") {
            return Self::new_windbg();
        }
        if !nih_log_env_str.is_empty() {
            match Self::new_file_path(nih_log_env_str) {
                Ok(target) => return target,
                // TODO: Print this using the actual logger
                Err(err) => eprintln!(
                    "Could not open '{nih_log_env_str}' from NIH_LOG for logging, falling back to \
                     STDERR: {err}"
                ),
            }
        }

        #[cfg(windows)]
        return Self::new_stderr_or_windbg();
        #[cfg(not(windows))]
        return Self::new_stderr();
    }
}

/// Whether to use colors when outputting to STDERR. Considers the `CLICOLOR`, `CLICOLOR_FORCE`, and
/// `NO_COLOR` environment variables, and whether or not STDERR is attached to a real TTY.
fn stderr_color_support() -> ColorChoice {
    if let Ok(value) = std::env::var("CLICOLOR_FORCE") {
        if value.trim() != "0" {
            return ColorChoice::Always;
        }
    }

    if let Ok(value) = std::env::var("NO_COLOR") {
        if value.trim() != "0" {
            return ColorChoice::Never;
        }
    }

    if let Ok(value) = std::env::var("CLICOLOR") {
        if value.trim() == "0" {
            return ColorChoice::Never;
        }
    }

    // If `CLICOLOR` is unset or set to a truthy value, and colors aren't forced, then terminal
    // support determines whether or not colors are used
    if atty::is(atty::Stream::Stderr) {
        ColorChoice::Auto
    } else {
        ColorChoice::Never
    }
}