use crate::style::Style;
use std::collections::HashMap;
use std::sync::{Arc, OnceLock, RwLock};
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct HlGroup(pub u32);
struct HlGroupRegistry {
name_to_id: HashMap<String, HlGroup>,
id_to_name: Vec<String>,
}
impl HlGroupRegistry {
fn new() -> Self {
Self {
name_to_id: HashMap::new(),
id_to_name: Vec::new(),
}
}
fn intern(&mut self, name: &str) -> HlGroup {
if let Some(id) = self.name_to_id.get(name) {
return *id;
}
let id = HlGroup(self.id_to_name.len() as u32);
self.name_to_id.insert(name.to_string(), id);
self.id_to_name.push(name.to_string());
id
}
}
fn registry() -> &'static RwLock<HlGroupRegistry> {
static REG: OnceLock<RwLock<HlGroupRegistry>> = OnceLock::new();
REG.get_or_init(|| RwLock::new(HlGroupRegistry::new()))
}
pub fn intern(name: &str) -> HlGroup {
if let Some(id) = registry().read().unwrap().name_to_id.get(name).copied() {
return id;
}
registry().write().unwrap().intern(name)
}
pub fn name_of(g: HlGroup) -> Option<String> {
registry()
.read()
.unwrap()
.id_to_name
.get(g.0 as usize)
.cloned()
}
pub fn intern_anonymous_style(style: Style) -> HlGroup {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut h = DefaultHasher::new();
style.hash(&mut h);
let style_hash = h.finish();
if let Some(&id) = anon_hash_to_group().read().unwrap().get(&style_hash) {
return id;
}
let key = format!("__anon__/{:016x}", style_hash);
let id = intern(&key);
anon_hash_to_group().write().unwrap().insert(style_hash, id);
anon_styles().write().unwrap().insert(id, style);
id
}
fn anon_styles() -> &'static RwLock<HashMap<HlGroup, Style>> {
static MAP: OnceLock<RwLock<HashMap<HlGroup, Style>>> = OnceLock::new();
MAP.get_or_init(|| RwLock::new(HashMap::new()))
}
fn anon_hash_to_group() -> &'static RwLock<HashMap<u64, HlGroup>> {
static MAP: OnceLock<RwLock<HashMap<u64, HlGroup>>> = OnceLock::new();
MAP.get_or_init(|| RwLock::new(HashMap::new()))
}
fn anon_resolve(id: HlGroup) -> Option<Style> {
anon_styles().read().unwrap().get(&id).copied()
}
pub fn reset_for_test() {
let mut r = registry().write().unwrap();
r.name_to_id.clear();
r.id_to_name.clear();
anon_styles().write().unwrap().clear();
anon_hash_to_group().write().unwrap().clear();
}
pub fn registry_len() -> usize {
registry().read().unwrap().id_to_name.len()
}
pub fn registry_counts() -> (usize, usize) {
let named = registry().read().unwrap().id_to_name.len();
let anon = anon_styles().read().unwrap().len();
(named, anon)
}
pub fn names_since(from_idx: usize) -> Vec<String> {
let r = registry().read().unwrap();
r.id_to_name.get(from_idx..).unwrap_or(&[]).to_vec()
}
#[derive(Debug, Clone, Default)]
pub struct Theme {
styles: HashMap<HlGroup, Style>,
is_light: bool,
}
impl Theme {
pub fn new() -> Self {
Self::default()
}
pub fn set(&mut self, name: impl Into<String>, style: Style) {
let id = intern(&name.into());
self.styles.insert(id, style);
}
pub fn get(&self, name: &str) -> Style {
self.resolve(intern(name))
}
pub fn resolve(&self, hl: HlGroup) -> Style {
if let Some(style) = self.styles.get(&hl).copied() {
return style;
}
anon_resolve(hl).unwrap_or_default()
}
pub fn id_for(&self, name: &str) -> HlGroup {
intern(name)
}
pub fn contains(&self, hl: HlGroup) -> bool {
self.styles.contains_key(&hl)
}
pub fn is_light(&self) -> bool {
self.is_light
}
pub fn set_light(&mut self, light: bool) {
self.is_light = light;
}
pub fn iter(&self) -> impl Iterator<Item = (HlGroup, &Style)> {
self.styles.iter().map(|(k, v)| (*k, v))
}
pub fn len(&self) -> usize {
self.styles.len()
}
pub fn is_empty(&self) -> bool {
self.styles.is_empty()
}
}
fn active_slot() -> &'static RwLock<Arc<Theme>> {
static SLOT: OnceLock<RwLock<Arc<Theme>>> = OnceLock::new();
SLOT.get_or_init(|| RwLock::new(Arc::new(Theme::new())))
}
pub fn active() -> Arc<Theme> {
active_slot().read().unwrap().clone()
}
pub fn set_active(theme: Arc<Theme>) {
*active_slot().write().unwrap() = theme;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Color;
#[test]
fn unknown_name_returns_default() {
let t = Theme::new();
assert_eq!(t.get("Nonexistent"), Style::default());
}
#[test]
fn set_and_get_round_trip() {
let mut t = Theme::new();
let s = Style {
fg: Some(Color::Red),
bold: true,
..Style::default()
};
t.set("Error", s);
assert_eq!(t.get("Error"), s);
}
#[test]
fn set_overwrites_existing_set() {
let mut t = Theme::new();
t.set("X", Style::new().bg(Color::AnsiValue(1)));
let direct = Style::new().bg(Color::AnsiValue(2));
t.set("X", direct);
assert_eq!(t.get("X"), direct);
}
#[test]
fn light_flag_round_trips() {
let mut t = Theme::new();
assert!(!t.is_light());
t.set_light(true);
assert!(t.is_light());
}
#[test]
fn intern_returns_same_id_for_same_name() {
assert_eq!(
intern("style_audit_intern_a"),
intern("style_audit_intern_a")
);
}
#[test]
fn intern_returns_different_ids_for_different_names() {
assert_ne!(
intern("style_audit_intern_b"),
intern("style_audit_intern_c")
);
}
#[test]
fn name_of_round_trips_interned_id() {
let id = intern("style_audit_name_of");
assert_eq!(name_of(id), Some("style_audit_name_of".to_string()));
}
#[test]
fn name_of_returns_none_for_unminted_id() {
assert_eq!(name_of(HlGroup(u32::MAX)), None);
}
#[test]
fn intern_anonymous_style_returns_same_id_for_equal_styles() {
let s = Style::new().fg(Color::Red).bold();
assert_eq!(intern_anonymous_style(s), intern_anonymous_style(s));
}
#[test]
fn intern_anonymous_style_returns_different_ids_for_distinct_styles() {
let s1 = Style::new().fg(Color::Red).bold();
let s2 = Style::new().fg(Color::Blue).bold();
assert_ne!(intern_anonymous_style(s1), intern_anonymous_style(s2));
}
#[test]
fn theme_resolves_anonymous_style_via_fallthrough() {
let t = Theme::new();
let style = Style::new().fg(Color::Cyan).italic();
let id = intern_anonymous_style(style);
assert_eq!(t.resolve(id), style);
}
#[test]
fn contains_reports_set_names_only() {
let mut t = Theme::new();
t.set("style_audit_contains_a", Style::new().bold());
assert!(t.contains(t.id_for("style_audit_contains_a")));
assert!(!t.contains(t.id_for("style_audit_contains_unknown")));
}
}