#![allow(clippy::cast_possible_wrap)]
use crate::error::Result;
use crate::stats::AnalysisResult;
use crate::tui::event::{Event, EventHandler};
use crate::tui::ui;
use crossterm::ExecutableCommand;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::prelude::*;
use std::io::stdout;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Metric {
#[default]
Commits,
Additions,
Deletions,
NetLines,
FilesChanged,
}
impl Metric {
#[must_use]
pub fn next(self) -> Self {
match self {
Self::Commits => Self::Additions,
Self::Additions => Self::Deletions,
Self::Deletions => Self::NetLines,
Self::NetLines => Self::FilesChanged,
Self::FilesChanged => Self::Commits,
}
}
#[must_use]
pub fn prev(self) -> Self {
match self {
Self::Commits => Self::FilesChanged,
Self::Additions => Self::Commits,
Self::Deletions => Self::Additions,
Self::NetLines => Self::Deletions,
Self::FilesChanged => Self::NetLines,
}
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Commits => "Commits",
Self::Additions => "Additions",
Self::Deletions => "Deletions",
Self::NetLines => "Net Lines",
Self::FilesChanged => "Files Changed",
}
}
}
pub struct App {
pub result: AnalysisResult,
pub metric: Metric,
pub should_quit: bool,
pub single_metric: bool,
}
impl App {
#[must_use]
pub fn new(result: AnalysisResult, single_metric: bool) -> Self {
Self {
result,
metric: Metric::default(),
should_quit: false,
single_metric,
}
}
pub fn run(&mut self) -> Result<()> {
terminal::enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
let event_handler = EventHandler::new(250);
let result = self.main_loop(&mut terminal, &event_handler);
terminal::disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
result
}
fn main_loop<B: Backend>(
&mut self,
terminal: &mut Terminal<B>,
event_handler: &EventHandler,
) -> Result<()> {
while !self.should_quit {
terminal.draw(|frame| ui::render(frame, self))?;
match event_handler.next()? {
Event::Key(key) => self.handle_key(key),
Event::Tick | Event::Resize(_, _) => {}
}
}
Ok(())
}
fn handle_key(&mut self, key: KeyEvent) {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => {
self.should_quit = true;
}
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.should_quit = true;
}
KeyCode::Tab | KeyCode::Right | KeyCode::Char('l') => {
self.metric = self.metric.next();
}
KeyCode::BackTab | KeyCode::Left | KeyCode::Char('h') => {
self.metric = self.metric.prev();
}
KeyCode::Char('m') => {
self.single_metric = !self.single_metric;
}
_ => {}
}
}
#[must_use]
pub fn metric_values(&self) -> Vec<(String, i64)> {
self.values_for_metric(self.metric)
}
#[must_use]
pub fn values_for_metric(&self, metric: Metric) -> Vec<(String, i64)> {
self.result
.stats
.iter()
.map(|s| {
let value = match metric {
Metric::Commits => i64::from(s.commits),
Metric::Additions => s.additions as i64,
Metric::Deletions => s.deletions as i64,
Metric::NetLines => s.net_lines,
Metric::FilesChanged => i64::from(s.files_changed),
};
(s.label.clone(), value)
})
.collect()
}
#[must_use]
pub fn all_metrics() -> [Metric; 5] {
[
Metric::Commits,
Metric::Additions,
Metric::Deletions,
Metric::NetLines,
Metric::FilesChanged,
]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::stats::{PeriodStats, TotalStats};
use chrono::NaiveDate;
fn make_result() -> AnalysisResult {
AnalysisResult {
repository: "test".to_string(),
period: "daily".to_string(),
from: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
to: NaiveDate::from_ymd_opt(2024, 1, 7).unwrap(),
stats: vec![PeriodStats {
label: "2024-01-01".to_string(),
date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
commits: 5,
additions: 100,
deletions: 20,
net_lines: 80,
files_changed: 10,
}],
total: TotalStats::default(),
}
}
#[test]
fn test_metric_cycle() {
let metric = Metric::Commits;
assert_eq!(metric.next(), Metric::Additions);
assert_eq!(metric.prev(), Metric::FilesChanged);
}
#[test]
fn test_metric_values() {
let result = make_result();
let app = App::new(result, false);
let values = app.metric_values();
assert_eq!(values.len(), 1);
assert_eq!(values[0], ("2024-01-01".to_string(), 5));
}
#[test]
fn test_all_metrics() {
let metrics = App::all_metrics();
assert_eq!(metrics.len(), 5);
}
}