git_contribution_analyzer/
ui.rs1use crate::{
2 app::{App, AuthorSummary},
3 git::Contribution,
4};
5use std::io;
6use tui::{
7 backend::CrosstermBackend,
8 layout::{Constraint, Direction, Layout, Rect},
9 style::{Color, Modifier, Style},
10 text::Spans,
11 widgets::{Block, Borders, Cell, Paragraph, Row, Table, Tabs},
12 Frame,
13};
14
15pub fn render_loading_screen(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &App) {
16 let size = f.size();
17
18 let block = Block::default()
19 .title("Git Contribution Analyzer")
20 .borders(Borders::ALL);
21 f.render_widget(block, size);
22
23 let loading_text = format!(
24 "{} {}",
25 app.loading_message,
26 ".".repeat(((app.loading_progress % 4) + 1) as usize)
27 );
28
29 let loading_paragraph = Paragraph::new(loading_text)
30 .style(Style::default().fg(Color::Cyan))
31 .block(Block::default())
32 .alignment(tui::layout::Alignment::Center);
33
34 let loading_area = centered_rect(60, 20, size);
35 f.render_widget(loading_paragraph, loading_area);
36}
37
38pub fn render_main_view(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &App) {
39 let size = f.size();
40
41 let main_block = Block::default()
42 .title("Git Contribution Analyzer")
43 .borders(Borders::ALL);
44 f.render_widget(main_block, size);
45
46 let chunks = Layout::default()
47 .direction(Direction::Vertical)
48 .constraints([
49 Constraint::Length(3), Constraint::Min(10), Constraint::Length(3), ])
53 .split(size);
54
55 let mut tab_titles = app
56 .repositories
57 .iter()
58 .map(|repo| Spans::from(repo.clone()))
59 .collect::<Vec<Spans>>();
60
61 tab_titles.push(Spans::from("Summary"));
62
63 let tabs = Tabs::new(tab_titles)
64 .block(Block::default().borders(Borders::ALL).title("Repositories"))
65 .select(app.current_tab)
66 .style(Style::default())
67 .highlight_style(
68 Style::default()
69 .fg(Color::Yellow)
70 .add_modifier(Modifier::BOLD),
71 );
72
73 f.render_widget(tabs, chunks[0]);
74
75 if app.current_tab < app.repositories.len() {
76 let repo_name = &app.repositories[app.current_tab];
77 if let Some(contributions) = app.contributions.get(repo_name) {
78 render_repository_tab(
79 f,
80 chunks[1],
81 repo_name,
82 contributions,
83 app.selected_in_tab[app.current_tab],
84 );
85 }
86 } else {
87 render_summary_tab(
88 f,
89 chunks[1],
90 &app.author_summaries,
91 app.selected_in_tab[app.current_tab],
92 );
93 }
94
95 if app.show_help {
96 render_help(f, chunks[2]);
97 } else {
98 render_help_shortcut(f, chunks[2]);
99 }
100}
101
102pub fn render_repository_tab(
103 f: &mut Frame<CrosstermBackend<io::Stdout>>,
104 area: Rect,
105 repo_name: &str,
106 contributions: &[Contribution],
107 selected: Option<usize>,
108) {
109 let header_cells = [
110 "Author",
111 "Email",
112 "Commits",
113 "Lines Added",
114 "Lines Deleted",
115 "Contribution %",
116 ]
117 .iter()
118 .map(|h| Cell::from(*h).style(Style::default().fg(Color::Yellow)));
119
120 let header = Row::new(header_cells).style(Style::default()).height(1);
121
122 let rows = contributions.iter().enumerate().map(|(i, c)| {
123 let style = if Some(i) == selected {
124 Style::default().add_modifier(Modifier::REVERSED)
125 } else {
126 Style::default()
127 };
128
129 let cells = [
130 Cell::from(c.author.clone()),
131 Cell::from(c.email.clone()),
132 Cell::from(c.commits.to_string()),
133 Cell::from(c.lines_added.to_string()),
134 Cell::from(c.lines_deleted.to_string()),
135 Cell::from(format!("{:.2}%", c.contribution_percent)),
136 ];
137
138 Row::new(cells).style(style).height(1)
139 });
140
141 let table = Table::new(rows)
142 .header(header)
143 .block(
144 Block::default()
145 .title(format!("Repository: {}", repo_name))
146 .borders(Borders::ALL),
147 )
148 .widths(&[
149 Constraint::Percentage(20),
150 Constraint::Percentage(30),
151 Constraint::Percentage(10),
152 Constraint::Percentage(13),
153 Constraint::Percentage(13),
154 Constraint::Percentage(14),
155 ])
156 .highlight_style(Style::default().add_modifier(Modifier::REVERSED))
157 .highlight_symbol("> ");
158
159 f.render_widget(table, area);
160}
161
162pub fn render_summary_tab(
163 f: &mut Frame<CrosstermBackend<io::Stdout>>,
164 area: Rect,
165 summaries: &[AuthorSummary],
166 selected: Option<usize>,
167) {
168 let header_cells = [
169 "Author",
170 "Email",
171 "Total Commits",
172 "Lines Added",
173 "Lines Deleted",
174 "Overall %",
175 "Preferred Repo",
176 "Preferred %",
177 ]
178 .iter()
179 .map(|h| Cell::from(*h).style(Style::default().fg(Color::Yellow)));
180
181 let header = Row::new(header_cells).style(Style::default()).height(1);
182
183 let rows = summaries.iter().enumerate().map(|(i, s)| {
184 let style = if Some(i) == selected {
185 Style::default().add_modifier(Modifier::REVERSED)
186 } else {
187 Style::default()
188 };
189
190 let cells = [
191 Cell::from(s.author.clone()),
192 Cell::from(s.email.clone()),
193 Cell::from(s.total_commits.to_string()),
194 Cell::from(s.total_lines_added.to_string()),
195 Cell::from(s.total_lines_deleted.to_string()),
196 Cell::from(format!("{:.2}%", s.overall_contribution_percent)),
197 Cell::from(s.preferred_repo.clone()),
198 Cell::from(format!("{:.2}%", s.preferred_repo_percent)),
199 ];
200
201 Row::new(cells).style(style).height(1)
202 });
203
204 let table = Table::new(rows)
205 .header(header)
206 .block(
207 Block::default()
208 .title("Summary Across All Repositories")
209 .borders(Borders::ALL),
210 )
211 .widths(&[
212 Constraint::Percentage(15),
213 Constraint::Percentage(20),
214 Constraint::Percentage(10),
215 Constraint::Percentage(10),
216 Constraint::Percentage(10),
217 Constraint::Percentage(10),
218 Constraint::Percentage(15),
219 Constraint::Percentage(10),
220 ])
221 .highlight_style(Style::default().add_modifier(Modifier::REVERSED))
222 .highlight_symbol("> ");
223
224 f.render_widget(table, area);
225}
226
227pub fn render_help_shortcut(f: &mut Frame<CrosstermBackend<io::Stdout>>, area: Rect) {
228 let help_text = "Press '?' to show help";
229 let help_paragraph = Paragraph::new(help_text)
230 .style(Style::default().fg(Color::Gray))
231 .alignment(tui::layout::Alignment::Center)
232 .block(Block::default().borders(Borders::ALL));
233
234 f.render_widget(help_paragraph, area);
235}
236
237pub fn render_help(f: &mut Frame<CrosstermBackend<io::Stdout>>, area: Rect) {
238 let help_text = vec![
239 Spans::from("↑/↓: Navigate entries | Tab/Shift+Tab: Switch repositories"),
240 Spans::from("?: Toggle help | q: Quit | h: Export HTML report"),
241 ];
242
243 let help_paragraph = Paragraph::new(help_text)
244 .style(Style::default())
245 .alignment(tui::layout::Alignment::Center)
246 .block(Block::default().borders(Borders::ALL).title("Help"));
247
248 f.render_widget(help_paragraph, area);
249}
250
251pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
252 let popup_layout = Layout::default()
253 .direction(Direction::Vertical)
254 .constraints([
255 Constraint::Percentage((100 - percent_y) / 2),
256 Constraint::Percentage(percent_y),
257 Constraint::Percentage((100 - percent_y) / 2),
258 ])
259 .split(r);
260
261 Layout::default()
262 .direction(Direction::Horizontal)
263 .constraints([
264 Constraint::Percentage((100 - percent_x) / 2),
265 Constraint::Percentage(percent_x),
266 Constraint::Percentage((100 - percent_x) / 2),
267 ])
268 .split(popup_layout[1])[1]
269}