Skip to main content

vtcode_commons/
diff_theme.rs

1//! Diff theme configuration and color palettes
2//!
3//! Uses subtle red/green tints 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        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
95// ── Standard ANSI red/green backgrounds ────────────────────────────────────
96
97/// Get background color for addition lines based on theme and color level.
98pub 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
105/// Get background color for deletion lines based on theme and color level.
106pub 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
113/// Get gutter foreground color for light theme (dark theme uses dimmed default).
114pub fn diff_gutter_fg_light(_level: DiffColorLevel) -> Color {
115    Color::Ansi(AnsiColor::Black)
116}
117
118/// Get gutter background color for addition lines in light theme.
119pub fn diff_gutter_bg_add_light(_level: DiffColorLevel) -> Color {
120    Color::Ansi(AnsiColor::BrightGreen)
121}
122
123/// Get gutter background color for deletion lines in light theme.
124pub 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}