1use 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 return DiffColorLevel::TrueColor;
81 }
82
83 base_level
84}
85
86fn is_windows_terminal(term_program: Option<&str>) -> bool {
87 let Some(program) = term_program else {
88 return false;
89 };
90
91 let normalized = program.trim().to_ascii_lowercase();
92 normalized.contains("windows_terminal") || normalized.contains("windows terminal")
93}
94
95pub fn diff_add_bg(theme: DiffTheme, _level: DiffColorLevel) -> Color {
99 match theme {
100 DiffTheme::Dark => Color::Rgb(anstyle::RgbColor(20, 58, 45)),
101 DiffTheme::Light => Color::Rgb(anstyle::RgbColor(218, 246, 225)),
102 }
103}
104
105pub fn diff_del_bg(theme: DiffTheme, _level: DiffColorLevel) -> Color {
107 match theme {
108 DiffTheme::Dark => Color::Rgb(anstyle::RgbColor(70, 38, 42)),
109 DiffTheme::Light => Color::Rgb(anstyle::RgbColor(255, 224, 224)),
110 }
111}
112
113pub fn diff_gutter_fg_light(_level: DiffColorLevel) -> Color {
115 Color::Ansi(AnsiColor::Black)
116}
117
118pub fn diff_gutter_bg_add_light(_level: DiffColorLevel) -> Color {
120 Color::Ansi(AnsiColor::BrightGreen)
121}
122
123pub fn diff_gutter_bg_del_light(_level: DiffColorLevel) -> Color {
125 Color::Ansi(AnsiColor::BrightRed)
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn dark_add_bg_is_subtle_green_tint() {
134 let bg = diff_add_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
135 assert_eq!(bg, Color::Rgb(anstyle::RgbColor(20, 58, 45)));
136 }
137
138 #[test]
139 fn dark_del_bg_is_subtle_red_tint() {
140 let bg = diff_del_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
141 assert_eq!(bg, Color::Rgb(anstyle::RgbColor(70, 38, 42)));
142 }
143
144 #[test]
145 fn light_add_bg_is_subtle_green_tint() {
146 let bg = diff_add_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
147 assert_eq!(bg, Color::Rgb(anstyle::RgbColor(218, 246, 225)));
148 }
149
150 #[test]
151 fn light_del_bg_is_subtle_red_tint() {
152 let bg = diff_del_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
153 assert_eq!(bg, Color::Rgb(anstyle::RgbColor(255, 224, 224)));
154 }
155
156 #[test]
157 fn all_levels_use_same_theme_tints() {
158 for level in [
159 DiffColorLevel::TrueColor,
160 DiffColorLevel::Ansi256,
161 DiffColorLevel::Ansi16,
162 ] {
163 assert_eq!(diff_add_bg(DiffTheme::Dark, level), Color::Rgb(anstyle::RgbColor(20, 58, 45)));
164 assert_eq!(diff_del_bg(DiffTheme::Dark, level), Color::Rgb(anstyle::RgbColor(70, 38, 42)));
165 assert_eq!(diff_add_bg(DiffTheme::Light, level), Color::Rgb(anstyle::RgbColor(218, 246, 225)));
166 assert_eq!(diff_del_bg(DiffTheme::Light, level), Color::Rgb(anstyle::RgbColor(255, 224, 224)));
167 }
168 }
169
170 #[test]
171 fn wt_session_promotes_ansi16_to_truecolor() {
172 assert_eq!(diff_color_level_for_terminal(DiffColorLevel::Ansi16, None, true, false), DiffColorLevel::TrueColor);
173 }
174
175 #[test]
176 fn windows_terminal_term_program_promotes_ansi16_to_truecolor() {
177 assert_eq!(
178 diff_color_level_for_terminal(DiffColorLevel::Ansi16, Some("Windows_Terminal"), false, false),
179 DiffColorLevel::TrueColor
180 );
181 }
182
183 #[test]
184 fn non_windows_terminal_keeps_ansi16() {
185 assert_eq!(
186 diff_color_level_for_terminal(DiffColorLevel::Ansi16, Some("WezTerm"), false, false),
187 DiffColorLevel::Ansi16
188 );
189 }
190
191 #[test]
192 fn force_color_keeps_ansi16_when_wt_session_exists() {
193 assert_eq!(diff_color_level_for_terminal(DiffColorLevel::Ansi16, None, true, true), DiffColorLevel::Ansi16);
194 }
195
196 #[test]
197 fn force_color_keeps_ansi256_when_wt_session_exists() {
198 assert_eq!(diff_color_level_for_terminal(DiffColorLevel::Ansi256, None, true, true), DiffColorLevel::Ansi256);
199 }
200
201 #[test]
202 fn base_level_detects_truecolor_from_colorterm() {
203 assert_eq!(base_diff_color_level("truecolor", "xterm-256color"), DiffColorLevel::TrueColor);
204 }
205
206 #[test]
207 fn base_level_detects_ansi256_from_term() {
208 assert_eq!(base_diff_color_level("", "xterm-256color"), DiffColorLevel::Ansi256);
209 }
210
211 #[test]
212 fn base_level_falls_back_to_ansi16() {
213 assert_eq!(base_diff_color_level("", "xterm"), DiffColorLevel::Ansi16);
214 }
215}