gilt 1.6.0

Fast, beautiful terminal formatting for Rust — styles, tables, trees, syntax highlighting, progress bars, markdown.
Documentation
//! Terminal theme definitions for color resolution and export rendering.
//!
//! Provides [`TerminalTheme`] and several built-in themes (default, SVG export,
//! Monokai, Dimmed Monokai, Night Owlish) used when resolving named/system
//! colors to RGB values.

use crate::color::color_triplet::ColorTriplet;
use crate::palette::Palette;
use std::sync::LazyLock;

/// A terminal theme definition consisting of foreground, background, and ANSI colors.
pub struct TerminalTheme {
    /// Background color of the terminal.
    pub background_color: ColorTriplet,
    /// Foreground (text) color of the terminal.
    pub foreground_color: ColorTriplet,
    /// ANSI color palette (typically 16 colors: 8 normal + 8 bright).
    pub ansi_colors: Palette,
}

impl TerminalTheme {
    /// Creates a new terminal theme.
    ///
    /// # Arguments
    /// * `background` - Background color as (r, g, b) tuple
    /// * `foreground` - Foreground color as (r, g, b) tuple
    /// * `normal` - Normal intensity ANSI colors (typically 8 colors)
    /// * `bright` - Optional bright intensity ANSI colors. If None, normal colors are duplicated.
    pub fn new(
        background: (u8, u8, u8),
        foreground: (u8, u8, u8),
        normal: Vec<(u8, u8, u8)>,
        bright: Option<Vec<(u8, u8, u8)>>,
    ) -> Self {
        let mut colors = normal;
        match bright {
            Some(b) => colors.extend(b),
            None => {
                let dup = colors.clone();
                colors.extend(dup);
            }
        }
        Self {
            background_color: ColorTriplet::new(background.0, background.1, background.2),
            foreground_color: ColorTriplet::new(foreground.0, foreground.1, foreground.2),
            ansi_colors: Palette::new(colors),
        }
    }
}

/// Default terminal theme with standard colors.
pub static DEFAULT_TERMINAL_THEME: LazyLock<TerminalTheme> = LazyLock::new(|| {
    TerminalTheme::new(
        (255, 255, 255),
        (0, 0, 0),
        vec![
            (0, 0, 0),
            (128, 0, 0),
            (0, 128, 0),
            (128, 128, 0),
            (0, 0, 128),
            (128, 0, 128),
            (0, 128, 128),
            (192, 192, 192),
        ],
        Some(vec![
            (128, 128, 128),
            (255, 0, 0),
            (0, 255, 0),
            (255, 255, 0),
            (0, 0, 255),
            (255, 0, 255),
            (0, 255, 255),
            (255, 255, 255),
        ]),
    )
});

/// SVG export theme with neutral colors suitable for rendering.
///
/// Layout: 8 normal (ANSI 0-7) + 8 bright (ANSI 8-15).
/// Finding #17: the previous definition had 9 normal entries, shifting all
/// bright-color indices by one. The stray 9th entry (154,155,153) has been
/// relocated as the first bright color (bright-black / ANSI 8).
pub static SVG_EXPORT_THEME: LazyLock<TerminalTheme> = LazyLock::new(|| {
    TerminalTheme::new(
        (41, 41, 41),
        (197, 200, 198),
        // Normal colors: ANSI 0-7
        vec![
            (75, 78, 85),    // 0 black
            (204, 85, 90),   // 1 red
            (152, 168, 75),  // 2 green
            (208, 179, 68),  // 3 yellow
            (96, 138, 177),  // 4 blue
            (152, 114, 159), // 5 magenta
            (104, 160, 179), // 6 cyan
            (197, 200, 198), // 7 white
        ],
        // Bright colors: ANSI 8-15
        Some(vec![
            (154, 155, 153), // 8  bright black (was the stray 9th normal entry)
            (255, 38, 39),   // 9  bright red
            (0, 130, 61),    // 10 bright green
            (208, 132, 66),  // 11 bright yellow
            (25, 132, 233),  // 12 bright blue
            (255, 44, 122),  // 13 bright magenta
            (57, 130, 128),  // 14 bright cyan
            (253, 253, 197), // 15 bright white
        ]),
    )
});

