colored_text 0.4.1

A simple library for adding colors and styles to terminal text
Documentation
use std::cell::RefCell;
use std::io::IsTerminal;

/// Runtime color policy for rendered output.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ColorMode {
    /// Enable styling only when stdout is a terminal.
    #[default]
    Auto,
    /// Always emit styling, even when stdout is not a terminal.
    ///
    /// `NO_COLOR` still takes precedence and disables styled output.
    Always,
    /// Never emit styling.
    Never,
}

/// Output target used when rendering styled text explicitly.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RenderTarget {
    /// Resolve terminal capability from stdout.
    Stdout,
    /// Resolve terminal capability from stderr.
    Stderr,
    /// Use an explicit terminal capability for a custom destination.
    Terminal(bool),
}

/// Configuration for controlling runtime color behavior.
///
/// The active configuration is stored per thread. This makes it straightforward
/// to force a specific color mode in tests or narrow execution paths without
/// changing global process state.
#[derive(Clone, Debug)]
pub struct ColorizeConfig {
    color_mode: ColorMode,
}

thread_local! {
    static CONFIG: RefCell<ColorizeConfig> = RefCell::new(ColorizeConfig::default());
    #[cfg(test)]
    #[allow(clippy::missing_const_for_thread_local)]
    static STDOUT_TERMINAL_OVERRIDE: RefCell<Option<bool>> = RefCell::new(None);
    #[cfg(test)]
    #[allow(clippy::missing_const_for_thread_local)]
    static STDERR_TERMINAL_OVERRIDE: RefCell<Option<bool>> = RefCell::new(None);
}

impl Default for ColorizeConfig {
    fn default() -> Self {
        Self {
            color_mode: ColorMode::Auto,
        }
    }
}

impl ColorizeConfig {
    /// Set the runtime color policy for the current thread.
    ///
    /// In [`ColorMode::Auto`], styling is emitted only when stdout is a
    /// terminal. In [`ColorMode::Always`], styling is emitted regardless of
    /// terminal detection. In [`ColorMode::Never`], styling is disabled.
    pub fn set_color_mode(mode: ColorMode) {
        CONFIG.with(|config| config.borrow_mut().color_mode = mode);
    }

    /// Get the runtime color policy for the current thread.
    pub fn color_mode() -> ColorMode {
        CONFIG.with(|config| config.borrow().color_mode)
    }

    /// Compatibility shim for the previous API.
    ///
    /// `true` maps to [`ColorMode::Auto`], and `false` maps to
    /// [`ColorMode::Always`].
    #[deprecated(note = "use ColorizeConfig::set_color_mode(ColorMode) instead")]
    pub fn set_terminal_check(check: bool) {
        let mode = if check {
            ColorMode::Auto
        } else {
            ColorMode::Always
        };
        Self::set_color_mode(mode);
    }
}

/// Evaluate the current runtime color policy for this thread.
///
/// This respects [`ColorizeConfig::color_mode()`], and `NO_COLOR` takes
/// precedence over both [`ColorMode::Auto`] and [`ColorMode::Always`].
pub(crate) fn should_colorize() -> bool {
    should_colorize_for(RenderTarget::Stdout)
}

/// Evaluate the current runtime color policy for a specific render target.
pub(crate) fn should_colorize_for(target: RenderTarget) -> bool {
    match ColorizeConfig::color_mode() {
        ColorMode::Never => false,
        ColorMode::Always => std::env::var_os("NO_COLOR").is_none(),
        ColorMode::Auto => std::env::var_os("NO_COLOR").is_none() && target_is_terminal(target),
    }
}

fn target_is_terminal(target: RenderTarget) -> bool {
    match target {
        RenderTarget::Stdout => stdout_is_terminal(),
        RenderTarget::Stderr => stderr_is_terminal(),
        RenderTarget::Terminal(value) => value,
    }
}

fn stdout_is_terminal() -> bool {
    #[cfg(test)]
    if let Some(value) = STDOUT_TERMINAL_OVERRIDE.with(|override_value| *override_value.borrow()) {
        return value;
    }

    std::io::stdout().is_terminal()
}

fn stderr_is_terminal() -> bool {
    #[cfg(test)]
    if let Some(value) = STDERR_TERMINAL_OVERRIDE.with(|override_value| *override_value.borrow()) {
        return value;
    }

    std::io::stderr().is_terminal()
}

#[cfg(test)]
pub(crate) fn set_terminal_override_for_tests(value: Option<bool>) {
    STDOUT_TERMINAL_OVERRIDE.with(|override_value| *override_value.borrow_mut() = value);
}

#[cfg(test)]
pub(crate) fn get_terminal_override_for_tests() -> Option<bool> {
    STDOUT_TERMINAL_OVERRIDE.with(|override_value| *override_value.borrow())
}

#[cfg(test)]
pub(crate) fn set_stderr_terminal_override_for_tests(value: Option<bool>) {
    STDERR_TERMINAL_OVERRIDE.with(|override_value| *override_value.borrow_mut() = value);
}

#[cfg(test)]
pub(crate) fn get_stderr_terminal_override_for_tests() -> Option<bool> {
    STDERR_TERMINAL_OVERRIDE.with(|override_value| *override_value.borrow())
}