lazier 0.9.2

A fast terminal user interface for git, written in Rust with ratatui and gitoxide
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

pub enum Action {
    Quit,
    NextPanel,
    PrevPanel,
    FocusPanel(usize),
    Up,
    Down,
    PageUp,
    PageDown,
    Top,
    Bottom,
    DiffScroll(i32),
    Refresh,
    ZoomGraph,
    /// Change between the whole history and the main line alone.
    FirstParent,
    Help,
    ToggleLog,
    InteractiveRebase,
    SplitCommit,
    RewordCommit,
    RevertCommit,
    CherryPick,
    BisectBad,
    BisectGood,
    BisectSkip,
    BisectReset,
    Worktrees,
    RebaseContinue,
    RebaseSkip,
    RebaseAbort,
    // Files panel
    ToggleStage,
    StageAll,
    CommitPrompt,
    CommitEditor,
    StashPrompt,
    EnterHunks,
    DiscardChanges,
    DeleteFile,
    IgnorePrompt,
    AmendLast,
    ForcePush,
    ResetPrompt,
    ReflogList,
    CopyId,
    TagPrompt,
    PushTags,
    FixupInto,
    OpenInEditor,
    SearchPrompt,
    ClearFilter,
    BlameFile,
    ToggleMark,
    Absorb,
    SubmoduleList,
    MarkForCompare,
    TakeOurs,
    TakeTheirs,
    // Branches panel
    Checkout,
    NewBranchPrompt,
    DeleteBranch {
        force: bool,
    },
    RenameBranchPrompt,
    MergeBranch,
    RebaseOnto,
    Push,
    Pull,
    Fetch,
    ShellPrompt,
    // Stash panel
    ApplyStash,
    PopStash,
    DropStash,
}

/// Map a key to an action. Global keys come first. Panel keys depend on the
/// panel in focus.
pub fn action_for(key: KeyEvent, focus: usize) -> Option<Action> {
    let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
    let global = match key.code {
        KeyCode::Char('d') if ctrl => Some(Action::PageDown),
        KeyCode::Char('u') if ctrl => Some(Action::PageUp),
        KeyCode::Char('q') => Some(Action::Quit),
        KeyCode::Tab => Some(Action::NextPanel),
        KeyCode::BackTab => Some(Action::PrevPanel),
        KeyCode::Char(c @ '1'..='5') => Some(Action::FocusPanel(c as usize - '1' as usize)),
        // The zero key focuses the diff pane.
        KeyCode::Char('0') => Some(Action::FocusPanel(5)),
        KeyCode::Char('?') => Some(Action::Help),
        KeyCode::Char('W') => Some(Action::Worktrees),
        KeyCode::Char('U') => Some(Action::ReflogList),
        KeyCode::Char('/') => Some(Action::SearchPrompt),
        KeyCode::Char('M') => Some(Action::SubmoduleList),
        KeyCode::Esc => Some(Action::ClearFilter),
        KeyCode::Char('F') => Some(Action::ForcePush),
        KeyCode::Char('@') => Some(Action::ToggleLog),
        KeyCode::Char('j') | KeyCode::Down => Some(Action::Down),
        KeyCode::Char('k') | KeyCode::Up => Some(Action::Up),
        KeyCode::PageDown => Some(Action::PageDown),
        KeyCode::PageUp => Some(Action::PageUp),
        KeyCode::Char('g') => Some(Action::Top),
        KeyCode::Char('G') => Some(Action::Bottom),
        // Shift J and K scroll the diff pane.
        KeyCode::Char('J') => Some(Action::DiffScroll(3)),
        KeyCode::Char('K') => Some(Action::DiffScroll(-3)),
        KeyCode::Char('r') => Some(Action::Refresh),
        _ => None,
    };
    if global.is_some() {
        return global;
    }
    let panel = panel_action(focus, key.code);
    if panel.is_some() {
        return panel;
    }
    // These work in every panel. A panel key with the same letter wins,
    // for example p on the stash panel, which pops a stash.
    match key.code {
        KeyCode::Char('P') => Some(Action::Push),
        KeyCode::Char('p') => Some(Action::Pull),
        KeyCode::Char('f') => Some(Action::Fetch),
        KeyCode::Char(':') => Some(Action::ShellPrompt),
        _ => None,
    }
}

