use ratatui::style::{Color, Modifier, Style};
use crate::types::{FindingType, Severity, Zone};
#[derive(Debug, Clone)]
pub struct ThemeColors {
pub name: &'static str,
pub bg: Color,
pub fg: Color,
pub border: Color,
pub border_focused: Color,
pub accent: Color,
pub muted: Color,
pub zone_green: Color,
pub zone_yellow: Color,
pub zone_red: Color,
pub severity_critical: Color,
pub severity_high: Color,
pub severity_medium: Color,
pub severity_low: Color,
pub severity_info: Color,
pub diff_added: Color,
pub diff_removed: Color,
pub diff_header: Color,
pub user_msg: Color,
pub assistant_msg: Color,
pub system_msg: Color,
pub selection_bg: Color,
pub status_bar_bg: Color,
pub status_bar_fg: Color,
pub tool_call_border: Color,
pub tool_result_ok: Color,
pub tool_result_err: Color,
pub thinking_fg: Color,
pub user_msg_bg: Color,
}
const THEMES_JSON: &str = include_str!("../data/themes.json");
#[derive(serde::Deserialize)]
#[allow(dead_code)]
struct ThemeEntry {
name: String,
aliases: Vec<String>,
syntect: String,
bg: [u8; 3], fg: [u8; 3],
border: [u8; 3], border_focused: [u8; 3],
accent: [u8; 3], muted: [u8; 3],
zone_green: [u8; 3], zone_yellow: [u8; 3], zone_red: [u8; 3],
severity_critical: [u8; 3], severity_high: [u8; 3],
severity_medium: [u8; 3], severity_low: [u8; 3], severity_info: [u8; 3],
diff_added: [u8; 3], diff_removed: [u8; 3], diff_header: [u8; 3],
user_msg: [u8; 3], assistant_msg: [u8; 3], system_msg: [u8; 3],
selection_bg: [u8; 3], status_bar_bg: [u8; 3], status_bar_fg: [u8; 3],
tool_call_border: [u8; 3], tool_result_ok: [u8; 3], tool_result_err: [u8; 3],
thinking_fg: [u8; 3],
#[serde(default = "default_user_msg_bg")]
user_msg_bg: [u8; 3],
}
const fn default_user_msg_bg() -> [u8; 3] {
[40, 42, 54]
}
const fn rgb(c: [u8; 3]) -> Color {
Color::Rgb(c[0], c[1], c[2])
}
fn load_theme_entries() -> Vec<ThemeEntry> {
serde_json::from_str(THEMES_JSON).expect("themes.json should be valid")
}
impl ThemeColors {
pub const fn palette_colors(&self) -> [Color; 8] {
[
self.bg, self.fg, self.accent, self.border,
self.zone_green, self.zone_yellow, self.zone_red, self.muted,
]
}
fn from_entry(entry: &ThemeEntry) -> Self {
Self {
name: Box::leak(entry.name.clone().into_boxed_str()),
bg: rgb(entry.bg), fg: rgb(entry.fg),
border: rgb(entry.border), border_focused: rgb(entry.border_focused),
accent: rgb(entry.accent), muted: rgb(entry.muted),
zone_green: rgb(entry.zone_green), zone_yellow: rgb(entry.zone_yellow), zone_red: rgb(entry.zone_red),
severity_critical: rgb(entry.severity_critical), severity_high: rgb(entry.severity_high),
severity_medium: rgb(entry.severity_medium), severity_low: rgb(entry.severity_low), severity_info: rgb(entry.severity_info),
diff_added: rgb(entry.diff_added), diff_removed: rgb(entry.diff_removed), diff_header: rgb(entry.diff_header),
user_msg: rgb(entry.user_msg), assistant_msg: rgb(entry.assistant_msg), system_msg: rgb(entry.system_msg),
selection_bg: rgb(entry.selection_bg), status_bar_bg: rgb(entry.status_bar_bg), status_bar_fg: rgb(entry.status_bar_fg),
tool_call_border: rgb(entry.tool_call_border), tool_result_ok: rgb(entry.tool_result_ok), tool_result_err: rgb(entry.tool_result_err),
thinking_fg: rgb(entry.thinking_fg),
user_msg_bg: rgb(entry.user_msg_bg),
}
}
pub fn dark() -> Self {
Self::from_name("dark")
}
pub fn high_contrast() -> Self {
let mut t = Self::from_name("dark");
t.name = "High Contrast";
t.border = Color::White;
t.border_focused = Color::LightCyan;
t.accent = Color::LightCyan;
t.fg = Color::White;
t.zone_green = Color::LightGreen;
t.zone_yellow = Color::LightYellow;
t.zone_red = Color::LightRed;
t.severity_critical = Color::LightRed;
t.severity_high = Color::LightRed;
t.severity_medium = Color::LightYellow;
t.severity_low = Color::LightBlue;
t.severity_info = Color::White;
t.diff_added = Color::LightGreen;
t.diff_removed = Color::LightRed;
t.diff_header = Color::LightCyan;
t.user_msg = Color::LightCyan;
t.assistant_msg = Color::LightGreen;
t.system_msg = Color::LightYellow;
t.selection_bg = Color::Rgb(80, 80, 120);
t.status_bar_bg = Color::Rgb(60, 60, 80);
t.status_bar_fg = Color::White;
t.tool_call_border = Color::LightCyan;
t.tool_result_ok = Color::LightGreen;
t.tool_result_err = Color::LightRed;
t.thinking_fg = Color::Gray;
t.user_msg_bg = Color::Rgb(40, 40, 60);
t
}
pub fn from_name(name: &str) -> Self {
let lower = name.to_lowercase();
if lower == "high contrast" || lower == "high-contrast" || lower == "high_contrast" {
return Self::high_contrast();
}
let entries = load_theme_entries();
for entry in &entries {
if entry.name.to_lowercase() == lower {
return Self::from_entry(entry);
}
for alias in &entry.aliases {
if alias.to_lowercase() == lower {
return Self::from_entry(entry);
}
}
}
Self::from_entry(&entries[0])
}
}
pub fn list_themes() -> Vec<ThemeColors> {
load_theme_entries()
.iter()
.map(ThemeColors::from_entry)
.collect()
}
static THEME: std::sync::OnceLock<std::sync::Mutex<ThemeColors>> = std::sync::OnceLock::new();
pub fn init_theme(name: &str) {
let colors = ThemeColors::from_name(name);
if let Some(mutex) = THEME.get() {
*mutex.lock().expect("theme lock") = colors;
} else {
let _ = THEME.set(std::sync::Mutex::new(colors));
}
}
pub fn theme() -> ThemeColors {
THEME
.get().map_or_else(ThemeColors::dark, |m| m.lock().expect("theme lock").clone())
}
pub fn current_theme_name() -> String {
theme().name.to_string()
}
pub fn zone_color(zone: Zone) -> Color {
let t = theme();
match zone {
Zone::Green => t.zone_green,
Zone::Yellow => t.zone_yellow,
Zone::Red => t.zone_red,
}
}
pub fn severity_color(severity: Severity) -> Color {
let t = theme();
match severity {
Severity::Critical => t.severity_critical,
Severity::High => t.severity_high,
Severity::Medium => t.severity_medium,
Severity::Low => t.severity_low,
Severity::Info => t.severity_info,
}
}
pub fn finding_type_color(ft: FindingType) -> Color {
let t = theme();
match ft {
FindingType::A => t.accent, FindingType::B => t.zone_green, FindingType::C => t.zone_yellow, }
}
pub fn border_style(focused: bool) -> Style {
let t = theme();
if focused {
Style::default().fg(t.border_focused)
} else {
Style::default().fg(t.border)
}
}
pub fn title_style() -> Style {
let t = theme();
Style::default().fg(t.accent).add_modifier(Modifier::BOLD)
}
pub fn status_bar_style() -> Style {
let t = theme();
Style::default().bg(t.status_bar_bg).fg(t.status_bar_fg)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zone_color_mapping() {
init_theme("dark");
let t = theme();
assert_eq!(zone_color(Zone::Green), t.zone_green);
assert_eq!(zone_color(Zone::Yellow), t.zone_yellow);
assert_eq!(zone_color(Zone::Red), t.zone_red);
}
#[test]
fn test_severity_color_mapping() {
init_theme("dark");
let t = theme();
assert_eq!(severity_color(Severity::Critical), t.severity_critical);
assert_eq!(severity_color(Severity::Info), t.severity_info);
}
#[test]
fn test_8_theme_presets() {
let themes = list_themes();
assert_eq!(themes.len(), 8);
assert_eq!(themes[0].name, "Complior Dark");
assert_eq!(themes[1].name, "Complior Light");
assert_eq!(themes[2].name, "Solarized Dark");
assert_eq!(themes[3].name, "Solarized Light");
assert_eq!(themes[4].name, "Dracula");
assert_eq!(themes[5].name, "Nord");
assert_eq!(themes[6].name, "Monokai");
assert_eq!(themes[7].name, "Gruvbox");
}
#[test]
fn test_theme_palette_completeness() {
for t in list_themes() {
let palette = t.palette_colors();
assert_eq!(palette.len(), 8, "Theme {} missing palette colors", t.name);
}
}
#[test]
fn test_from_name_all_variants() {
assert_eq!(ThemeColors::from_name("dark").name, "Complior Dark");
assert_eq!(ThemeColors::from_name("Dracula").name, "Dracula");
assert_eq!(ThemeColors::from_name("nord").name, "Nord");
assert_eq!(ThemeColors::from_name("unknown").name, "Complior Dark");
}
}