use crate::highlight::TokenKind;
use ratatui::style::Color;
use std::sync::OnceLock;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Palette {
pub dir: Color,
pub image: Color,
pub video: Color,
pub pdf: Color,
pub sheet: Color,
pub doc: Color,
pub code: Color,
pub archive: Color,
pub other: Color,
pub dim: Color,
pub accent: Color,
pub selection: Color,
pub keyword: Color,
}
static PALETTE: OnceLock<Palette> = OnceLock::new();
pub fn init(p: Palette) {
let _ = PALETTE.set(p);
}
pub fn palette() -> &'static Palette {
PALETTE.get_or_init(Palette::sucher_dark)
}
const fn hex(c: u32) -> Color {
Color::Rgb((c >> 16) as u8, (c >> 8) as u8, c as u8)
}
impl Palette {
pub fn sucher_dark() -> Self {
Palette {
dir: Color::Rgb(96, 165, 250),
image: Color::Rgb(196, 160, 250),
video: Color::Rgb(244, 114, 182),
pdf: Color::Rgb(248, 113, 113),
sheet: Color::Rgb(74, 222, 128),
doc: Color::Rgb(252, 211, 77),
code: Color::Rgb(134, 239, 172),
archive: Color::Rgb(251, 146, 60),
other: Color::Rgb(205, 205, 215),
dim: Color::Rgb(120, 120, 132),
accent: Color::Rgb(125, 211, 252),
selection: Color::Rgb(38, 44, 62),
keyword: Color::Rgb(147, 197, 253),
}
}
pub fn sucher_light() -> Self {
Palette {
dir: hex(0x2563eb),
image: hex(0x7c3aed),
video: hex(0xdb2777),
pdf: hex(0xdc2626),
sheet: hex(0x16a34a),
doc: hex(0xca8a04),
code: hex(0x0d9488),
archive: hex(0xea580c),
other: hex(0x1f2937),
dim: hex(0x6b7280),
accent: hex(0x0284c7),
selection: hex(0xdbeafe), keyword: hex(0x4338ca),
}
}
pub fn catppuccin_mocha() -> Self {
Palette {
dir: hex(0x89b4fa), image: hex(0xcba6f7), video: hex(0xf5c2e7), pdf: hex(0xf38ba8), sheet: hex(0xa6e3a1), doc: hex(0xf9e2af), code: hex(0x94e2d5), archive: hex(0xfab387), other: hex(0xcdd6f4), dim: hex(0x7f849c), accent: hex(0x89dceb), selection: hex(0x2c3149), keyword: hex(0xb4befe), }
}
pub fn gruvbox_dark() -> Self {
Palette {
dir: hex(0x83a598), image: hex(0xd3869b), video: hex(0xfb4934), pdf: hex(0xcc241d), sheet: hex(0xb8bb26), doc: hex(0xfabd2f), code: hex(0x8ec07c), archive: hex(0xfe8019), other: hex(0xebdbb2), dim: hex(0x928374), accent: hex(0x689d6a), selection: hex(0x3c3836), keyword: hex(0x458588), }
}
pub fn tokyo_night() -> Self {
Palette {
dir: hex(0x7aa2f7), image: hex(0xbb9af7), video: hex(0xf7768e), pdf: hex(0xdb4b4b), sheet: hex(0x9ece6a), doc: hex(0xe0af68), code: hex(0x73daca), archive: hex(0xff9e64), other: hex(0xc0caf5), dim: hex(0x565f89), accent: hex(0x7dcfff), selection: hex(0x283457), keyword: hex(0x9d7cd8), }
}
pub fn by_name(name: &str) -> Option<Palette> {
Some(match name {
"sucher-dark" => Palette::sucher_dark(),
"sucher-light" => Palette::sucher_light(),
"catppuccin-mocha" => Palette::catppuccin_mocha(),
"gruvbox-dark" => Palette::gruvbox_dark(),
"tokyo-night" => Palette::tokyo_night(),
_ => return None,
})
}
#[cfg(test)]
fn kind_colors(&self) -> [Color; 9] {
[
self.dir,
self.image,
self.video,
self.pdf,
self.sheet,
self.doc,
self.code,
self.archive,
self.other,
]
}
}
pub fn token_color(kind: TokenKind) -> Color {
let p = palette();
match kind {
TokenKind::Keyword => p.keyword,
TokenKind::Str => p.doc, TokenKind::Comment => p.dim, TokenKind::Number => p.image, TokenKind::Plain => p.other, }
}
#[cfg(test)]
mod tests {
use super::*;
type NamedPalette = (&'static str, fn() -> Palette);
const BUILTINS: &[NamedPalette] = &[
("sucher-dark", Palette::sucher_dark),
("sucher-light", Palette::sucher_light),
("catppuccin-mocha", Palette::catppuccin_mocha),
("gruvbox-dark", Palette::gruvbox_dark),
("tokyo-night", Palette::tokyo_night),
];
#[test]
fn token_kinds_map_to_distinct_colors() {
let colors = [
token_color(TokenKind::Keyword),
token_color(TokenKind::Str),
token_color(TokenKind::Comment),
token_color(TokenKind::Number),
token_color(TokenKind::Plain),
];
for i in 0..colors.len() {
for j in (i + 1)..colors.len() {
assert_ne!(colors[i], colors[j], "colours {i} and {j} collide");
}
}
}
#[test]
fn every_builtin_has_distinct_kind_colors() {
for (name, make) in BUILTINS {
let colors = make().kind_colors();
for i in 0..colors.len() {
for j in (i + 1)..colors.len() {
assert_ne!(
colors[i], colors[j],
"{name}: kind colours {i} and {j} collide"
);
}
}
}
}
#[test]
fn by_name_round_trips_known_names() {
for (name, make) in BUILTINS {
assert_eq!(
Palette::by_name(name),
Some(make()),
"{name} should resolve to its palette"
);
}
}
#[test]
fn by_name_rejects_unknown() {
assert_eq!(Palette::by_name("nonsense"), None);
assert_eq!(Palette::by_name(""), None);
assert_eq!(Palette::by_name("Sucher-Dark"), None); }
}