limb 0.1.0

A focused CLI for git worktree management
Documentation
//! Crossterm event polling and dispatch for the picker.

use std::time::Duration;

use anyhow::{Context, Result};
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};

use super::app::{App, Outcome};

const PAGE_SIZE: usize = 10;
const POLL_TIMEOUT: Duration = Duration::from_millis(200);

/// Polls for one input event and dispatches it into `app`.
///
/// Returns `Outcome::Continue` if no event arrived before
/// `POLL_TIMEOUT`. The main loop uses that as its redraw tick.
///
/// # Errors
///
/// Returns an error if crossterm's underlying poll / read fails (rare;
/// usually indicates the terminal has disappeared).
pub fn next(app: &mut App) -> Result<Outcome> {
    if !event::poll(POLL_TIMEOUT).context("crossterm event poll")? {
        return Ok(Outcome::Continue);
    }
    match event::read().context("crossterm event read")? {
        Event::Key(key) if key.kind == event::KeyEventKind::Press => {
            if app.help_visible {
                return Ok(dispatch_help_key(app, key));
            }
            if app.filter_active {
                return Ok(dispatch_filter_key(app, key));
            }
            Ok(dispatch_nav_key(app, key))
        }
        Event::Mouse(me) => Ok(dispatch_mouse(app, me)),
        _ => Ok(Outcome::Continue),
    }
}

fn dispatch_nav_key(app: &mut App, key: KeyEvent) -> Outcome {
    if key.modifiers.contains(KeyModifiers::CONTROL)
        && matches!(key.code, KeyCode::Char('c' | 'g' | 'd'))
    {
        return Outcome::Cancel;
    }
    match key.code {
        KeyCode::Esc | KeyCode::Char('q') => Outcome::Cancel,
        KeyCode::Enter => app
            .selected_path()
            .map_or(Outcome::Continue, Outcome::Select),
        KeyCode::Char('/') => {
            app.open_filter();
            Outcome::Continue
        }
        KeyCode::Char('?') => {
            app.toggle_help();
            Outcome::Continue
        }
        KeyCode::Up | KeyCode::Char('k') => {
            app.move_up();
            Outcome::Continue
        }
        KeyCode::Down | KeyCode::Char('j') => {
            app.move_down();
            Outcome::Continue
        }
        KeyCode::PageUp => {
            app.page_up(PAGE_SIZE);
            Outcome::Continue
        }
        KeyCode::PageDown => {
            app.page_down(PAGE_SIZE);
            Outcome::Continue
        }
        KeyCode::Home | KeyCode::Char('g') => {
            app.move_first();
            Outcome::Continue
        }
        KeyCode::End | KeyCode::Char('G') => {
            app.move_last();
            Outcome::Continue
        }
        _ => Outcome::Continue,
    }
}

fn dispatch_filter_key(app: &mut App, key: KeyEvent) -> Outcome {
    if key.modifiers.contains(KeyModifiers::CONTROL)
        && matches!(key.code, KeyCode::Char('c' | 'g' | 'd'))
    {
        return Outcome::Cancel;
    }
    match key.code {
        KeyCode::Esc => {
            app.close_filter(true);
            Outcome::Continue
        }
        KeyCode::Enter => {
            app.close_filter(false);
            app.selected_path()
                .map_or(Outcome::Continue, Outcome::Select)
        }
        KeyCode::Backspace => {
            app.filter_pop();
            Outcome::Continue
        }
        KeyCode::Up => {
            app.move_up();
            Outcome::Continue
        }
        KeyCode::Down => {
            app.move_down();
            Outcome::Continue
        }
        KeyCode::PageUp => {
            app.page_up(PAGE_SIZE);
            Outcome::Continue
        }
        KeyCode::PageDown => {
            app.page_down(PAGE_SIZE);
            Outcome::Continue
        }
        KeyCode::Char(c) => {
            app.filter_push(c);
            Outcome::Continue
        }
        _ => Outcome::Continue,
    }
}

fn dispatch_help_key(app: &mut App, key: KeyEvent) -> Outcome {
    if key.modifiers.contains(KeyModifiers::CONTROL)
        && matches!(key.code, KeyCode::Char('c' | 'g' | 'd'))
    {
        return Outcome::Cancel;
    }
    match key.code {
        KeyCode::Esc | KeyCode::Char('?' | 'q') => {
            app.toggle_help();
            Outcome::Continue
        }
        _ => Outcome::Continue,
    }
}

fn dispatch_mouse(app: &mut App, me: MouseEvent) -> Outcome {
    match me.kind {
        MouseEventKind::ScrollUp => {
            app.move_up();
            Outcome::Continue
        }
        MouseEventKind::ScrollDown => {
            app.move_down();
            Outcome::Continue
        }
        _ => Outcome::Continue,
    }
}