use ratatui::style::Color;
pub const C_BRAND: Color = Color::Rgb(255, 100, 30);
pub const C_ACCENT: Color = Color::Rgb(80, 200, 255);
pub const C_SUCCESS: Color = Color::Rgb(80, 220, 120);
pub const C_DIM: Color = Color::Rgb(120, 120, 130);
pub const C_FG: Color = Color::White;
pub const C_SEL_BG: Color = Color::Rgb(40, 60, 80);
pub const C_DIR: Color = Color::Rgb(255, 210, 80);
pub const C_MATCH: Color = Color::Rgb(80, 220, 120);
#[derive(Debug, Clone, PartialEq)]
pub struct Theme {
pub brand: Color,
pub accent: Color,
pub success: Color,
pub dim: Color,
pub fg: Color,
pub sel_bg: Color,
pub dir: Color,
pub match_file: Color,
}
impl Default for Theme {
fn default() -> Self {
Self {
brand: C_BRAND,
accent: C_ACCENT,
success: C_SUCCESS,
dim: C_DIM,
fg: C_FG,
sel_bg: C_SEL_BG,
dir: C_DIR,
match_file: C_MATCH,
}
}
}
impl Theme {
pub fn brand(mut self, color: Color) -> Self {
self.brand = color;
self
}
pub fn accent(mut self, color: Color) -> Self {
self.accent = color;
self
}
pub fn success(mut self, color: Color) -> Self {
self.success = color;
self
}
pub fn dim(mut self, color: Color) -> Self {
self.dim = color;
self
}
pub fn fg(mut self, color: Color) -> Self {
self.fg = color;
self
}
pub fn sel_bg(mut self, color: Color) -> Self {
self.sel_bg = color;
self
}
pub fn dir(mut self, color: Color) -> Self {
self.dir = color;
self
}
pub fn match_file(mut self, color: Color) -> Self {
self.match_file = color;
self
}
}
impl Theme {
pub fn dracula() -> Self {
Self {
brand: Color::Rgb(255, 121, 198), accent: Color::Rgb(139, 233, 253), dir: Color::Rgb(241, 250, 140), sel_bg: Color::Rgb(68, 71, 90), success: Color::Rgb(80, 250, 123), match_file: Color::Rgb(80, 250, 123), dim: Color::Rgb(98, 114, 164), fg: Color::Rgb(248, 248, 242), }
}
pub fn nord() -> Self {
Self {
brand: Color::Rgb(136, 192, 208), accent: Color::Rgb(129, 161, 193), dir: Color::Rgb(235, 203, 139), sel_bg: Color::Rgb(59, 66, 82), success: Color::Rgb(163, 190, 140), match_file: Color::Rgb(163, 190, 140), dim: Color::Rgb(76, 86, 106), fg: Color::Rgb(216, 222, 233), }
}
pub fn solarized_dark() -> Self {
Self {
brand: Color::Rgb(38, 139, 210), accent: Color::Rgb(42, 161, 152), dir: Color::Rgb(181, 137, 0), sel_bg: Color::Rgb(7, 54, 66), success: Color::Rgb(133, 153, 0), match_file: Color::Rgb(133, 153, 0), dim: Color::Rgb(88, 110, 117), fg: Color::Rgb(131, 148, 150), }
}
pub fn solarized_light() -> Self {
Self {
brand: Color::Rgb(38, 139, 210), accent: Color::Rgb(42, 161, 152), dir: Color::Rgb(181, 137, 0), sel_bg: Color::Rgb(238, 232, 213), success: Color::Rgb(133, 153, 0), match_file: Color::Rgb(0, 110, 100), dim: Color::Rgb(147, 161, 161), fg: Color::Rgb(101, 123, 131), }
}
pub fn gruvbox_dark() -> Self {
Self {
brand: Color::Rgb(254, 128, 25), accent: Color::Rgb(250, 189, 47), dir: Color::Rgb(250, 189, 47), sel_bg: Color::Rgb(60, 56, 54), success: Color::Rgb(184, 187, 38), match_file: Color::Rgb(142, 192, 124), dim: Color::Rgb(146, 131, 116), fg: Color::Rgb(235, 219, 178), }
}
pub fn gruvbox_light() -> Self {
Self {
brand: Color::Rgb(214, 93, 14), accent: Color::Rgb(215, 153, 33), dir: Color::Rgb(181, 118, 20), sel_bg: Color::Rgb(213, 196, 161), success: Color::Rgb(121, 116, 14), match_file: Color::Rgb(104, 157, 106), dim: Color::Rgb(146, 131, 116), fg: Color::Rgb(60, 56, 54), }
}
pub fn catppuccin_latte() -> Self {
Self {
brand: Color::Rgb(136, 57, 239), accent: Color::Rgb(30, 102, 245), dir: Color::Rgb(254, 100, 11), sel_bg: Color::Rgb(204, 208, 218), success: Color::Rgb(64, 160, 43), match_file: Color::Rgb(23, 146, 153), dim: Color::Rgb(156, 160, 176), fg: Color::Rgb(76, 79, 105), }
}
pub fn catppuccin_frappe() -> Self {
Self {
brand: Color::Rgb(202, 158, 230), accent: Color::Rgb(140, 170, 238), dir: Color::Rgb(229, 200, 144), sel_bg: Color::Rgb(65, 69, 89), success: Color::Rgb(166, 209, 137), match_file: Color::Rgb(129, 200, 190), dim: Color::Rgb(115, 121, 148), fg: Color::Rgb(198, 208, 245), }
}
pub fn catppuccin_macchiato() -> Self {
Self {
brand: Color::Rgb(198, 160, 246), accent: Color::Rgb(138, 173, 244), dir: Color::Rgb(238, 212, 159), sel_bg: Color::Rgb(54, 58, 79), success: Color::Rgb(166, 218, 149), match_file: Color::Rgb(139, 213, 202), dim: Color::Rgb(110, 115, 141), fg: Color::Rgb(202, 211, 245), }
}
pub fn catppuccin_mocha() -> Self {
Self {
brand: Color::Rgb(203, 166, 247), accent: Color::Rgb(137, 180, 250), dir: Color::Rgb(249, 226, 175), sel_bg: Color::Rgb(49, 50, 68), success: Color::Rgb(166, 227, 161), match_file: Color::Rgb(148, 226, 213), dim: Color::Rgb(108, 112, 134), fg: Color::Rgb(205, 214, 244), }
}
pub fn tokyo_night() -> Self {
Self {
brand: Color::Rgb(187, 154, 247), accent: Color::Rgb(122, 162, 247), dir: Color::Rgb(224, 175, 104), sel_bg: Color::Rgb(41, 46, 66), success: Color::Rgb(158, 206, 106), match_file: Color::Rgb(115, 218, 202), dim: Color::Rgb(86, 95, 137), fg: Color::Rgb(192, 202, 245), }
}
pub fn tokyo_night_storm() -> Self {
Self {
brand: Color::Rgb(187, 154, 247), accent: Color::Rgb(122, 162, 247), dir: Color::Rgb(224, 175, 104), sel_bg: Color::Rgb(45, 49, 75), success: Color::Rgb(158, 206, 106), match_file: Color::Rgb(115, 218, 202), dim: Color::Rgb(86, 95, 137), fg: Color::Rgb(192, 202, 245), }
}
pub fn tokyo_night_light() -> Self {
Self {
brand: Color::Rgb(90, 74, 120), accent: Color::Rgb(46, 126, 233), dir: Color::Rgb(140, 108, 62), sel_bg: Color::Rgb(208, 213, 227), success: Color::Rgb(72, 94, 48), match_file: Color::Rgb(15, 75, 110), dim: Color::Rgb(132, 140, 176), fg: Color::Rgb(52, 59, 88), }
}
pub fn kanagawa_wave() -> Self {
Self {
brand: Color::Rgb(210, 126, 153), accent: Color::Rgb(126, 156, 216), dir: Color::Rgb(220, 165, 97), sel_bg: Color::Rgb(42, 42, 55), success: Color::Rgb(118, 148, 106), match_file: Color::Rgb(106, 149, 137), dim: Color::Rgb(114, 113, 105), fg: Color::Rgb(220, 215, 186), }
}
pub fn kanagawa_dragon() -> Self {
Self {
brand: Color::Rgb(210, 126, 153), accent: Color::Rgb(139, 164, 176), dir: Color::Rgb(200, 170, 109), sel_bg: Color::Rgb(40, 39, 39), success: Color::Rgb(135, 169, 135), match_file: Color::Rgb(142, 164, 162), dim: Color::Rgb(166, 166, 156), fg: Color::Rgb(197, 201, 197), }
}
pub fn kanagawa_lotus() -> Self {
Self {
brand: Color::Rgb(160, 154, 190), accent: Color::Rgb(77, 105, 155), dir: Color::Rgb(119, 113, 63), sel_bg: Color::Rgb(231, 219, 160), success: Color::Rgb(111, 137, 78), match_file: Color::Rgb(78, 140, 162), dim: Color::Rgb(196, 178, 138), fg: Color::Rgb(84, 84, 100), }
}
pub fn moonfly() -> Self {
Self {
brand: Color::Rgb(174, 129, 255), accent: Color::Rgb(128, 160, 255), dir: Color::Rgb(227, 199, 138), sel_bg: Color::Rgb(28, 28, 28), success: Color::Rgb(140, 200, 95), match_file: Color::Rgb(121, 219, 195), dim: Color::Rgb(78, 78, 78), fg: Color::Rgb(178, 178, 178), }
}
pub fn nightfly() -> Self {
Self {
brand: Color::Rgb(199, 146, 234), accent: Color::Rgb(130, 170, 255), dir: Color::Rgb(255, 202, 40), sel_bg: Color::Rgb(11, 41, 66), success: Color::Rgb(161, 205, 94), match_file: Color::Rgb(33, 199, 168), dim: Color::Rgb(75, 100, 121), fg: Color::Rgb(172, 187, 203), }
}
pub fn oxocarbon() -> Self {
Self {
brand: Color::Rgb(255, 126, 182), accent: Color::Rgb(120, 169, 255), dir: Color::Rgb(255, 213, 0), sel_bg: Color::Rgb(38, 38, 38), success: Color::Rgb(66, 190, 101), match_file: Color::Rgb(51, 177, 255), dim: Color::Rgb(82, 82, 82), fg: Color::Rgb(242, 244, 248), }
}
pub fn grape() -> Self {
Self::default()
.brand(Color::Rgb(200, 120, 255))
.accent(Color::Rgb(130, 180, 255))
.dir(Color::Rgb(200, 160, 255))
.sel_bg(Color::Rgb(50, 35, 80))
.success(Color::Rgb(160, 110, 255))
.match_file(Color::Rgb(180, 130, 255))
.dim(Color::Rgb(110, 100, 130))
}
pub fn ocean() -> Self {
Self::default()
.brand(Color::Rgb(0, 200, 180))
.accent(Color::Rgb(0, 175, 210))
.dir(Color::Rgb(100, 220, 210))
.sel_bg(Color::Rgb(0, 50, 70))
.success(Color::Rgb(80, 230, 200))
.match_file(Color::Rgb(80, 230, 200))
.dim(Color::Rgb(80, 120, 130))
.fg(Color::Rgb(200, 240, 245))
}
pub fn sunset() -> Self {
Self::default()
.brand(Color::Rgb(255, 80, 80))
.accent(Color::Rgb(255, 150, 50))
.dir(Color::Rgb(255, 200, 60))
.sel_bg(Color::Rgb(80, 30, 20))
.success(Color::Rgb(255, 180, 80))
.match_file(Color::Rgb(255, 180, 80))
.dim(Color::Rgb(140, 100, 80))
.fg(Color::Rgb(255, 235, 210))
}
pub fn forest() -> Self {
Self::default()
.brand(Color::Rgb(100, 200, 80))
.accent(Color::Rgb(80, 160, 80))
.dir(Color::Rgb(170, 220, 100))
.sel_bg(Color::Rgb(20, 50, 20))
.success(Color::Rgb(120, 210, 90))
.match_file(Color::Rgb(120, 210, 90))
.dim(Color::Rgb(90, 120, 80))
.fg(Color::Rgb(210, 235, 200))
}
pub fn rose() -> Self {
Self::default()
.brand(Color::Rgb(255, 100, 150))
.accent(Color::Rgb(255, 140, 180))
.dir(Color::Rgb(255, 180, 200))
.sel_bg(Color::Rgb(80, 20, 40))
.success(Color::Rgb(255, 160, 190))
.match_file(Color::Rgb(255, 160, 190))
.dim(Color::Rgb(140, 90, 110))
.fg(Color::Rgb(255, 230, 235))
}
pub fn mono() -> Self {
Self::default()
.brand(Color::Rgb(220, 220, 220))
.accent(Color::Rgb(180, 180, 180))
.dir(Color::Rgb(200, 200, 200))
.sel_bg(Color::Rgb(50, 50, 55))
.success(Color::Rgb(200, 200, 200))
.match_file(Color::Rgb(230, 230, 230))
.dim(Color::Rgb(110, 110, 115))
.fg(Color::Rgb(210, 210, 210))
}
pub fn neon() -> Self {
Self::default()
.brand(Color::Rgb(255, 0, 200))
.accent(Color::Rgb(0, 255, 200))
.dir(Color::Rgb(255, 220, 0))
.sel_bg(Color::Rgb(30, 0, 50))
.success(Color::Rgb(0, 255, 130))
.match_file(Color::Rgb(0, 255, 130))
.dim(Color::Rgb(100, 80, 120))
.fg(Color::Rgb(230, 230, 255))
}
pub fn all_presets() -> Vec<(&'static str, &'static str, Theme)> {
vec![
(
"Default",
"The built-in palette — orange title, cyan borders, yellow dirs",
Theme::default(),
),
(
"Grape",
"Deep violet & soft blue — easy on the eyes in dark environments",
Theme::grape(),
),
(
"Ocean",
"Teal & aquamarine — calm, nautical feel",
Theme::ocean(),
),
(
"Sunset",
"Warm amber & rose — vibrant, high-energy palette",
Theme::sunset(),
),
(
"Forest",
"Earthy greens & bark browns — natural, low-contrast",
Theme::forest(),
),
(
"Rose",
"Pinks & corals — playful, pastel-inspired",
Theme::rose(),
),
(
"Mono",
"Greyscale only — maximally distraction-free",
Theme::mono(),
),
(
"Neon",
"Electric brights on near-black — synthwave / retro",
Theme::neon(),
),
(
"Dracula",
"Pink, cyan & purple on dark grey",
Theme::dracula(),
),
("Nord", "Arctic bluish tones", Theme::nord()),
(
"Solarized Dark",
"Precision colours for machines and people — dark",
Theme::solarized_dark(),
),
(
"Solarized Light",
"Precision colours for machines and people — light",
Theme::solarized_light(),
),
(
"Gruvbox Dark",
"Retro groove — dark warm background",
Theme::gruvbox_dark(),
),
(
"Gruvbox Light",
"Retro groove — light warm background",
Theme::gruvbox_light(),
),
(
"Catppuccin Latte",
"Soothing pastel — light",
Theme::catppuccin_latte(),
),
(
"Catppuccin Frappé",
"Soothing pastel — medium-dark",
Theme::catppuccin_frappe(),
),
(
"Catppuccin Macchiato",
"Soothing pastel — dark",
Theme::catppuccin_macchiato(),
),
(
"Catppuccin Mocha",
"Soothing pastel — darkest",
Theme::catppuccin_mocha(),
),
(
"Tokyo Night",
"A clean dark blue / purple night",
Theme::tokyo_night(),
),
(
"Tokyo Night Storm",
"Tokyo Night on a slightly lighter background",
Theme::tokyo_night_storm(),
),
(
"Tokyo Night Light",
"Tokyo Night inverted to a light background",
Theme::tokyo_night_light(),
),
(
"Kanagawa Wave",
"Deep blue ink brushed on parchment",
Theme::kanagawa_wave(),
),
(
"Kanagawa Dragon",
"Darker earth tones — charcoal & moss",
Theme::kanagawa_dragon(),
),
(
"Kanagawa Lotus",
"Light parchment variant of Kanagawa",
Theme::kanagawa_lotus(),
),
(
"Moonfly",
"Deep dark background with vibrant accents",
Theme::moonfly(),
),
("Nightfly", "Deep ocean blues", Theme::nightfly()),
(
"Oxocarbon",
"IBM Carbon Design System inspired",
Theme::oxocarbon(),
),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_theme_brand_matches_constant() {
assert_eq!(Theme::default().brand, C_BRAND);
}
#[test]
fn default_theme_accent_matches_constant() {
assert_eq!(Theme::default().accent, C_ACCENT);
}
#[test]
fn default_theme_success_matches_constant() {
assert_eq!(Theme::default().success, C_SUCCESS);
}
#[test]
fn default_theme_dim_matches_constant() {
assert_eq!(Theme::default().dim, C_DIM);
}
#[test]
fn default_theme_fg_matches_constant() {
assert_eq!(Theme::default().fg, C_FG);
}
#[test]
fn default_theme_sel_bg_matches_constant() {
assert_eq!(Theme::default().sel_bg, C_SEL_BG);
}
#[test]
fn default_theme_dir_matches_constant() {
assert_eq!(Theme::default().dir, C_DIR);
}
#[test]
fn default_theme_match_file_matches_constant() {
assert_eq!(Theme::default().match_file, C_MATCH);
}
#[test]
fn builder_brand_overrides_field() {
let color = Color::Rgb(1, 2, 3);
let theme = Theme::default().brand(color);
assert_eq!(theme.brand, color);
}
#[test]
fn builder_accent_overrides_field() {
let color = Color::Rgb(4, 5, 6);
let theme = Theme::default().accent(color);
assert_eq!(theme.accent, color);
}
#[test]
fn builder_success_overrides_field() {
let color = Color::Rgb(7, 8, 9);
let theme = Theme::default().success(color);
assert_eq!(theme.success, color);
}
#[test]
fn builder_dim_overrides_field() {
let color = Color::Rgb(10, 11, 12);
let theme = Theme::default().dim(color);
assert_eq!(theme.dim, color);
}
#[test]
fn builder_fg_overrides_field() {
let color = Color::Rgb(13, 14, 15);
let theme = Theme::default().fg(color);
assert_eq!(theme.fg, color);
}
#[test]
fn builder_sel_bg_overrides_field() {
let color = Color::Rgb(16, 17, 18);
let theme = Theme::default().sel_bg(color);
assert_eq!(theme.sel_bg, color);
}
#[test]
fn builder_dir_overrides_field() {
let color = Color::Rgb(19, 20, 21);
let theme = Theme::default().dir(color);
assert_eq!(theme.dir, color);
}
#[test]
fn builder_match_file_overrides_field() {
let color = Color::Rgb(22, 23, 24);
let theme = Theme::default().match_file(color);
assert_eq!(theme.match_file, color);
}
#[test]
fn builder_chained_overrides_multiple_fields() {
let brand = Color::Rgb(1, 0, 0);
let accent = Color::Rgb(0, 1, 0);
let theme = Theme::default().brand(brand).accent(accent);
assert_eq!(theme.brand, brand);
assert_eq!(theme.accent, accent);
assert_eq!(theme.dim, C_DIM);
}
#[test]
fn builder_does_not_mutate_other_fields() {
let original = Theme::default();
let modified = original.clone().brand(Color::Red);
assert_eq!(modified.accent, original.accent);
assert_eq!(modified.success, original.success);
assert_eq!(modified.dim, original.dim);
assert_eq!(modified.fg, original.fg);
assert_eq!(modified.sel_bg, original.sel_bg);
assert_eq!(modified.dir, original.dir);
assert_eq!(modified.match_file, original.match_file);
}
#[test]
fn all_presets_is_non_empty() {
assert!(!Theme::all_presets().is_empty());
}
#[test]
fn all_presets_names_are_non_empty() {
for (name, _, _) in Theme::all_presets() {
assert!(!name.is_empty(), "preset has an empty name");
}
}
#[test]
fn all_presets_descriptions_are_non_empty() {
for (name, desc, _) in Theme::all_presets() {
assert!(!desc.is_empty(), "preset '{name}' has an empty description");
}
}
#[test]
fn all_presets_names_are_unique() {
let presets = Theme::all_presets();
let mut seen = std::collections::HashSet::new();
for (name, _, _) in &presets {
assert!(seen.insert(*name), "duplicate preset name: '{name}'");
}
}
#[test]
fn all_presets_first_entry_is_default() {
let presets = Theme::all_presets();
let (name, _, theme) = &presets[0];
assert_eq!(*name, "Default");
assert_eq!(*theme, Theme::default());
}
#[test]
fn all_presets_contains_dracula() {
let names: Vec<&str> = Theme::all_presets().iter().map(|(n, _, _)| *n).collect();
assert!(names.contains(&"Dracula"), "Dracula preset missing");
}
#[test]
fn all_presets_contains_nord() {
let names: Vec<&str> = Theme::all_presets().iter().map(|(n, _, _)| *n).collect();
assert!(names.contains(&"Nord"), "Nord preset missing");
}
#[test]
fn all_presets_contains_catppuccin_mocha() {
let names: Vec<&str> = Theme::all_presets().iter().map(|(n, _, _)| *n).collect();
assert!(
names.contains(&"Catppuccin Mocha"),
"Catppuccin Mocha preset missing"
);
}
#[test]
fn all_presets_count_is_at_least_27() {
assert!(
Theme::all_presets().len() >= 27,
"expected at least 27 presets"
);
}
#[test]
fn named_preset_dracula_differs_from_default() {
assert_ne!(Theme::dracula(), Theme::default());
}
#[test]
fn named_preset_nord_differs_from_dracula() {
assert_ne!(Theme::nord(), Theme::dracula());
}
#[test]
fn theme_clone_equals_original() {
let t = Theme::dracula();
assert_eq!(t.clone(), t);
}
#[test]
fn theme_partial_eq_reflexive() {
let t = Theme::catppuccin_mocha();
assert_eq!(t, t.clone());
}
}