Skip to main content

vtcode_commons/
diff_theme.rs

1//! Diff theme configuration and color palettes
2//!
3//! Provides terminal-adaptive styling that adjusts background tints based on:
4//!   1. [`DiffTheme`] (Dark/Light) — detected from terminal background
5//!   2. [`DiffColorLevel`] (TrueColor/Ansi256/Ansi16) — from terminal capability
6//!
7//! Colors are selected for WCAG AA accessibility contrast ratios (4.5:1 minimum).
8
9use anstyle::{Ansi256Color, AnsiColor, Color, RgbColor};
10
11use crate::ansi_capabilities::{ColorScheme, detect_color_scheme};
12use crate::color256_theme::adjust_index_for_theme;
13
14/// Terminal background theme for diff rendering.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum DiffTheme {
17    Dark,
18    Light,
19}
20
21impl DiffTheme {
22    /// Detect theme from the terminal environment.
23    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/// Terminal color capability level for palette selection.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum DiffColorLevel {
38    TrueColor,
39    Ansi256,
40    Ansi16,
41}
42
43impl DiffColorLevel {
44    /// Detect color level from terminal capabilities.
45    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
49        if colorterm.contains("truecolor") || colorterm.contains("24bit") {
50            Self::TrueColor
51        } else if term.contains("256") {
52            Self::Ansi256
53        } else {
54            Self::Ansi16
55        }
56    }
57}
58
59// ── Truecolor palette (WCAG AA compliant) ──────────────────────────────────
60
61// Dark theme: darker backgrounds with higher contrast for better readability
62// Green kept dark, red softened with lower saturation
63const DARK_TC_ADD_LINE_BG: (u8, u8, u8) = (25, 45, 35); // #192D23 - Dark teal green
64const DARK_TC_DEL_LINE_BG: (u8, u8, u8) = (90, 40, 40); // #5A2828 - Muted dark red (lower alpha feel)
65
66// Light theme: light backgrounds with sufficient contrast for dark text
67// Red background made more pastel/muted
68const LIGHT_TC_ADD_LINE_BG: (u8, u8, u8) = (215, 240, 215); // #D7F0D7 - Light green
69const LIGHT_TC_DEL_LINE_BG: (u8, u8, u8) = (255, 235, 235); // #FFEBEB - Soft pastel red (muted)
70const LIGHT_TC_ADD_NUM_BG: (u8, u8, u8) = (175, 225, 175); // #AFE1AF - Gutter green
71const LIGHT_TC_DEL_NUM_BG: (u8, u8, u8) = (250, 210, 210); // #FAD2D2 - Muted gutter red
72const LIGHT_TC_GUTTER_FG: (u8, u8, u8) = (25, 25, 25); // #191919 - Near-black for contrast
73
74// ── 256-color palette ──────────────────────────────────────────────────────
75
76const DARK_256_ADD_LINE_BG: u8 = 22; // DarkGreen
77const DARK_256_DEL_LINE_BG: u8 = 52; // DarkRed
78
79const LIGHT_256_ADD_LINE_BG: u8 = 194; // LightGreen
80const LIGHT_256_DEL_LINE_BG: u8 = 224; // LightRed/Pink
81const LIGHT_256_ADD_NUM_BG: u8 = 157; // SeaGreen
82const LIGHT_256_DEL_NUM_BG: u8 = 217; // LightPink
83const LIGHT_256_GUTTER_FG: u8 = 236; // DarkGray
84
85// ── Helper functions ───────────────────────────────────────────────────────
86
87fn rgb(t: (u8, u8, u8)) -> Color {
88    Color::Rgb(RgbColor(t.0, t.1, t.2))
89}
90
91fn indexed(i: u8, theme: DiffTheme) -> Color {
92    let adjusted = adjust_index_for_theme(i, theme.is_light());
93    Color::Ansi256(Ansi256Color(adjusted))
94}
95
96/// Get background color for addition lines based on theme and color level.
97pub fn diff_add_bg(theme: DiffTheme, level: DiffColorLevel) -> Color {
98    match (theme, level) {
99        (DiffTheme::Dark, DiffColorLevel::TrueColor) => rgb(DARK_TC_ADD_LINE_BG),
100        (DiffTheme::Dark, DiffColorLevel::Ansi256) => indexed(DARK_256_ADD_LINE_BG, theme),
101        (DiffTheme::Dark, DiffColorLevel::Ansi16) => Color::Ansi(AnsiColor::Green),
102        (DiffTheme::Light, DiffColorLevel::TrueColor) => rgb(LIGHT_TC_ADD_LINE_BG),
103        (DiffTheme::Light, DiffColorLevel::Ansi256) => indexed(LIGHT_256_ADD_LINE_BG, theme),
104        (DiffTheme::Light, DiffColorLevel::Ansi16) => Color::Ansi(AnsiColor::BrightGreen),
105    }
106}
107
108/// Get background color for deletion lines based on theme and color level.
109pub fn diff_del_bg(theme: DiffTheme, level: DiffColorLevel) -> Color {
110    match (theme, level) {
111        (DiffTheme::Dark, DiffColorLevel::TrueColor) => rgb(DARK_TC_DEL_LINE_BG),
112        (DiffTheme::Dark, DiffColorLevel::Ansi256) => indexed(DARK_256_DEL_LINE_BG, theme),
113        (DiffTheme::Dark, DiffColorLevel::Ansi16) => Color::Ansi(AnsiColor::Red),
114        (DiffTheme::Light, DiffColorLevel::TrueColor) => rgb(LIGHT_TC_DEL_LINE_BG),
115        (DiffTheme::Light, DiffColorLevel::Ansi256) => indexed(LIGHT_256_DEL_LINE_BG, theme),
116        (DiffTheme::Light, DiffColorLevel::Ansi16) => Color::Ansi(AnsiColor::BrightRed),
117    }
118}
119
120/// Get gutter foreground color for light theme (dark theme uses dimmed default).
121pub fn diff_gutter_fg_light(level: DiffColorLevel) -> Color {
122    match level {
123        DiffColorLevel::TrueColor => rgb(LIGHT_TC_GUTTER_FG),
124        DiffColorLevel::Ansi256 => indexed(LIGHT_256_GUTTER_FG, DiffTheme::Light),
125        DiffColorLevel::Ansi16 => Color::Ansi(AnsiColor::Black),
126    }
127}
128
129/// Get gutter background color for addition lines in light theme.
130pub fn diff_gutter_bg_add_light(level: DiffColorLevel) -> Color {
131    match level {
132        DiffColorLevel::TrueColor => rgb(LIGHT_TC_ADD_NUM_BG),
133        DiffColorLevel::Ansi256 => indexed(LIGHT_256_ADD_NUM_BG, DiffTheme::Light),
134        DiffColorLevel::Ansi16 => Color::Ansi(AnsiColor::BrightGreen),
135    }
136}
137
138/// Get gutter background color for deletion lines in light theme.
139pub fn diff_gutter_bg_del_light(level: DiffColorLevel) -> Color {
140    match level {
141        DiffColorLevel::TrueColor => rgb(LIGHT_TC_DEL_NUM_BG),
142        DiffColorLevel::Ansi256 => indexed(LIGHT_256_DEL_NUM_BG, DiffTheme::Light),
143        DiffColorLevel::Ansi16 => Color::Ansi(AnsiColor::BrightRed),
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn dark_truecolor_add_bg_is_rgb() {
153        let bg = diff_add_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
154        assert!(matches!(bg, Color::Rgb(RgbColor(25, 45, 35))));
155    }
156
157    #[test]
158    fn dark_truecolor_del_bg_is_rgb() {
159        let bg = diff_del_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
160        assert!(matches!(bg, Color::Rgb(RgbColor(90, 40, 40))));
161    }
162
163    #[test]
164    fn light_truecolor_add_bg_is_accessible() {
165        let bg = diff_add_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
166        assert!(matches!(bg, Color::Rgb(RgbColor(215, 240, 215))));
167    }
168
169    #[test]
170    fn light_truecolor_del_bg_is_accessible() {
171        let bg = diff_del_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
172        assert!(matches!(bg, Color::Rgb(RgbColor(255, 235, 235))));
173    }
174
175    #[test]
176    fn dark_256_uses_indexed_colors() {
177        let add = diff_add_bg(DiffTheme::Dark, DiffColorLevel::Ansi256);
178        let del = diff_del_bg(DiffTheme::Dark, DiffColorLevel::Ansi256);
179        assert!(matches!(add, Color::Ansi256(Ansi256Color(22))));
180        assert!(matches!(del, Color::Ansi256(Ansi256Color(52))));
181    }
182
183    #[test]
184    fn light_256_defaults_to_non_harmonious_adjustment() {
185        let add = diff_add_bg(DiffTheme::Light, DiffColorLevel::Ansi256);
186        let del = diff_del_bg(DiffTheme::Light, DiffColorLevel::Ansi256);
187        assert!(matches!(add, Color::Ansi256(Ansi256Color(22))));
188        assert!(matches!(del, Color::Ansi256(Ansi256Color(52))));
189    }
190
191    #[test]
192    fn dark_ansi16_uses_named_colors() {
193        let add = diff_add_bg(DiffTheme::Dark, DiffColorLevel::Ansi16);
194        let del = diff_del_bg(DiffTheme::Dark, DiffColorLevel::Ansi16);
195        assert_eq!(add, Color::Ansi(AnsiColor::Green));
196        assert_eq!(del, Color::Ansi(AnsiColor::Red));
197    }
198}