use std::collections::{HashMap, VecDeque};
use ratatui::style::Color;
use crate::tui::theme;
pub const MAIN_BRANCH_COLOR: usize = 0;
pub const PALETTE_SIZE: usize = 12;
const GRAPH_PALETTE: [Color; 12] = theme::GRAPH_PALETTE;
pub fn get_graph_color(idx: usize) -> Color {
GRAPH_PALETTE[idx % GRAPH_PALETTE.len()]
}
#[derive(Debug, Clone)]
pub struct ColorContext {
pub lane: usize,
pub is_main_branch: bool,
pub fork_point: Option<String>,
pub parent_color: Option<usize>,
}
impl ColorContext {
pub fn new(lane: usize) -> Self {
Self {
lane,
is_main_branch: false,
fork_point: None,
parent_color: None,
}
}
pub fn with_main_branch(mut self, is_main: bool) -> Self {
self.is_main_branch = is_main;
self
}
pub fn with_fork_point(mut self, fork_point: Option<String>) -> Self {
self.fork_point = fork_point;
self
}
pub fn with_parent_color(mut self, parent_color: Option<usize>) -> Self {
self.parent_color = parent_color;
self
}
}
pub struct PenaltyBasedColorAssigner {
lane_colors: HashMap<usize, usize>,
color_history: VecDeque<usize>,
fork_colors: HashMap<String, Vec<usize>>,
history_size: usize,
}
impl PenaltyBasedColorAssigner {
pub fn new() -> Self {
Self {
lane_colors: HashMap::new(),
color_history: VecDeque::new(),
fork_colors: HashMap::new(),
history_size: 8,
}
}
pub fn main_color(&self) -> usize {
MAIN_BRANCH_COLOR
}
pub fn assign_with_context(&mut self, ctx: &ColorContext) -> usize {
if ctx.is_main_branch {
self.update_state(ctx.lane, MAIN_BRANCH_COLOR, ctx.fork_point.as_ref());
return MAIN_BRANCH_COLOR;
}
let mut penalties = [0.0f32; PALETTE_SIZE];
for neighbor in [ctx.lane.saturating_sub(1), ctx.lane + 1] {
if neighbor != ctx.lane {
if let Some(&color) = self.lane_colors.get(&neighbor) {
penalties[color] += 10.0;
if let Some(similar) = self.similar_color(color) {
penalties[similar] += 5.0;
}
}
}
}
for (age, &color) in self.color_history.iter().enumerate() {
let decay = 1.0 - (age as f32 / self.history_size as f32);
penalties[color] += 3.0 * decay;
}
if let Some(ref fork) = ctx.fork_point {
if let Some(siblings) = self.fork_colors.get(fork) {
for &color in siblings {
penalties[color] += 8.0;
}
}
}
penalties[MAIN_BRANCH_COLOR] += 100.0;
let best = (1..PALETTE_SIZE)
.min_by(|&a, &b| {
penalties[a]
.partial_cmp(&penalties[b])
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(1);
self.update_state(ctx.lane, best, ctx.fork_point.as_ref());
best
}
fn update_state(&mut self, lane: usize, color: usize, fork_point: Option<&String>) {
self.lane_colors.insert(lane, color);
self.color_history.push_front(color);
if self.color_history.len() > self.history_size {
self.color_history.pop_back();
}
if let Some(fork) = fork_point {
self.fork_colors
.entry(fork.clone())
.or_default()
.push(color);
}
}
fn similar_color(&self, color: usize) -> Option<usize> {
match color {
0 => Some(10), 10 => Some(0),
1 => Some(7), 7 => Some(1),
2 => Some(6), 6 => Some(2),
3 => Some(5), 5 => Some(3),
4 => Some(8), 8 => Some(4),
9 => Some(11), 11 => Some(9),
_ => None,
}
}
pub fn get_lane_color(&self, lane: usize) -> Option<usize> {
self.lane_colors.get(&lane).copied()
}
pub fn release_lane(&mut self, lane: usize) {
self.lane_colors.remove(&lane);
}
}
impl Default for PenaltyBasedColorAssigner {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_graph_color() {
assert_eq!(get_graph_color(0), theme::SKY);
assert_eq!(get_graph_color(1), theme::BLUE);
assert_eq!(get_graph_color(12), theme::SKY); }
#[test]
fn test_penalty_assigner_main_color() {
let assigner = PenaltyBasedColorAssigner::new();
assert_eq!(assigner.main_color(), MAIN_BRANCH_COLOR);
}
#[test]
fn test_penalty_assigner_main_branch_always_color_0() {
let mut assigner = PenaltyBasedColorAssigner::new();
let ctx = ColorContext::new(0).with_main_branch(true);
let color = assigner.assign_with_context(&ctx);
assert_eq!(color, MAIN_BRANCH_COLOR);
}
#[test]
fn test_penalty_assigner_skips_main_color() {
let mut assigner = PenaltyBasedColorAssigner::new();
let ctx = ColorContext::new(1);
let color = assigner.assign_with_context(&ctx);
assert_ne!(color, MAIN_BRANCH_COLOR);
}
#[test]
fn test_penalty_assigner_adjacent_lanes_different_colors() {
let mut assigner = PenaltyBasedColorAssigner::new();
let ctx0 = ColorContext::new(0);
let color0 = assigner.assign_with_context(&ctx0);
let ctx1 = ColorContext::new(1);
let color1 = assigner.assign_with_context(&ctx1);
assert_ne!(
color0, color1,
"Adjacent lanes should have different colors"
);
}
#[test]
fn test_penalty_assigner_fork_siblings_different_colors() {
let mut assigner = PenaltyBasedColorAssigner::new();
let fork_point = "base_commit".to_string();
let ctx1 = ColorContext::new(1).with_fork_point(Some(fork_point.clone()));
let color1 = assigner.assign_with_context(&ctx1);
let ctx2 = ColorContext::new(2).with_fork_point(Some(fork_point.clone()));
let color2 = assigner.assign_with_context(&ctx2);
let ctx3 = ColorContext::new(3).with_fork_point(Some(fork_point));
let color3 = assigner.assign_with_context(&ctx3);
assert_ne!(color1, color2, "Fork siblings should have different colors");
assert_ne!(color2, color3, "Fork siblings should have different colors");
assert_ne!(color1, color3, "Fork siblings should have different colors");
}
#[test]
fn test_penalty_assigner_history_avoidance() {
let mut assigner = PenaltyBasedColorAssigner::new();
let mut colors = Vec::new();
for i in 0..5 {
let ctx = ColorContext::new(i * 3); let color = assigner.assign_with_context(&ctx);
colors.push(color);
}
for i in 0..4 {
for j in (i + 1)..5 {
if i + 1 < j {
}
}
}
let unique_count = colors
.iter()
.collect::<std::collections::HashSet<_>>()
.len();
assert!(
unique_count >= 3,
"Should have color variation due to history"
);
}
#[test]
fn test_penalty_assigner_get_lane_color() {
let mut assigner = PenaltyBasedColorAssigner::new();
let ctx = ColorContext::new(5);
let color = assigner.assign_with_context(&ctx);
assert_eq!(assigner.get_lane_color(5), Some(color));
assert_eq!(assigner.get_lane_color(0), None);
}
#[test]
fn test_penalty_assigner_release_lane() {
let mut assigner = PenaltyBasedColorAssigner::new();
let ctx = ColorContext::new(3);
let _color = assigner.assign_with_context(&ctx);
assert!(assigner.get_lane_color(3).is_some());
assigner.release_lane(3);
assert!(assigner.get_lane_color(3).is_none());
}
#[test]
fn test_color_context_builder() {
let ctx = ColorContext::new(5)
.with_main_branch(true)
.with_fork_point(Some("abc123".to_string()))
.with_parent_color(Some(3));
assert_eq!(ctx.lane, 5);
assert!(ctx.is_main_branch);
assert_eq!(ctx.fork_point, Some("abc123".to_string()));
assert_eq!(ctx.parent_color, Some(3));
}
#[test]
fn test_penalty_assigner_many_lanes() {
let mut assigner = PenaltyBasedColorAssigner::new();
for i in 0..20 {
let ctx = ColorContext::new(i);
let color = assigner.assign_with_context(&ctx);
assert!(
color < PALETTE_SIZE,
"Color index should be within palette size"
);
}
}
}