1use std::collections::{HashMap, VecDeque};
4
5use ratatui::style::Color;
6
7pub const MAIN_BRANCH_COLOR: usize = 0;
9
10pub const PALETTE_SIZE: usize = 16;
12
13const GRAPH_PALETTE: [Color; 16] = [
16 Color::Blue, Color::Green, Color::Yellow, Color::Magenta, Color::Cyan, Color::Red, Color::LightBlue, Color::LightGreen, Color::LightYellow, Color::LightMagenta, Color::LightCyan, Color::LightRed, Color::Rgb(255, 165, 0), Color::Rgb(128, 0, 128), Color::Rgb(0, 128, 128), Color::Rgb(255, 105, 180), ];
33
34pub fn get_graph_color(idx: usize) -> Color {
36 GRAPH_PALETTE[idx % GRAPH_PALETTE.len()]
37}
38
39pub struct ColorAssigner {
41 next_idx: usize,
43 used_colors: Vec<bool>,
45}
46
47impl ColorAssigner {
48 pub fn new() -> Self {
49 let mut used_colors = vec![false; GRAPH_PALETTE.len()];
50 used_colors[MAIN_BRANCH_COLOR] = true; Self {
52 next_idx: 1,
53 used_colors,
54 }
55 }
56
57 pub fn main_color(&self) -> usize {
59 MAIN_BRANCH_COLOR
60 }
61
62 pub fn assign(&mut self) -> usize {
64 for i in 1..GRAPH_PALETTE.len() {
66 let idx = (self.next_idx + i - 1) % GRAPH_PALETTE.len();
67 if idx != MAIN_BRANCH_COLOR && !self.used_colors[idx] {
68 self.used_colors[idx] = true;
69 self.next_idx = (idx + 1) % GRAPH_PALETTE.len();
70 return idx;
71 }
72 }
73 let idx = if self.next_idx == MAIN_BRANCH_COLOR {
75 1
76 } else {
77 self.next_idx
78 };
79 self.next_idx = (idx + 1) % GRAPH_PALETTE.len();
80 if self.next_idx == MAIN_BRANCH_COLOR {
81 self.next_idx = 1;
82 }
83 idx
84 }
85
86 pub fn release(&mut self, idx: usize) {
88 if idx != MAIN_BRANCH_COLOR && idx < self.used_colors.len() {
89 self.used_colors[idx] = false;
90 }
91 }
92}
93
94impl Default for ColorAssigner {
95 fn default() -> Self {
96 Self::new()
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct ColorContext {
103 pub lane: usize,
105 pub is_main_branch: bool,
107 pub fork_point: Option<String>,
109 pub parent_color: Option<usize>,
111}
112
113impl ColorContext {
114 pub fn new(lane: usize) -> Self {
116 Self {
117 lane,
118 is_main_branch: false,
119 fork_point: None,
120 parent_color: None,
121 }
122 }
123
124 pub fn with_main_branch(mut self, is_main: bool) -> Self {
126 self.is_main_branch = is_main;
127 self
128 }
129
130 pub fn with_fork_point(mut self, fork_point: Option<String>) -> Self {
132 self.fork_point = fork_point;
133 self
134 }
135
136 pub fn with_parent_color(mut self, parent_color: Option<usize>) -> Self {
138 self.parent_color = parent_color;
139 self
140 }
141}
142
143pub struct PenaltyBasedColorAssigner {
150 lane_colors: HashMap<usize, usize>,
152 color_history: VecDeque<usize>,
154 fork_colors: HashMap<String, Vec<usize>>,
156 history_size: usize,
158}
159
160impl PenaltyBasedColorAssigner {
161 pub fn new() -> Self {
163 Self {
164 lane_colors: HashMap::new(),
165 color_history: VecDeque::new(),
166 fork_colors: HashMap::new(),
167 history_size: 8,
168 }
169 }
170
171 pub fn main_color(&self) -> usize {
173 MAIN_BRANCH_COLOR
174 }
175
176 pub fn assign_with_context(&mut self, ctx: &ColorContext) -> usize {
178 if ctx.is_main_branch {
180 self.update_state(ctx.lane, MAIN_BRANCH_COLOR, ctx.fork_point.as_ref());
181 return MAIN_BRANCH_COLOR;
182 }
183
184 let mut penalties = [0.0f32; PALETTE_SIZE];
186
187 for neighbor in [ctx.lane.saturating_sub(1), ctx.lane + 1] {
190 if neighbor != ctx.lane {
191 if let Some(&color) = self.lane_colors.get(&neighbor) {
192 penalties[color] += 10.0;
193 if let Some(similar) = self.similar_color(color) {
195 penalties[similar] += 5.0;
196 }
197 }
198 }
199 }
200
201 for (age, &color) in self.color_history.iter().enumerate() {
204 let decay = 1.0 - (age as f32 / self.history_size as f32);
205 penalties[color] += 3.0 * decay;
206 }
207
208 if let Some(ref fork) = ctx.fork_point {
211 if let Some(siblings) = self.fork_colors.get(fork) {
212 for &color in siblings {
213 penalties[color] += 8.0;
214 }
215 }
216 }
217
218 penalties[MAIN_BRANCH_COLOR] += 100.0;
220
221 let best = (1..PALETTE_SIZE)
223 .min_by(|&a, &b| {
224 penalties[a]
225 .partial_cmp(&penalties[b])
226 .unwrap_or(std::cmp::Ordering::Equal)
227 })
228 .unwrap_or(1);
229
230 self.update_state(ctx.lane, best, ctx.fork_point.as_ref());
232
233 best
234 }
235
236 fn update_state(&mut self, lane: usize, color: usize, fork_point: Option<&String>) {
238 self.lane_colors.insert(lane, color);
239
240 self.color_history.push_front(color);
241 if self.color_history.len() > self.history_size {
242 self.color_history.pop_back();
243 }
244
245 if let Some(fork) = fork_point {
246 self.fork_colors
247 .entry(fork.clone())
248 .or_default()
249 .push(color);
250 }
251 }
252
253 fn similar_color(&self, color: usize) -> Option<usize> {
255 match color {
260 0 => Some(6), 6 => Some(0),
262 1 => Some(7), 7 => Some(1),
264 2 => Some(8), 8 => Some(2),
266 3 => Some(9), 9 => Some(3),
268 4 => Some(10), 10 => Some(4),
270 5 => Some(11), 11 => Some(5),
272 _ => None,
273 }
274 }
275
276 pub fn get_lane_color(&self, lane: usize) -> Option<usize> {
278 self.lane_colors.get(&lane).copied()
279 }
280
281 pub fn release_lane(&mut self, lane: usize) {
283 self.lane_colors.remove(&lane);
284 }
285}
286
287impl Default for PenaltyBasedColorAssigner {
288 fn default() -> Self {
289 Self::new()
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn test_get_graph_color() {
299 assert_eq!(get_graph_color(0), Color::Blue);
300 assert_eq!(get_graph_color(1), Color::Green);
301 assert_eq!(get_graph_color(16), Color::Blue); }
303
304 #[test]
305 fn test_color_assigner_main_color() {
306 let assigner = ColorAssigner::new();
307 assert_eq!(assigner.main_color(), MAIN_BRANCH_COLOR);
308 }
309
310 #[test]
311 fn test_color_assigner_assign_skips_main() {
312 let mut assigner = ColorAssigner::new();
313 let color1 = assigner.assign();
314 assert_ne!(color1, MAIN_BRANCH_COLOR);
315 }
316
317 #[test]
318 fn test_color_assigner_assign_unique() {
319 let mut assigner = ColorAssigner::new();
320 let c1 = assigner.assign();
321 let c2 = assigner.assign();
322 let c3 = assigner.assign();
323 assert_ne!(c1, c2);
324 assert_ne!(c2, c3);
325 assert_ne!(c1, c3);
326 }
327
328 #[test]
329 fn test_color_assigner_release() {
330 let mut assigner = ColorAssigner::new();
331 let c1 = assigner.assign();
332 let _c2 = assigner.assign();
333 assigner.release(c1);
334 let c3 = assigner.assign();
336 assert!(!assigner.used_colors[c1] || c3 == c1);
338 }
339
340 #[test]
343 fn test_penalty_assigner_main_color() {
344 let assigner = PenaltyBasedColorAssigner::new();
345 assert_eq!(assigner.main_color(), MAIN_BRANCH_COLOR);
346 }
347
348 #[test]
349 fn test_penalty_assigner_main_branch_always_color_0() {
350 let mut assigner = PenaltyBasedColorAssigner::new();
351 let ctx = ColorContext::new(0).with_main_branch(true);
352 let color = assigner.assign_with_context(&ctx);
353 assert_eq!(color, MAIN_BRANCH_COLOR);
354 }
355
356 #[test]
357 fn test_penalty_assigner_skips_main_color() {
358 let mut assigner = PenaltyBasedColorAssigner::new();
359 let ctx = ColorContext::new(1);
360 let color = assigner.assign_with_context(&ctx);
361 assert_ne!(color, MAIN_BRANCH_COLOR);
362 }
363
364 #[test]
365 fn test_penalty_assigner_adjacent_lanes_different_colors() {
366 let mut assigner = PenaltyBasedColorAssigner::new();
367
368 let ctx0 = ColorContext::new(0);
370 let color0 = assigner.assign_with_context(&ctx0);
371
372 let ctx1 = ColorContext::new(1);
374 let color1 = assigner.assign_with_context(&ctx1);
375
376 assert_ne!(color0, color1, "隣接レーンは異なる色になるべき");
377 }
378
379 #[test]
380 fn test_penalty_assigner_fork_siblings_different_colors() {
381 let mut assigner = PenaltyBasedColorAssigner::new();
382 let fork_point = "base_commit".to_string();
383
384 let ctx1 = ColorContext::new(1).with_fork_point(Some(fork_point.clone()));
386 let color1 = assigner.assign_with_context(&ctx1);
387
388 let ctx2 = ColorContext::new(2).with_fork_point(Some(fork_point.clone()));
389 let color2 = assigner.assign_with_context(&ctx2);
390
391 let ctx3 = ColorContext::new(3).with_fork_point(Some(fork_point));
392 let color3 = assigner.assign_with_context(&ctx3);
393
394 assert_ne!(color1, color2, "Fork siblingsは異なる色になるべき");
395 assert_ne!(color2, color3, "Fork siblingsは異なる色になるべき");
396 assert_ne!(color1, color3, "Fork siblingsは異なる色になるべき");
397 }
398
399 #[test]
400 fn test_penalty_assigner_history_avoidance() {
401 let mut assigner = PenaltyBasedColorAssigner::new();
402
403 let mut colors = Vec::new();
405 for i in 0..5 {
406 let ctx = ColorContext::new(i * 3); let color = assigner.assign_with_context(&ctx);
408 colors.push(color);
409 }
410
411 for i in 0..4 {
413 for j in (i + 1)..5 {
414 if i + 1 < j {
415 }
418 }
419 }
420 let unique_count = colors
422 .iter()
423 .collect::<std::collections::HashSet<_>>()
424 .len();
425 assert!(unique_count >= 3, "履歴により色のバリエーションがあるべき");
426 }
427
428 #[test]
429 fn test_penalty_assigner_get_lane_color() {
430 let mut assigner = PenaltyBasedColorAssigner::new();
431 let ctx = ColorContext::new(5);
432 let color = assigner.assign_with_context(&ctx);
433
434 assert_eq!(assigner.get_lane_color(5), Some(color));
435 assert_eq!(assigner.get_lane_color(0), None);
436 }
437
438 #[test]
439 fn test_penalty_assigner_release_lane() {
440 let mut assigner = PenaltyBasedColorAssigner::new();
441 let ctx = ColorContext::new(3);
442 let _color = assigner.assign_with_context(&ctx);
443
444 assert!(assigner.get_lane_color(3).is_some());
445 assigner.release_lane(3);
446 assert!(assigner.get_lane_color(3).is_none());
447 }
448
449 #[test]
450 fn test_color_context_builder() {
451 let ctx = ColorContext::new(5)
452 .with_main_branch(true)
453 .with_fork_point(Some("abc123".to_string()))
454 .with_parent_color(Some(3));
455
456 assert_eq!(ctx.lane, 5);
457 assert!(ctx.is_main_branch);
458 assert_eq!(ctx.fork_point, Some("abc123".to_string()));
459 assert_eq!(ctx.parent_color, Some(3));
460 }
461
462 #[test]
463 fn test_penalty_assigner_many_lanes() {
464 let mut assigner = PenaltyBasedColorAssigner::new();
465
466 for i in 0..20 {
468 let ctx = ColorContext::new(i);
469 let color = assigner.assign_with_context(&ctx);
470 assert!(color < PALETTE_SIZE, "色インデックスがパレットサイズ内");
472 }
473 }
474}