Skip to main content

void_graph/
color.rs

1//! Color theming and graph coloring for void-graph TUI.
2//!
3//! Adapted from [Serie](https://github.com/lusingander/serie) by lusingander.
4//! Licensed under MIT.
5
6use ratatui::style::Color as RatatuiColor;
7use smart_default::SmartDefault;
8
9/// Color theme for the TUI.
10///
11/// All colors default to sensible terminal colors.
12#[derive(Debug, Clone, PartialEq, Eq, SmartDefault)]
13pub struct ColorTheme {
14    #[default(RatatuiColor::Reset)]
15    pub fg: RatatuiColor,
16    #[default(RatatuiColor::Reset)]
17    pub bg: RatatuiColor,
18
19    #[default(RatatuiColor::White)]
20    pub list_selected_fg: RatatuiColor,
21    #[default(RatatuiColor::DarkGray)]
22    pub list_selected_bg: RatatuiColor,
23    #[default(RatatuiColor::Yellow)]
24    pub list_ref_paren_fg: RatatuiColor,
25    #[default(RatatuiColor::Green)]
26    pub list_ref_branch_fg: RatatuiColor,
27    #[default(RatatuiColor::Red)]
28    pub list_ref_remote_branch_fg: RatatuiColor,
29    #[default(RatatuiColor::Yellow)]
30    pub list_ref_tag_fg: RatatuiColor,
31    #[default(RatatuiColor::Magenta)]
32    pub list_ref_stash_fg: RatatuiColor,
33    #[default(RatatuiColor::Cyan)]
34    pub list_head_fg: RatatuiColor,
35    #[default(RatatuiColor::Reset)]
36    pub list_subject_fg: RatatuiColor,
37    #[default(RatatuiColor::Cyan)]
38    pub list_name_fg: RatatuiColor,
39    #[default(RatatuiColor::Yellow)]
40    pub list_hash_fg: RatatuiColor,
41    #[default(RatatuiColor::Magenta)]
42    pub list_date_fg: RatatuiColor,
43    #[default(RatatuiColor::Black)]
44    pub list_match_fg: RatatuiColor,
45    #[default(RatatuiColor::Yellow)]
46    pub list_match_bg: RatatuiColor,
47
48    #[default(RatatuiColor::Reset)]
49    pub detail_label_fg: RatatuiColor,
50    #[default(RatatuiColor::Reset)]
51    pub detail_name_fg: RatatuiColor,
52    #[default(RatatuiColor::Reset)]
53    pub detail_date_fg: RatatuiColor,
54    #[default(RatatuiColor::Blue)]
55    pub detail_email_fg: RatatuiColor,
56    #[default(RatatuiColor::Reset)]
57    pub detail_hash_fg: RatatuiColor,
58    #[default(RatatuiColor::Green)]
59    pub detail_ref_branch_fg: RatatuiColor,
60    #[default(RatatuiColor::Red)]
61    pub detail_ref_remote_branch_fg: RatatuiColor,
62    #[default(RatatuiColor::Yellow)]
63    pub detail_ref_tag_fg: RatatuiColor,
64    #[default(RatatuiColor::Green)]
65    pub detail_file_change_add_fg: RatatuiColor,
66    #[default(RatatuiColor::Yellow)]
67    pub detail_file_change_modify_fg: RatatuiColor,
68    #[default(RatatuiColor::Red)]
69    pub detail_file_change_delete_fg: RatatuiColor,
70    #[default(RatatuiColor::Magenta)]
71    pub detail_file_change_move_fg: RatatuiColor,
72
73    #[default(RatatuiColor::White)]
74    pub ref_selected_fg: RatatuiColor,
75    #[default(RatatuiColor::DarkGray)]
76    pub ref_selected_bg: RatatuiColor,
77
78    #[default(RatatuiColor::Green)]
79    pub help_block_title_fg: RatatuiColor,
80    #[default(RatatuiColor::Yellow)]
81    pub help_key_fg: RatatuiColor,
82
83    #[default(RatatuiColor::Reset)]
84    pub virtual_cursor_fg: RatatuiColor,
85    #[default(RatatuiColor::Reset)]
86    pub status_input_fg: RatatuiColor,
87    #[default(RatatuiColor::DarkGray)]
88    pub status_input_transient_fg: RatatuiColor,
89    #[default(RatatuiColor::Cyan)]
90    pub status_info_fg: RatatuiColor,
91    #[default(RatatuiColor::Green)]
92    pub status_success_fg: RatatuiColor,
93    #[default(RatatuiColor::Yellow)]
94    pub status_warn_fg: RatatuiColor,
95    #[default(RatatuiColor::Red)]
96    pub status_error_fg: RatatuiColor,
97
98    #[default(RatatuiColor::DarkGray)]
99    pub divider_fg: RatatuiColor,
100
101    #[default(RatatuiColor::Blue)]
102    pub graph_line_fg: RatatuiColor,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct GraphColor {
107    r: u8,
108    g: u8,
109    b: u8,
110    a: u8,
111}
112
113impl GraphColor {
114    pub fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
115        Self { r, g, b, a }
116    }
117
118    pub fn from_rgb(r: u8, g: u8, b: u8) -> Self {
119        Self::from_rgba(r, g, b, 255)
120    }
121
122    #[cfg(feature = "image-export")]
123    pub fn to_image_color(self) -> image::Rgba<u8> {
124        image::Rgba([self.r, self.g, self.b, self.a])
125    }
126
127    pub fn to_ratatui_color(self) -> RatatuiColor {
128        RatatuiColor::Rgb(self.r, self.g, self.b)
129    }
130
131    fn transparent() -> Self {
132        Self::from_rgba(0, 0, 0, 0)
133    }
134}
135
136#[derive(Debug, Clone)]
137pub struct GraphColorSet {
138    pub colors: Vec<GraphColor>,
139    pub edge_color: GraphColor,
140    pub background_color: GraphColor,
141}
142
143impl GraphColorSet {
144    /// Create a new GraphColorSet from color strings.
145    ///
146    /// `branches` should be hex color strings like "#ff0000" or "#ff0000ff".
147    /// `edge` and `background` are optional hex color strings.
148    pub fn new(branches: &[String], edge: Option<&str>, background: Option<&str>) -> Self {
149        let colors = branches
150            .iter()
151            .filter_map(|s| parse_rgba_color(s))
152            .collect::<Vec<_>>();
153
154        // Use default colors if no valid branch colors provided
155        let colors = if colors.is_empty() {
156            Self::default_branch_colors()
157        } else {
158            colors
159        };
160
161        let edge_color = edge
162            .and_then(parse_rgba_color)
163            .unwrap_or(GraphColor::transparent());
164        let background_color = background
165            .and_then(parse_rgba_color)
166            .unwrap_or(GraphColor::transparent());
167
168        Self {
169            colors,
170            edge_color,
171            background_color,
172        }
173    }
174
175    /// Default branch colors for the graph.
176    fn default_branch_colors() -> Vec<GraphColor> {
177        vec![
178            GraphColor::from_rgb(0x4A, 0x9E, 0xCD), // Blue
179            GraphColor::from_rgb(0x6C, 0xC6, 0x44), // Green
180            GraphColor::from_rgb(0xE0, 0x6C, 0x75), // Red
181            GraphColor::from_rgb(0xE5, 0xC0, 0x7B), // Yellow
182            GraphColor::from_rgb(0xC6, 0x78, 0xDD), // Magenta
183            GraphColor::from_rgb(0x56, 0xB6, 0xC2), // Cyan
184            GraphColor::from_rgb(0xD1, 0x9A, 0x66), // Orange
185        ]
186    }
187
188    pub fn get(&self, index: usize) -> GraphColor {
189        self.colors[index % self.colors.len()]
190    }
191}
192
193impl Default for GraphColorSet {
194    fn default() -> Self {
195        Self {
196            colors: Self::default_branch_colors(),
197            edge_color: GraphColor::transparent(),
198            background_color: GraphColor::transparent(),
199        }
200    }
201}
202
203fn parse_rgba_color(s: &str) -> Option<GraphColor> {
204    if !s.starts_with('#') {
205        return None;
206    }
207
208    let s = &s[1..];
209    let l = s.len();
210    if l != 6 && l != 8 {
211        return None;
212    }
213
214    let r = u8::from_str_radix(&s[0..2], 16).ok()?;
215    let g = u8::from_str_radix(&s[2..4], 16).ok()?;
216    let b = u8::from_str_radix(&s[4..6], 16).ok()?;
217    if l == 6 {
218        Some(GraphColor::from_rgb(r, g, b))
219    } else {
220        let a = u8::from_str_radix(&s[6..8], 16).ok()?;
221        Some(GraphColor::from_rgba(r, g, b, a))
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use rstest::rstest;
228
229    use super::*;
230
231    #[rstest]
232    #[case("#ff0000", Some(GraphColor { r: 255, g: 0, b: 0, a: 255}))]
233    #[case("#AABBCCDD", Some(GraphColor { r: 170, g: 187, b: 204, a: 221}))]
234    #[case("#ff000", None)]
235    #[case("#fff", None)]
236    #[case("000000", None)]
237    #[case("##123456", None)]
238    fn test_parse_rgba_color(#[case] input: &str, #[case] expected: Option<GraphColor>) {
239        assert_eq!(parse_rgba_color(input), expected);
240    }
241}