/// Monokai theme with dark background and vibrant colors.
pub static MONOKAI: LazyLock<TerminalTheme> = LazyLock::new(|| {
    TerminalTheme::new(
        (12, 12, 12),
        (217, 217, 217),
        vec![
            (26, 26, 26),
            (244, 0, 95),
            (152, 224, 36),
            (253, 151, 31),
            (157, 101, 255),
            (244, 0, 95),
            (88, 209, 235),
            (196, 197, 181),
            (98, 94, 76),
        ],
        Some(vec![
            (244, 0, 95),
            (152, 224, 36),
            (224, 213, 97),
            (157, 101, 255),
            (244, 0, 95),
            (88, 209, 235),
            (246, 246, 239),
        ]),
    )
});

/// Dimmed Monokai theme with muted colors.
pub static DIMMED_MONOKAI: LazyLock<TerminalTheme> = LazyLock::new(|| {
    TerminalTheme::new(
        (25, 25, 25),
        (185, 188, 186),
        vec![
            (58, 61, 67),
            (190, 63, 72),
            (135, 154, 59),
            (197, 166, 53),
            (79, 118, 161),
            (133, 92, 141),
            (87, 143, 164),
            (185, 188, 186),
            (136, 137, 135),
        ],
        Some(vec![
            (251, 0, 31),
            (15, 114, 47),
            (196, 112, 51),
            (24, 109, 227),
            (251, 0, 103),
            (46, 112, 109),
            (253, 255, 185),
        ]),
    )
});

