inspect-format 0.1.0

Formatting and rendering for inspect-rs
Documentation
//! Style representation and terminal styling primitives.

#[cfg(feature = "color")]
pub use anstyle::{Ansi256Color, AnsiColor, Color, Effects, Reset, RgbColor};

/// Standard ANSI color representation for zero-dependency / no-color builds.
#[cfg(not(feature = "color"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AnsiColor {
    Black,
    Red,
    Green,
    Yellow,
    Blue,
    Magenta,
    Cyan,
    White,
    BrightBlack,
    BrightRed,
    BrightGreen,
    BrightYellow,
    BrightBlue,
    BrightMagenta,
    BrightCyan,
    BrightWhite,
}

/// A terminal style defining text effects and foreground/background colors.
///
/// When the `color` feature is enabled, this is backed by `anstyle::Style`
/// which renders zero-allocation ANSI escape codes. When disabled, this is a
/// zero-cost no-op type that emits nothing.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Style {
    #[cfg(feature = "color")]
    inner: anstyle::Style,
}

impl Style {
    /// Create a new, unstyled style.
    #[inline]
    pub const fn new() -> Self {
        #[cfg(feature = "color")]
        {
            Self { inner: anstyle::Style::new() }
        }
        #[cfg(not(feature = "color"))]
        {
            Self {}
        }
    }

    /// Set bold effect.
    #[inline]
    #[allow(unused_mut)]
    pub const fn bold(mut self) -> Self {
        #[cfg(feature = "color")]
        {
            self.inner = self.inner.bold();
        }
        self
    }

    /// Set dimmed/faint effect.
    #[inline]
    #[allow(unused_mut)]
    pub const fn dimmed(mut self) -> Self {
        #[cfg(feature = "color")]
        {
            self.inner = self.inner.dimmed();
        }
        self
    }

    /// Set italic effect.
    #[inline]
    #[allow(unused_mut)]
    pub const fn italic(mut self) -> Self {
        #[cfg(feature = "color")]
        {
            self.inner = self.inner.italic();
        }
        self
    }

    /// Set underline effect.
    #[inline]
    #[allow(unused_mut)]
    pub const fn underline(mut self) -> Self {
        #[cfg(feature = "color")]
        {
            self.inner = self.inner.underline();
        }
        self
    }

    /// Set 24-bit Truecolor (RGB) foreground color.
    #[inline]
    #[allow(unused_mut)]
    pub const fn fg_rgb(mut self, r: u8, g: u8, b: u8) -> Self {
        #[cfg(feature = "color")]
        {
            self.inner = self.inner.fg_color(Some(anstyle::Color::Rgb(anstyle::RgbColor(r, g, b))));
        }
        let _ = (r, g, b);
        self
    }

    /// Set 8-bit ANSI 256-color palette foreground color.
    #[inline]
    #[allow(unused_mut)]
    pub const fn fg_ansi256(mut self, code: u8) -> Self {
        #[cfg(feature = "color")]
        {
            self.inner =
                self.inner.fg_color(Some(anstyle::Color::Ansi256(anstyle::Ansi256Color(code))));
        }
        let _ = code;
        self
    }

    /// Set 4-bit standard ANSI 16-color foreground color.
    #[inline]
    #[allow(unused_mut)]
    pub const fn fg_ansi(mut self, color: AnsiColor) -> Self {
        #[cfg(feature = "color")]
        {
            self.inner = self.inner.fg_color(Some(anstyle::Color::Ansi(color)));
        }
        let _ = color;
        self
    }

    /// True if no styling effects or colors are configured.
    #[inline]
    pub fn is_plain(&self) -> bool {
        #[cfg(feature = "color")]
        {
            self.inner == anstyle::Style::new()
        }
        #[cfg(not(feature = "color"))]
        {
            true
        }
    }

    /// Render the ANSI sequence to activate this style.
    #[inline]
    pub fn render(&self) -> impl core::fmt::Display {
        #[cfg(feature = "color")]
        {
            self.inner.render()
        }
        #[cfg(not(feature = "color"))]
        {
            ""
        }
    }

    /// Render the ANSI sequence to reset styles.
    #[inline]
    pub fn render_reset(&self) -> impl core::fmt::Display {
        #[cfg(feature = "color")]
        {
            self.inner.render_reset()
        }
        #[cfg(not(feature = "color"))]
        {
            ""
        }
    }

    #[cfg(feature = "color")]
    /// Get the underlying anstyle::Style.
    pub const fn inner(&self) -> anstyle::Style {
        self.inner
    }

