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