use ratatui::style::Color;
use saphyr::{LoadableYamlNode, Yaml};
use crate::settings::Settings;
const DARK_YAML: &str = include_str!("../data/themes/dark.yaml");
const LIGHT_YAML: &str = include_str!("../data/themes/light.yaml");
#[derive(Debug, Clone)]
pub struct Theme {
bg: Color,
bg_active: Color,
bg_inactive: Color,
fg: Color,
fg_dim: Color,
fg_active: Color,
accent: Color,
selection_bg: Color,
selection_fg: Color,
gutter_bg: Color,
level_error: Color,
level_warn: Color,
level_info: Color,
level_success: Color,
level_debug: Color,
level_trace: Color,
tree_dir: Color,
status_bar_bg: Color,
status_bar_fg: Color,
}
impl Theme {
pub fn bg(&self) -> Color { self.bg }
pub fn bg_active(&self) -> Color { self.bg_active }
pub fn bg_inactive(&self) -> Color { self.bg_inactive }
pub fn fg(&self) -> Color { self.fg }
pub fn fg_dim(&self) -> Color { self.fg_dim }
pub fn fg_active(&self) -> Color { self.fg_active }
pub fn accent(&self) -> Color { self.accent }
pub fn selection_bg(&self) -> Color { self.selection_bg }
pub fn selection_fg(&self) -> Color { self.selection_fg }
pub fn gutter_bg(&self) -> Color { self.gutter_bg }
pub fn level_error(&self) -> Color { self.level_error }
pub fn level_warn(&self) -> Color { self.level_warn }
pub fn level_info(&self) -> Color { self.level_info }
pub fn level_success(&self) -> Color { self.level_success }
pub fn level_debug(&self) -> Color { self.level_debug }
pub fn level_trace(&self) -> Color { self.level_trace }
pub fn tree_dir(&self) -> Color { self.tree_dir }
pub fn status_bar_bg(&self) -> Color { self.status_bar_bg }
pub fn status_bar_fg(&self) -> Color { self.status_bar_fg }
pub fn resolve(settings: &Settings) -> Self {
let theme_name = settings
.get_optional::<String>("theme.name")
.cloned()
.unwrap_or_else(|| "dark".to_owned());
let name: &str = &theme_name;
let mut theme = Self::load_named(name, settings);
macro_rules! override_key {
($field:ident) => {
let key = concat!("theme.colors.", stringify!($field));
if let Some(val) = settings.get_optional::<String>(key) {
theme.$field = parse_color(val);
}
};
}
override_key!(bg);
override_key!(bg_active);
override_key!(bg_inactive);
override_key!(fg);
override_key!(fg_dim);
override_key!(fg_active);
override_key!(accent);
override_key!(selection_bg);
override_key!(selection_fg);
override_key!(gutter_bg);
override_key!(level_error);
override_key!(level_warn);
override_key!(level_info);
override_key!(level_success);
override_key!(level_debug);
override_key!(level_trace);
override_key!(tree_dir);
override_key!(status_bar_bg);
override_key!(status_bar_fg);
theme
}
fn load_named(name: &str, settings: &Settings) -> Self {
let yaml_text = Self::find_external_yaml(name, settings)
.or_else(|| embedded_yaml(name))
.unwrap_or_else(|| {
log::warn!("Theme '{}' not found, falling back to 'dark'", name);
DARK_YAML.to_owned()
});
Self::parse_yaml_with_inheritance(&yaml_text, settings)
}
fn find_external_yaml(name: &str, settings: &Settings) -> Option<String> {
let filename = format!("{}.yaml", name);
let project_theme = settings.project_config_dir().join("themes").join(&filename);
if project_theme.exists()
&& let Ok(text) = std::fs::read_to_string(&project_theme) {
return Some(text);
}
let global_theme = settings.global_config_dir().join("themes").join(&filename);
if global_theme.exists()
&& let Ok(text) = std::fs::read_to_string(&global_theme) {
return Some(text);
}
None
}
fn parse_yaml_with_inheritance(yaml_text: &str, settings: &Settings) -> Self {
let map = load_yaml_map(yaml_text);
let parent = map
.get("extends")
.and_then(|v| embedded_yaml(v.as_str()));
let mut base = if let Some(parent_yaml) = parent {
Self::from_yaml_map(&load_yaml_map(&parent_yaml))
} else {
Self::dark_fallback()
};
Self::apply_map_to(&map, &mut base, settings);
base
}
fn from_yaml_map(map: &std::collections::HashMap<String, String>) -> Self {
let get = |key: &str| {
map.get(key)
.map(|s| parse_color(s))
.unwrap_or(Color::Reset)
};
Self {
bg: get("bg"),
bg_active: get("bg_active"),
bg_inactive: get("bg_inactive"),
fg: get("fg"),
fg_dim: get("fg_dim"),
fg_active: get("fg_active"),
accent: get("accent"),
selection_bg: get("selection_bg"),
selection_fg: get("selection_fg"),
gutter_bg: get("gutter_bg"),
level_error: get("level_error"),
level_warn: get("level_warn"),
level_info: get("level_info"),
level_success: get("level_success"),
level_debug: get("level_debug"),
level_trace: get("level_trace"),
tree_dir: get("tree_dir"),
status_bar_bg: get("status_bar_bg"),
status_bar_fg: get("status_bar_fg"),
}
}
fn apply_map_to(
map: &std::collections::HashMap<String, String>,
base: &mut Self,
_settings: &Settings,
) {
macro_rules! apply {
($field:ident) => {
if let Some(v) = map.get(stringify!($field)) {
base.$field = parse_color(v);
}
};
}
apply!(bg);
apply!(bg_active);
apply!(bg_inactive);
apply!(fg);
apply!(fg_dim);
apply!(fg_active);
apply!(accent);
apply!(selection_bg);
apply!(selection_fg);
apply!(gutter_bg);
apply!(level_error);
apply!(level_warn);
apply!(level_info);
apply!(level_success);
apply!(level_debug);
apply!(level_trace);
apply!(tree_dir);
apply!(status_bar_bg);
apply!(status_bar_fg);
}
fn dark_fallback() -> Self {
Self::from_yaml_map(&load_yaml_map(DARK_YAML))
}
}
fn embedded_yaml(name: &str) -> Option<String> {
match name {
"dark" => Some(DARK_YAML.to_owned()),
"light" => Some(LIGHT_YAML.to_owned()),
_ => None,
}
}
fn load_yaml_map(text: &str) -> std::collections::HashMap<String, String> {
let mut map = std::collections::HashMap::new();
let nodes: Vec<Yaml> = match Yaml::load_from_str(text) {
Ok(n) => n,
Err(e) => {
log::error!("Failed to parse theme YAML: {e}");
return map;
}
};
if let Some(Yaml::Mapping(mapping)) = nodes.first() {
for (k, v) in mapping.iter() {
if let (Some(key), Some(val)) = (k.as_str(), v.as_str()) {
map.insert(key.to_owned(), val.to_owned());
}
}
}
map
}
pub fn parse_color(s: &str) -> Color {
let s = s.trim();
if s.eq_ignore_ascii_case("default") || s.eq_ignore_ascii_case("reset") {
return Color::Reset;
}
if let Some(hex) = s.strip_prefix('#')
&& hex.len() == 6
&& let (Ok(r), Ok(g), Ok(b)) = (
u8::from_str_radix(&hex[0..2], 16),
u8::from_str_radix(&hex[2..4], 16),
u8::from_str_radix(&hex[4..6], 16),
) {
return Color::Rgb(r, g, b);
}
match s.to_ascii_lowercase().as_str() {
"black" => Color::Black,
"red" => Color::Red,
"green" => Color::Green,
"yellow" => Color::Yellow,
"blue" => Color::Blue,
"magenta" => Color::Magenta,
"cyan" => Color::Cyan,
"white" => Color::White,
"gray" | "grey" => Color::Gray,
"darkgray" | "darkgrey" | "dark_gray" | "dark_grey" => Color::DarkGray,
other => {
log::warn!("Unknown colour '{}', using Reset", other);
Color::Reset
}
}
}