Skip to main content

vtcode_tui/utils/
diff_styles.rs

1//! Unified diff styles for TUI rendering
2//!
3//! Re-exports diff theme from vtcode-commons and provides
4//! ratatui-specific style helpers for diff rendering.
5
6// Re-export diff theme from vtcode-commons
7pub use vtcode_commons::diff_theme::{
8    DiffColorLevel, DiffTheme, diff_add_bg, diff_del_bg, diff_gutter_bg_add_light,
9    diff_gutter_bg_del_light, diff_gutter_fg_light,
10};
11pub use vtcode_commons::styling::DiffColorPalette;
12
13use ratatui::style::{Color as RatatuiColor, Modifier, Style as RatatuiStyle};
14
15// ── Conversion helpers ─────────────────────────────────────────────────────
16
17/// Convert anstyle Color to ratatui Color
18fn ratatui_color_from_anstyle(color: anstyle::Color) -> RatatuiColor {
19    match color {
20        anstyle::Color::Ansi(c) => match c {
21            anstyle::AnsiColor::Black => RatatuiColor::Black,
22            anstyle::AnsiColor::Red => RatatuiColor::Red,
23            anstyle::AnsiColor::Green => RatatuiColor::Green,
24            anstyle::AnsiColor::Yellow => RatatuiColor::Yellow,
25            anstyle::AnsiColor::Blue => RatatuiColor::Blue,
26            anstyle::AnsiColor::Magenta => RatatuiColor::Magenta,
27            anstyle::AnsiColor::Cyan => RatatuiColor::Cyan,
28            anstyle::AnsiColor::White => RatatuiColor::White,
29            anstyle::AnsiColor::BrightBlack => RatatuiColor::DarkGray,
30            anstyle::AnsiColor::BrightRed => RatatuiColor::LightRed,
31            anstyle::AnsiColor::BrightGreen => RatatuiColor::LightGreen,
32            anstyle::AnsiColor::BrightYellow => RatatuiColor::LightYellow,
33            anstyle::AnsiColor::BrightBlue => RatatuiColor::LightBlue,
34            anstyle::AnsiColor::BrightMagenta => RatatuiColor::LightMagenta,
35            anstyle::AnsiColor::BrightCyan => RatatuiColor::LightCyan,
36            anstyle::AnsiColor::BrightWhite => RatatuiColor::White,
37        },
38        anstyle::Color::Ansi256(c) => RatatuiColor::Indexed(c.0),
39        anstyle::Color::Rgb(c) => RatatuiColor::Rgb(c.0, c.1, c.2),
40    }
41}
42
43// ── TUI-specific diff line styling ─────────────────────────────────────────
44
45/// Diff line type for style selection.
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum DiffLineType {
48    Insert,
49    Delete,
50    Context,
51}
52
53/// Full-width line background style. Context lines use terminal default.
54pub fn style_line_bg(kind: DiffLineType, theme: DiffTheme, level: DiffColorLevel) -> RatatuiStyle {
55    match kind {
56        DiffLineType::Insert => {
57            RatatuiStyle::default().bg(ratatui_color_from_anstyle(diff_add_bg(theme, level)))
58        }
59        DiffLineType::Delete => {
60            RatatuiStyle::default().bg(ratatui_color_from_anstyle(diff_del_bg(theme, level)))
61        }
62        DiffLineType::Context => RatatuiStyle::default(),
63    }
64}
65
66/// Gutter (line number) style.
67///
68/// Keep gutter signs/numbers dim and on standard ANSI red/green without bold.
69pub fn style_gutter(kind: DiffLineType, theme: DiffTheme, level: DiffColorLevel) -> RatatuiStyle {
70    let _ = (theme, level);
71    match kind {
72        DiffLineType::Insert => RatatuiStyle::default()
73            .fg(RatatuiColor::Green)
74            .add_modifier(Modifier::DIM)
75            .remove_modifier(Modifier::BOLD),
76        DiffLineType::Delete => RatatuiStyle::default()
77            .fg(RatatuiColor::Red)
78            .add_modifier(Modifier::DIM)
79            .remove_modifier(Modifier::BOLD),
80        DiffLineType::Context => RatatuiStyle::default().add_modifier(Modifier::DIM),
81    }
82}
83
84/// Sign character (`+`/`-`) style.
85/// Uses standard ANSI red/green without bold for consistency.
86pub fn style_sign(kind: DiffLineType, _theme: DiffTheme, _level: DiffColorLevel) -> RatatuiStyle {
87    match kind {
88        DiffLineType::Insert => RatatuiStyle::default()
89            .fg(RatatuiColor::Green)
90            .add_modifier(Modifier::DIM)
91            .remove_modifier(Modifier::BOLD),
92        DiffLineType::Delete => RatatuiStyle::default()
93            .fg(RatatuiColor::Red)
94            .add_modifier(Modifier::DIM)
95            .remove_modifier(Modifier::BOLD),
96        DiffLineType::Context => RatatuiStyle::default(),
97    }
98}
99
100/// Content style for plain (non-syntax-highlighted) diff lines.
101///
102/// Dark + ANSI16: black fg on colored bg for contrast.
103/// Light: bg only, no fg override.
104/// Dark + TrueColor/256: colored fg + tinted bg.
105pub fn style_content(kind: DiffLineType, theme: DiffTheme, level: DiffColorLevel) -> RatatuiStyle {
106    match (kind, theme, level) {
107        // Dark + ANSI16: force Black fg on colored bg for contrast
108        (DiffLineType::Insert, DiffTheme::Dark, DiffColorLevel::Ansi16) => RatatuiStyle::default()
109            .fg(RatatuiColor::Black)
110            .bg(ratatui_color_from_anstyle(diff_add_bg(theme, level))),
111        (DiffLineType::Delete, DiffTheme::Dark, DiffColorLevel::Ansi16) => RatatuiStyle::default()
112            .fg(RatatuiColor::Black)
113            .bg(ratatui_color_from_anstyle(diff_del_bg(theme, level))),
114        // Light: bg only, no fg override
115        (DiffLineType::Insert, DiffTheme::Light, _) => {
116            RatatuiStyle::default().bg(ratatui_color_from_anstyle(diff_add_bg(theme, level)))
117        }
118        (DiffLineType::Delete, DiffTheme::Light, _) => {
119            RatatuiStyle::default().bg(ratatui_color_from_anstyle(diff_del_bg(theme, level)))
120        }
121        // Dark + TrueColor/256: colored fg + tinted bg
122        (DiffLineType::Insert, DiffTheme::Dark, _) => RatatuiStyle::default()
123            .fg(RatatuiColor::Green)
124            .bg(ratatui_color_from_anstyle(diff_add_bg(theme, level))),
125        (DiffLineType::Delete, DiffTheme::Dark, _) => RatatuiStyle::default()
126            .fg(RatatuiColor::Red)
127            .bg(ratatui_color_from_anstyle(diff_del_bg(theme, level))),
128        // Context: terminal default
129        (DiffLineType::Context, _, _) => RatatuiStyle::default(),
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn dark_truecolor_add_bg_is_rgb() {
139        let bg = diff_add_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
140        assert!(matches!(
141            bg,
142            anstyle::Color::Rgb(anstyle::RgbColor(25, 45, 35))
143        ));
144    }
145
146    #[test]
147    fn dark_truecolor_del_bg_is_rgb() {
148        let bg = diff_del_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
149        assert!(matches!(
150            bg,
151            anstyle::Color::Rgb(anstyle::RgbColor(90, 40, 40))
152        ));
153    }
154
155    #[test]
156    fn light_truecolor_add_bg_is_accessible() {
157        let bg = diff_add_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
158        assert!(matches!(
159            bg,
160            anstyle::Color::Rgb(anstyle::RgbColor(215, 240, 215))
161        ));
162    }
163
164    #[test]
165    fn light_truecolor_del_bg_is_accessible() {
166        let bg = diff_del_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
167        assert!(matches!(
168            bg,
169            anstyle::Color::Rgb(anstyle::RgbColor(255, 235, 235))
170        ));
171    }
172
173    #[test]
174    fn dark_256_uses_indexed_colors() {
175        let add = diff_add_bg(DiffTheme::Dark, DiffColorLevel::Ansi256);
176        let del = diff_del_bg(DiffTheme::Dark, DiffColorLevel::Ansi256);
177        assert!(matches!(
178            add,
179            anstyle::Color::Ansi256(anstyle::Ansi256Color(22))
180        ));
181        assert!(matches!(
182            del,
183            anstyle::Color::Ansi256(anstyle::Ansi256Color(52))
184        ));
185    }
186
187    #[test]
188    fn dark_ansi16_uses_named_colors() {
189        let add = diff_add_bg(DiffTheme::Dark, DiffColorLevel::Ansi16);
190        let del = diff_del_bg(DiffTheme::Dark, DiffColorLevel::Ansi16);
191        assert_eq!(add, anstyle::Color::Ansi(anstyle::AnsiColor::Green));
192        assert_eq!(del, anstyle::Color::Ansi(anstyle::AnsiColor::Red));
193    }
194
195    #[test]
196    fn context_line_bg_is_default() {
197        let style = style_line_bg(
198            DiffLineType::Context,
199            DiffTheme::Dark,
200            DiffColorLevel::TrueColor,
201        );
202        assert_eq!(style, RatatuiStyle::default());
203    }
204
205    #[test]
206    fn dark_gutter_is_dim() {
207        let style = style_gutter(
208            DiffLineType::Context,
209            DiffTheme::Dark,
210            DiffColorLevel::TrueColor,
211        );
212        assert!(style.add_modifier.contains(Modifier::DIM));
213    }
214
215    #[test]
216    fn insert_gutter_is_dim_standard_green_no_bold() {
217        let style = style_gutter(
218            DiffLineType::Insert,
219            DiffTheme::Light,
220            DiffColorLevel::TrueColor,
221        );
222        assert_eq!(style.fg, Some(RatatuiColor::Green));
223        assert!(style.add_modifier.contains(Modifier::DIM));
224        assert!(style.sub_modifier.contains(Modifier::BOLD));
225    }
226
227    #[test]
228    fn dark_ansi16_content_forces_black_fg() {
229        let style = style_content(
230            DiffLineType::Insert,
231            DiffTheme::Dark,
232            DiffColorLevel::Ansi16,
233        );
234        assert_eq!(style.fg, Some(RatatuiColor::Black));
235    }
236
237    #[test]
238    fn sign_style_always_uses_standard_colors() {
239        let add_sign = style_sign(
240            DiffLineType::Insert,
241            DiffTheme::Dark,
242            DiffColorLevel::TrueColor,
243        );
244        let del_sign = style_sign(
245            DiffLineType::Delete,
246            DiffTheme::Light,
247            DiffColorLevel::Ansi16,
248        );
249        assert_eq!(add_sign.fg, Some(RatatuiColor::Green));
250        assert_eq!(del_sign.fg, Some(RatatuiColor::Red));
251        assert!(add_sign.add_modifier.contains(Modifier::DIM));
252        assert!(del_sign.add_modifier.contains(Modifier::DIM));
253        assert!(add_sign.sub_modifier.contains(Modifier::BOLD));
254        assert!(del_sign.sub_modifier.contains(Modifier::BOLD));
255    }
256}