    /// Convert this style's colors to ANSI 256 colors.
    pub fn to_ansi256(self) -> Self {
        #[cfg(feature = "color")]
        {
            let mut styled = self.inner;
            if let Some(color) = styled.get_fg_color() {
                let converted = match color {
                    anstyle::Color::Rgb(rgb) => anstyle::Color::Ansi256(anstyle::Ansi256Color(
                        rgb_to_ansi256(rgb.0, rgb.1, rgb.2),
                    )),
                    other => other,
                };
                styled = styled.fg_color(Some(converted));
            }
            Self { inner: styled }
        }
        #[cfg(not(feature = "color"))]
        {
            self
        }
    }

    /// Convert this style's colors to standard 16 ANSI colors.
    pub fn to_ansi16(self) -> Self {
        #[cfg(feature = "color")]
        {
            let mut styled = self.inner;
            if let Some(color) = styled.get_fg_color() {
                let converted = match color {
                    anstyle::Color::Rgb(rgb) => {
                        anstyle::Color::Ansi(rgb_to_ansi16(rgb.0, rgb.1, rgb.2))
                    }
                    anstyle::Color::Ansi256(code) => {
                        let (r, g, b) = ansi256_to_rgb(code.0);
                        anstyle::Color::Ansi(rgb_to_ansi16(r, g, b))
                    }
                    anstyle::Color::Ansi(ansi) => anstyle::Color::Ansi(ansi),
                };
                styled = styled.fg_color(Some(converted));
            }
            Self { inner: styled }
        }
        #[cfg(not(feature = "color"))]
        {
            self
        }
    }

    /// Strip all foreground/background colors, preserving effects like Bold and Dimmed.
    pub fn to_monochrome(self) -> Self {
        #[cfg(feature = "color")]
        {
            let styled = self.inner.fg_color(None).bg_color(None);
            Self { inner: styled }
        }
        #[cfg(not(feature = "color"))]
        {
            self
        }
    }
}

#[cfg(feature = "color")]
impl From<anstyle::Style> for Style {
    fn from(inner: anstyle::Style) -> Self {
        Self { inner }
    }
}

#[cfg(feature = "color")]
impl From<Style> for anstyle::Style {
    fn from(style: Style) -> Self {
        style.inner
    }
}

/// Convert an RGB color to the closest index in the standard xterm 256-color palette.
pub fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
    if r == g && g == b {
        if r < 8 {
            16
        } else if r > 248 {
            231
        } else {
            (((r as u16 - 8) * 24) / 240) as u8 + 232
        }
    } else {
        let r_idx = ((r as u16 * 5) + 127) / 255;
        let g_idx = ((g as u16 * 5) + 127) / 255;
        let b_idx = ((b as u16 * 5) + 127) / 255;
        (16 + 36 * r_idx + 6 * g_idx + b_idx) as u8
    }
}

/// Convert an ANSI 256-color index to approximate (r, g, b) values.
pub fn ansi256_to_rgb(code: u8) -> (u8, u8, u8) {
    if code < 16 {
        ANSI16_PALETTE[code as usize].1
    } else if code < 232 {
        let idx = code - 16;
        let b = (idx % 6) * 51;
        let g = ((idx / 6) % 6) * 51;
        let r = (idx / 36) * 51;
        (r, g, b)
    } else {
        let gray = (code - 232) * 10 + 8;
        (gray, gray, gray)
    }
}

static ANSI16_PALETTE: [(AnsiColor, (u8, u8, u8)); 16] = [
    (AnsiColor::Black, (0, 0, 0)),
    (AnsiColor::Red, (205, 49, 49)),
    (AnsiColor::Green, (13, 188, 121)),
    (AnsiColor::Yellow, (229, 229, 16)),
    (AnsiColor::Blue, (36, 114, 200)),
    (AnsiColor::Magenta, (188, 63, 188)),
    (AnsiColor::Cyan, (17, 168, 205)),
    (AnsiColor::White, (229, 229, 229)),
    (AnsiColor::BrightBlack, (102, 102, 102)),
    (AnsiColor::BrightRed, (241, 76, 76)),
    (AnsiColor::BrightGreen, (35, 209, 139)),
    (AnsiColor::BrightYellow, (245, 245, 67)),
    (AnsiColor::BrightBlue, (59, 142, 234)),
    (AnsiColor::BrightMagenta, (214, 112, 214)),
    (AnsiColor::BrightCyan, (41, 184, 219)),
    (AnsiColor::BrightWhite, (255, 255, 255)),
];

/// Convert an RGB color to the closest standard ANSI 16 color using Euclidean distance.
pub fn rgb_to_ansi16(r: u8, g: u8, b: u8) -> AnsiColor {
    let mut closest = AnsiColor::White;
    let mut min_dist = u32::MAX;

    for (color, (pr, pg, pb)) in ANSI16_PALETTE {
        let dr = (r as i32) - (pr as i32);
        let dg = (g as i32) - (pg as i32);
        let db = (b as i32) - (pb as i32);
        let dist = (dr * dr + dg * dg + db * db) as u32;
        if dist < min_dist {
            min_dist = dist;
            closest = color;
        }
    }

    closest
}