use std::collections::{BTreeSet, HashMap};
use std::path::Path;
use anyhow::{Context, Result, anyhow};
use include_dir::{Dir, include_dir};
use ratatui::style::{Color, Modifier, Style};
use super::{Theme, parse};
pub const ANSI: &str = "ansi";
static BUILTINS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/assets/themes");
pub fn available() -> Vec<String> {
let mut names: BTreeSet<String> = BTreeSet::new();
names.insert(ANSI.to_string());
for file in BUILTINS.files() {
if let Some(stem) = theme_stem(file.path()) {
names.insert(stem);
}
}
for dir in crate::config::theme_dirs() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
if let Some(stem) = theme_stem(&entry.path()) {
names.insert(stem);
}
}
}
names.into_iter().collect()
}
pub fn load_by_name(name: &str) -> Result<Theme> {
if name == ANSI {
return Ok(ansi_theme());
}
for dir in crate::config::theme_dirs() {
let path = dir.join(format!("{name}.toml"));
if path.exists() {
let text = std::fs::read_to_string(&path)
.with_context(|| format!("reading theme {}", path.display()))?;
return parse(name, &text);
}
}
let file = BUILTINS
.get_file(format!("{name}.toml"))
.ok_or_else(|| anyhow!("unknown theme: {name}"))?;
let text = file
.contents_utf8()
.ok_or_else(|| anyhow!("bundled theme {name}.toml is not UTF-8"))?;
parse(name, text)
}
pub fn ansi_theme() -> Theme {
use Color::*;
let bold = Modifier::BOLD;
let italic = Modifier::ITALIC;
let underline = Modifier::UNDERLINED;
let mut s: HashMap<String, Style> = HashMap::new();
let mut put = |scope: &str, style: Style| {
s.insert(scope.to_string(), style);
};
let fg = |c: Color| Style::default().fg(c);
for k in ["keyword", "include", "conditional", "repeat", "exception"] {
put(k, fg(Magenta));
}
put("namespace", fg(Yellow));
put("parameter", fg(White));
put("constructor", fg(Yellow));
put("string", fg(Green));
put("string.escape", fg(LightGreen));
put("character", fg(Green));
put("number", fg(LightRed));
put("boolean", fg(LightRed));
put("constant", fg(LightRed));
put("constant.builtin", fg(LightRed).add_modifier(bold));
put("comment", fg(DarkGray).add_modifier(italic));
put("function", fg(LightBlue));
put("function.macro", fg(LightMagenta));
put("function.builtin", fg(LightBlue));
put("method", fg(LightBlue));
put("type", fg(Yellow));
put("type.builtin", fg(Magenta).add_modifier(bold));
put("variable.parameter", fg(White));
put("variable.builtin", fg(Cyan).add_modifier(bold));
put("property", fg(White));
put("field", fg(White));
put("label", fg(Yellow));
put("operator", fg(White));
put("punctuation.bracket", fg(Gray));
put("punctuation.delimiter", fg(Gray));
put("punctuation.special", fg(Yellow));
put("attribute", fg(LightMagenta));
put("tag", fg(LightBlue));
put("markup.heading", fg(LightBlue).add_modifier(bold));
put("markup.heading.marker", fg(Magenta));
put("markup.list", fg(DarkGray));
put("markup.raw", fg(Green));
put("markup.link.url", fg(Cyan).add_modifier(underline));
put("markup.link.label", fg(LightBlue));
put("diff.plus", fg(Green));
put("diff.minus", fg(Red));
Theme {
name: ANSI.to_string(),
scopes: s,
}
}
fn theme_stem(path: &Path) -> Option<String> {
if path.extension()?.to_str()? != "toml" {
return None;
}
Some(path.file_stem()?.to_str()?.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_bundled_theme_parses() {
for file in BUILTINS.files() {
let Some(stem) = theme_stem(file.path()) else {
continue;
};
let text = file.contents_utf8().expect("bundled theme is UTF-8");
parse(&stem, text).unwrap_or_else(|e| panic!("bundled theme {stem}: {e}"));
}
}
#[test]
fn bundled_themes_resolve_core_scopes() {
for file in BUILTINS.files() {
let Some(stem) = theme_stem(file.path()) else {
continue;
};
let text = file.contents_utf8().unwrap();
let theme = parse(&stem, text).unwrap();
for scope in ["keyword", "string", "comment", "function", "type"] {
assert!(
theme.style_for(scope).fg.is_some(),
"theme {stem}: scope `{scope}` resolved to no color \
(palette typo?)"
);
}
assert!(
theme.ui_background().is_some(),
"theme {stem}: ui.background has no bg (palette typo?)"
);
}
}
#[test]
fn ansi_is_synthesized_not_a_file() {
assert!(BUILTINS.get_file("ansi.toml").is_none());
assert!(load_by_name(ANSI).is_ok());
assert!(available().iter().any(|n| n == ANSI));
}
#[test]
fn rgb_theme_is_bundled() {
assert!(BUILTINS.get_file("catppuccin-mocha.toml").is_some());
let t = load_by_name("catppuccin-mocha").unwrap();
assert!(matches!(
t.style_for("keyword").fg,
Some(ratatui::style::Color::Rgb(..))
));
}
#[test]
fn unknown_theme_errors() {
assert!(load_by_name("no-such-theme").is_err());
}
}