1use ratatui::style::Color as RatatuiColor;
7use smart_default::SmartDefault;
8
9#[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 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 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 fn default_branch_colors() -> Vec<GraphColor> {
177 vec![
178 GraphColor::from_rgb(0x4A, 0x9E, 0xCD), GraphColor::from_rgb(0x6C, 0xC6, 0x44), GraphColor::from_rgb(0xE0, 0x6C, 0x75), GraphColor::from_rgb(0xE5, 0xC0, 0x7B), GraphColor::from_rgb(0xC6, 0x78, 0xDD), GraphColor::from_rgb(0x56, 0xB6, 0xC2), GraphColor::from_rgb(0xD1, 0x9A, 0x66), ]
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}