/// Night Owlish theme with light background.
pub static NIGHT_OWLISH: LazyLock<TerminalTheme> = LazyLock::new(|| {
    TerminalTheme::new(
        (255, 255, 255),
        (64, 63, 83),
        vec![
            (1, 22, 39),
            (211, 66, 62),
            (42, 162, 152),
            (218, 170, 1),
            (72, 118, 214),
            (64, 63, 83),
            (8, 145, 106),
            (122, 129, 129),
            (122, 129, 129),
        ],
        Some(vec![
            (247, 110, 110),
            (73, 208, 197),
            (218, 194, 107),
            (92, 167, 228),
            (105, 112, 152),
            (0, 201, 144),
            (152, 159, 177),
        ]),
    )
});

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

    #[test]
    fn test_default_terminal_theme_foreground_background() {
        assert_eq!(DEFAULT_TERMINAL_THEME.foreground_color.red, 0);
        assert_eq!(DEFAULT_TERMINAL_THEME.foreground_color.green, 0);
        assert_eq!(DEFAULT_TERMINAL_THEME.foreground_color.blue, 0);

        assert_eq!(DEFAULT_TERMINAL_THEME.background_color.red, 255);
        assert_eq!(DEFAULT_TERMINAL_THEME.background_color.green, 255);
        assert_eq!(DEFAULT_TERMINAL_THEME.background_color.blue, 255);
    }

    #[test]
    fn test_default_terminal_theme_ansi_black() {
        let black = DEFAULT_TERMINAL_THEME.ansi_colors.get(0);
        assert_eq!(black.red, 0);
        assert_eq!(black.green, 0);
        assert_eq!(black.blue, 0);
    }

    #[test]
    fn test_default_terminal_theme_ansi_dark_red() {
        let dark_red = DEFAULT_TERMINAL_THEME.ansi_colors.get(1);
        assert_eq!(dark_red.red, 128);
        assert_eq!(dark_red.green, 0);
        assert_eq!(dark_red.blue, 0);
    }

    #[test]
    fn test_svg_export_theme_background() {
        assert_eq!(SVG_EXPORT_THEME.background_color.red, 41);
        assert_eq!(SVG_EXPORT_THEME.background_color.green, 41);
        assert_eq!(SVG_EXPORT_THEME.background_color.blue, 41);
    }

    /// Finding #17: SVG_EXPORT_THEME must have exactly 16 ANSI palette entries
    /// (8 normal + 8 bright) so bright-color indices are correct.
    #[test]
    fn test_svg_export_theme_palette_is_16_entries() {
        // Normal colors 0-7: index 7 should be (197,200,198) — the white entry.
        let normal_white = SVG_EXPORT_THEME.ansi_colors.get(7);
        assert_eq!(
            (normal_white.red, normal_white.green, normal_white.blue),
            (197, 200, 198),
            "ANSI 7 (normal white) should be (197,200,198)"
        );

        // Bright colors 8-15: index 8 was the stray 9th entry (154,155,153).
        let bright_black = SVG_EXPORT_THEME.ansi_colors.get(8);
        assert_eq!(
            (bright_black.red, bright_black.green, bright_black.blue),
            (154, 155, 153),
            "ANSI 8 (bright black) should be (154,155,153) — relocated from old index 8"
        );

        // ANSI 9 (bright red) should be (255,38,39).
        let bright_red = SVG_EXPORT_THEME.ansi_colors.get(9);
        assert_eq!(
            (bright_red.red, bright_red.green, bright_red.blue),
            (255, 38, 39),
            "ANSI 9 (bright red) should be (255,38,39)"
        );

        // ANSI 15 (bright white) should be (253,253,197).
        let bright_white = SVG_EXPORT_THEME.ansi_colors.get(15);
        assert_eq!(
            (bright_white.red, bright_white.green, bright_white.blue),
            (253, 253, 197),
            "ANSI 15 (bright white) should be (253,253,197)"
        );
    }

    #[test]
    fn test_monokai_foreground() {
        assert_eq!(MONOKAI.foreground_color.red, 217);
        assert_eq!(MONOKAI.foreground_color.green, 217);
        assert_eq!(MONOKAI.foreground_color.blue, 217);
    }

    #[test]
    fn test_theme_with_no_bright_colors() {
        let theme = TerminalTheme::new(
            (255, 255, 255),
            (0, 0, 0),
            vec![(0, 0, 0), (128, 0, 0), (0, 128, 0), (128, 128, 0)],
            None,
        );

        // Should have 8 colors total (4 normal + 4 duplicated)
        // Verify first normal color
        let color0 = theme.ansi_colors.get(0);
        assert_eq!(color0.red, 0);
        assert_eq!(color0.green, 0);
        assert_eq!(color0.blue, 0);

        // Verify first duplicated color (at index 4)
        let color4 = theme.ansi_colors.get(4);
        assert_eq!(color4.red, 0);
        assert_eq!(color4.green, 0);
        assert_eq!(color4.blue, 0);

        // Verify second normal color
        let color1 = theme.ansi_colors.get(1);
        assert_eq!(color1.red, 128);
        assert_eq!(color1.green, 0);
        assert_eq!(color1.blue, 0);

        // Verify second duplicated color (at index 5)
        let color5 = theme.ansi_colors.get(5);
        assert_eq!(color5.red, 128);
        assert_eq!(color5.green, 0);
        assert_eq!(color5.blue, 0);
    }

    #[test]
    fn test_dimmed_monokai_theme() {
        assert_eq!(DIMMED_MONOKAI.background_color.red, 25);
        assert_eq!(DIMMED_MONOKAI.background_color.green, 25);
        assert_eq!(DIMMED_MONOKAI.background_color.blue, 25);

        assert_eq!(DIMMED_MONOKAI.foreground_color.red, 185);
        assert_eq!(DIMMED_MONOKAI.foreground_color.green, 188);
        assert_eq!(DIMMED_MONOKAI.foreground_color.blue, 186);
    }

    #[test]
    fn test_night_owlish_theme() {
        assert_eq!(NIGHT_OWLISH.background_color.red, 255);
        assert_eq!(NIGHT_OWLISH.background_color.green, 255);
        assert_eq!(NIGHT_OWLISH.background_color.blue, 255);

        assert_eq!(NIGHT_OWLISH.foreground_color.red, 64);
        assert_eq!(NIGHT_OWLISH.foreground_color.green, 63);
        assert_eq!(NIGHT_OWLISH.foreground_color.blue, 83);
    }
}