1pub 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 crate::ui::syntax_highlight::{DiffScopeBackgroundRgbs, diff_scope_background_rgbs};
14use ratatui::style::{Color as RatatuiColor, Modifier, Style as RatatuiStyle};
15use vtcode_commons::color256_theme::rgb_to_ansi256_for_theme;
16
17fn ratatui_color_from_anstyle(color: anstyle::Color) -> RatatuiColor {
21 match color {
22 anstyle::Color::Ansi(c) => match c {
23 anstyle::AnsiColor::Black => RatatuiColor::Black,
24 anstyle::AnsiColor::Red => RatatuiColor::Red,
25 anstyle::AnsiColor::Green => RatatuiColor::Green,
26 anstyle::AnsiColor::Yellow => RatatuiColor::Yellow,
27 anstyle::AnsiColor::Blue => RatatuiColor::Blue,
28 anstyle::AnsiColor::Magenta => RatatuiColor::Magenta,
29 anstyle::AnsiColor::Cyan => RatatuiColor::Cyan,
30 anstyle::AnsiColor::White => RatatuiColor::White,
31 anstyle::AnsiColor::BrightBlack => RatatuiColor::DarkGray,
32 anstyle::AnsiColor::BrightRed => RatatuiColor::LightRed,
33 anstyle::AnsiColor::BrightGreen => RatatuiColor::LightGreen,
34 anstyle::AnsiColor::BrightYellow => RatatuiColor::LightYellow,
35 anstyle::AnsiColor::BrightBlue => RatatuiColor::LightBlue,
36 anstyle::AnsiColor::BrightMagenta => RatatuiColor::LightMagenta,
37 anstyle::AnsiColor::BrightCyan => RatatuiColor::LightCyan,
38 anstyle::AnsiColor::BrightWhite => RatatuiColor::White,
39 },
40 anstyle::Color::Ansi256(c) => RatatuiColor::Indexed(c.0),
41 anstyle::Color::Rgb(c) => RatatuiColor::Rgb(c.0, c.1, c.2),
42 }
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum DiffLineType {
50 Insert,
51 Delete,
52 Context,
53}
54
55#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
56struct ResolvedDiffBackgrounds {
57 add: Option<RatatuiColor>,
58 del: Option<RatatuiColor>,
59}
60
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub struct DiffRenderStyleContext {
64 theme: DiffTheme,
65 level: DiffColorLevel,
66 backgrounds: ResolvedDiffBackgrounds,
67}
68
69pub fn current_diff_render_style_context() -> DiffRenderStyleContext {
71 let theme = DiffTheme::detect();
72 let level = DiffColorLevel::detect();
73 diff_render_style_context_for(theme, level, scope_backgrounds_for_level(level))
74}
75
76fn diff_render_style_context_for(
77 theme: DiffTheme,
78 level: DiffColorLevel,
79 scope_backgrounds: DiffScopeBackgroundRgbs,
80) -> DiffRenderStyleContext {
81 DiffRenderStyleContext {
82 theme,
83 level,
84 backgrounds: resolve_diff_backgrounds_for(theme, level, scope_backgrounds),
85 }
86}
87
88fn resolve_diff_backgrounds_for(
89 theme: DiffTheme,
90 level: DiffColorLevel,
91 scope_backgrounds: DiffScopeBackgroundRgbs,
92) -> ResolvedDiffBackgrounds {
93 let mut resolved = fallback_diff_backgrounds(theme, level);
94 if level == DiffColorLevel::Ansi16 {
95 return resolved;
96 }
97
98 if let Some(rgb) = scope_backgrounds.inserted
99 && let Some(color) = color_from_rgb_for_level(rgb, theme, level)
100 {
101 resolved.add = Some(color);
102 }
103
104 if let Some(rgb) = scope_backgrounds.deleted
105 && let Some(color) = color_from_rgb_for_level(rgb, theme, level)
106 {
107 resolved.del = Some(color);
108 }
109
110 resolved
111}
112
113fn fallback_diff_backgrounds(theme: DiffTheme, level: DiffColorLevel) -> ResolvedDiffBackgrounds {
114 match level {
115 DiffColorLevel::Ansi16 => ResolvedDiffBackgrounds::default(),
116 DiffColorLevel::TrueColor | DiffColorLevel::Ansi256 => ResolvedDiffBackgrounds {
117 add: Some(ratatui_color_from_anstyle(diff_add_bg(theme, level))),
118 del: Some(ratatui_color_from_anstyle(diff_del_bg(theme, level))),
119 },
120 }
121}
122
123fn color_from_rgb_for_level(
124 rgb: (u8, u8, u8),
125 theme: DiffTheme,
126 level: DiffColorLevel,
127) -> Option<RatatuiColor> {
128 match level {
129 DiffColorLevel::TrueColor => Some(RatatuiColor::Rgb(rgb.0, rgb.1, rgb.2)),
130 DiffColorLevel::Ansi256 => Some(RatatuiColor::Indexed(rgb_to_ansi256_for_theme(
131 rgb.0,
132 rgb.1,
133 rgb.2,
134 theme.is_light(),
135 ))),
136 DiffColorLevel::Ansi16 => None,
137 }
138}
139
140pub fn content_background(
141 kind: DiffLineType,
142 style_context: DiffRenderStyleContext,
143) -> Option<RatatuiColor> {
144 match kind {
145 DiffLineType::Insert => style_context.backgrounds.add,
146 DiffLineType::Delete => style_context.backgrounds.del,
147 DiffLineType::Context => None,
148 }
149}
150
151pub fn style_line_bg(kind: DiffLineType, style_context: DiffRenderStyleContext) -> RatatuiStyle {
153 match kind {
154 DiffLineType::Insert => style_context
155 .backgrounds
156 .add
157 .map_or_else(RatatuiStyle::default, |bg| RatatuiStyle::default().bg(bg)),
158 DiffLineType::Delete => style_context
159 .backgrounds
160 .del
161 .map_or_else(RatatuiStyle::default, |bg| RatatuiStyle::default().bg(bg)),
162 DiffLineType::Context => RatatuiStyle::default(),
163 }
164}
165
166fn scope_backgrounds_for_level(level: DiffColorLevel) -> DiffScopeBackgroundRgbs {
167 match level {
168 DiffColorLevel::Ansi16 => DiffScopeBackgroundRgbs::default(),
169 DiffColorLevel::TrueColor | DiffColorLevel::Ansi256 => diff_scope_background_rgbs(),
170 }
171}
172
173pub fn style_gutter(kind: DiffLineType) -> RatatuiStyle {
177 match kind {
178 DiffLineType::Insert => RatatuiStyle::default()
179 .fg(RatatuiColor::Green)
180 .add_modifier(Modifier::DIM)
181 .remove_modifier(Modifier::BOLD),
182 DiffLineType::Delete => RatatuiStyle::default()
183 .fg(RatatuiColor::Red)
184 .add_modifier(Modifier::DIM)
185 .remove_modifier(Modifier::BOLD),
186 DiffLineType::Context => RatatuiStyle::default().add_modifier(Modifier::DIM),
187 }
188}
189
190pub fn style_sign(kind: DiffLineType) -> RatatuiStyle {
193 match kind {
194 DiffLineType::Insert => RatatuiStyle::default()
195 .fg(RatatuiColor::Green)
196 .add_modifier(Modifier::DIM)
197 .remove_modifier(Modifier::BOLD),
198 DiffLineType::Delete => RatatuiStyle::default()
199 .fg(RatatuiColor::Red)
200 .add_modifier(Modifier::DIM)
201 .remove_modifier(Modifier::BOLD),
202 DiffLineType::Context => RatatuiStyle::default(),
203 }
204}
205
206pub fn style_content(kind: DiffLineType, style_context: DiffRenderStyleContext) -> RatatuiStyle {
212 let bg = content_background(kind, style_context);
213 match (kind, style_context.theme, style_context.level, bg) {
214 (DiffLineType::Context, _, _, _) => RatatuiStyle::default(),
215 (DiffLineType::Insert, _, DiffColorLevel::Ansi16, _) => {
216 RatatuiStyle::default().fg(RatatuiColor::Green)
217 }
218 (DiffLineType::Delete, _, DiffColorLevel::Ansi16, _) => {
219 RatatuiStyle::default().fg(RatatuiColor::Red)
220 }
221 (DiffLineType::Insert, DiffTheme::Light, _, Some(bg)) => RatatuiStyle::default().bg(bg),
222 (DiffLineType::Delete, DiffTheme::Light, _, Some(bg)) => RatatuiStyle::default().bg(bg),
223 (DiffLineType::Insert, DiffTheme::Dark, _, Some(bg)) => {
224 RatatuiStyle::default().fg(RatatuiColor::Green).bg(bg)
225 }
226 (DiffLineType::Delete, DiffTheme::Dark, _, Some(bg)) => {
227 RatatuiStyle::default().fg(RatatuiColor::Red).bg(bg)
228 }
229 (DiffLineType::Insert, DiffTheme::Light, _, None)
230 | (DiffLineType::Delete, DiffTheme::Light, _, None) => RatatuiStyle::default(),
231 (DiffLineType::Insert, DiffTheme::Dark, _, None) => {
232 RatatuiStyle::default().fg(RatatuiColor::Green)
233 }
234 (DiffLineType::Delete, DiffTheme::Dark, _, None) => {
235 RatatuiStyle::default().fg(RatatuiColor::Red)
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 fn test_style_context(theme: DiffTheme, level: DiffColorLevel) -> DiffRenderStyleContext {
245 diff_render_style_context_for(theme, level, scope_backgrounds_for_level(level))
246 }
247
248 #[test]
249 fn dark_truecolor_add_bg_is_rgb() {
250 let bg = diff_add_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
251 assert!(matches!(
252 bg,
253 anstyle::Color::Rgb(anstyle::RgbColor(25, 45, 35))
254 ));
255 }
256
257 #[test]
258 fn dark_truecolor_del_bg_is_rgb() {
259 let bg = diff_del_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
260 assert!(matches!(
261 bg,
262 anstyle::Color::Rgb(anstyle::RgbColor(90, 40, 40))
263 ));
264 }
265
266 #[test]
267 fn light_truecolor_add_bg_is_accessible() {
268 let bg = diff_add_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
269 assert!(matches!(
270 bg,
271 anstyle::Color::Rgb(anstyle::RgbColor(215, 240, 215))
272 ));
273 }
274
275 #[test]
276 fn light_truecolor_del_bg_is_accessible() {
277 let bg = diff_del_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
278 assert!(matches!(
279 bg,
280 anstyle::Color::Rgb(anstyle::RgbColor(255, 235, 235))
281 ));
282 }
283
284 #[test]
285 fn dark_256_uses_indexed_colors() {
286 let add = diff_add_bg(DiffTheme::Dark, DiffColorLevel::Ansi256);
287 let del = diff_del_bg(DiffTheme::Dark, DiffColorLevel::Ansi256);
288 assert!(matches!(
289 add,
290 anstyle::Color::Ansi256(anstyle::Ansi256Color(22))
291 ));
292 assert!(matches!(
293 del,
294 anstyle::Color::Ansi256(anstyle::Ansi256Color(52))
295 ));
296 }
297
298 #[test]
299 fn dark_ansi16_uses_named_colors() {
300 let add = diff_add_bg(DiffTheme::Dark, DiffColorLevel::Ansi16);
301 let del = diff_del_bg(DiffTheme::Dark, DiffColorLevel::Ansi16);
302 assert_eq!(add, anstyle::Color::Ansi(anstyle::AnsiColor::Green));
303 assert_eq!(del, anstyle::Color::Ansi(anstyle::AnsiColor::Red));
304 }
305
306 #[test]
307 fn context_line_bg_is_default() {
308 let style = style_line_bg(
309 DiffLineType::Context,
310 test_style_context(DiffTheme::Dark, DiffColorLevel::TrueColor),
311 );
312 assert_eq!(style, RatatuiStyle::default());
313 }
314
315 #[test]
316 fn dark_gutter_is_dim() {
317 let style = style_gutter(DiffLineType::Context);
318 assert!(style.add_modifier.contains(Modifier::DIM));
319 }
320
321 #[test]
322 fn insert_gutter_is_dim_standard_green_no_bold() {
323 let style = style_gutter(DiffLineType::Insert);
324 assert_eq!(style.fg, Some(RatatuiColor::Green));
325 assert!(style.add_modifier.contains(Modifier::DIM));
326 assert!(style.sub_modifier.contains(Modifier::BOLD));
327 }
328
329 #[test]
330 fn dark_ansi16_content_uses_foreground_only() {
331 let style = style_content(
332 DiffLineType::Insert,
333 test_style_context(DiffTheme::Dark, DiffColorLevel::Ansi16),
334 );
335 assert_eq!(style.fg, Some(RatatuiColor::Green));
336 assert_eq!(style.bg, None);
337 }
338
339 #[test]
340 fn sign_style_always_uses_standard_colors() {
341 let add_sign = style_sign(DiffLineType::Insert);
342 let del_sign = style_sign(DiffLineType::Delete);
343 assert_eq!(add_sign.fg, Some(RatatuiColor::Green));
344 assert_eq!(del_sign.fg, Some(RatatuiColor::Red));
345 assert!(add_sign.add_modifier.contains(Modifier::DIM));
346 assert!(del_sign.add_modifier.contains(Modifier::DIM));
347 assert!(add_sign.sub_modifier.contains(Modifier::BOLD));
348 assert!(del_sign.sub_modifier.contains(Modifier::BOLD));
349 }
350
351 #[test]
352 fn theme_scope_backgrounds_override_truecolor_fallback_when_available() {
353 let style_context = diff_render_style_context_for(
354 DiffTheme::Dark,
355 DiffColorLevel::TrueColor,
356 DiffScopeBackgroundRgbs {
357 inserted: Some((1, 2, 3)),
358 deleted: Some((4, 5, 6)),
359 },
360 );
361
362 assert_eq!(
363 style_line_bg(DiffLineType::Insert, style_context),
364 RatatuiStyle::default().bg(RatatuiColor::Rgb(1, 2, 3))
365 );
366 assert_eq!(
367 style_line_bg(DiffLineType::Delete, style_context),
368 RatatuiStyle::default().bg(RatatuiColor::Rgb(4, 5, 6))
369 );
370 }
371
372 #[test]
373 fn theme_scope_backgrounds_quantize_to_ansi256() {
374 let style_context = diff_render_style_context_for(
375 DiffTheme::Dark,
376 DiffColorLevel::Ansi256,
377 DiffScopeBackgroundRgbs {
378 inserted: Some((0, 95, 0)),
379 deleted: None,
380 },
381 );
382 assert_eq!(
383 style_line_bg(DiffLineType::Insert, style_context),
384 RatatuiStyle::default().bg(RatatuiColor::Indexed(22))
385 );
386 assert_eq!(
387 style_line_bg(DiffLineType::Delete, style_context),
388 RatatuiStyle::default().bg(RatatuiColor::Indexed(52))
389 );
390 }
391
392 #[test]
393 fn ansi16_disables_line_backgrounds_even_with_scope_colors() {
394 let style_context = diff_render_style_context_for(
395 DiffTheme::Dark,
396 DiffColorLevel::Ansi16,
397 DiffScopeBackgroundRgbs {
398 inserted: Some((8, 9, 10)),
399 deleted: Some((11, 12, 13)),
400 },
401 );
402 assert_eq!(
403 style_line_bg(DiffLineType::Insert, style_context),
404 RatatuiStyle::default()
405 );
406 assert_eq!(
407 style_line_bg(DiffLineType::Delete, style_context),
408 RatatuiStyle::default()
409 );
410 }
411
412 #[test]
413 fn ansi16_content_has_no_background() {
414 let style_context = diff_render_style_context_for(
415 DiffTheme::Dark,
416 DiffColorLevel::Ansi16,
417 DiffScopeBackgroundRgbs::default(),
418 );
419 let add = style_content(DiffLineType::Insert, style_context);
420 let del = style_content(DiffLineType::Delete, style_context);
421 assert_eq!(add.fg, Some(RatatuiColor::Green));
422 assert_eq!(add.bg, None);
423 assert_eq!(del.fg, Some(RatatuiColor::Red));
424 assert_eq!(del.bg, None);
425 }
426
427 #[test]
428 fn partial_scope_override_keeps_missing_side_fallback() {
429 let style_context = diff_render_style_context_for(
430 DiffTheme::Dark,
431 DiffColorLevel::TrueColor,
432 DiffScopeBackgroundRgbs {
433 inserted: Some((12, 34, 56)),
434 deleted: None,
435 },
436 );
437 assert_eq!(
438 content_background(DiffLineType::Insert, style_context),
439 Some(RatatuiColor::Rgb(12, 34, 56))
440 );
441 assert_eq!(
442 content_background(DiffLineType::Delete, style_context),
443 Some(RatatuiColor::Rgb(90, 40, 40))
444 );
445 }
446}