1use anstyle::{Ansi256Color, AnsiColor, Color, RgbColor};
10
11use crate::ansi_capabilities::{ColorScheme, detect_color_scheme};
12use crate::color256_theme::adjust_index_for_theme;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum DiffTheme {
17 Dark,
18 Light,
19}
20
21impl DiffTheme {
22 pub fn detect() -> Self {
24 match detect_color_scheme() {
25 ColorScheme::Light => Self::Light,
26 ColorScheme::Dark | ColorScheme::Unknown => Self::Dark,
27 }
28 }
29
30 pub fn is_light(self) -> bool {
31 self == Self::Light
32 }
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum DiffColorLevel {
38 TrueColor,
39 Ansi256,
40 Ansi16,
41}
42
43impl DiffColorLevel {
44 pub fn detect() -> Self {
46 let colorterm = std::env::var("COLORTERM").unwrap_or_default();
47 let term = std::env::var("TERM").unwrap_or_default();
48 let term_program = std::env::var("TERM_PROGRAM").ok();
49 let has_wt_session = std::env::var_os("WT_SESSION").is_some();
50 let has_force_color_override = std::env::var_os("FORCE_COLOR").is_some();
51
52 diff_color_level_for_terminal(
53 base_diff_color_level(&colorterm, &term),
54 term_program.as_deref(),
55 has_wt_session,
56 has_force_color_override,
57 )
58 }
59}
60
61fn base_diff_color_level(colorterm: &str, term: &str) -> DiffColorLevel {
62 let colorterm = colorterm.to_ascii_lowercase();
63 let term = term.to_ascii_lowercase();
64
65 if colorterm.contains("truecolor") || colorterm.contains("24bit") {
66 DiffColorLevel::TrueColor
67 } else if term.contains("256") {
68 DiffColorLevel::Ansi256
69 } else {
70 DiffColorLevel::Ansi16
71 }
72}
73
74fn diff_color_level_for_terminal(
75 base_level: DiffColorLevel,
76 term_program: Option<&str>,
77 has_wt_session: bool,
78 has_force_color_override: bool,
79) -> DiffColorLevel {
80 if has_force_color_override {
81 return base_level;
82 }
83
84 if has_wt_session || (base_level == DiffColorLevel::Ansi16 && is_windows_terminal(term_program))
85 {
86 return DiffColorLevel::TrueColor;
87 }
88
89 base_level
90}
91
92fn is_windows_terminal(term_program: Option<&str>) -> bool {
93 let Some(program) = term_program else {
94 return false;
95 };
96
97 let normalized = program.trim().to_ascii_lowercase();
98 normalized.contains("windows_terminal") || normalized.contains("windows terminal")
99}
100
101const DARK_TC_ADD_LINE_BG: (u8, u8, u8) = (25, 45, 35); const DARK_TC_DEL_LINE_BG: (u8, u8, u8) = (90, 40, 40); const LIGHT_TC_ADD_LINE_BG: (u8, u8, u8) = (215, 240, 215); const LIGHT_TC_DEL_LINE_BG: (u8, u8, u8) = (255, 235, 235); const LIGHT_TC_ADD_NUM_BG: (u8, u8, u8) = (175, 225, 175); const LIGHT_TC_DEL_NUM_BG: (u8, u8, u8) = (250, 210, 210); const LIGHT_TC_GUTTER_FG: (u8, u8, u8) = (25, 25, 25); const DARK_256_ADD_LINE_BG: u8 = 22; const DARK_256_DEL_LINE_BG: u8 = 52; const LIGHT_256_ADD_LINE_BG: u8 = 194; const LIGHT_256_DEL_LINE_BG: u8 = 224; const LIGHT_256_ADD_NUM_BG: u8 = 157; const LIGHT_256_DEL_NUM_BG: u8 = 217; const LIGHT_256_GUTTER_FG: u8 = 236; fn rgb(t: (u8, u8, u8)) -> Color {
130 Color::Rgb(RgbColor(t.0, t.1, t.2))
131}
132
133fn indexed(i: u8, theme: DiffTheme) -> Color {
134 let adjusted = adjust_index_for_theme(i, theme.is_light());
135 Color::Ansi256(Ansi256Color(adjusted))
136}
137
138pub fn diff_add_bg(theme: DiffTheme, level: DiffColorLevel) -> Color {
140 match (theme, level) {
141 (DiffTheme::Dark, DiffColorLevel::TrueColor) => rgb(DARK_TC_ADD_LINE_BG),
142 (DiffTheme::Dark, DiffColorLevel::Ansi256) => indexed(DARK_256_ADD_LINE_BG, theme),
143 (DiffTheme::Dark, DiffColorLevel::Ansi16) => Color::Ansi(AnsiColor::Green),
144 (DiffTheme::Light, DiffColorLevel::TrueColor) => rgb(LIGHT_TC_ADD_LINE_BG),
145 (DiffTheme::Light, DiffColorLevel::Ansi256) => indexed(LIGHT_256_ADD_LINE_BG, theme),
146 (DiffTheme::Light, DiffColorLevel::Ansi16) => Color::Ansi(AnsiColor::BrightGreen),
147 }
148}
149
150pub fn diff_del_bg(theme: DiffTheme, level: DiffColorLevel) -> Color {
152 match (theme, level) {
153 (DiffTheme::Dark, DiffColorLevel::TrueColor) => rgb(DARK_TC_DEL_LINE_BG),
154 (DiffTheme::Dark, DiffColorLevel::Ansi256) => indexed(DARK_256_DEL_LINE_BG, theme),
155 (DiffTheme::Dark, DiffColorLevel::Ansi16) => Color::Ansi(AnsiColor::Red),
156 (DiffTheme::Light, DiffColorLevel::TrueColor) => rgb(LIGHT_TC_DEL_LINE_BG),
157 (DiffTheme::Light, DiffColorLevel::Ansi256) => indexed(LIGHT_256_DEL_LINE_BG, theme),
158 (DiffTheme::Light, DiffColorLevel::Ansi16) => Color::Ansi(AnsiColor::BrightRed),
159 }
160}
161
162pub fn diff_gutter_fg_light(level: DiffColorLevel) -> Color {
164 match level {
165 DiffColorLevel::TrueColor => rgb(LIGHT_TC_GUTTER_FG),
166 DiffColorLevel::Ansi256 => indexed(LIGHT_256_GUTTER_FG, DiffTheme::Light),
167 DiffColorLevel::Ansi16 => Color::Ansi(AnsiColor::Black),
168 }
169}
170
171pub fn diff_gutter_bg_add_light(level: DiffColorLevel) -> Color {
173 match level {
174 DiffColorLevel::TrueColor => rgb(LIGHT_TC_ADD_NUM_BG),
175 DiffColorLevel::Ansi256 => indexed(LIGHT_256_ADD_NUM_BG, DiffTheme::Light),
176 DiffColorLevel::Ansi16 => Color::Ansi(AnsiColor::BrightGreen),
177 }
178}
179
180pub fn diff_gutter_bg_del_light(level: DiffColorLevel) -> Color {
182 match level {
183 DiffColorLevel::TrueColor => rgb(LIGHT_TC_DEL_NUM_BG),
184 DiffColorLevel::Ansi256 => indexed(LIGHT_256_DEL_NUM_BG, DiffTheme::Light),
185 DiffColorLevel::Ansi16 => Color::Ansi(AnsiColor::BrightRed),
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn dark_truecolor_add_bg_is_rgb() {
195 let bg = diff_add_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
196 assert!(matches!(bg, Color::Rgb(RgbColor(25, 45, 35))));
197 }
198
199 #[test]
200 fn dark_truecolor_del_bg_is_rgb() {
201 let bg = diff_del_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
202 assert!(matches!(bg, Color::Rgb(RgbColor(90, 40, 40))));
203 }
204
205 #[test]
206 fn light_truecolor_add_bg_is_accessible() {
207 let bg = diff_add_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
208 assert!(matches!(bg, Color::Rgb(RgbColor(215, 240, 215))));
209 }
210
211 #[test]
212 fn light_truecolor_del_bg_is_accessible() {
213 let bg = diff_del_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
214 assert!(matches!(bg, Color::Rgb(RgbColor(255, 235, 235))));
215 }
216
217 #[test]
218 fn dark_256_uses_indexed_colors() {
219 let add = diff_add_bg(DiffTheme::Dark, DiffColorLevel::Ansi256);
220 let del = diff_del_bg(DiffTheme::Dark, DiffColorLevel::Ansi256);
221 assert!(matches!(add, Color::Ansi256(Ansi256Color(22))));
222 assert!(matches!(del, Color::Ansi256(Ansi256Color(52))));
223 }
224
225 #[test]
226 fn light_256_defaults_to_non_harmonious_adjustment() {
227 let add = diff_add_bg(DiffTheme::Light, DiffColorLevel::Ansi256);
228 let del = diff_del_bg(DiffTheme::Light, DiffColorLevel::Ansi256);
229 assert!(matches!(add, Color::Ansi256(Ansi256Color(22))));
230 assert!(matches!(del, Color::Ansi256(Ansi256Color(52))));
231 }
232
233 #[test]
234 fn dark_ansi16_uses_named_colors() {
235 let add = diff_add_bg(DiffTheme::Dark, DiffColorLevel::Ansi16);
236 let del = diff_del_bg(DiffTheme::Dark, DiffColorLevel::Ansi16);
237 assert_eq!(add, Color::Ansi(AnsiColor::Green));
238 assert_eq!(del, Color::Ansi(AnsiColor::Red));
239 }
240
241 #[test]
242 fn wt_session_promotes_ansi16_to_truecolor() {
243 assert_eq!(
244 diff_color_level_for_terminal(DiffColorLevel::Ansi16, None, true, false),
245 DiffColorLevel::TrueColor
246 );
247 }
248
249 #[test]
250 fn windows_terminal_term_program_promotes_ansi16_to_truecolor() {
251 assert_eq!(
252 diff_color_level_for_terminal(
253 DiffColorLevel::Ansi16,
254 Some("Windows_Terminal"),
255 false,
256 false
257 ),
258 DiffColorLevel::TrueColor
259 );
260 }
261
262 #[test]
263 fn non_windows_terminal_keeps_ansi16() {
264 assert_eq!(
265 diff_color_level_for_terminal(DiffColorLevel::Ansi16, Some("WezTerm"), false, false),
266 DiffColorLevel::Ansi16
267 );
268 }
269
270 #[test]
271 fn force_color_keeps_ansi16_when_wt_session_exists() {
272 assert_eq!(
273 diff_color_level_for_terminal(DiffColorLevel::Ansi16, None, true, true),
274 DiffColorLevel::Ansi16
275 );
276 }
277
278 #[test]
279 fn force_color_keeps_ansi256_when_wt_session_exists() {
280 assert_eq!(
281 diff_color_level_for_terminal(DiffColorLevel::Ansi256, None, true, true),
282 DiffColorLevel::Ansi256
283 );
284 }
285
286 #[test]
287 fn base_level_detects_truecolor_from_colorterm() {
288 assert_eq!(
289 base_diff_color_level("truecolor", "xterm-256color"),
290 DiffColorLevel::TrueColor
291 );
292 }
293
294 #[test]
295 fn base_level_detects_ansi256_from_term() {
296 assert_eq!(
297 base_diff_color_level("", "xterm-256color"),
298 DiffColorLevel::Ansi256
299 );
300 }
301
302 #[test]
303 fn base_level_falls_back_to_ansi16() {
304 assert_eq!(base_diff_color_level("", "xterm"), DiffColorLevel::Ansi16);
305 }
306}