cargo-guard 0.0.1

Cargo build script allow list
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};

use super::{
    app::{App, Decision},
    diagnostic::Diagnostic,
};

#[derive(Debug, PartialEq, Eq)]
pub enum Action {
    Continue,
    Quit,
    Decide(Decision),
    NextPackage,
    PreviousPackage,
    Skip,
    CopySelection,
    CancelPin,
}

pub fn handle_key(app: &mut App, key: KeyEvent) -> Action {
    if key.kind == KeyEventKind::Release {
        return Action::Continue;
    }
    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
        return Action::Quit;
    }
    if let Some(dialog) = &mut app.pin_dialog {
        return dialog.handle_key(key);
    }
    if key.code == KeyCode::Char('?') && key.kind == KeyEventKind::Press {
        app.show_help = !app.show_help;
        app.help.reset_view();
        app.selection.clear();
        return Action::Continue;
    }
    if app.show_help {
        return handle_help_key(&mut app.show_help, &mut app.help, key);
    }
    if matches!(key.code, KeyCode::Char('a' | 'r')) && !app.can_decide() {
        return Action::Continue;
    }
    match key.code {
        KeyCode::Char('j') if app.queue_mode => return Action::NextPackage,
        KeyCode::Char('k') if app.queue_mode => return Action::PreviousPackage,
        KeyCode::Char('s') if app.queue_mode && key.kind == KeyEventKind::Press => {
            return Action::Skip;
        }
        KeyCode::Char('a') if key.kind == KeyEventKind::Press && key.modifiers.is_empty() => {
            return Action::Decide(Decision::ApproveFingerprint);
        }
        KeyCode::Char('r') if key.kind == KeyEventKind::Press && key.modifiers.is_empty() => {
            return Action::Decide(Decision::Reject);
        }
        KeyCode::Char('y') if key.kind == KeyEventKind::Press && key.modifiers.is_empty() => {
            return Action::CopySelection;
        }
        KeyCode::Esc if app.selection.clear() => {}
        KeyCode::Esc if app.diagnostic.selection.clear() => {}
        KeyCode::Char('q') | KeyCode::Esc => return Action::Quit,
        KeyCode::Down => app.scroll(1, 0),
        KeyCode::Up => app.scroll(-1, 0),
        KeyCode::Right | KeyCode::Char('l') => app.scroll(0, 4),
        KeyCode::Left | KeyCode::Char('h') => app.scroll(0, -4),
        KeyCode::PageDown => app.scroll(app.height.max(1) as isize, 0),
        KeyCode::PageUp => app.scroll(-(app.height.max(1) as isize), 0),
        KeyCode::Home | KeyCode::Char('g') => app.scroll(isize::MIN, 0),
        KeyCode::End | KeyCode::Char('G') => app.scroll(isize::MAX, 0),
        KeyCode::Tab => app.switch_file(false),
        KeyCode::BackTab => app.switch_file(true),
        KeyCode::Char(']') => app.jump_change(false),
        KeyCode::Char('[') => app.jump_change(true),
        _ => {}
    }
    Action::Continue
}

