gitkraft_tui/features/diff/
view.rs1use ratatui::layout::{Constraint, Direction, Layout, Rect};
2use ratatui::style::{Modifier, Style};
3use ratatui::text::{Line, Span};
4use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
5use ratatui::Frame;
6
7use gitkraft_core::DiffLine;
8
9use crate::app::{ActivePane, App};
10
11pub fn render(app: &mut App, frame: &mut Frame, area: Rect) {
18 let theme = app.theme();
19 let is_active = app.active_pane == ActivePane::DiffView;
20 let border_color = if is_active {
21 theme.border_active
22 } else {
23 theme.border_inactive
24 };
25
26 if !app.tab().commit_files.is_empty() {
28 let chunks = Layout::default()
29 .direction(Direction::Horizontal)
30 .constraints([Constraint::Length(30), Constraint::Min(20)])
31 .split(area);
32
33 render_file_list(app, frame, chunks[0], border_color);
34 render_diff_content(app, frame, chunks[1], border_color);
35 } else {
36 render_diff_content(app, frame, area, border_color);
37 }
38}
39
40fn render_file_list(
42 app: &mut App,
43 frame: &mut Frame,
44 area: Rect,
45 border_color: ratatui::style::Color,
46) {
47 let theme = app.theme();
48 let tab = app.tab();
49 let commit_diff_file_index = tab.commit_diff_file_index;
50
51 let block = Block::default()
52 .title(format!(" Files ({}) ", tab.commit_files.len()))
53 .borders(Borders::ALL)
54 .border_style(Style::default().fg(border_color));
55
56 let items: Vec<ListItem> = tab
57 .commit_files
58 .iter()
59 .enumerate()
60 .map(|(i, diff)| {
61 let is_selected = i == commit_diff_file_index;
62 let file_name = diff.file_name();
63 let status_char = format!("{}", diff.status);
64
65 let status_color = match diff.status.color_category() {
66 gitkraft_core::StatusColorCategory::Added => theme.success,
67 gitkraft_core::StatusColorCategory::Modified => theme.warning,
68 gitkraft_core::StatusColorCategory::Deleted => theme.error,
69 gitkraft_core::StatusColorCategory::Renamed => theme.accent,
70 };
71
72 let name_color = if is_selected {
73 theme.text_primary
74 } else {
75 theme.text_secondary
76 };
77
78 let line = Line::from(vec![
79 Span::styled(
80 format!("{} ", status_char),
81 Style::default()
82 .fg(status_color)
83 .add_modifier(Modifier::BOLD),
84 ),
85 Span::styled(file_name.to_string(), Style::default().fg(name_color)),
86 ]);
87
88 ListItem::new(line)
89 })
90 .collect();
91
92 let mut list_state = ratatui::widgets::ListState::default();
93 list_state.select(Some(commit_diff_file_index));
94
95 let list = List::new(items)
96 .block(block)
97 .highlight_style(
98 Style::default()
99 .bg(theme.sel_bg)
100 .add_modifier(Modifier::REVERSED),
101 )
102 .highlight_symbol("▸ ");
103
104 frame.render_stateful_widget(list, area, &mut list_state);
105}
106
107fn render_diff_content(
109 app: &mut App,
110 frame: &mut Frame,
111 area: Rect,
112 border_color: ratatui::style::Color,
113) {
114 let theme = app.theme();
115 let tab = app.tab_mut();
116
117 let title = match &tab.selected_diff {
118 Some(diff) => {
119 let name = diff.display_path();
120 if name.is_empty() {
121 " Diff ".to_string()
122 } else {
123 format!(" Diff: {} ", name)
124 }
125 }
126 None => " Diff ".to_string(),
127 };
128
129 let block = Block::default()
130 .title(title)
131 .borders(Borders::ALL)
132 .border_style(Style::default().fg(border_color));
133
134 match &tab.selected_diff {
135 None => {
136 let placeholder = Paragraph::new(Line::from(vec![Span::styled(
137 "Select a commit or file to view diff",
138 Style::default().fg(theme.text_muted),
139 )]))
140 .block(block)
141 .alignment(ratatui::layout::Alignment::Center);
142
143 frame.render_widget(placeholder, area);
144 }
145 Some(diff) => {
146 let mut lines: Vec<Line> = Vec::new();
147
148 for hunk in &diff.hunks {
149 for line in &hunk.lines {
150 let styled_line = match line {
151 DiffLine::Addition(s) => Line::from(Span::styled(
152 format!("+{}", s),
153 Style::default().fg(theme.diff_add),
154 )),
155 DiffLine::Deletion(s) => Line::from(Span::styled(
156 format!("-{}", s),
157 Style::default().fg(theme.diff_del),
158 )),
159 DiffLine::Context(s) => Line::from(Span::styled(
160 format!(" {}", s),
161 Style::default().fg(theme.diff_context),
162 )),
163 DiffLine::HunkHeader(s) => Line::from(Span::styled(
164 s.clone(),
165 Style::default()
166 .fg(theme.diff_hunk)
167 .add_modifier(Modifier::BOLD),
168 )),
169 };
170 lines.push(styled_line);
171 }
172 }
173
174 let content_height = lines.len() as u16;
176 let visible_height = area.height.saturating_sub(2); if content_height > visible_height {
178 if tab.diff_scroll > content_height.saturating_sub(visible_height) {
179 tab.diff_scroll = content_height.saturating_sub(visible_height);
180 }
181 } else {
182 tab.diff_scroll = 0;
183 }
184
185 let paragraph = Paragraph::new(lines)
186 .block(block)
187 .wrap(Wrap { trim: false })
188 .scroll((tab.diff_scroll, 0));
189
190 frame.render_widget(paragraph, area);
191 }
192 }
193}