use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, List, ListItem};
use ratatui::Frame;
use crate::app::{ActivePane, App};
fn graph_color(color_index: usize, graph_colors: &[Color; 8]) -> Color {
graph_colors[color_index % graph_colors.len()]
}
fn build_graph_spans(
row: &gitkraft_core::GraphRow,
graph_colors: &[Color; 8],
) -> Vec<Span<'static>> {
let width = row.width;
if width == 0 {
return vec![Span::styled(
"● ",
Style::default().fg(graph_color(row.node_color, graph_colors)),
)];
}
let mut column_passthrough: Vec<Option<usize>> = vec![None; width]; let mut has_left_cross = false; let mut has_right_cross = false; let mut left_cross_color: usize = 0;
let mut right_cross_color: usize = 0;
let mut cross_left_col: usize = row.node_column;
let mut cross_right_col: usize = row.node_column;
for edge in &row.edges {
if edge.from_column == edge.to_column {
column_passthrough[edge.to_column] = Some(edge.color_index);
} else {
let target = edge.to_column;
if target < row.node_column {
has_left_cross = true;
left_cross_color = edge.color_index;
if target < cross_left_col {
cross_left_col = target;
}
} else if target > row.node_column {
has_right_cross = true;
right_cross_color = edge.color_index;
if target > cross_right_col {
cross_right_col = target;
}
}
}
}
let mut spans: Vec<Span<'static>> = Vec::with_capacity(width + 1);
for col in 0..width {
if col == row.node_column {
spans.push(Span::styled(
"● ".to_string(),
Style::default()
.fg(graph_color(row.node_color, graph_colors))
.add_modifier(Modifier::BOLD),
));
} else if let Some(ci) = column_passthrough.get(col).copied().flatten() {
let in_left_range = has_left_cross && col >= cross_left_col && col < row.node_column;
let in_right_range = has_right_cross && col > row.node_column && col <= cross_right_col;
if in_left_range || in_right_range {
let cross_ci = if in_left_range {
left_cross_color
} else {
right_cross_color
};
spans.push(Span::styled(
"├─".to_string(),
Style::default().fg(graph_color(cross_ci, graph_colors)),
));
} else {
spans.push(Span::styled(
"│ ".to_string(),
Style::default().fg(graph_color(ci, graph_colors)),
));
}
} else {
let in_left_range = has_left_cross && col >= cross_left_col && col < row.node_column;
let in_right_range = has_right_cross && col > row.node_column && col <= cross_right_col;
if in_left_range {
if col == cross_left_col {
spans.push(Span::styled(
"╭─".to_string(),
Style::default().fg(graph_color(left_cross_color, graph_colors)),
));
} else {
spans.push(Span::styled(
"──".to_string(),
Style::default().fg(graph_color(left_cross_color, graph_colors)),
));
}
} else if in_right_range {
if col == cross_right_col {
spans.push(Span::styled(
"─╮".to_string(),
Style::default().fg(graph_color(right_cross_color, graph_colors)),
));
} else {
spans.push(Span::styled(
"──".to_string(),
Style::default().fg(graph_color(right_cross_color, graph_colors)),
));
}
} else {
spans.push(Span::styled(
" ".to_string(),
Style::default().fg(Color::DarkGray),
));
}
}
}
spans.push(Span::styled(" ", Style::default()));
spans
}
pub fn render(app: &mut App, frame: &mut Frame, area: Rect) {
let theme = app.theme();
let is_active = app.active_pane == ActivePane::CommitLog;
let border_color = if is_active {
theme.border_active
} else {
theme.border_inactive
};
let selected_count = app.tab().selected_commits.len();
let title = if app.tab().search_active {
format!(
" Commit Log — Search: \"{}\" ({}) ",
app.tab().search_query,
app.tab().search_results.len()
)
} else if selected_count > 1 {
format!(
" Commit Log ({}) — {} selected [J/K shrink] ",
app.tab().commits.len(),
selected_count
)
} else if is_active {
format!(" Commit Log ({}) [J/K select] ", app.tab().commits.len())
} else {
format!(" Commit Log ({}) ", app.tab().commits.len())
};
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color))
.style(Style::default().bg(theme.bg));
let commits_to_show: &Vec<gitkraft_core::CommitInfo> =
if app.tab().search_active && !app.tab().search_results.is_empty() {
&app.tab().search_results
} else {
&app.tab().commits
};
if commits_to_show.is_empty() {
let items: Vec<ListItem> = vec![ListItem::new(Line::from(vec![Span::styled(
" No commits yet",
Style::default().fg(theme.text_muted),
)]))];
let list = List::new(items).block(block);
frame.render_widget(list, area);
return;
}
let show_graph = !app.tab().search_active || app.tab().search_results.is_empty();
let items: Vec<ListItem> = commits_to_show
.iter()
.enumerate()
.map(|(idx, commit)| {
let summary = gitkraft_core::truncate_str(&commit.summary, 50);
let relative = commit.relative_time();
let is_primary_selected = app.tab().commit_list_state.selected() == Some(idx);
let badge_span =
if let Some(pos) = app.tab().selected_commits.iter().position(|&i| i == idx) {
Span::styled(format!("{:>2}", pos + 1), Style::default().fg(theme.accent))
} else {
Span::raw(" ")
};
let mut spans = vec![badge_span, Span::raw(" ")];
if show_graph {
if let Some(row) = app.tab().graph_rows.get(idx) {
spans.extend(build_graph_spans(row, &theme.graph_colors));
} else {
spans.push(Span::raw(" "));
}
} else {
spans.push(Span::raw(" "));
}
spans.push(Span::styled(
format!("{} ", commit.short_oid),
Style::default().fg(theme.warning),
));
spans.push(Span::styled(
summary,
Style::default().fg(theme.text_primary),
));
spans.push(Span::styled(
format!(" ({}", commit.author_name),
Style::default().fg(theme.accent),
));
spans.push(Span::styled(
format!(", {})", relative),
Style::default().fg(theme.text_muted),
));
let style = if !is_primary_selected && app.tab().selected_commits.contains(&idx) {
Style::default().bg(theme.sel_bg)
} else {
Style::default()
};
ListItem::new(Line::from(spans)).style(style)
})
.collect();
let list = List::new(items)
.block(block)
.highlight_style(
Style::default()
.bg(theme.sel_bg)
.fg(theme.text_primary)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("▶ ");
frame.render_stateful_widget(list, area, &mut app.tab_mut().commit_list_state);
if !app.tab().commit_action_items.is_empty() {
render_action_popup(app, frame, area);
}
}
fn render_action_popup(app: &mut App, frame: &mut Frame, area: Rect) {
let theme = app.theme();
let mut items: Vec<ListItem> = Vec::new();
let cursor = app.tab().commit_action_cursor;
let mut flat_idx: usize = 0;
for (group_idx, group) in gitkraft_core::COMMIT_MENU_GROUPS.iter().enumerate() {
if group_idx > 0 {
items.push(ListItem::new(Line::from(Span::styled(
"─────────────────────────────",
Style::default().fg(theme.border_inactive),
))));
}
for &kind in *group {
let is_selected = flat_idx == cursor;
let style = if is_selected {
Style::default()
.fg(theme.text_primary)
.bg(theme.sel_bg)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.text_primary)
};
let prefix = if is_selected { "▶ " } else { " " };
items.push(ListItem::new(Line::from(vec![
Span::styled(prefix, style),
Span::styled(kind.label(), style),
])));
flat_idx += 1;
}
}
let popup_width = (gitkraft_core::COMMIT_MENU_GROUPS
.iter()
.flat_map(|g| g.iter())
.map(|k| k.label().len())
.max()
.unwrap_or(30)
+ 6) as u16;
let popup_height = (items.len() + 2) as u16;
let x = area.x + area.width.saturating_sub(popup_width) / 2;
let y = area.y + area.height.saturating_sub(popup_height) / 2;
let popup_rect = Rect {
x,
y,
width: popup_width.min(area.width),
height: popup_height.min(area.height),
};
let oid_short = app
.tab()
.pending_commit_action_oid
.as_deref()
.map(|o| &o[..o.len().min(7)])
.unwrap_or("?");
let block = Block::default()
.title(format!(" Actions: {oid_short} "))
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.border_active))
.style(Style::default().bg(theme.bg));
frame.render_widget(Clear, popup_rect);
let list = List::new(items).block(block);
frame.render_widget(list, popup_rect);
}