1use anstyle::{AnsiColor, Color};
6
7use crate::ansi_capabilities::{ColorScheme, detect_color_scheme};
8use crate::color_policy::color_output_enabled;
9use crate::color256_theme::rgb_to_ansi256_for_theme;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum DiffTheme {
14 Dark,
15 Light,
16}
17
18impl DiffTheme {
19 pub fn detect() -> Self {
21 match detect_color_scheme() {
22 ColorScheme::Light => Self::Light,
23 ColorScheme::Dark | ColorScheme::Unknown => Self::Dark,
24 }
25 }
26
27 pub fn is_light(self) -> bool {
28 self == Self::Light
29 }
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum DiffColorLevel {
35 TrueColor,
36 Ansi256,
37 Ansi16,
38}
39
40impl DiffColorLevel {
41 pub fn detect() -> Self {
43 if !color_output_enabled() {
47 return Self::Ansi16;
48 }
49 let colorterm = std::env::var("COLORTERM").unwrap_or_default();
50 let term = std::env::var("TERM").unwrap_or_default();
51 let term_program = std::env::var("TERM_PROGRAM").ok();
52 let has_wt_session = std::env::var_os("WT_SESSION").is_some();
53 let has_force_color_override = std::env::var_os("FORCE_COLOR").is_some();
54
55 diff_color_level_for_terminal(
56 base_diff_color_level(&colorterm, &term),
57 term_program.as_deref(),
58 has_wt_session,
59 has_force_color_override,
60 )
61 }
62}
63
64fn base_diff_color_level(colorterm: &str, term: &str) -> DiffColorLevel {
65 let colorterm = colorterm.to_ascii_lowercase();
66 let term = term.to_ascii_lowercase();
67
68 if colorterm.contains("truecolor") || colorterm.contains("24bit") {
69 DiffColorLevel::TrueColor
70 } else if term.contains("256") {
71 DiffColorLevel::Ansi256
72 } else {
73 DiffColorLevel::Ansi16
74 }
75}
76
77fn diff_color_level_for_terminal(
78 base_level: DiffColorLevel,
79 term_program: Option<&str>,
80 has_wt_session: bool,
81 has_force_color_override: bool,
82) -> DiffColorLevel {
83 if has_force_color_override {
84 return base_level;
85 }
86
87 if has_wt_session || (base_level == DiffColorLevel::Ansi16 && is_windows_terminal(term_program)) {
88 return DiffColorLevel::TrueColor;
89 }
90
91 base_level
92}
93
94fn is_windows_terminal(term_program: Option<&str>) -> bool {
95 let Some(program) = term_program else {
96 return false;
97 };
98
99 let normalized = program.trim().to_ascii_lowercase();
100 normalized.contains("windows_terminal") || normalized.contains("windows terminal")
101}
102
103fn capability_color(rgb: (u8, u8, u8), theme: DiffTheme, level: DiffColorLevel) -> Color {
106 match level {
107 DiffColorLevel::TrueColor => Color::Rgb(anstyle::RgbColor(rgb.0, rgb.1, rgb.2)),
108 DiffColorLevel::Ansi256 => {
109 Color::Ansi256(anstyle::Ansi256Color(rgb_to_ansi256_for_theme(rgb.0, rgb.1, rgb.2, theme.is_light())))
110 }
111 DiffColorLevel::Ansi16 => Color::Rgb(anstyle::RgbColor(rgb.0, rgb.1, rgb.2)),
114 }
115}
116
117pub fn diff_add_fg(theme: DiffTheme, level: DiffColorLevel) -> Color {
119 match level {
120 DiffColorLevel::Ansi16 => Color::Ansi(if theme.is_light() {
121 AnsiColor::Green
122 } else {
123 AnsiColor::BrightGreen
124 }),
125 DiffColorLevel::TrueColor | DiffColorLevel::Ansi256 => {
126 capability_color(if theme.is_light() { (0, 92, 43) } else { (85, 255, 85) }, theme, level)
127 }
128 }
129}
130
131pub fn diff_del_fg(theme: DiffTheme, level: DiffColorLevel) -> Color {
133 match level {
134 DiffColorLevel::Ansi16 => Color::Ansi(if theme.is_light() {
135 AnsiColor::Red
136 } else {
137 AnsiColor::BrightRed
138 }),
139 DiffColorLevel::TrueColor | DiffColorLevel::Ansi256 => capability_color(
140 if theme.is_light() {
141 (140, 20, 25)
142 } else {
143 (255, 180, 180)
144 },
145 theme,
146 level,
147 ),
148 }
149}
150
151pub fn diff_gutter_fg(theme: DiffTheme, level: DiffColorLevel) -> Color {
153 match level {
154 DiffColorLevel::Ansi16 => Color::Ansi(if theme.is_light() {
155 AnsiColor::Black
156 } else {
157 AnsiColor::BrightWhite
158 }),
159 DiffColorLevel::TrueColor | DiffColorLevel::Ansi256 => capability_color(
160 if theme.is_light() {
161 (70, 80, 75)
162 } else {
163 (165, 175, 170)
166 },
167 theme,
168 level,
169 ),
170 }
171}
172
173pub fn diff_add_bg(theme: DiffTheme, level: DiffColorLevel) -> Color {
177 match theme {
178 DiffTheme::Dark => capability_color((20, 58, 45), theme, level),
179 DiffTheme::Light => capability_color((218, 246, 225), theme, level),
180 }
181}
182
183pub fn diff_del_bg(theme: DiffTheme, level: DiffColorLevel) -> Color {
185 match theme {
186 DiffTheme::Dark => capability_color((70, 38, 42), theme, level),
187 DiffTheme::Light => capability_color((255, 224, 224), theme, level),
188 }
189}
190
191pub fn diff_add_word_bg(theme: DiffTheme, level: DiffColorLevel) -> Color {
196 match theme {
197 DiffTheme::Dark => capability_color((36, 100, 70), theme, level),
198 DiffTheme::Light => capability_color((168, 230, 190), theme, level),
199 }
200}
201
202pub fn diff_del_word_bg(theme: DiffTheme, level: DiffColorLevel) -> Color {
204 match theme {
205 DiffTheme::Dark => capability_color((140, 52, 58), theme, level),
206 DiffTheme::Light => capability_color((255, 186, 186), theme, level),
207 }
208}
209
210pub fn diff_hunk_bg(theme: DiffTheme, level: DiffColorLevel) -> Color {
215 match theme {
216 DiffTheme::Dark => capability_color((30, 45, 62), theme, level),
217 DiffTheme::Light => capability_color((221, 235, 244), theme, level),
218 }
219}
220
221pub fn diff_gutter_fg_light(_level: DiffColorLevel) -> Color {
223 Color::Ansi(AnsiColor::Black)
224}
225
226pub fn diff_gutter_bg_add_light(_level: DiffColorLevel) -> Color {
228 Color::Ansi(AnsiColor::BrightGreen)
229}
230
231pub fn diff_gutter_bg_del_light(_level: DiffColorLevel) -> Color {
233 Color::Ansi(AnsiColor::BrightRed)
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn dark_add_bg_is_subtle_green_tint() {
242 let bg = diff_add_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
243 assert_eq!(bg, Color::Rgb(anstyle::RgbColor(20, 58, 45)));
244 }
245
246 #[test]
247 fn dark_del_bg_is_subtle_red_tint() {
248 let bg = diff_del_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
249 assert_eq!(bg, Color::Rgb(anstyle::RgbColor(70, 38, 42)));
250 }
251
252 #[test]
253 fn light_add_bg_is_subtle_green_tint() {
254 let bg = diff_add_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
255 assert_eq!(bg, Color::Rgb(anstyle::RgbColor(218, 246, 225)));
256 }
257
258 #[test]
259 fn light_del_bg_is_subtle_red_tint() {
260 let bg = diff_del_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
261 assert_eq!(bg, Color::Rgb(anstyle::RgbColor(255, 224, 224)));
262 }
263
264 #[test]
265 fn backgrounds_follow_terminal_capability() {
266 assert_eq!(diff_add_bg(DiffTheme::Dark, DiffColorLevel::TrueColor), Color::Rgb(anstyle::RgbColor(20, 58, 45)));
267 assert_eq!(
268 diff_del_bg(DiffTheme::Light, DiffColorLevel::TrueColor),
269 Color::Rgb(anstyle::RgbColor(255, 224, 224))
270 );
271 assert!(matches!(diff_add_bg(DiffTheme::Dark, DiffColorLevel::Ansi256), Color::Ansi256(_)));
272 assert!(matches!(diff_add_word_bg(DiffTheme::Light, DiffColorLevel::Ansi256), Color::Ansi256(_)));
273 assert!(matches!(diff_gutter_fg(DiffTheme::Dark, DiffColorLevel::Ansi256), Color::Ansi256(_)));
274 }
275
276 #[test]
277 fn wt_session_promotes_ansi16_to_truecolor() {
278 assert_eq!(diff_color_level_for_terminal(DiffColorLevel::Ansi16, None, true, false), DiffColorLevel::TrueColor);
279 }
280
281 #[test]
282 fn windows_terminal_term_program_promotes_ansi16_to_truecolor() {
283 assert_eq!(
284 diff_color_level_for_terminal(DiffColorLevel::Ansi16, Some("Windows_Terminal"), false, false),
285 DiffColorLevel::TrueColor
286 );
287 }
288
289 #[test]
290 fn non_windows_terminal_keeps_ansi16() {
291 assert_eq!(
292 diff_color_level_for_terminal(DiffColorLevel::Ansi16, Some("WezTerm"), false, false),
293 DiffColorLevel::Ansi16
294 );
295 }
296
297 #[test]
298 fn force_color_keeps_ansi16_when_wt_session_exists() {
299 assert_eq!(diff_color_level_for_terminal(DiffColorLevel::Ansi16, None, true, true), DiffColorLevel::Ansi16);
300 }
301
302 #[test]
303 fn force_color_keeps_ansi256_when_wt_session_exists() {
304 assert_eq!(diff_color_level_for_terminal(DiffColorLevel::Ansi256, None, true, true), DiffColorLevel::Ansi256);
305 }
306
307 #[test]
308 fn base_level_detects_truecolor_from_colorterm() {
309 assert_eq!(base_diff_color_level("truecolor", "xterm-256color"), DiffColorLevel::TrueColor);
310 }
311
312 #[test]
313 fn base_level_detects_ansi256_from_term() {
314 assert_eq!(base_diff_color_level("", "xterm-256color"), DiffColorLevel::Ansi256);
315 }
316
317 #[test]
318 fn base_level_falls_back_to_ansi16() {
319 assert_eq!(base_diff_color_level("", "xterm"), DiffColorLevel::Ansi16);
320 }
321}