/// True when the panel in focus gives this key a meaning of its own. The
/// global meaning is then hidden, thus the hint bar must not offer it.
pub fn panel_claims(focus: usize, key: &str) -> bool {
    let mut chars = key.chars();
    match (chars.next(), chars.next()) {
        (Some(c), None) => panel_action(focus, KeyCode::Char(c)).is_some(),
        _ => false,
    }
}

fn panel_action(focus: usize, code: KeyCode) -> Option<Action> {
    match (focus, code) {
        (1, KeyCode::Char(' ')) => Some(Action::ToggleStage),
        (1, KeyCode::Char('a')) => Some(Action::StageAll),
        (1, KeyCode::Char('c')) => Some(Action::CommitPrompt),
        (1, KeyCode::Char('C')) => Some(Action::CommitEditor),
        (1, KeyCode::Char('s')) => Some(Action::StashPrompt),
        (1, KeyCode::Enter) => Some(Action::EnterHunks),
        (1, KeyCode::Char('d')) => Some(Action::DiscardChanges),
        (1, KeyCode::Char('x')) => Some(Action::DeleteFile),
        (1, KeyCode::Char('i')) => Some(Action::IgnorePrompt),
        (1, KeyCode::Char('A')) => Some(Action::AmendLast),
        (3, KeyCode::Char('R')) => Some(Action::ResetPrompt),
        (3, KeyCode::Char('c')) => Some(Action::CopyId),
        (3, KeyCode::Char('t')) => Some(Action::TagPrompt),
        (3, KeyCode::Char('T')) => Some(Action::PushTags),
        (3, KeyCode::Char('f')) => Some(Action::FixupInto),
        (3, KeyCode::Char('s')) => Some(Action::SplitCommit),
        (1, KeyCode::Char('e')) => Some(Action::OpenInEditor),
        (1, KeyCode::Char('b')) => Some(Action::BlameFile),
        (1, KeyCode::Char('v')) => Some(Action::ToggleMark),
        (1, KeyCode::Char('z')) => Some(Action::Absorb),
        (3, KeyCode::Char(' ')) => Some(Action::MarkForCompare),
        (1, KeyCode::Char('o')) => Some(Action::TakeOurs),
        (1, KeyCode::Char('t')) => Some(Action::TakeTheirs),
        (3, KeyCode::Enter) => Some(Action::ZoomGraph),
        // The m key changes to the main line and back again.
        (3, KeyCode::Char('m')) => Some(Action::FirstParent),
        (3, KeyCode::Char('i')) => Some(Action::InteractiveRebase),
        (3, KeyCode::Char('w')) => Some(Action::RewordCommit),
        (3, KeyCode::Char('v')) => Some(Action::RevertCommit),
        (3, KeyCode::Char('y')) => Some(Action::CherryPick),
        // Bisect. The b key starts a bisect, or marks the commit as bad.
        (3, KeyCode::Char('b')) => Some(Action::BisectBad),
        (3, KeyCode::Char('o')) => Some(Action::BisectGood),
        (3, KeyCode::Char('S')) => Some(Action::BisectSkip),
        (3, KeyCode::Char('A')) => Some(Action::BisectReset),
        (2, KeyCode::Enter) => Some(Action::Checkout),
        (2, KeyCode::Char('n')) => Some(Action::NewBranchPrompt),
        (2, KeyCode::Char('d')) => Some(Action::DeleteBranch { force: false }),
        (2, KeyCode::Char('D')) => Some(Action::DeleteBranch { force: true }),
        (2, KeyCode::Char('R')) => Some(Action::RenameBranchPrompt),
        (2, KeyCode::Char('m')) => Some(Action::MergeBranch),
        // The o key means "onto": rebase what you have onto that branch.
        (2, KeyCode::Char('o')) => Some(Action::RebaseOnto),
        (4, KeyCode::Enter | KeyCode::Char('a')) => Some(Action::ApplyStash),
        (4, KeyCode::Char('p')) => Some(Action::PopStash),
        (4, KeyCode::Char('d')) => Some(Action::DropStash),
        _ => None,
    }
}