inspect-format 0.1.0

Formatting and rendering for inspect-rs
Documentation
//! Terminal color choice and detection logic.

use std::io::IsTerminal;

/// User preference for emitting ANSI color escape codes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorChoice {
    /// Automatically enable color if writing to a TTY and environment allows it.
    #[default]
    Auto,

    /// Always emit ANSI color codes, even when redirected or piped.
    Always,

    /// Never emit ANSI color codes.
    Never,
}

impl ColorChoice {
    /// Determine whether color should be active given the terminal/TTY status of the output.
    ///
    /// Respects:
    /// - `NO_COLOR` (<https://no-color.org>)
    /// - `FORCE_COLOR` (<https://force-color.org>)
    /// - `TERM=dumb`
    /// - Stream TTY status
    pub fn should_color(self, is_terminal: bool) -> bool {
        match self {
            Self::Always => true,
            Self::Never => false,
            Self::Auto => Self::detect_auto(is_terminal),
        }
    }

    /// Automatically detect whether color is supported for the current environment.
    pub fn detect_auto(is_terminal: bool) -> bool {
        Self::detect_with(is_terminal, |k| std::env::var(k).ok())
    }

    /// Internal helper that tests environment variables using a mockable lookup closure.
    pub fn detect_with(is_terminal: bool, get_var: impl Fn(&str) -> Option<String>) -> bool {
        // 1. NO_COLOR takes precedence over automatic detection:
        // Any non-empty string disables color.
        if let Some(val) = get_var("NO_COLOR") {
            if !val.is_empty() {
                return false;
            }
        }

        // 2. FORCE_COLOR forces color when set to non-empty and non-"0".
        if let Some(val) = get_var("FORCE_COLOR") {
            if !val.is_empty() && val != "0" {
                return true;
            }
        }

        // 3. TERM=dumb disables color.
        if let Some(term) = get_var("TERM") {
            if term == "dumb" {
                return false;
            }
        }

        // 4. Output stream must be an interactive terminal.
        is_terminal
    }

    /// Resolve whether stdout should currently display colors.
    pub fn resolve_stdout(self) -> bool {
        self.should_color(std::io::stdout().is_terminal())
    }

    /// Resolve whether stderr should currently display colors.
    pub fn resolve_stderr(self) -> bool {
        self.should_color(std::io::stderr().is_terminal())
    }
}

/// Color capability tier supported by the terminal.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum ColorLevel {
    /// Automatically detected from terminal capabilities and environment.
    #[default]
    Auto,

    /// Full 24-bit Truecolor (16 million RGB colors).
    Truecolor,

    /// 8-bit ANSI 256-color palette.
    Ansi256,

    /// 4-bit standard ANSI 16-color palette.
    Ansi16,

    /// No colors supported or color disabled.
    None,
}

impl ColorLevel {
    /// Detect the terminal's color capability tier from environment variables.
    pub fn detect() -> Self {
        Self::detect_with(|k| std::env::var(k).ok())
    }

    /// Test color capability using a mockable environment lookup closure.
    pub fn detect_with(get_var: impl Fn(&str) -> Option<String>) -> Self {
        // Check if color is explicitly disabled
        if let Some(val) = get_var("NO_COLOR") {
            if !val.is_empty() {
                return Self::None;
            }
        }

        if let Some(term) = get_var("TERM") {
            if term == "dumb" {
                return Self::None;
            }
        }

        // Truecolor check: COLORTERM=truecolor or 24bit
        if let Some(colorterm) = get_var("COLORTERM") {
            if colorterm == "truecolor" || colorterm == "24bit" {
                return Self::Truecolor;
            }
        }

        // Known truecolor / 256color terminal indicators in TERM
        if let Some(term) = get_var("TERM") {
            let term_lower = term.to_lowercase();
            if term_lower.contains("direct")
                || term_lower.contains("truecolor")
                || term_lower.contains("kitty")
                || term_lower.contains("alacritty")
                || term_lower.contains("wezterm")
                || term_lower.contains("ghostty")
            {
                return Self::Truecolor;
            }
            if term_lower.contains("256color") || term_lower.contains("xterm") {
                return Self::Ansi256;
            }
        }

        // Default baseline Unix terminal capability
        Self::Ansi16
    }
}