yuru-tui 0.2.3

Terminal user interface for Yuru
Documentation
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

use crate::api::{BindingAction, KeyBinding};
use crate::state::TuiAction;

pub(crate) fn classify_key(
    key: KeyEvent,
    page_rows: usize,
    expect_keys: &[String],
    bindings: &[KeyBinding],
) -> KeyDecision {
    if let Some(name) = key_name(key) {
        if expect_keys.iter().any(|expected| expected == &name) {
            return KeyDecision::Accept(Some(name));
        }
        if let Some(binding) = bindings.iter().find(|binding| binding.key == name) {
            return match binding.action {
                BindingAction::Accept => KeyDecision::Accept(None),
                BindingAction::Abort => KeyDecision::Abort,
                BindingAction::ClearQuery => KeyDecision::Action(TuiAction::ClearQuery),
                BindingAction::MoveSelectionUp => KeyDecision::Action(TuiAction::MoveSelectionUp),
                BindingAction::MoveSelectionDown => {
                    KeyDecision::Action(TuiAction::MoveSelectionDown)
                }
                BindingAction::MoveSelectionFirst => {
                    KeyDecision::Action(TuiAction::MoveSelectionFirst)
                }
                BindingAction::MoveSelectionLast => {
                    KeyDecision::Action(TuiAction::MoveSelectionLast)
                }
                BindingAction::PageUp => KeyDecision::Action(TuiAction::PageUp(page_rows)),
                BindingAction::PageDown => KeyDecision::Action(TuiAction::PageDown(page_rows)),
                BindingAction::ToggleMark => KeyDecision::Action(TuiAction::ToggleMark),
                BindingAction::ToggleMarkAndDown => {
                    KeyDecision::Action(TuiAction::ToggleMarkAndDown)
                }
                BindingAction::ToggleMarkAndUp => KeyDecision::Action(TuiAction::ToggleMarkAndUp),
                BindingAction::MoveCursorStart => KeyDecision::Action(TuiAction::MoveCursorStart),
                BindingAction::MoveCursorEnd => KeyDecision::Action(TuiAction::MoveCursorEnd),
                BindingAction::MoveCursorLeft => KeyDecision::Action(TuiAction::MoveCursorLeft),
                BindingAction::MoveCursorRight => KeyDecision::Action(TuiAction::MoveCursorRight),
                BindingAction::MoveCursorWordLeft => {
                    KeyDecision::Action(TuiAction::MoveCursorWordLeft)
                }
                BindingAction::MoveCursorWordRight => {
                    KeyDecision::Action(TuiAction::MoveCursorWordRight)
                }
                BindingAction::Backspace => KeyDecision::Action(TuiAction::Backspace),
                BindingAction::Delete => KeyDecision::Action(TuiAction::Delete),
                BindingAction::DeleteToEnd => KeyDecision::Action(TuiAction::DeleteToEnd),
                BindingAction::DeleteWord => KeyDecision::Action(TuiAction::DeleteWord),
                BindingAction::PreviewUp => KeyDecision::Action(TuiAction::PreviewUp),
                BindingAction::PreviewDown => KeyDecision::Action(TuiAction::PreviewDown),
                BindingAction::PreviewPageUp => {
                    KeyDecision::Action(TuiAction::PreviewPageUp(page_rows))
                }
                BindingAction::PreviewPageDown => {
                    KeyDecision::Action(TuiAction::PreviewPageDown(page_rows))
                }
                BindingAction::PreviewTop => KeyDecision::Action(TuiAction::PreviewTop),
                BindingAction::PreviewBottom => KeyDecision::Action(TuiAction::PreviewBottom),
            };
        }
    }

    match (key.code, key.modifiers) {
        (KeyCode::Enter, _) => KeyDecision::Accept(None),
        (KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => KeyDecision::Abort,
        (KeyCode::Char('u'), KeyModifiers::CONTROL) => KeyDecision::Action(TuiAction::ClearQuery),
        (KeyCode::Char('a'), KeyModifiers::CONTROL) | (KeyCode::Home, _) => {
            KeyDecision::Action(TuiAction::MoveCursorStart)
        }
        (KeyCode::Char('e'), KeyModifiers::CONTROL) | (KeyCode::End, _) => {
            KeyDecision::Action(TuiAction::MoveCursorEnd)
        }
        (KeyCode::Char('b'), KeyModifiers::CONTROL) => {
            KeyDecision::Action(TuiAction::MoveCursorLeft)
        }
        (KeyCode::Char('f'), KeyModifiers::CONTROL) => {
            KeyDecision::Action(TuiAction::MoveCursorRight)
        }
        (KeyCode::Char('k'), KeyModifiers::CONTROL) => KeyDecision::Action(TuiAction::DeleteToEnd),
        (KeyCode::Char('w'), KeyModifiers::CONTROL) => KeyDecision::Action(TuiAction::DeleteWord),
        (KeyCode::Left, KeyModifiers::ALT) => KeyDecision::Action(TuiAction::MoveCursorWordLeft),
        (KeyCode::Right, KeyModifiers::ALT) => KeyDecision::Action(TuiAction::MoveCursorWordRight),
        (KeyCode::Char('b'), KeyModifiers::ALT) => {
            KeyDecision::Action(TuiAction::MoveCursorWordLeft)
        }
        (KeyCode::Char('f'), KeyModifiers::ALT) => {
            KeyDecision::Action(TuiAction::MoveCursorWordRight)
        }
        (KeyCode::Char('h'), KeyModifiers::CONTROL) => KeyDecision::Action(TuiAction::Backspace),
        (KeyCode::Char('d'), KeyModifiers::CONTROL) => KeyDecision::Action(TuiAction::DeleteOrExit),
        (KeyCode::Up, modifiers) if modifiers.contains(KeyModifiers::SHIFT) => {
            KeyDecision::Action(TuiAction::PreviewUp)
        }
        (KeyCode::Down, modifiers) if modifiers.contains(KeyModifiers::SHIFT) => {
            KeyDecision::Action(TuiAction::PreviewDown)
        }
        (KeyCode::PageUp, modifiers) if modifiers.contains(KeyModifiers::SHIFT) => {
            KeyDecision::Action(TuiAction::PreviewPageUp(page_rows))
        }
        (KeyCode::PageDown, modifiers) if modifiers.contains(KeyModifiers::SHIFT) => {
            KeyDecision::Action(TuiAction::PreviewPageDown(page_rows))
        }
        (KeyCode::Char('n'), KeyModifiers::CONTROL) | (KeyCode::Down, _) => {
            KeyDecision::Action(TuiAction::MoveSelectionDown)
        }
        (KeyCode::Char('p'), KeyModifiers::CONTROL) | (KeyCode::Up, _) => {
            KeyDecision::Action(TuiAction::MoveSelectionUp)
        }
        (KeyCode::PageUp, _) => KeyDecision::Action(TuiAction::PageUp(page_rows)),
        (KeyCode::PageDown, _) => KeyDecision::Action(TuiAction::PageDown(page_rows)),
        (KeyCode::Tab, _) => KeyDecision::Action(TuiAction::ToggleMarkAndDown),
        (KeyCode::BackTab, _) => KeyDecision::Action(TuiAction::ToggleMarkAndUp),
        (KeyCode::Backspace, _) => KeyDecision::Action(TuiAction::Backspace),
        (KeyCode::Delete, _) => KeyDecision::Action(TuiAction::Delete),
        (KeyCode::Left, _) => KeyDecision::Action(TuiAction::MoveCursorLeft),
        (KeyCode::Right, _) => KeyDecision::Action(TuiAction::MoveCursorRight),
        (KeyCode::Char(ch), modifiers)
            if modifiers.is_empty() || modifiers == KeyModifiers::SHIFT =>
        {
            KeyDecision::Action(TuiAction::Insert(ch))
        }
        _ => KeyDecision::Ignore,
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum KeyDecision {
    Accept(Option<String>),
    Abort,
    Action(TuiAction),
    Ignore,
}

fn key_name(key: KeyEvent) -> Option<String> {
    match (key.code, key.modifiers) {
        (KeyCode::Enter, _) => Some("enter".to_string()),
        (KeyCode::Esc, _) => Some("esc".to_string()),
        (KeyCode::Tab, _) => Some("tab".to_string()),
        (KeyCode::BackTab, _) => Some("shift-tab".to_string()),
        (KeyCode::Up, modifiers) if modifiers.contains(KeyModifiers::SHIFT) => {
            Some("shift-up".to_string())
        }
        (KeyCode::Down, modifiers) if modifiers.contains(KeyModifiers::SHIFT) => {
            Some("shift-down".to_string())
        }
        (KeyCode::PageUp, modifiers) if modifiers.contains(KeyModifiers::SHIFT) => {
            Some("shift-page-up".to_string())
        }
        (KeyCode::PageDown, modifiers) if modifiers.contains(KeyModifiers::SHIFT) => {
            Some("shift-page-down".to_string())
        }
        (KeyCode::Home, _) => Some("home".to_string()),
        (KeyCode::End, _) => Some("end".to_string()),
        (KeyCode::Up, _) => Some("up".to_string()),
        (KeyCode::Down, _) => Some("down".to_string()),
        (KeyCode::PageUp, _) => Some("page-up".to_string()),
        (KeyCode::PageDown, _) => Some("page-down".to_string()),
        (KeyCode::Char(ch), KeyModifiers::CONTROL) => Some(format!("ctrl-{ch}")),
        (KeyCode::Char(ch), KeyModifiers::ALT) => Some(format!("alt-{ch}")),
        (KeyCode::Char(ch), modifiers)
            if modifiers == KeyModifiers::CONTROL | KeyModifiers::SHIFT =>
        {
            Some(format!("ctrl-{}", ch.to_ascii_lowercase()))
        }
        (KeyCode::Char(ch), modifiers) if modifiers == KeyModifiers::ALT | KeyModifiers::SHIFT => {
            Some(format!("alt-{}", ch.to_ascii_lowercase()))
        }
        _ => None,
    }
}