1use anstyle::{Color, RgbColor, Style};
2use anyhow::{Context, Result, anyhow};
3use once_cell::sync::Lazy;
4use parking_lot::RwLock;
5use vtcode_config::constants::ui;
6
7use crate::color_math::{contrast_ratio, ensure_contrast, lighten};
8use crate::registry::theme_definition;
9use crate::types::{
10 ColorAccessibilityConfig, DEFAULT_THEME_ID, ThemeDefinition, ThemeStyles, ThemeValidationResult,
11};
12
13#[derive(Clone, Debug)]
14struct ActiveTheme {
15 definition: &'static ThemeDefinition,
16 styles: ThemeStyles,
17}
18
19static COLOR_CONFIG: Lazy<RwLock<ColorAccessibilityConfig>> =
20 Lazy::new(|| RwLock::new(ColorAccessibilityConfig::default()));
21
22fn current_color_config() -> impl std::ops::Deref<Target = ColorAccessibilityConfig> {
23 COLOR_CONFIG.read()
24}
25
26static ACTIVE: Lazy<RwLock<ActiveTheme>> = Lazy::new(|| {
27 let default = theme_definition(DEFAULT_THEME_ID).expect("default theme must exist");
28 let styles = default
29 .palette
30 .build_styles_with_accessibility(¤t_color_config());
31 RwLock::new(ActiveTheme {
32 definition: default,
33 styles,
34 })
35});
36
37pub fn set_color_accessibility_config(config: ColorAccessibilityConfig) {
39 *COLOR_CONFIG.write() = config;
40}
41
42pub fn get_minimum_contrast() -> f32 {
44 COLOR_CONFIG.read().minimum_contrast
45}
46
47pub fn is_bold_bright_mode() -> bool {
49 COLOR_CONFIG.read().bold_is_bright
50}
51
52pub fn is_safe_colors_only() -> bool {
54 COLOR_CONFIG.read().safe_colors_only
55}
56
57pub fn set_active_theme(theme_id: &str) -> Result<()> {
59 let id_lc = theme_id.trim().to_lowercase();
60 let theme =
61 theme_definition(id_lc.as_str()).ok_or_else(|| anyhow!("Unknown theme '{theme_id}'"))?;
62
63 let styles = theme
64 .palette
65 .build_styles_with_accessibility(¤t_color_config());
66 let mut guard = ACTIVE.write();
67 guard.definition = theme;
68 guard.styles = styles;
69 Ok(())
70}
71
72pub fn active_theme_id() -> String {
74 ACTIVE.read().definition.id.to_string()
75}
76
77pub fn active_theme_label() -> String {
79 ACTIVE.read().definition.label.to_string()
80}
81
82pub fn active_styles() -> ThemeStyles {
84 ACTIVE.read().styles.clone()
85}
86
87pub fn banner_color() -> RgbColor {
89 let guard = ACTIVE.read();
90 let accent = guard.definition.palette.logo_accent;
91 let secondary = guard.definition.palette.secondary_accent;
92 let background = guard.definition.palette.background;
93 drop(guard);
94
95 let min_contrast = get_minimum_contrast();
96 let candidate = lighten(accent, ui::THEME_LOGO_ACCENT_BANNER_LIGHTEN_RATIO);
97 ensure_contrast(
98 candidate,
99 background,
100 min_contrast,
101 &[
102 lighten(accent, ui::THEME_PRIMARY_STATUS_SECONDARY_LIGHTEN_RATIO),
103 lighten(
104 secondary,
105 ui::THEME_LOGO_ACCENT_BANNER_SECONDARY_LIGHTEN_RATIO,
106 ),
107 accent,
108 ],
109 )
110}
111
112pub fn banner_style() -> Style {
114 let accent = banner_color();
115 Style::new().fg_color(Some(Color::Rgb(accent))).bold()
116}
117
118pub fn logo_accent_color() -> RgbColor {
120 ACTIVE.read().definition.palette.logo_accent
121}
122
123pub fn resolve_theme(preferred: Option<String>) -> String {
125 preferred
126 .and_then(|candidate| {
127 let trimmed = candidate.trim().to_lowercase();
128 if trimmed.is_empty() {
129 None
130 } else if theme_definition(trimmed.as_str()).is_some() {
131 Some(trimmed)
132 } else {
133 None
134 }
135 })
136 .unwrap_or_else(|| DEFAULT_THEME_ID.to_string())
137}
138
139pub fn ensure_theme(theme_id: &str) -> Result<&'static str> {
141 theme_definition(theme_id)
142 .map(|definition| definition.label)
143 .context("Theme not found")
144}
145
146pub fn rebuild_active_styles() {
148 let mut guard = ACTIVE.write();
149 guard.styles = guard
150 .definition
151 .palette
152 .build_styles_with_accessibility(¤t_color_config());
153}
154
155pub fn validate_theme_contrast(theme_id: &str) -> ThemeValidationResult {
157 let mut result = ThemeValidationResult {
158 is_valid: true,
159 warnings: Vec::new(),
160 errors: Vec::new(),
161 };
162
163 let theme = match theme_definition(theme_id) {
164 Some(theme) => theme,
165 None => {
166 result.is_valid = false;
167 result.errors.push(format!("Unknown theme: {}", theme_id));
168 return result;
169 }
170 };
171
172 let palette = &theme.palette;
173 let bg = palette.background;
174 let min_contrast = get_minimum_contrast();
175
176 for (name, color) in [
177 ("foreground", palette.foreground),
178 ("primary_accent", palette.primary_accent),
179 ("secondary_accent", palette.secondary_accent),
180 ("alert", palette.alert),
181 ("logo_accent", palette.logo_accent),
182 ] {
183 let ratio = contrast_ratio(color, bg);
184 if ratio < min_contrast {
185 result.warnings.push(format!(
186 "{} ({:02X}{:02X}{:02X}) has contrast ratio {:.2} < {:.1} against background",
187 name, color.0, color.1, color.2, ratio, min_contrast
188 ));
189 }
190 }
191
192 result
193}