use crate::core::Rgba;
use crate::view::theme::Theme;
use crate::view::themes::theme_by_name;
use serde_json::{Map, Value};
use std::path::Path;
use std::sync::RwLock;
static USER_THEMES: RwLock<Vec<Theme>> = RwLock::new(Vec::new());
type ColorField = (&'static str, fn(&mut Theme) -> &mut Rgba);
const COLORS: &[ColorField] = &[
("canvas", |t| &mut t.canvas),
("surface", |t| &mut t.surface),
("elevated_surface", |t| &mut t.elevated_surface),
("border", |t| &mut t.border),
("border_subtle", |t| &mut t.border_subtle),
("border_focused", |t| &mut t.border_focused),
("text", |t| &mut t.text),
("text_muted", |t| &mut t.text_muted),
("accent", |t| &mut t.accent),
("accent_hover", |t| &mut t.accent_hover),
("accent_text", |t| &mut t.accent_text),
("danger", |t| &mut t.danger),
("danger_hover", |t| &mut t.danger_hover),
("danger_text", |t| &mut t.danger_text),
("warning", |t| &mut t.warning),
("success", |t| &mut t.success),
("created", |t| &mut t.created),
("modified", |t| &mut t.modified),
("deleted", |t| &mut t.deleted),
("conflict", |t| &mut t.conflict),
("hover", |t| &mut t.hover),
("active", |t| &mut t.active),
("selection", |t| &mut t.selection),
("shadow", |t| &mut t.shadow),
];
const SYNTAX: &[ColorField] = &[
("keyword", |t| &mut t.syntax.keyword),
("function", |t| &mut t.syntax.function),
("type", |t| &mut t.syntax.type_name),
("string", |t| &mut t.syntax.string),
("comment", |t| &mut t.syntax.comment),
("constant", |t| &mut t.syntax.constant),
("attribute", |t| &mut t.syntax.attribute),
("operator", |t| &mut t.syntax.operator),
("property", |t| &mut t.syntax.property),
("tag", |t| &mut t.syntax.tag),
("title", |t| &mut t.syntax.title),
("link", |t| &mut t.syntax.link),
];
pub fn parse_color(text: &str) -> Result<Rgba, String> {
let digits = text.strip_prefix('#').ok_or_else(|| format!("«{text}» debe empezar con #"))?;
let value = u32::from_str_radix(digits, 16).map_err(|_| format!("«{text}» no es un color hexadecimal"))?;
match digits.len() {
6 => Ok(Rgba::hex(value)),
8 => Ok(Rgba::hex(value >> 8).with_alpha((value & 0xff) as f32 / 255.0)),
_ => Err(format!("«{text}» debe tener 6 u 8 dígitos")),
}
}
fn apply_colors(
theme: &mut Theme,
table: &[ColorField],
values: &Map<String, Value>,
section: &str,
) -> Result<(), String> {
for (key, value) in values {
let (_, field) =
table.iter().find(|(name, _)| name == key).ok_or_else(|| format!("{section}.{key}: campo desconocido"))?;
let text = value.as_str().ok_or_else(|| format!("{section}.{key}: se esperaba un texto"))?;
*field(theme) = parse_color(text).map_err(|error| format!("{section}.{key}: {error}"))?;
}
Ok(())
}
pub fn parse_theme(json: &str) -> Result<Theme, String> {
let root: Value = serde_json::from_str(json).map_err(|error| error.to_string())?;
let root = root.as_object().ok_or("se esperaba un objeto")?;
let name = root.get("name").and_then(Value::as_str).ok_or("falta «name»")?;
let dark = root.get("dark").and_then(Value::as_bool);
let base = match root.get("extends").and_then(Value::as_str) {
Some(base) => theme_by_name(base).ok_or_else(|| format!("extends: no existe el tema «{base}»"))?,
None if dark == Some(false) => Theme::light(),
None => Theme::dark(),
};
let mut theme = Theme { name: name.to_owned(), is_dark: dark.unwrap_or(base.is_dark), ..base };
if let Some(colors) = root.get("colors").and_then(Value::as_object) {
apply_colors(&mut theme, COLORS, colors, "colors")?;
}
if let Some(syntax) = root.get("syntax").and_then(Value::as_object) {
apply_colors(&mut theme, SYNTAX, syntax, "syntax")?;
}
if let Some(ansi) = root.get("ansi") {
let list = ansi.as_array().filter(|list| list.len() == 16).ok_or("ansi: se esperaban 16 colores")?;
for (slot, value) in theme.ansi.iter_mut().zip(list) {
*slot = parse_color(value.as_str().unwrap_or_default()).map_err(|error| format!("ansi: {error}"))?;
}
}
Ok(theme)
}
pub fn load_theme_dir(dir: &Path) -> (Vec<Theme>, Vec<String>) {
let Ok(entries) = std::fs::read_dir(dir) else { return (Vec::new(), Vec::new()) };
let mut paths: Vec<_> = entries.flatten().map(|entry| entry.path()).collect();
paths.retain(|path| path.extension().is_some_and(|extension| extension == "json"));
paths.sort();
let (mut themes, mut errors) = (Vec::new(), Vec::new());
for path in paths {
let file = path.file_name().map(|name| name.to_string_lossy().into_owned()).unwrap_or_default();
match std::fs::read_to_string(&path).map_err(|error| error.to_string()).and_then(|text| parse_theme(&text)) {
Ok(theme) => themes.push(theme),
Err(error) => errors.push(format!("Tema {file}: {error}")),
}
}
(themes, errors)
}
pub fn set_user_themes(themes: Vec<Theme>) {
if let Ok(mut current) = USER_THEMES.write() {
*current = themes;
}
}
pub fn user_themes() -> Vec<Theme> {
USER_THEMES.read().map(|themes| themes.clone()).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hereda_y_sobrescribe() {
let theme = parse_theme(r##"{"name":"Mio","extends":"One Dark","colors":{"accent":"#ff0000"}}"##).unwrap();
assert_eq!(theme.name, "Mio");
assert_eq!(theme.accent, Rgba::hex(0xff0000));
assert_eq!(theme.canvas, theme_by_name("One Dark").unwrap().canvas);
}
#[test]
fn color_con_alfa() {
let color = parse_color("#00ff0080").unwrap();
assert!((color.a - 128.0 / 255.0).abs() < 1e-6);
}
#[test]
fn errores_nombran_el_campo() {
let error = parse_theme(r##"{"name":"X","colors":{"accent":"rojo"}}"##).unwrap_err();
assert!(error.starts_with("colors.accent"), "{error}");
assert!(parse_theme(r##"{"name":"X","colors":{"nada":"#fff000"}}"##).unwrap_err().contains("desconocido"));
assert!(parse_theme(r##"{"name":"X","extends":"No Existe"}"##).is_err());
}
}