#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Color {
Rgb(u8, u8, u8),
Ansi(u8),
}
#[derive(Clone, Copy, Debug)]
pub enum Gradient {
Stops(&'static [(u8, u8, u8)]),
Thresholds,
}
#[derive(Debug)]
pub struct Theme {
pub name: &'static str,
pub user: Color,
pub path: Color,
pub branch: Color,
pub model: Color,
pub dim: Color,
pub gradient: Gradient,
}
pub const RESET: &str = "\x1b[0m";
pub const BOLD: &str = "\x1b[1m";
pub fn fg(c: Color) -> String {
match c {
Color::Rgb(r, g, b) => format!("\x1b[38;2;{r};{g};{b}m"),
Color::Ansi(n) => format!("\x1b[{n}m"),
}
}
use Color::{Ansi, Rgb};
static THEMES: [Theme; 7] = [
Theme {
name: "default",
user: Rgb(125, 211, 252),
path: Rgb(196, 181, 253),
branch: Rgb(244, 114, 182),
model: Rgb(129, 140, 248),
dim: Rgb(100, 116, 139),
gradient: Gradient::Stops(&[(96, 165, 250), (167, 139, 250), (244, 114, 182)]),
},
Theme {
name: "classic",
user: Ansi(36),
path: Ansi(33),
branch: Ansi(35),
model: Ansi(34),
dim: Ansi(90),
gradient: Gradient::Thresholds,
},
Theme {
name: "nord",
user: Rgb(136, 192, 208),
path: Rgb(129, 161, 193),
branch: Rgb(180, 142, 173),
model: Rgb(143, 188, 187),
dim: Rgb(76, 86, 106),
gradient: Gradient::Stops(&[(163, 190, 140), (235, 203, 139), (191, 97, 106)]),
},
Theme {
name: "catppuccin-mocha",
user: Rgb(137, 220, 235),
path: Rgb(180, 190, 254),
branch: Rgb(245, 194, 231),
model: Rgb(203, 166, 247),
dim: Rgb(108, 112, 134),
gradient: Gradient::Stops(&[(166, 227, 161), (249, 226, 175), (243, 139, 168)]),
},
Theme {
name: "gruvbox",
user: Rgb(142, 192, 124),
path: Rgb(250, 189, 47),
branch: Rgb(211, 134, 155),
model: Rgb(131, 165, 152),
dim: Rgb(146, 131, 116),
gradient: Gradient::Stops(&[(184, 187, 38), (254, 128, 25), (251, 73, 52)]),
},
Theme {
name: "dracula",
user: Rgb(139, 233, 253),
path: Rgb(189, 147, 249),
branch: Rgb(255, 121, 198),
model: Rgb(80, 250, 123),
dim: Rgb(98, 114, 164),
gradient: Gradient::Stops(&[(80, 250, 123), (241, 250, 140), (255, 85, 85)]),
},
Theme {
name: "tokyo-night",
user: Rgb(125, 207, 255),
path: Rgb(122, 162, 247),
branch: Rgb(187, 154, 247),
model: Rgb(42, 195, 222),
dim: Rgb(86, 95, 137),
gradient: Gradient::Stops(&[(158, 206, 106), (224, 175, 104), (247, 118, 142)]),
},
];
impl Theme {
pub fn all() -> &'static [Theme] {
&THEMES
}
pub fn default_theme() -> &'static Theme {
&THEMES[0]
}
pub fn by_name(name: &str) -> Option<&'static Theme> {
let name = name.trim();
let name = if name.eq_ignore_ascii_case("catppuccin") {
"catppuccin-mocha"
} else {
name
};
THEMES.iter().find(|t| t.name.eq_ignore_ascii_case(name))
}
pub fn grad_color(&self, pct: u32) -> String {
match self.gradient {
Gradient::Thresholds => fg(Ansi(if pct < 50 {
32
} else if pct < 80 {
33
} else {
31
})),
Gradient::Stops(stops) => {
let p = pct.min(100) as i32;
let n = stops.len() as i32 - 1;
if n <= 0 {
let (r, g, b) = stops[0];
return fg(Rgb(r, g, b));
}
let seg = (p * n / 100).min(n - 1);
let t = p * n - seg * 100; let (r0, g0, b0) = stops[seg as usize];
let (r1, g1, b1) = stops[seg as usize + 1];
let lerp = |a: u8, b: u8| (a as i32 + (b as i32 - a as i32) * t / 100) as u8;
fg(Rgb(lerp(r0, r1), lerp(g0, g1), lerp(b0, b1)))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_theme_resolves_by_name() {
for t in Theme::all() {
assert_eq!(Theme::by_name(t.name).unwrap().name, t.name);
assert_eq!(Theme::by_name(&t.name.to_uppercase()).unwrap().name, t.name);
}
assert_eq!(
Theme::by_name("catppuccin").unwrap().name,
"catppuccin-mocha"
);
assert!(Theme::by_name("bogus").is_none());
assert_eq!(Theme::default_theme().name, "default");
}
#[test]
fn gradient_hits_stops_at_0_50_100() {
let t = Theme::default_theme();
assert_eq!(t.grad_color(0), fg(Rgb(96, 165, 250)));
assert_eq!(t.grad_color(50), fg(Rgb(167, 139, 250)));
assert_eq!(t.grad_color(100), fg(Rgb(244, 114, 182)));
assert_eq!(t.grad_color(250), fg(Rgb(244, 114, 182)));
assert_eq!(t.grad_color(25), fg(Rgb(131, 152, 250)));
}
#[test]
fn classic_uses_thresholds() {
let t = Theme::by_name("classic").unwrap();
assert_eq!(t.grad_color(0), "\x1b[32m");
assert_eq!(t.grad_color(49), "\x1b[32m");
assert_eq!(t.grad_color(50), "\x1b[33m");
assert_eq!(t.grad_color(80), "\x1b[31m");
}
}