use crate::tui::app::{App, DiffLineType, FocusedPane};
use ratatui::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Frame,
};
pub fn render_diff(f: &mut Frame, app: &App, area: Rect) {
let branch = app.selected_branch();
let is_focused = app.focused_pane == FocusedPane::Diff;
let title = if let Some(b) = branch {
if let Some(parent) = &b.parent {
format!(" Diff: {} ← {} ", b.name, parent)
} else {
format!(" {} ", b.name)
}
} else {
" Diff ".to_string()
};
let (border_color, title_style) = if is_focused {
(
Color::Cyan,
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
} else {
(Color::DarkGray, Style::default().fg(Color::DarkGray))
};
let mut all_content: Vec<Line> = Vec::new();
if !app.diff_stat.is_empty() {
let total_add: usize = app.diff_stat.iter().map(|s| s.additions).sum();
let total_del: usize = app.diff_stat.iter().map(|s| s.deletions).sum();
all_content.push(Line::from(vec![
Span::styled(
format!("{} files changed, ", app.diff_stat.len()),
Style::default().fg(Color::White),
),
Span::styled(
format!("{} insertions(+)", total_add),
Style::default().fg(Color::Green),
),
Span::raw(", "),
Span::styled(
format!("{} deletions(-)", total_del),
Style::default().fg(Color::Red),
),
]));
all_content.push(Line::from(""));
let max_file_len = app
.diff_stat
.iter()
.map(|s| s.file.len())
.max()
.unwrap_or(20)
.min(40);
for stat in &app.diff_stat {
let file = if stat.file.len() > max_file_len {
format!("...{}", &stat.file[stat.file.len() - max_file_len + 3..])
} else {
stat.file.clone()
};
let total_changes = stat.additions + stat.deletions;
let bar_width = 30.min(total_changes);
let add_bars = if total_changes > 0 {
(stat.additions * bar_width) / total_changes
} else {
0
};
let del_bars = bar_width.saturating_sub(add_bars);
all_content.push(Line::from(vec![
Span::styled(
format!("{:width$}", file, width = max_file_len),
Style::default().fg(Color::White),
),
Span::raw(" | "),
Span::styled(
format!("{:>4}", total_changes),
Style::default().fg(Color::Yellow),
),
Span::raw(" "),
Span::styled("+".repeat(add_bars), Style::default().fg(Color::Green)),
Span::styled("-".repeat(del_bars), Style::default().fg(Color::Red)),
]));
}
all_content.push(Line::from(""));
all_content.push(Line::from(vec![Span::styled(
"─".repeat(60),
Style::default().fg(Color::DarkGray),
)]));
all_content.push(Line::from(""));
}
if app.selected_diff.is_empty() {
if branch.map(|b| b.is_trunk).unwrap_or(true) {
all_content.push(Line::from(Span::styled(
"No diff for trunk",
Style::default().fg(Color::DarkGray),
)));
} else if app.diff_stat.is_empty() {
all_content.push(Line::from(Span::styled(
"No changes",
Style::default().fg(Color::DarkGray),
)));
}
} else {
let diff_lines: Vec<Line> = app
.selected_diff
.iter()
.map(|diff_line| {
let style = match diff_line.line_type {
DiffLineType::Addition => Style::default().fg(Color::Green),
DiffLineType::Deletion => Style::default().fg(Color::Red),
DiffLineType::Hunk => Style::default().fg(Color::Cyan),
DiffLineType::Header => Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
DiffLineType::Context => Style::default().fg(Color::White),
};
Line::from(Span::styled(diff_line.content.clone(), style))
})
.collect();
all_content.extend(diff_lines);
}
let content: Vec<Line> = all_content.into_iter().skip(app.diff_scroll).collect();
let title_with_scroll = if !app.selected_diff.is_empty() && app.diff_scroll > 0 {
format!("{} [line {}]", title, app.diff_scroll + 1)
} else {
title
};
let paragraph = Paragraph::new(content).block(
Block::default()
.borders(Borders::ALL)
.title(Span::styled(title_with_scroll, title_style))
.border_style(Style::default().fg(border_color)),
);
f.render_widget(paragraph, area);
}