Skip to main content

vtcode_theme/
types.rs

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
6/// Identifier for the default theme.
7pub const DEFAULT_THEME_ID: &str = defaults::DEFAULT_THEME;
8
9const DEFAULT_MIN_CONTRAST: f32 = ui::THEME_MIN_CONTRAST_RATIO;
10
11/// Color accessibility configuration loaded from vtcode.toml.
12#[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/// Palette describing UI colors for the terminal experience.
30#[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/// Styles computed from palette colors.
252#[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/// Logical grouping of built-in themes.
280#[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/// Theme validation result.
288#[derive(Debug, Clone)]
289pub struct ThemeValidationResult {
290    pub is_valid: bool,
291    pub warnings: Vec<String>,
292    pub errors: Vec<String>,
293}