use ratatui_core::style::Color;
use crate::style::{CodeTheme, Theme};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct NamedTheme {
pub name: &'static str,
pub label: &'static str,
pub theme: Theme,
}
pub const PRESETS: &[NamedTheme] = &[
NamedTheme {
name: "solarized-dark",
label: "Solarized Dark",
theme: SOLARIZED_DARK,
},
NamedTheme {
name: "solarized-light",
label: "Solarized Light",
theme: SOLARIZED_LIGHT,
},
NamedTheme {
name: "gruvbox-dark",
label: "Gruvbox Dark",
theme: GRUVBOX_DARK,
},
NamedTheme {
name: "light",
label: "Light",
theme: LIGHT,
},
NamedTheme {
name: "dracula",
label: "Dracula",
theme: DRACULA,
},
NamedTheme {
name: "terminal",
label: "Terminal",
theme: TERMINAL,
},
];
pub fn by_name(name: &str) -> Option<Theme> {
PRESETS
.iter()
.find(|p| p.name.eq_ignore_ascii_case(name))
.map(|p| p.theme)
}
pub const SOLARIZED_DARK: Theme = Theme {
background: Color::Rgb(0, 43, 54), surface: Color::Rgb(7, 54, 66), text: Color::Rgb(131, 148, 150), muted: Color::Rgb(88, 110, 117), dim: Color::Rgb(58, 82, 92), accent: Color::Rgb(38, 139, 210), accent_alt: Color::Rgb(42, 161, 152), border: Color::Rgb(58, 82, 92),
border_focused: Color::Rgb(38, 139, 210),
selection_bg: Color::Rgb(7, 54, 66), selection_fg: Color::Rgb(147, 161, 161), code: CodeTheme {
heading: Color::Rgb(147, 161, 161), link: Color::Rgb(38, 139, 210), background: Color::Rgb(7, 54, 66), text: Color::Rgb(131, 148, 150), label: Color::Rgb(88, 110, 117), keyword: Color::Rgb(133, 153, 0), function: Color::Rgb(38, 139, 210), type_name: Color::Rgb(181, 137, 0), constant: Color::Rgb(211, 54, 130), string: Color::Rgb(42, 161, 152), comment: Color::Rgb(88, 110, 117), punctuation: Color::Rgb(101, 123, 131), },
};
pub const SOLARIZED_LIGHT: Theme = Theme {
background: Color::Rgb(253, 246, 227), surface: Color::Rgb(238, 232, 213), text: Color::Rgb(101, 123, 131), muted: Color::Rgb(147, 161, 161), dim: Color::Rgb(203, 198, 182), accent: Color::Rgb(38, 139, 210), accent_alt: Color::Rgb(42, 161, 152), border: Color::Rgb(147, 161, 161), border_focused: Color::Rgb(38, 139, 210),
selection_bg: Color::Rgb(238, 232, 213), selection_fg: Color::Rgb(88, 110, 117), code: CodeTheme {
heading: Color::Rgb(88, 110, 117), link: Color::Rgb(38, 139, 210), background: Color::Rgb(238, 232, 213), text: Color::Rgb(101, 123, 131), label: Color::Rgb(147, 161, 161), keyword: Color::Rgb(133, 153, 0), function: Color::Rgb(38, 139, 210), type_name: Color::Rgb(181, 137, 0), constant: Color::Rgb(211, 54, 130), string: Color::Rgb(42, 161, 152), comment: Color::Rgb(147, 161, 161), punctuation: Color::Rgb(101, 123, 131), },
};
pub const GRUVBOX_DARK: Theme = Theme {
background: Color::Rgb(40, 40, 40), surface: Color::Rgb(60, 56, 54), text: Color::Rgb(235, 219, 178), muted: Color::Rgb(168, 153, 132), dim: Color::Rgb(102, 92, 84), accent: Color::Rgb(254, 128, 25), accent_alt: Color::Rgb(142, 192, 124), border: Color::Rgb(102, 92, 84), border_focused: Color::Rgb(254, 128, 25),
selection_bg: Color::Rgb(80, 73, 69), selection_fg: Color::Rgb(251, 241, 199), code: CodeTheme {
heading: Color::Rgb(235, 219, 178), link: Color::Rgb(131, 165, 152), background: Color::Rgb(29, 32, 33), text: Color::Rgb(235, 219, 178), label: Color::Rgb(168, 153, 132), keyword: Color::Rgb(251, 73, 52), function: Color::Rgb(142, 192, 124), type_name: Color::Rgb(250, 189, 47), constant: Color::Rgb(211, 134, 155), string: Color::Rgb(184, 187, 38), comment: Color::Rgb(146, 131, 116), punctuation: Color::Rgb(168, 153, 132), },
};
pub const LIGHT: Theme = Theme {
background: Color::Rgb(250, 250, 250),
surface: Color::Rgb(240, 240, 240),
text: Color::Rgb(30, 30, 30),
muted: Color::Rgb(110, 110, 110),
dim: Color::Rgb(190, 190, 190),
accent: Color::Rgb(30, 102, 204), accent_alt: Color::Rgb(0, 135, 120), border: Color::Rgb(200, 200, 200),
border_focused: Color::Rgb(30, 102, 204),
selection_bg: Color::Rgb(210, 226, 250),
selection_fg: Color::Rgb(20, 40, 70),
code: CodeTheme {
heading: Color::Rgb(20, 20, 20),
link: Color::Rgb(30, 102, 204),
background: Color::Rgb(240, 240, 240),
text: Color::Rgb(40, 40, 40),
label: Color::Rgb(110, 110, 110),
keyword: Color::Rgb(170, 13, 145), function: Color::Rgb(30, 102, 204), type_name: Color::Rgb(0, 92, 197),
constant: Color::Rgb(155, 76, 0), string: Color::Rgb(24, 128, 56), comment: Color::Rgb(120, 130, 120),
punctuation: Color::Rgb(90, 90, 90),
},
};
pub const DRACULA: Theme = Theme {
background: Color::Rgb(40, 42, 54),
surface: Color::Rgb(68, 71, 90), text: Color::Rgb(248, 248, 242),
muted: Color::Rgb(98, 114, 164), dim: Color::Rgb(73, 76, 98),
accent: Color::Rgb(189, 147, 249), accent_alt: Color::Rgb(255, 121, 198), border: Color::Rgb(98, 114, 164),
border_focused: Color::Rgb(189, 147, 249),
selection_bg: Color::Rgb(68, 71, 90),
selection_fg: Color::Rgb(248, 248, 242),
code: CodeTheme {
heading: Color::Rgb(248, 248, 242),
link: Color::Rgb(139, 233, 253), background: Color::Rgb(33, 34, 44),
text: Color::Rgb(248, 248, 242),
label: Color::Rgb(98, 114, 164),
keyword: Color::Rgb(255, 121, 198), function: Color::Rgb(80, 250, 123), type_name: Color::Rgb(139, 233, 253), constant: Color::Rgb(189, 147, 249), string: Color::Rgb(241, 250, 140), comment: Color::Rgb(98, 114, 164),
punctuation: Color::Rgb(248, 248, 242),
},
};
pub const TERMINAL: Theme = Theme {
background: Color::Reset,
surface: Color::Reset,
text: Color::Reset,
muted: Color::Indexed(8),
dim: Color::Indexed(8),
accent: Color::Indexed(4),
accent_alt: Color::Indexed(6),
border: Color::Indexed(8),
border_focused: Color::Indexed(4),
selection_bg: Color::Indexed(4),
selection_fg: Color::Indexed(15),
code: CodeTheme {
heading: Color::Reset,
link: Color::Indexed(4),
background: Color::Reset,
text: Color::Reset,
label: Color::Indexed(8),
keyword: Color::Indexed(5),
function: Color::Indexed(4),
type_name: Color::Indexed(6),
constant: Color::Indexed(3),
string: Color::Indexed(2),
comment: Color::Indexed(8),
punctuation: Color::Indexed(8),
},
};
impl Theme {
pub const fn solarized_dark() -> Theme {
SOLARIZED_DARK
}
pub const fn solarized_light() -> Theme {
SOLARIZED_LIGHT
}
pub const fn gruvbox_dark() -> Theme {
GRUVBOX_DARK
}
pub const fn light() -> Theme {
LIGHT
}
pub const fn dracula() -> Theme {
DRACULA
}
pub const fn terminal() -> Theme {
TERMINAL
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn by_name_matches_presets_case_insensitively() {
for p in PRESETS {
assert_eq!(by_name(p.name), Some(p.theme));
assert_eq!(by_name(&p.name.to_uppercase()), Some(p.theme));
}
assert_eq!(by_name("no-such-theme"), None);
}
#[test]
fn constructors_match_catalog() {
assert_eq!(Theme::solarized_dark(), by_name("solarized-dark").unwrap());
assert_eq!(
Theme::solarized_light(),
by_name("solarized-light").unwrap()
);
assert_eq!(Theme::gruvbox_dark(), by_name("gruvbox-dark").unwrap());
assert_eq!(Theme::light(), by_name("light").unwrap());
assert_eq!(Theme::dracula(), by_name("dracula").unwrap());
assert_eq!(Theme::terminal(), by_name("terminal").unwrap());
}
#[test]
fn the_terminal_preset_names_no_color_of_its_own() {
let t = TERMINAL;
let slots = [
t.background,
t.surface,
t.text,
t.muted,
t.dim,
t.accent,
t.accent_alt,
t.border,
t.border_focused,
t.selection_bg,
t.selection_fg,
t.code.heading,
t.code.link,
t.code.background,
t.code.text,
t.code.label,
t.code.keyword,
t.code.function,
t.code.type_name,
t.code.constant,
t.code.string,
t.code.comment,
t.code.punctuation,
];
for slot in slots {
assert!(
matches!(slot, Color::Reset | Color::Indexed(0..=15)),
"{slot:?} is not something the terminal resolves"
);
}
}
#[test]
fn preset_names_are_unique_and_kebab_case() {
let mut seen = std::collections::HashSet::new();
for p in PRESETS {
assert!(seen.insert(p.name), "duplicate preset name {}", p.name);
assert!(
p.name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
"preset name {} is not kebab-case",
p.name
);
assert!(!p.label.is_empty());
}
}
#[test]
fn every_preset_has_semantic_status_colors() {
use crate::SemanticRole;
for preset in PRESETS {
let theme = preset.theme;
assert_eq!(
theme.semantic_color(SemanticRole::Success),
theme.code.string,
"{} success",
preset.name
);
assert_eq!(
theme.semantic_color(SemanticRole::Warning),
theme.code.constant,
"{} warning",
preset.name
);
assert_eq!(
theme.semantic_color(SemanticRole::Danger),
theme.code.keyword,
"{} danger",
preset.name
);
assert_eq!(
theme.semantic_color(SemanticRole::Info),
theme.code.link,
"{} info",
preset.name
);
}
}
}