1use anstyle::{Color, Effects, RgbColor, Style};
2use vtcode_config::constants::{defaults, ui};
3
4use crate::color_math::{balance_text_luminance, ensure_contrast, lighten, mix};
5
6pub const DEFAULT_THEME_ID: &str = defaults::DEFAULT_THEME;
8
9const DEFAULT_MIN_CONTRAST: f32 = ui::THEME_MIN_CONTRAST_RATIO;
10
11#[derive(Clone, Debug)]
13pub struct ColorAccessibilityConfig {
14 pub minimum_contrast: f32,
15 pub bold_is_bright: bool,
16 pub safe_colors_only: bool,
17}
18
19impl Default for ColorAccessibilityConfig {
20 fn default() -> Self {
21 Self {
22 minimum_contrast: DEFAULT_MIN_CONTRAST,
23 bold_is_bright: false,
24 safe_colors_only: false,
25 }
26 }
27}
28
29#[derive(Clone, Debug)]
31pub struct ThemePalette {
32 pub primary_accent: RgbColor,
33 pub background: RgbColor,
34 pub foreground: RgbColor,
35 pub secondary_accent: RgbColor,
36 pub alert: RgbColor,
37 pub logo_accent: RgbColor,
38}
39
40impl ThemePalette {
41 fn style_from(color: RgbColor, bold: bool, bold_is_bright: bool) -> Style {
42 let mut style = Style::new().fg_color(Some(Color::Rgb(color)));
43 if bold && !bold_is_bright {
44 style = style.bold();
45 }
46 style
47 }
48
49 pub(crate) fn build_styles_with_accessibility(
50 &self,
51 accessibility: &ColorAccessibilityConfig,
52 ) -> ThemeStyles {
53 let min_contrast = accessibility.minimum_contrast;
54 let primary = self.primary_accent;
55 let background = self.background;
56 let secondary = self.secondary_accent;
57 let logo_accent = self.logo_accent;
58 let bold_is_bright = accessibility.bold_is_bright;
59
60 let fallback_light = RgbColor(
61 ui::THEME_COLOR_WHITE_RED,
62 ui::THEME_COLOR_WHITE_GREEN,
63 ui::THEME_COLOR_WHITE_BLUE,
64 );
65
66 let text_color = ensure_contrast(
67 self.foreground,
68 background,
69 min_contrast,
70 &[
71 lighten(self.foreground, ui::THEME_FOREGROUND_LIGHTEN_RATIO),
72 lighten(secondary, ui::THEME_SECONDARY_LIGHTEN_RATIO),
73 fallback_light,
74 ],
75 );
76 let text_color = balance_text_luminance(text_color, background, min_contrast);
77
78 let info_color = ensure_contrast(
79 secondary,
80 background,
81 min_contrast,
82 &[
83 lighten(secondary, ui::THEME_SECONDARY_LIGHTEN_RATIO),
84 text_color,
85 fallback_light,
86 ],
87 );
88 let info_color = balance_text_luminance(info_color, background, min_contrast);
89
90 let light_tool_color = lighten(text_color, ui::THEME_MIX_RATIO);
91 let tool_color = ensure_contrast(
92 light_tool_color,
93 background,
94 min_contrast,
95 &[
96 lighten(light_tool_color, ui::THEME_TOOL_BODY_LIGHTEN_RATIO),
97 info_color,
98 text_color,
99 ],
100 );
101 let tool_body_candidate = mix(light_tool_color, text_color, ui::THEME_TOOL_BODY_MIX_RATIO);
102 let tool_body_color = ensure_contrast(
103 tool_body_candidate,
104 background,
105 min_contrast,
106 &[
107 lighten(light_tool_color, ui::THEME_TOOL_BODY_LIGHTEN_RATIO),
108 text_color,
109 fallback_light,
110 ],
111 );
112 let tool_style = Style::new().fg_color(Some(Color::Rgb(tool_color)));
113 let tool_detail_style = Style::new().fg_color(Some(Color::Rgb(tool_body_color)));
114
115 let response_color = ensure_contrast(
116 text_color,
117 background,
118 min_contrast,
119 &[
120 lighten(text_color, ui::THEME_RESPONSE_COLOR_LIGHTEN_RATIO),
121 fallback_light,
122 ],
123 );
124 let response_color = balance_text_luminance(response_color, background, min_contrast);
125
126 let reasoning_color = ensure_contrast(
127 lighten(text_color, 0.25),
128 background,
129 min_contrast,
130 &[lighten(text_color, 0.15), text_color, fallback_light],
131 );
132 let reasoning_color = balance_text_luminance(reasoning_color, background, min_contrast);
133 let reasoning_style = Self::style_from(reasoning_color, false, bold_is_bright)
134 .effects(Effects::DIMMED | Effects::ITALIC);
135
136 let user_color = ensure_contrast(
137 lighten(secondary, ui::THEME_USER_COLOR_LIGHTEN_RATIO),
138 background,
139 min_contrast,
140 &[
141 lighten(secondary, ui::THEME_SECONDARY_USER_COLOR_LIGHTEN_RATIO),
142 info_color,
143 text_color,
144 ],
145 );
146 let user_color = balance_text_luminance(user_color, background, min_contrast);
147
148 let alert_color = ensure_contrast(
149 self.alert,
150 background,
151 min_contrast,
152 &[
153 lighten(self.alert, ui::THEME_LUMINANCE_LIGHTEN_RATIO),
154 fallback_light,
155 text_color,
156 ],
157 );
158 let alert_color = balance_text_luminance(alert_color, background, min_contrast);
159
160 let tool_output_style = Style::new();
161
162 let pty_output_candidate = lighten(tool_body_color, ui::THEME_PTY_OUTPUT_LIGHTEN_RATIO);
163 let pty_output_color = ensure_contrast(
164 pty_output_candidate,
165 background,
166 min_contrast,
167 &[
168 lighten(text_color, ui::THEME_PTY_OUTPUT_LIGHTEN_RATIO),
169 tool_body_color,
170 text_color,
171 ],
172 );
173 let pty_output_style = Style::new().fg_color(Some(Color::Rgb(pty_output_color)));
174
175 let primary_style_color = balance_text_luminance(
176 ensure_contrast(primary, background, min_contrast, &[text_color]),
177 background,
178 min_contrast,
179 );
180 let secondary_style_color = balance_text_luminance(
181 ensure_contrast(
182 secondary,
183 background,
184 min_contrast,
185 &[info_color, text_color],
186 ),
187 background,
188 min_contrast,
189 );
190 let logo_style_color = balance_text_luminance(
191 ensure_contrast(
192 logo_accent,
193 background,
194 min_contrast,
195 &[secondary_style_color, text_color],
196 ),
197 background,
198 min_contrast,
199 );
200
201 ThemeStyles {
202 info: Self::style_from(info_color, true, bold_is_bright),
203 error: Self::style_from(alert_color, true, bold_is_bright),
204 output: Self::style_from(text_color, false, bold_is_bright),
205 response: Self::style_from(response_color, false, bold_is_bright),
206 reasoning: reasoning_style,
207 tool: tool_style,
208 tool_detail: tool_detail_style,
209 tool_output: tool_output_style,
210 pty_output: pty_output_style,
211 status: Self::style_from(
212 ensure_contrast(
213 lighten(primary_style_color, ui::THEME_PRIMARY_STATUS_LIGHTEN_RATIO),
214 background,
215 min_contrast,
216 &[
217 lighten(
218 primary_style_color,
219 ui::THEME_PRIMARY_STATUS_SECONDARY_LIGHTEN_RATIO,
220 ),
221 info_color,
222 text_color,
223 ],
224 ),
225 true,
226 bold_is_bright,
227 ),
228 mcp: Self::style_from(
229 ensure_contrast(
230 lighten(logo_style_color, ui::THEME_SECONDARY_LIGHTEN_RATIO),
231 background,
232 min_contrast,
233 &[
234 lighten(logo_style_color, ui::THEME_LOGO_ACCENT_BANNER_LIGHTEN_RATIO),
235 info_color,
236 fallback_light,
237 ],
238 ),
239 true,
240 bold_is_bright,
241 ),
242 user: Self::style_from(user_color, false, bold_is_bright),
243 primary: Self::style_from(primary_style_color, false, bold_is_bright),
244 secondary: Self::style_from(secondary_style_color, false, bold_is_bright),
245 background: Color::Rgb(background),
246 foreground: Color::Rgb(text_color),
247 }
248 }
249}
250
251#[derive(Clone, Debug)]
253pub struct ThemeStyles {
254 pub info: Style,
255 pub error: Style,
256 pub output: Style,
257 pub response: Style,
258 pub reasoning: Style,
259 pub tool: Style,
260 pub tool_detail: Style,
261 pub tool_output: Style,
262 pub pty_output: Style,
263 pub status: Style,
264 pub mcp: Style,
265 pub user: Style,
266 pub primary: Style,
267 pub secondary: Style,
268 pub background: Color,
269 pub foreground: Color,
270}
271
272#[derive(Clone, Debug)]
273pub struct ThemeDefinition {
274 pub id: &'static str,
275 pub label: &'static str,
276 pub palette: ThemePalette,
277}
278
279#[derive(Clone, Debug, PartialEq, Eq)]
281pub struct ThemeSuite {
282 pub id: &'static str,
283 pub label: &'static str,
284 pub theme_ids: Vec<&'static str>,
285}
286
287#[derive(Debug, Clone)]
289pub struct ThemeValidationResult {
290 pub is_valid: bool,
291 pub warnings: Vec<String>,
292 pub errors: Vec<String>,
293}