glog-tui 0.1.2

An interactive git log and git show browser
use crate::app::{App, Mode};
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind};

pub fn handle(event: Event, app: &mut App) {
    if let Event::Key(key) = event {
        if key.kind != KeyEventKind::Press {
            return;
        }
        if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
            app.quit = true;
            return;
        }
        if app.search_input.is_some() {
            match key.code {
                KeyCode::Esc => app.search_input = None,
                KeyCode::Enter => app.submit_search(),
                KeyCode::Backspace => {
                    app.search_input.as_mut().unwrap().pop();
                }
                KeyCode::Char(c) => app.search_input.as_mut().unwrap().push(c),
                _ => {}
            }
            return;
        }
        if app.show_help {
            match key.code {
                KeyCode::Esc | KeyCode::Char('h') | KeyCode::Char('q') => app.show_help = false,
                _ => {}
            }
            return;
        }
        match key.code {
            KeyCode::Char('q') => app.quit = true,
            KeyCode::Tab => app.switch_mode(),
            KeyCode::Enter if app.mode == Mode::Log => app.switch_mode(),
            KeyCode::Enter | KeyCode::Char('z') if app.mode == Mode::Show => app.toggle_show_file(),
            KeyCode::Char('L') if app.mode == Mode::Show => app.toggle_all_lockfiles(),
            KeyCode::Char('[') if app.mode == Mode::Show => app.jump_show_file(-1),
            KeyCode::Char(']') if app.mode == Mode::Show => app.jump_show_file(1),
            KeyCode::Esc if app.mode == Mode::Show => app.switch_mode(),
            KeyCode::Up | KeyCode::Char('k') => app.move_by(-1, 1),
            KeyCode::Down | KeyCode::Char('j') => app.move_by(1, 1),
            KeyCode::PageUp | KeyCode::Char('b') => app.move_by(-2, 20),
            KeyCode::PageDown | KeyCode::Char(' ') => app.move_by(2, 20),
            KeyCode::Char('f') if app.mode == Mode::Show => app.move_by(2, 20),
            KeyCode::Left if app.mode == Mode::Show => show_adjacent(app, -1),
            KeyCode::Right if app.mode == Mode::Show => show_adjacent(app, 1),
            KeyCode::Char('g') | KeyCode::Home => app.top(),
            KeyCode::Char('G') | KeyCode::End => app.bottom(),
            KeyCode::Char('/') => app.begin_search(false),
            KeyCode::Char('?') => app.begin_search(true),
            KeyCode::Char('n') => app.repeat_search(false),
            KeyCode::Char('N') => app.repeat_search(true),
            KeyCode::Char('h') => app.show_help = true,
            _ => {}
        }
    } else if let Event::Mouse(mouse) = event {
        match mouse.kind {
            MouseEventKind::ScrollUp => app.move_by(-1, 3),
            MouseEventKind::ScrollDown => app.move_by(1, 3),
            MouseEventKind::Down(MouseButton::Left) if mouse.row == 0 => {
                if (app.log_tab_start..app.log_tab_end).contains(&mouse.column) {
                    app.mode = Mode::Log;
                    app.show_help = false;
                } else if (app.show_tab_start..app.show_tab_end).contains(&mouse.column) {
                    if app.mode != Mode::Show {
                        app.switch_mode();
                    }
                    app.show_help = false;
                }
            }
            MouseEventKind::Down(MouseButton::Left)
                if app.mode == Mode::Log && mouse.row >= app.log_row_origin =>
            {
                let row = usize::from(mouse.row - app.log_row_origin);
                if let Some(Some(selected)) = app.visible_log_rows.get(row).copied() {
                    if app.selected == selected {
                        app.switch_mode();
                    } else {
                        app.selected = selected;
                    }
                }
            }
            MouseEventKind::Down(MouseButton::Left)
                if app.mode == Mode::Show && mouse.row >= app.show_row_origin =>
            {
                let row = usize::from(mouse.row - app.show_row_origin);
                if row < app.visible_show_rows {
                    app.click_show_row(row);
                }
            }
            _ => {}
        }
    }
}

fn show_adjacent(app: &mut App, delta: isize) {
    if app.move_selection(delta) {
        app.show_offset = 0;
        app.load_show();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crossterm::event::{KeyEvent, KeyModifiers, MouseEvent};

    fn key(code: KeyCode) -> Event {
        Event::Key(KeyEvent::new(code, KeyModifiers::NONE))
    }

    #[test]
    fn enter_opens_show_and_escape_returns_to_log() {
        let mut app = App::new(Vec::new());
        handle(key(KeyCode::Enter), &mut app);
        assert_eq!(app.mode, Mode::Show);
        handle(key(KeyCode::Esc), &mut app);
        assert_eq!(app.mode, Mode::Log);
        handle(key(KeyCode::Esc), &mut app);
        assert!(!app.quit);
    }

    #[test]
    fn question_mark_starts_reverse_search() {
        let mut app = App::new(Vec::new());
        handle(key(KeyCode::Char('?')), &mut app);
        assert_eq!(app.search_input.as_deref(), Some(""));
        assert!(app.search_reverse);
    }

    #[test]
    fn help_captures_escape() {
        let mut app = App::new(Vec::new());
        handle(key(KeyCode::Char('h')), &mut app);
        assert!(app.show_help);
        handle(key(KeyCode::Esc), &mut app);
        assert!(!app.show_help);
    }

    #[test]
    fn header_tabs_are_clickable() {
        let mut app = App::new(Vec::new());
        let click = |column| {
            Event::Mouse(MouseEvent {
                kind: MouseEventKind::Down(MouseButton::Left),
                column,
                row: 0,
                modifiers: KeyModifiers::NONE,
            })
        };
        handle(click(app.show_tab_start), &mut app);
        assert_eq!(app.mode, Mode::Show);
        handle(click(app.log_tab_start), &mut app);
        assert_eq!(app.mode, Mode::Log);
    }

    #[test]
    fn clicking_a_log_row_selects_then_opens_its_commit() {
        let mut app = App::new(Vec::new());
        app.log_row_origin = 1;
        app.visible_log_rows = vec![Some(4), Some(5), Some(6)];
        handle(
            Event::Mouse(MouseEvent {
                kind: MouseEventKind::Down(MouseButton::Left),
                column: 20,
                row: 2,
                modifiers: KeyModifiers::NONE,
            }),
            &mut app,
        );
        assert_eq!(app.selected, 5);
        assert_eq!(app.mode, Mode::Log);
        handle(
            Event::Mouse(MouseEvent {
                kind: MouseEventKind::Down(MouseButton::Left),
                column: 20,
                row: 2,
                modifiers: KeyModifiers::NONE,
            }),
            &mut app,
        );
        assert_eq!(app.mode, Mode::Show);
    }

    #[test]
    fn f_pages_forward_in_show() {
        let mut app = App::new(Vec::new());
        app.mode = Mode::Show;
        handle(key(KeyCode::Char('f')), &mut app);
        assert_eq!(app.show_offset, 20);
    }

    #[test]
    fn control_c_quits_even_during_search() {
        let mut app = App::new(Vec::new());
        app.begin_search(false);
        handle(
            Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)),
            &mut app,
        );
        assert!(app.quit);
    }
}