use crate::stats::ActivityStats;
use crate::tui::app::{App, Metric};
use crate::tui::chart_type::ChartType;
use crate::tui::widgets::{
chart_width, render_diverging_bar_chart, render_line_chart_for_metric,
render_vertical_bar_chart,
};
use ratatui::layout::Flex;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Paragraph};
pub fn render(frame: &mut Frame, app: &App) {
let area = frame.area();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(10), Constraint::Length(3), ])
.split(area);
render_header(frame, chunks[0], app);
if app.single_metric() {
render_single_chart(frame, chunks[1], app);
} else {
render_split_charts(frame, chunks[1], app);
}
render_footer(frame, chunks[2], app);
}
fn render_header(frame: &mut Frame, area: Rect, app: &App) {
let title = format!(
" {} | {} | {} ",
app.result.repository,
app.result.period,
format_date_range(&app.result.from.to_string(), &app.result.to.to_string())
);
let header = Paragraph::new(title)
.style(Style::default().fg(Color::Cyan).bold())
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan)),
);
frame.render_widget(header, area);
}
fn render_single_chart(frame: &mut Frame, area: Rect, app: &App) {
match app.chart_type() {
ChartType::Commits => render_line_chart_for_metric(frame, area, app, Metric::Commits),
ChartType::FilesChanged => {
render_line_chart_for_metric(frame, area, app, Metric::FilesChanged);
}
ChartType::AddDel => render_diverging_bar_chart(frame, area, app),
ChartType::Weekday => {
let centered = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Max(chart_width(7))])
.flex(Flex::Center)
.split(area)[0];
render_weekday_chart(frame, centered, &app.activity_stats);
}
ChartType::Hour => {
let centered = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Max(chart_width(24))])
.flex(Flex::Center)
.split(area)[0];
render_hourly_chart(frame, centered, &app.activity_stats);
}
}
}
fn render_split_charts(frame: &mut Frame, area: Rect, app: &App) {
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Ratio(3, 4), Constraint::Ratio(1, 4)])
.split(area);
let top_cols = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(3, 4), Constraint::Ratio(1, 4)])
.split(rows[0]);
let top_left_rows = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(top_cols[0]);
render_line_chart_for_metric(frame, top_left_rows[0], app, Metric::Commits);
render_line_chart_for_metric(frame, top_left_rows[1], app, Metric::FilesChanged);
render_diverging_bar_chart(frame, top_cols[1], app);
let bottom_cols = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 5), Constraint::Ratio(4, 5)])
.split(rows[1]);
render_weekday_chart(frame, bottom_cols[0], &app.activity_stats);
render_hourly_chart(frame, bottom_cols[1], &app.activity_stats);
}
fn render_weekday_chart(frame: &mut Frame, area: Rect, stats: &ActivityStats) {
let labels = ActivityStats::weekday_labels();
render_vertical_bar_chart(frame, area, "Weekday", &labels, &stats.weekday, Color::Cyan);
}
fn render_hourly_chart(frame: &mut Frame, area: Rect, stats: &ActivityStats) {
let labels: Vec<&str> = (0..24).map(hour_label).collect();
render_vertical_bar_chart(frame, area, "Hour", &labels, &stats.hourly, Color::Magenta);
}
fn hour_label(hour: usize) -> &'static str {
match hour {
0 => "0",
1 => "1",
2 => "2",
3 => "3",
4 => "4",
5 => "5",
6 => "6",
7 => "7",
8 => "8",
9 => "9",
10 => "10",
11 => "11",
12 => "12",
13 => "13",
14 => "14",
15 => "15",
16 => "16",
17 => "17",
18 => "18",
19 => "19",
20 => "20",
21 => "21",
22 => "22",
23 => "23",
_ => "",
}
}
fn render_footer(frame: &mut Frame, area: Rect, app: &App) {
let mode_indicator = if app.single_metric() {
format!("Single: {}", app.chart_type().name())
} else {
"Split".to_string()
};
let nav_hint = if app.single_metric() {
"[Tab] Switch | "
} else {
""
};
let help_text = format!(" {nav_hint}[m] Mode: {mode_indicator} | [q] Quit ");
let total = &app.result.total;
let summary = format!(
"Total: {} commits | +{} -{} | {} files",
total.commits, total.additions, total.deletions, total.files_changed
);
let footer_text = format!("{help_text}\n{summary}");
let footer = Paragraph::new(footer_text)
.style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray)),
);
frame.render_widget(footer, area);
}
fn format_date_range(from: &str, to: &str) -> String {
format!("{from} → {to}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_date_range() {
assert_eq!(
format_date_range("2024-01-01", "2024-01-07"),
"2024-01-01 → 2024-01-07"
);
}
}