use std::collections::BTreeMap;
use std::sync::LazyLock;
use ratatui::style::Color;
use serde::Deserialize;
pub const DEFAULT: &str = "mocha";
pub const BUILT_IN: [(&str, &str); 9] = [
("latte", include_str!("../themes/latte.toml")),
("frappe", include_str!("../themes/frappe.toml")),
("macchiato", include_str!("../themes/macchiato.toml")),
("mocha", include_str!("../themes/mocha.toml")),
("tokyonight", include_str!("../themes/tokyonight.toml")),
("dracula", include_str!("../themes/dracula.toml")),
("gruvbox", include_str!("../themes/gruvbox.toml")),
("nord", include_str!("../themes/nord.toml")),
("rose-pine", include_str!("../themes/rose-pine.toml")),
];
#[derive(Deserialize)]
struct ThemeFile {
name: Option<String>,
#[serde(default)]
palette: BTreeMap<String, String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Colors {
pub name: String,
pub bg: Color,
pub panel: Color,
pub selected: Color,
pub border: Color,
pub dim: Color,
pub subtext: Color,
pub text: Color,
pub red: Color,
pub warning: Color,
pub ok: Color,
pub accent: Color,
pub flagged: Color,
pub quote: Color,
pub quote_deeper: Color,
pub link: Color,
pub tag: Color,
}
fn hex(s: &str) -> Option<Color> {
let s = s.strip_prefix('#')?;
if s.len() != 6 {
return None;
}
let n = u32::from_str_radix(s, 16).ok()?;
Some(Color::Rgb((n >> 16) as u8, (n >> 8) as u8, n as u8))
}
impl Colors {
pub fn named(id: &str) -> Self {
let text = BUILT_IN
.iter()
.find(|(name, _)| *name == id)
.or_else(|| BUILT_IN.iter().find(|(name, _)| *name == DEFAULT))
.map(|(_, text)| *text)
.expect("the default theme is built in");
Self::parse(text).expect("built-in themes parse")
}
fn parse(text: &str) -> Option<Self> {
let file: ThemeFile = toml::from_str(text).ok()?;
let p = |name: &str| file.palette.get(name).and_then(|c| hex(c));
Some(Self {
name: file.name.unwrap_or_default(),
bg: p("bg")?,
panel: p("bg_alt")?,
selected: p("surface")?,
border: p("overlay")?,
dim: p("comment")?,
subtext: p("subtext")?,
text: p("fg")?,
red: p("red")?,
warning: p("yellow")?,
ok: p("green")?,
accent: p("accent")?,
flagged: p("orange")?,
quote: p("cyan")?,
quote_deeper: p("purple")?,
link: p("blue")?,
tag: p("pink")?,
})
}
}
pub fn names() -> &'static [(&'static str, String)] {
static NAMES: LazyLock<Vec<(&'static str, String)>> = LazyLock::new(|| {
BUILT_IN
.iter()
.map(|(id, text)| (*id, Colors::parse(text).map(|c| c.name).unwrap_or_default()))
.collect()
});
&NAMES
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_built_in_theme_has_the_whole_palette() {
for (id, text) in BUILT_IN {
assert!(Colors::parse(text).is_some(), "{id}");
}
assert_eq!(Colors::named("nope"), Colors::named(DEFAULT));
assert_eq!(names().len(), BUILT_IN.len());
assert!(names().iter().all(|(_, name)| !name.is_empty()));
}
}