1use anstyle::{AnsiColor, Color, Style};
4
5#[derive(Debug, Clone, Copy)]
7pub struct ColorPalette {
8 pub success: Color, pub error: Color, pub warning: Color, pub info: Color, pub accent: Color, pub primary: Color, pub muted: Color, }
16
17impl Default for ColorPalette {
18 fn default() -> Self {
19 Self {
20 success: Color::Ansi(AnsiColor::Green),
21 error: Color::Ansi(AnsiColor::Red),
22 warning: Color::Ansi(AnsiColor::Red),
23 info: Color::Ansi(AnsiColor::Cyan),
24 accent: Color::Ansi(AnsiColor::Magenta),
25 primary: Color::Ansi(AnsiColor::Cyan),
26 muted: Color::Ansi(AnsiColor::BrightBlack),
27 }
28 }
29}
30
31pub fn render_styled(text: &str, color: Color, effects: Option<String>) -> String {
33 let mut style = Style::new().fg_color(Some(color));
34
35 if let Some(effects_str) = effects {
36 let mut ansi_effects = anstyle::Effects::new();
37
38 for effect in effects_str.split(',') {
39 let effect = effect.trim().to_lowercase();
40 match effect.as_str() {
41 "bold" => ansi_effects |= anstyle::Effects::BOLD,
42 "dim" | "dimmed" => ansi_effects |= anstyle::Effects::DIMMED,
43 "italic" => ansi_effects |= anstyle::Effects::ITALIC,
44 "underline" => ansi_effects |= anstyle::Effects::UNDERLINE,
45 "blink" => ansi_effects |= anstyle::Effects::BLINK,
46 "invert" | "reversed" => ansi_effects |= anstyle::Effects::INVERT,
47 "hidden" => ansi_effects |= anstyle::Effects::HIDDEN,
48 "strikethrough" => ansi_effects |= anstyle::Effects::STRIKETHROUGH,
49 _ => {}
50 }
51 }
52
53 style = style.effects(ansi_effects);
54 }
55
56 format!("{}{}{}", style, text, "\x1b[0m")
58}
59
60pub fn style_from_color_name(name: &str) -> Style {
62 let (color_name, dimmed) = if let Some(idx) = name.find(':') {
63 let (color, modifier) = name.split_at(idx);
64 (color, modifier.strip_prefix(':').unwrap_or(""))
65 } else {
66 (name, "")
67 };
68
69 let color = match color_name.to_lowercase().as_str() {
70 "red" => Color::Ansi(AnsiColor::Red),
71 "green" => Color::Ansi(AnsiColor::Green),
72 "blue" => Color::Ansi(AnsiColor::Blue),
73 "yellow" => Color::Ansi(AnsiColor::Yellow),
74 "cyan" => Color::Ansi(AnsiColor::Cyan),
75 "magenta" | "purple" => Color::Ansi(AnsiColor::Magenta),
76 "white" => Color::Ansi(AnsiColor::White),
77 "black" => Color::Ansi(AnsiColor::Black),
78 _ => return Style::new(),
79 };
80
81 let mut style = Style::new().fg_color(Some(color));
82 if dimmed.eq_ignore_ascii_case("dimmed") {
83 style = style.dimmed();
84 }
85 style
86}
87
88pub fn bold_color(color: AnsiColor) -> Style {
90 Style::new().bold().fg_color(Some(Color::Ansi(color)))
91}
92
93pub fn dimmed_color(color: AnsiColor) -> Style {
95 Style::new().dimmed().fg_color(Some(Color::Ansi(color)))
96}
97
98#[derive(Debug, Clone, Copy)]
100pub struct DiffColorPalette {
101 pub added_fg: Color,
102 pub added_bg: Color,
103 pub removed_fg: Color,
104 pub removed_bg: Color,
105 pub header_fg: Color,
106 pub header_bg: Color,
107}
108
109impl Default for DiffColorPalette {
110 fn default() -> Self {
111 Self {
112 added_fg: Color::Ansi(AnsiColor::Green),
113 added_bg: Color::Rgb(anstyle::RgbColor(10, 24, 10)),
114 removed_fg: Color::Ansi(AnsiColor::Red),
115 removed_bg: Color::Rgb(anstyle::RgbColor(24, 10, 10)),
116 header_fg: Color::Ansi(AnsiColor::Cyan),
117 header_bg: Color::Rgb(anstyle::RgbColor(10, 16, 20)),
118 }
119 }
120}
121
122impl DiffColorPalette {
123 pub fn added_style(&self) -> Style {
124 Style::new().fg_color(Some(self.added_fg))
125 }
126
127 pub fn removed_style(&self) -> Style {
128 Style::new().fg_color(Some(self.removed_fg))
129 }
130
131 pub fn header_style(&self) -> Style {
132 Style::new().fg_color(Some(self.header_fg))
133 }
134}
135
136use ratatui::style::{Color as RatatuiColor, Modifier, Style as RatatuiStyle};
146
147use super::ansi_capabilities::{CAPABILITIES, ColorDepth, ColorScheme, detect_color_scheme};
148
149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
151pub enum DiffTheme {
152 Dark,
153 Light,
154}
155
156impl DiffTheme {
157 pub fn detect() -> Self {
159 match detect_color_scheme() {
160 ColorScheme::Light => Self::Light,
161 ColorScheme::Dark | ColorScheme::Unknown => Self::Dark,
162 }
163 }
164
165 pub fn is_light(self) -> bool {
166 self == Self::Light
167 }
168}
169
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
172pub enum DiffColorLevel {
173 TrueColor,
174 Ansi256,
175 Ansi16,
176}
177
178impl DiffColorLevel {
179 pub fn detect() -> Self {
181 match CAPABILITIES.color_depth {
182 ColorDepth::TrueColor => Self::TrueColor,
183 ColorDepth::Color256 => Self::Ansi256,
184 ColorDepth::Basic16 | ColorDepth::None => Self::Ansi16,
185 }
186 }
187}
188
189const DARK_TC_ADD_LINE_BG: (u8, u8, u8) = (33, 58, 43); const DARK_TC_DEL_LINE_BG: (u8, u8, u8) = (74, 34, 29); const LIGHT_TC_ADD_LINE_BG: (u8, u8, u8) = (218, 251, 225); const LIGHT_TC_DEL_LINE_BG: (u8, u8, u8) = (255, 235, 233); const LIGHT_TC_ADD_NUM_BG: (u8, u8, u8) = (172, 238, 187); const LIGHT_TC_DEL_NUM_BG: (u8, u8, u8) = (255, 206, 203); const LIGHT_TC_GUTTER_FG: (u8, u8, u8) = (31, 35, 40); const DARK_256_ADD_LINE_BG: u8 = 22; const DARK_256_DEL_LINE_BG: u8 = 52; const LIGHT_256_ADD_LINE_BG: u8 = 194; const LIGHT_256_DEL_LINE_BG: u8 = 224; const LIGHT_256_ADD_NUM_BG: u8 = 157; const LIGHT_256_DEL_NUM_BG: u8 = 217; const LIGHT_256_GUTTER_FG: u8 = 236; fn rgb(t: (u8, u8, u8)) -> RatatuiColor {
214 RatatuiColor::Rgb(t.0, t.1, t.2)
215}
216
217fn indexed(i: u8) -> RatatuiColor {
218 RatatuiColor::Indexed(i)
219}
220
221pub fn add_line_bg(theme: DiffTheme, level: DiffColorLevel) -> RatatuiColor {
223 match (theme, level) {
224 (DiffTheme::Dark, DiffColorLevel::TrueColor) => rgb(DARK_TC_ADD_LINE_BG),
225 (DiffTheme::Dark, DiffColorLevel::Ansi256) => indexed(DARK_256_ADD_LINE_BG),
226 (DiffTheme::Dark, DiffColorLevel::Ansi16) => RatatuiColor::Green,
227 (DiffTheme::Light, DiffColorLevel::TrueColor) => rgb(LIGHT_TC_ADD_LINE_BG),
228 (DiffTheme::Light, DiffColorLevel::Ansi256) => indexed(LIGHT_256_ADD_LINE_BG),
229 (DiffTheme::Light, DiffColorLevel::Ansi16) => RatatuiColor::LightGreen,
230 }
231}
232
233pub fn del_line_bg(theme: DiffTheme, level: DiffColorLevel) -> RatatuiColor {
235 match (theme, level) {
236 (DiffTheme::Dark, DiffColorLevel::TrueColor) => rgb(DARK_TC_DEL_LINE_BG),
237 (DiffTheme::Dark, DiffColorLevel::Ansi256) => indexed(DARK_256_DEL_LINE_BG),
238 (DiffTheme::Dark, DiffColorLevel::Ansi16) => RatatuiColor::Red,
239 (DiffTheme::Light, DiffColorLevel::TrueColor) => rgb(LIGHT_TC_DEL_LINE_BG),
240 (DiffTheme::Light, DiffColorLevel::Ansi256) => indexed(LIGHT_256_DEL_LINE_BG),
241 (DiffTheme::Light, DiffColorLevel::Ansi16) => RatatuiColor::LightRed,
242 }
243}
244
245fn light_gutter_fg(level: DiffColorLevel) -> RatatuiColor {
248 match level {
249 DiffColorLevel::TrueColor => rgb(LIGHT_TC_GUTTER_FG),
250 DiffColorLevel::Ansi256 => indexed(LIGHT_256_GUTTER_FG),
251 DiffColorLevel::Ansi16 => RatatuiColor::Black,
252 }
253}
254
255fn light_add_num_bg(level: DiffColorLevel) -> RatatuiColor {
256 match level {
257 DiffColorLevel::TrueColor => rgb(LIGHT_TC_ADD_NUM_BG),
258 DiffColorLevel::Ansi256 => indexed(LIGHT_256_ADD_NUM_BG),
259 DiffColorLevel::Ansi16 => RatatuiColor::Green,
260 }
261}
262
263fn light_del_num_bg(level: DiffColorLevel) -> RatatuiColor {
264 match level {
265 DiffColorLevel::TrueColor => rgb(LIGHT_TC_DEL_NUM_BG),
266 DiffColorLevel::Ansi256 => indexed(LIGHT_256_DEL_NUM_BG),
267 DiffColorLevel::Ansi16 => RatatuiColor::Red,
268 }
269}
270
271#[derive(Clone, Copy, Debug, PartialEq, Eq)]
275pub enum DiffLineType {
276 Insert,
277 Delete,
278 Context,
279}
280
281pub fn style_line_bg(kind: DiffLineType, theme: DiffTheme, level: DiffColorLevel) -> RatatuiStyle {
283 match kind {
284 DiffLineType::Insert => RatatuiStyle::default().bg(add_line_bg(theme, level)),
285 DiffLineType::Delete => RatatuiStyle::default().bg(del_line_bg(theme, level)),
286 DiffLineType::Context => RatatuiStyle::default(),
287 }
288}
289
290pub fn style_gutter(kind: DiffLineType, theme: DiffTheme, level: DiffColorLevel) -> RatatuiStyle {
295 match (theme, kind) {
296 (DiffTheme::Light, DiffLineType::Insert) => RatatuiStyle::default()
297 .fg(light_gutter_fg(level))
298 .bg(light_add_num_bg(level)),
299 (DiffTheme::Light, DiffLineType::Delete) => RatatuiStyle::default()
300 .fg(light_gutter_fg(level))
301 .bg(light_del_num_bg(level)),
302 _ => RatatuiStyle::default().add_modifier(Modifier::DIM),
303 }
304}
305
306pub fn style_sign(kind: DiffLineType, theme: DiffTheme, level: DiffColorLevel) -> RatatuiStyle {
308 match kind {
309 DiffLineType::Insert => match theme {
310 DiffTheme::Light => RatatuiStyle::default().fg(RatatuiColor::Green),
311 DiffTheme::Dark => style_content(kind, theme, level),
312 },
313 DiffLineType::Delete => match theme {
314 DiffTheme::Light => RatatuiStyle::default().fg(RatatuiColor::Red),
315 DiffTheme::Dark => style_content(kind, theme, level),
316 },
317 DiffLineType::Context => RatatuiStyle::default(),
318 }
319}
320
321pub fn style_content(kind: DiffLineType, theme: DiffTheme, level: DiffColorLevel) -> RatatuiStyle {
323 match (kind, theme, level) {
324 (DiffLineType::Insert, DiffTheme::Dark, DiffColorLevel::Ansi16) => RatatuiStyle::default()
326 .fg(RatatuiColor::Black)
327 .bg(add_line_bg(theme, level)),
328 (DiffLineType::Delete, DiffTheme::Dark, DiffColorLevel::Ansi16) => RatatuiStyle::default()
329 .fg(RatatuiColor::Black)
330 .bg(del_line_bg(theme, level)),
331 (DiffLineType::Insert, DiffTheme::Light, _) => {
333 RatatuiStyle::default().bg(add_line_bg(theme, level))
334 }
335 (DiffLineType::Delete, DiffTheme::Light, _) => {
336 RatatuiStyle::default().bg(del_line_bg(theme, level))
337 }
338 (DiffLineType::Insert, DiffTheme::Dark, _) => RatatuiStyle::default()
340 .fg(RatatuiColor::Green)
341 .bg(add_line_bg(theme, level)),
342 (DiffLineType::Delete, DiffTheme::Dark, _) => RatatuiStyle::default()
343 .fg(RatatuiColor::Red)
344 .bg(del_line_bg(theme, level)),
345 (DiffLineType::Context, _, _) => RatatuiStyle::default(),
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn dark_truecolor_add_bg_is_rgb() {
356 let bg = add_line_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
357 assert!(matches!(bg, RatatuiColor::Rgb(33, 58, 43)));
358 }
359
360 #[test]
361 fn dark_truecolor_del_bg_is_rgb() {
362 let bg = del_line_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
363 assert!(matches!(bg, RatatuiColor::Rgb(74, 34, 29)));
364 }
365
366 #[test]
367 fn light_truecolor_add_bg_is_github_style() {
368 let bg = add_line_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
369 assert!(matches!(bg, RatatuiColor::Rgb(218, 251, 225)));
370 }
371
372 #[test]
373 fn light_truecolor_del_bg_is_github_style() {
374 let bg = del_line_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
375 assert!(matches!(bg, RatatuiColor::Rgb(255, 235, 233)));
376 }
377
378 #[test]
379 fn dark_256_uses_indexed_colors() {
380 let add = add_line_bg(DiffTheme::Dark, DiffColorLevel::Ansi256);
381 let del = del_line_bg(DiffTheme::Dark, DiffColorLevel::Ansi256);
382 assert!(matches!(add, RatatuiColor::Indexed(22)));
383 assert!(matches!(del, RatatuiColor::Indexed(52)));
384 }
385
386 #[test]
387 fn dark_ansi16_uses_named_colors() {
388 let add = add_line_bg(DiffTheme::Dark, DiffColorLevel::Ansi16);
389 let del = del_line_bg(DiffTheme::Dark, DiffColorLevel::Ansi16);
390 assert_eq!(add, RatatuiColor::Green);
391 assert_eq!(del, RatatuiColor::Red);
392 }
393
394 #[test]
395 fn context_line_bg_is_default() {
396 let style = style_line_bg(
397 DiffLineType::Context,
398 DiffTheme::Dark,
399 DiffColorLevel::TrueColor,
400 );
401 assert_eq!(style, RatatuiStyle::default());
402 }
403
404 #[test]
405 fn dark_gutter_is_dim() {
406 let style = style_gutter(
407 DiffLineType::Context,
408 DiffTheme::Dark,
409 DiffColorLevel::TrueColor,
410 );
411 assert!(style.add_modifier.contains(Modifier::DIM));
412 }
413
414 #[test]
415 fn light_gutter_has_opaque_bg() {
416 let style = style_gutter(
417 DiffLineType::Insert,
418 DiffTheme::Light,
419 DiffColorLevel::TrueColor,
420 );
421 assert!(style.bg.is_some());
422 assert!(style.fg.is_some());
423 }
424
425 #[test]
426 fn dark_ansi16_content_forces_black_fg() {
427 let style = style_content(
428 DiffLineType::Insert,
429 DiffTheme::Dark,
430 DiffColorLevel::Ansi16,
431 );
432 assert_eq!(style.fg, Some(RatatuiColor::Black));
433 }
434}