pub(super) fn handle_help_key(
    show_help: &mut bool,
    help: &mut Diagnostic,
    key: KeyEvent,
) -> Action {
    match key.code {
        KeyCode::Esc => *show_help = false,
        KeyCode::Char('q') => return Action::Quit,
        KeyCode::Down | KeyCode::Char('j') => help.scroll(1),
        KeyCode::Up | KeyCode::Char('k') => help.scroll(-1),
        KeyCode::PageDown => help.scroll(help.selection.area.height.max(1) as isize),
        KeyCode::PageUp => help.scroll(-(help.selection.area.height.max(1) as isize)),
        KeyCode::Home | KeyCode::Char('g') => help.scroll(isize::MIN),
        KeyCode::End | KeyCode::Char('G') => help.scroll(isize::MAX),
        _ => {}
    }
    Action::Continue
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::review::app::tests::fixture;

    #[test]
    fn accept_uses_fingerprints_and_help_blocks_decisions() {
        let (_root, mut app) = fixture(&["fn main() {}"]);
        let key = |c| KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE);
        handle_key(&mut app, key('?'));
        assert!(app.show_help);
        assert_eq!(handle_key(&mut app, key('a')), Action::Continue);
        handle_key(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
        assert!(!app.show_help);
        assert_eq!(
            handle_key(&mut app, key('a')),
            Action::Decide(Decision::ApproveFingerprint)
        );
        app.session.package.approval =
            Some(crate::config::ApprovedVersion::Fingerprint("hash".into()));
        assert_eq!(
            handle_key(&mut app, key('a')),
            Action::Decide(Decision::ApproveFingerprint)
        );
        assert_eq!(handle_key(&mut app, key('f')), Action::Continue);
        assert_eq!(
            handle_key(&mut app, key('r')),
            Action::Decide(Decision::Reject)
        );
    }

    #[test]
    fn approved_versions_and_blankets_ignore_approval_and_rejection_keys() {
        let (_root, mut app) = fixture(&["fn main() {}"]);
        for approval in [
            crate::config::ApprovedVersion::SemVer(app.session.package.version.clone()),
            crate::config::ApprovedVersion::Blanket(crate::config::BlanketApproval::Approved),
        ] {
            app.session.package.approval = Some(approval);
            for character in ['a', 'r', 'f'] {
                assert_eq!(
                    handle_key(
                        &mut app,
                        KeyEvent::new(KeyCode::Char(character), KeyModifiers::NONE)
                    ),
                    Action::Continue
                );
            }
        }
    }

    #[test]
    fn approval_actions_require_an_unmodified_key_press() {
        let (_root, mut app) = fixture(&["fn main() {}"]);
        for (character, action) in [
            ('a', Action::Decide(Decision::ApproveFingerprint)),
            ('r', Action::Decide(Decision::Reject)),
        ] {
            let key = KeyEvent::new(KeyCode::Char(character), KeyModifiers::NONE);
            assert_eq!(handle_key(&mut app, key), action);
            for kind in [KeyEventKind::Repeat, KeyEventKind::Release] {
                assert_eq!(
                    handle_key(&mut app, KeyEvent { kind, ..key }),
                    Action::Continue
                );
            }
            assert_eq!(
                handle_key(
                    &mut app,
                    KeyEvent {
                        modifiers: KeyModifiers::CONTROL,
                        ..key
                    }
                ),
                Action::Continue
            );
        }
        assert!(!app.session.config.path().exists());
    }

    #[test]
    fn package_navigation_does_not_scroll_source() {
        let (_root, mut app) = fixture(&["a\nb\nc\nd\ne\nf"]);
        app.set_viewport(2, 10);
        app.scroll(1, 0);
        let key = |c| KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE);
        for queue_mode in [false, true] {
            app.queue_mode = queue_mode;
            for (character, action) in [('j', Action::NextPackage), ('k', Action::PreviousPackage)]
            {
                assert_eq!(
                    handle_key(&mut app, key(character)),
                    if queue_mode { action } else { Action::Continue }
                );
                assert_eq!(app.top, 1);
            }
            for character in ['n', 'p'] {
                assert_eq!(handle_key(&mut app, key(character)), Action::Continue);
                assert_eq!(app.top, 1);
            }
        }
        for (code, top) in [
            (KeyCode::Down, 2),
            (KeyCode::Up, 1),
            (KeyCode::PageDown, 3),
            (KeyCode::PageUp, 1),
        ] {
            assert_eq!(
                handle_key(&mut app, KeyEvent::new(code, KeyModifiers::NONE)),
                Action::Continue
            );
            assert_eq!(app.top, top);
        }
    }

    #[test]
    fn handles_navigation_quit_and_key_release() {
        let (_root, mut app) = fixture(&["a\nb\nc\nd", "other"]);
        app.set_viewport(2, 10);
        let key = |code| KeyEvent::new(code, KeyModifiers::NONE);
        handle_key(&mut app, key(KeyCode::PageDown));
        assert_eq!(app.top, 2);
        handle_key(&mut app, key(KeyCode::Home));
        assert_eq!(app.top, 0);
        handle_key(&mut app, key(KeyCode::Tab));
        assert_eq!(app.selected, 1);
        handle_key(&mut app, key(KeyCode::BackTab));
        assert_eq!(app.selected, 0);
        let mut release = key(KeyCode::Char('q'));
        release.kind = KeyEventKind::Release;
        assert_eq!(handle_key(&mut app, release), Action::Continue);
        assert_eq!(handle_key(&mut app, key(KeyCode::Char('q'))), Action::Quit);
        assert_eq!(
            handle_key(
                &mut app,
                KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)
            ),
            Action::Quit
        );
    }
}