Skip to main content

vtcode_commons/
diff_theme.rs

1//! Diff theme configuration and color palettes
2//!
3//! Uses standard ANSI red/green for diff line backgrounds.
4
5use anstyle::{AnsiColor, Color};
6
7use crate::ansi_capabilities::{ColorScheme, detect_color_scheme};
8
9/// Terminal background theme for diff rendering.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum DiffTheme {
12    Dark,
13    Light,
14}
15
16impl DiffTheme {
17    /// Detect theme from the terminal environment.
18    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/// Terminal color capability level for palette selection.
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum DiffColorLevel {
33    TrueColor,
34    Ansi256,
35    Ansi16,
36}
37
38impl DiffColorLevel {
39    /// Detect color level from terminal capabilities.
40    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
96// ── Standard ANSI red/green backgrounds ────────────────────────────────────
97
98/// Get background color for addition lines based on theme and color level.
99pub 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
106/// Get background color for deletion lines based on theme and color level.
107pub 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
114/// Get gutter foreground color for light theme (dark theme uses dimmed default).
115pub fn diff_gutter_fg_light(_level: DiffColorLevel) -> Color {
116    Color::Ansi(AnsiColor::Black)
117}
118
119/// Get gutter background color for addition lines in light theme.
120pub fn diff_gutter_bg_add_light(_level: DiffColorLevel) -> Color {
121    Color::Ansi(AnsiColor::BrightGreen)
122}
123
124/// Get gutter background color for deletion lines in light theme.
125pub 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}