dyd 0.4.0

CLI for daily diffing of git repos
Documentation
use crate::event::Event;
use crate::manifest::Manifest;
use crate::repo::{Log, Repo, RepoStatus};
use crate::ui;

use indexmap::map::IndexMap;
use std::cmp::Ordering;
use std::error;
use std::path::PathBuf;
use std::sync::mpsc;
use tui::backend::Backend;
use tui::layout::{Constraint, Direction, Layout};
use tui::terminal::Frame;
use tui::widgets::TableState;

pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>;

#[derive(Debug, PartialEq)]
pub enum AppState {
    Init,
    Checking,
}

impl Default for AppState {
    fn default() -> Self {
        AppState::Init
    }
}

/// Selected pane
#[derive(Debug, PartialEq)]
pub enum SelectedPane {
    Diff,
    Repos,
}

impl Default for SelectedPane {
    fn default() -> Self {
        SelectedPane::Repos
    }
}

#[derive(Debug)]
pub struct App {
    pub difftool: String,
    pub repos: IndexMap<String, Repo>,
    pub repo_state: TableState,
    pub running: bool,
    pub root_path: PathBuf,
    pub selected_pane: SelectedPane,
    pub selected_repo_state: TableState,
    pub since: chrono::DateTime<chrono::Utc>,
    pub state: AppState,
}

impl From<Manifest> for App {
    fn from(manifest: Manifest) -> Self {
        let repos: IndexMap<String, Repo> = manifest
            .remotes
            .into_iter()
            .map(|(id, remote)| (id, remote.into()))
            .collect();

        let mut repo_state = TableState::default();
        if !repos.is_empty() {
            repo_state.select(Some(0))
        }

        let mut selected_repo_state = TableState::default();
        selected_repo_state.select(Some(0));

        Self {
            repos,
            repo_state,
            selected_repo_state,
            difftool: manifest.difftool,
            root_path: manifest.root.unwrap(),
            selected_pane: SelectedPane::default(),
            since: manifest.since_datetime.unwrap(),
            running: true,
            state: AppState::default(),
        }
    }
}

impl App {
    pub fn tick(&mut self, sender: mpsc::Sender<Event>) -> AppResult<()> {
        if self.state == AppState::Init {
            self.update(sender)?;
            self.state = AppState::Checking;
        }
        Ok(())
    }

    pub fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
        let size = frame.size();
        let layout = Layout::default()
            .direction(Direction::Horizontal)
            .margin(0)
            .constraints([Constraint::Percentage(75), Constraint::Percentage(25)].as_ref())
            .split(size);

        let sidebar = Layout::default()
            .direction(Direction::Vertical)
            .margin(0)
            .constraints([Constraint::Ratio(7, 10), Constraint::Ratio(3, 10)].as_ref())
            .split(layout[1]);

        frame.render_stateful_widget(ui::diff::render(self), layout[0], &mut self.selected_repo_state.clone());
        frame.render_stateful_widget(ui::repos::render(self), sidebar[0], &mut self.repo_state.clone());
        frame.render_widget(ui::help::render(self), sidebar[1]);
    }

    pub fn update(&self, sender: mpsc::Sender<Event>) -> AppResult<()> {
        for (id, repo) in &self.repos {
            repo.update(id.clone(), &self.root_path, sender.clone())?;
        }
        Ok(())
    }

    pub fn update_repo_status(&mut self, id: String, status: RepoStatus) -> AppResult<()> {
        if let Some(repo) = self.repos.get_mut(&id) {
            repo.status = status;
        }
        Ok(())
    }

    pub fn update_repo_logs(&mut self, id: String, logs: Vec<Log>) -> AppResult<()> {
        if let Some(repo) = self.repos.get_mut(&id) {
            repo.logs = logs;
            repo.status = RepoStatus::Finished;
        }

        self.repos.sort_by(&Self::sort_repos);

        Ok(())
    }

    fn sort_repos(_key1: &String, repo1: &Repo, _key2: &String, repo2: &Repo) -> Ordering {
        if !repo1.logs.is_empty() && !repo2.logs.is_empty() {
            let commit1 = repo1.logs[0].commit_datetime;
            let commit2 = repo2.logs[0].commit_datetime;

            if commit1 > commit2 {
                return Ordering::Less;
            };
            if commit1 < commit2 {
                return Ordering::Greater;
            };
        };

        Ordering::Equal
    }
}