use crate::color::color_triplet::ColorTriplet;
use crate::palette::Palette;
use std::sync::LazyLock;
pub struct TerminalTheme {
pub background_color: ColorTriplet,
pub foreground_color: ColorTriplet,
pub ansi_colors: Palette,
}
impl TerminalTheme {
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),
}
}
}
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),
]),
)
});
pub static SVG_EXPORT_THEME: LazyLock<TerminalTheme> = LazyLock::new(|| {
TerminalTheme::new(
(41, 41, 41),
(197, 200, 198),
vec![
(75, 78, 85), (204, 85, 90), (152, 168, 75), (208, 179, 68), (96, 138, 177), (152, 114, 159), (104, 160, 179), (197, 200, 198), ],
Some(vec![
(154, 155, 153), (255, 38, 39), (0, 130, 61), (208, 132, 66), (25, 132, 233), (255, 44, 122), (57, 130, 128), (253, 253, 197), ]),
)
});
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),
]),
)
});
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),
]),
)
});
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),
]),
)
});
pub static DRACULA: LazyLock<TerminalTheme> = LazyLock::new(|| {
TerminalTheme::new(
(40, 42, 54), (248, 248, 242), vec![
(40, 42, 54), (255, 85, 85), (80, 250, 123), (241, 250, 140), (189, 147, 249), (255, 121, 198), (139, 233, 253), (191, 191, 191), ],
Some(vec![
(98, 114, 164), (255, 110, 110), (105, 255, 156), (255, 255, 165), (214, 189, 255), (255, 146, 213), (164, 255, 255), (255, 255, 255), ]),
)
});
pub static NORD: LazyLock<TerminalTheme> = LazyLock::new(|| {
TerminalTheme::new(
(46, 52, 64), (216, 222, 233), vec![
(59, 66, 82), (191, 97, 106), (163, 190, 140), (235, 203, 139), (129, 161, 193), (180, 142, 173), (136, 192, 208), (229, 233, 240), ],
Some(vec![
(76, 86, 106), (191, 97, 106), (163, 190, 140), (235, 203, 139), (129, 161, 193), (180, 142, 173), (143, 188, 187), (236, 239, 244), ]),
)
});
pub static GRUVBOX: LazyLock<TerminalTheme> = LazyLock::new(|| {
TerminalTheme::new(
(40, 40, 40), (235, 219, 178), vec![
(40, 40, 40), (204, 36, 29), (152, 151, 26), (215, 153, 33), (69, 133, 136), (177, 98, 134), (104, 157, 106), (168, 153, 132), ],
Some(vec![
(146, 131, 116), (251, 73, 52), (184, 187, 38), (250, 189, 47), (131, 165, 152), (211, 134, 155), (142, 192, 124), (235, 219, 178), ]),
)
});
pub static MONOKAI_PRO: LazyLock<TerminalTheme> = LazyLock::new(|| {
TerminalTheme::new(
(45, 42, 46), (252, 252, 250), vec![
(45, 42, 46), (255, 97, 136), (169, 220, 118), (255, 216, 102), (120, 220, 232), (171, 157, 242), (120, 220, 232), (252, 252, 250), ],
Some(vec![
(114, 112, 114), (255, 97, 136), (169, 220, 118), (255, 216, 102), (120, 220, 232), (171, 157, 242), (120, 220, 232), (252, 252, 250), ]),
)
});
pub static SOLARIZED_DARK: LazyLock<TerminalTheme> = LazyLock::new(|| {
TerminalTheme::new(
(0, 43, 54), (131, 148, 150), vec![
(7, 54, 66), (220, 50, 47), (133, 153, 0), (181, 137, 0), (38, 139, 210), (211, 54, 130), (42, 161, 152), (238, 232, 213), ],
Some(vec![
(0, 43, 54), (203, 75, 22), (88, 110, 117), (101, 123, 131), (131, 148, 150), (108, 113, 196), (147, 161, 161), (253, 246, 227), ]),
)
});
pub struct ThemeRegistry;
impl ThemeRegistry {
pub fn names() -> &'static [&'static str] {
&["dracula", "nord", "gruvbox", "monokai", "solarized-dark"]
}
pub fn terminal_theme(name: &str) -> Option<&'static TerminalTheme> {
match name.to_ascii_lowercase().as_str() {
"dracula" => Some(&DRACULA),
"nord" => Some(&NORD),
"gruvbox" => Some(&GRUVBOX),
"monokai" => Some(&MONOKAI_PRO),
"solarized-dark" | "solarized" => Some(&SOLARIZED_DARK),
_ => None,
}
}
}
#[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);
}
#[test]
fn test_svg_export_theme_palette_is_16_entries() {
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)"
);
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"
);
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)"
);
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,
);
let color0 = theme.ansi_colors.get(0);
assert_eq!(color0.red, 0);
assert_eq!(color0.green, 0);
assert_eq!(color0.blue, 0);
let color4 = theme.ansi_colors.get(4);
assert_eq!(color4.red, 0);
assert_eq!(color4.green, 0);
assert_eq!(color4.blue, 0);
let color1 = theme.ansi_colors.get(1);
assert_eq!(color1.red, 128);
assert_eq!(color1.green, 0);
assert_eq!(color1.blue, 0);
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);
}
#[test]
fn test_registry_names_count() {
assert_eq!(ThemeRegistry::names().len(), 5);
}
#[test]
fn test_registry_names_contains_all() {
let names = ThemeRegistry::names();
assert!(names.contains(&"dracula"));
assert!(names.contains(&"nord"));
assert!(names.contains(&"gruvbox"));
assert!(names.contains(&"monokai"));
assert!(names.contains(&"solarized-dark"));
}
#[test]
fn test_registry_lookup_dracula() {
let theme = ThemeRegistry::terminal_theme("dracula").expect("dracula must resolve");
assert_eq!(theme.background_color.red, 40);
assert_eq!(theme.background_color.green, 42);
assert_eq!(theme.background_color.blue, 54);
}
#[test]
fn test_registry_lookup_case_insensitive() {
assert!(ThemeRegistry::terminal_theme("Dracula").is_some());
assert!(ThemeRegistry::terminal_theme("NORD").is_some());
assert!(ThemeRegistry::terminal_theme("Gruvbox").is_some());
assert!(ThemeRegistry::terminal_theme("MONOKAI").is_some());
assert!(ThemeRegistry::terminal_theme("Solarized-Dark").is_some());
}
#[test]
fn test_registry_solarized_alias() {
let a = ThemeRegistry::terminal_theme("solarized-dark").unwrap();
let b = ThemeRegistry::terminal_theme("solarized").unwrap();
assert_eq!(a.background_color.red, b.background_color.red);
}
#[test]
fn test_registry_unknown_returns_none() {
assert!(ThemeRegistry::terminal_theme("unknown-theme-xyz").is_none());
}
#[test]
fn test_registry_nord_background() {
let theme = ThemeRegistry::terminal_theme("nord").unwrap();
assert_eq!(theme.background_color.red, 46);
assert_eq!(theme.background_color.green, 52);
assert_eq!(theme.background_color.blue, 64);
}
#[test]
fn test_registry_gruvbox_background() {
let theme = ThemeRegistry::terminal_theme("gruvbox").unwrap();
assert_eq!(theme.background_color.red, 40);
assert_eq!(theme.background_color.green, 40);
assert_eq!(theme.background_color.blue, 40);
}
#[test]
fn test_registry_monokai_background() {
let theme = ThemeRegistry::terminal_theme("monokai").unwrap();
assert_eq!(theme.background_color.red, 45);
assert_eq!(theme.background_color.green, 42);
assert_eq!(theme.background_color.blue, 46);
}
#[test]
fn test_registry_solarized_dark_background() {
let theme = ThemeRegistry::terminal_theme("solarized-dark").unwrap();
assert_eq!(theme.background_color.red, 0);
assert_eq!(theme.background_color.green, 43);
assert_eq!(theme.background_color.blue, 54);
}
#[test]
fn test_dracula_has_16_ansi_colors() {
let theme = &*DRACULA;
let bright_white = theme.ansi_colors.get(15);
assert_eq!(bright_white.red, 255);
assert_eq!(bright_white.green, 255);
assert_eq!(bright_white.blue, 255);
}
}