vtcode_commons/
diff_theme.rs1use anstyle::{AnsiColor, Color};
6
7use crate::ansi_capabilities::{ColorScheme, detect_color_scheme};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum DiffTheme {
12 Dark,
13 Light,
14}
15
16impl DiffTheme {
17 pub fn detect() -> Self {
19 match detect_color_scheme() {
20 ColorScheme::Light => Self::Light,
21 ColorScheme::Dark | ColorScheme::Unknown => Self::Dark,
22 }
23 }
24
25 pub fn is_light(self) -> bool {
26 self == Self::Light
27 }
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum DiffColorLevel {
33 TrueColor,
34 Ansi256,
35 Ansi16,
36}
37
38impl DiffColorLevel {
39 pub fn detect() -> Self {
41 let colorterm = std::env::var("COLORTERM").unwrap_or_default();
42 let term = std::env::var("TERM").unwrap_or_default();
43 let term_program = std::env::var("TERM_PROGRAM").ok();
44 let has_wt_session = std::env::var_os("WT_SESSION").is_some();
45 let has_force_color_override = std::env::var_os("FORCE_COLOR").is_some();
46
47 diff_color_level_for_terminal(
48 base_diff_color_level(&colorterm, &term),
49 term_program.as_deref(),
50 has_wt_session,
51 has_force_color_override,
52 )
53 }
54}
55
56fn base_diff_color_level(colorterm: &str, term: &str) -> DiffColorLevel {
57 let colorterm = colorterm.to_ascii_lowercase();
58 let term = term.to_ascii_lowercase();
59
60 if colorterm.contains("truecolor") || colorterm.contains("24bit") {
61 DiffColorLevel::TrueColor
62 } else if term.contains("256") {
63 DiffColorLevel::Ansi256
64 } else {
65 DiffColorLevel::Ansi16
66 }
67}
68
69fn diff_color_level_for_terminal(
70 base_level: DiffColorLevel,
71 term_program: Option<&str>,
72 has_wt_session: bool,
73 has_force_color_override: bool,
74) -> DiffColorLevel {
75 if has_force_color_override {
76 return base_level;
77 }
78
79 if has_wt_session || (base_level == DiffColorLevel::Ansi16 && is_windows_terminal(term_program))
80 {
81 return DiffColorLevel::TrueColor;
82 }
83
84 base_level
85}
86
87fn is_windows_terminal(term_program: Option<&str>) -> bool {
88 let Some(program) = term_program else {
89 return false;
90 };
91
92 let normalized = program.trim().to_ascii_lowercase();
93 normalized.contains("windows_terminal") || normalized.contains("windows terminal")
94}
95
96pub fn diff_add_bg(theme: DiffTheme, _level: DiffColorLevel) -> Color {
100 match theme {
101 DiffTheme::Dark => Color::Ansi(AnsiColor::Green),
102 DiffTheme::Light => Color::Ansi(AnsiColor::BrightGreen),
103 }
104}
105
106pub fn diff_del_bg(theme: DiffTheme, _level: DiffColorLevel) -> Color {
108 match theme {
109 DiffTheme::Dark => Color::Ansi(AnsiColor::Red),
110 DiffTheme::Light => Color::Ansi(AnsiColor::BrightRed),
111 }
112}
113
114pub fn diff_gutter_fg_light(_level: DiffColorLevel) -> Color {
116 Color::Ansi(AnsiColor::Black)
117}
118
119pub fn diff_gutter_bg_add_light(_level: DiffColorLevel) -> Color {
121 Color::Ansi(AnsiColor::BrightGreen)
122}
123
124pub fn diff_gutter_bg_del_light(_level: DiffColorLevel) -> Color {
126 Color::Ansi(AnsiColor::BrightRed)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn dark_add_bg_is_ansi_green() {
135 let bg = diff_add_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
136 assert_eq!(bg, Color::Ansi(AnsiColor::Green));
137 }
138
139 #[test]
140 fn dark_del_bg_is_ansi_red() {
141 let bg = diff_del_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
142 assert_eq!(bg, Color::Ansi(AnsiColor::Red));
143 }
144
145 #[test]
146 fn light_add_bg_is_ansi_bright_green() {
147 let bg = diff_add_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
148 assert_eq!(bg, Color::Ansi(AnsiColor::BrightGreen));
149 }
150
151 #[test]
152 fn light_del_bg_is_ansi_bright_red() {
153 let bg = diff_del_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
154 assert_eq!(bg, Color::Ansi(AnsiColor::BrightRed));
155 }
156
157 #[test]
158 fn all_levels_use_same_ansi_colors() {
159 for level in [
160 DiffColorLevel::TrueColor,
161 DiffColorLevel::Ansi256,
162 DiffColorLevel::Ansi16,
163 ] {
164 assert_eq!(
165 diff_add_bg(DiffTheme::Dark, level),
166 Color::Ansi(AnsiColor::Green)
167 );
168 assert_eq!(
169 diff_del_bg(DiffTheme::Dark, level),
170 Color::Ansi(AnsiColor::Red)
171 );
172 assert_eq!(
173 diff_add_bg(DiffTheme::Light, level),
174 Color::Ansi(AnsiColor::BrightGreen)
175 );
176 assert_eq!(
177 diff_del_bg(DiffTheme::Light, level),
178 Color::Ansi(AnsiColor::BrightRed)
179 );
180 }
181 }
182
183 #[test]
184 fn wt_session_promotes_ansi16_to_truecolor() {
185 assert_eq!(
186 diff_color_level_for_terminal(DiffColorLevel::Ansi16, None, true, false),
187 DiffColorLevel::TrueColor
188 );
189 }
190
191 #[test]
192 fn windows_terminal_term_program_promotes_ansi16_to_truecolor() {
193 assert_eq!(
194 diff_color_level_for_terminal(
195 DiffColorLevel::Ansi16,
196 Some("Windows_Terminal"),
197 false,
198 false
199 ),
200 DiffColorLevel::TrueColor
201 );
202 }
203
204 #[test]
205 fn non_windows_terminal_keeps_ansi16() {
206 assert_eq!(
207 diff_color_level_for_terminal(DiffColorLevel::Ansi16, Some("WezTerm"), false, false),
208 DiffColorLevel::Ansi16
209 );
210 }
211
212 #[test]
213 fn force_color_keeps_ansi16_when_wt_session_exists() {
214 assert_eq!(
215 diff_color_level_for_terminal(DiffColorLevel::Ansi16, None, true, true),
216 DiffColorLevel::Ansi16
217 );
218 }
219
220 #[test]
221 fn force_color_keeps_ansi256_when_wt_session_exists() {
222 assert_eq!(
223 diff_color_level_for_terminal(DiffColorLevel::Ansi256, None, true, true),
224 DiffColorLevel::Ansi256
225 );
226 }
227
228 #[test]
229 fn base_level_detects_truecolor_from_colorterm() {
230 assert_eq!(
231 base_diff_color_level("truecolor", "xterm-256color"),
232 DiffColorLevel::TrueColor
233 );
234 }
235
236 #[test]
237 fn base_level_detects_ansi256_from_term() {
238 assert_eq!(
239 base_diff_color_level("", "xterm-256color"),
240 DiffColorLevel::Ansi256
241 );
242 }
243
244 #[test]
245 fn base_level_falls_back_to_ansi16() {
246 assert_eq!(base_diff_color_level("", "xterm"), DiffColorLevel::Ansi16);
247 }
248}