mod builtins;
mod parse;
use std::collections::HashMap;
use std::sync::{Arc, OnceLock, RwLock};
use ratatui::style::{Color, Modifier, Style};
pub use builtins::{ANSI, available, load_by_name};
pub use parse::parse;
#[derive(Debug, Clone)]
pub struct Theme {
pub name: String,
scopes: HashMap<String, Style>,
}
impl Theme {
pub fn style_for(&self, capture: &str) -> Style {
self.lookup(capture).unwrap_or_default()
}
fn lookup(&self, scope: &str) -> Option<Style> {
let mut candidate = scope;
loop {
if let Some(style) = self.scopes.get(candidate) {
return Some(*style);
}
match candidate.rfind('.') {
Some(i) => candidate = &candidate[..i],
None => return None,
}
}
}
fn ui(&self, scope: &str, fallback: Style) -> Style {
self.lookup(scope).unwrap_or(fallback)
}
fn ui_exact(&self, scope: &str, fallback: Style) -> Style {
self.scopes.get(scope).copied().unwrap_or(fallback)
}
}
impl Theme {
pub fn ui_background(&self) -> Option<Style> {
let style = self.lookup("ui.background")?;
style.bg.is_some().then_some(style)
}
pub fn ui_selection(&self) -> Style {
self.ui("ui.selection.primary", Style::default().bg(Color::DarkGray))
}
pub fn ui_search(&self) -> Style {
self.ui("ui.search", Style::default().bg(Color::DarkGray))
}
pub fn ui_linenr(&self) -> Style {
self.ui("ui.linenr", Style::default().fg(Color::DarkGray))
}
pub fn ui_linenr_selected(&self) -> Style {
self.ui("ui.linenr.selected", Style::default().fg(Color::Reset))
}
pub fn fold_marker(&self) -> Style {
self.ui(
"ui.fold",
Style::default()
.fg(Color::DarkGray)
.add_modifier(Modifier::ITALIC),
)
}
pub fn ui_statusline(&self) -> Style {
self.ui("ui.statusline", Style::default().bg(Color::DarkGray))
}
pub fn ui_popup(&self) -> Style {
self.ui("ui.popup", Style::default().bg(Color::Reset))
}
pub fn ui_popup_border(&self) -> Style {
self.ui_exact("ui.popup.border", Style::default().fg(Color::Gray))
}
pub fn ui_menu_selected(&self) -> Style {
self.ui(
"ui.menu.selected",
Style::default().add_modifier(Modifier::REVERSED),
)
}
}
fn cell() -> &'static RwLock<Arc<Theme>> {
static ACTIVE: OnceLock<RwLock<Arc<Theme>>> = OnceLock::new();
ACTIVE.get_or_init(|| RwLock::new(Arc::new(builtins::ansi_theme())))
}
pub fn active() -> Arc<Theme> {
cell().read().unwrap().clone()
}
pub fn set_active(theme: Theme) {
*cell().write().unwrap() = Arc::new(theme);
}
pub fn active_name() -> String {
cell().read().unwrap().name.clone()
}
pub fn style_for(capture: &str) -> Style {
active().style_for(capture)
}
#[cfg(test)]
mod tests {
use super::*;
fn theme_with(pairs: &[(&str, Style)]) -> Theme {
Theme {
name: "t".into(),
scopes: pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect(),
}
}
#[test]
fn exact_and_prefix_lookup() {
let t = theme_with(&[("function", Style::default().fg(Color::Blue))]);
assert_eq!(t.style_for("function").fg, Some(Color::Blue));
assert_eq!(t.style_for("function.method").fg, Some(Color::Blue));
}
#[test]
fn unknown_capture_is_default() {
let t = theme_with(&[]);
assert_eq!(t.style_for("nope"), Style::default());
}
#[test]
fn ui_accessor_falls_back_when_absent() {
let t = theme_with(&[]);
assert_eq!(t.ui_selection().bg, Some(Color::DarkGray));
}
#[test]
fn ui_accessor_prefers_theme_entry() {
let t = theme_with(&[("ui.selection", Style::default().bg(Color::Red))]);
assert_eq!(t.ui_selection().bg, Some(Color::Red));
}
}