use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use std::time::Duration;
use crate::model::{AppMode, AppState};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Action {
None,
Quit,
MoveUp,
MoveDown,
MoveLeft,
MoveRight,
EnterCommandMode,
CommandChar(char),
ExecuteCommand,
CancelCommand,
CommandBackspace,
Resize(u16, u16),
EnterSearchMode,
EnterSearchBackward,
SearchChar(char),
ExecuteSearch,
CancelSearch,
SearchBackspace,
FindNext,
FindPrevious,
DismissHelp,
HelpNextTab,
HelpPrevTab,
GotoFirstColumn,
GotoLastColumn,
GotoFirstVisibleColumn,
GotoMiddleVisibleColumn,
GotoLastVisibleColumn,
GotoColumn(usize),
PendingG,
AccumulateDigit(char),
ExecuteGotoColumn,
TranslationUp,
TranslationDown,
TranslationFrameLeft,
TranslationFrameRight,
TranslationConfirm,
StartTranslation,
TranslationCancel,
HalfPageUp,
HalfPageDown,
HalfPageLeft,
HalfPageRight,
PageUp,
PageDown,
PendingZ,
WordForward,
WordBackward,
WordEnd,
DismissErrorPopup,
FileBrowserUp,
FileBrowserDown,
FileBrowserSelect,
FileBrowserParent,
FileBrowserToggleAll,
FileBrowserQuit,
}
pub fn poll_event(timeout: Duration) -> Option<Event> {
if event::poll(timeout).ok()? {
event::read().ok()
} else {
None
}
}
pub fn handle_event(
event: Event,
mode: &AppMode,
show_help: bool,
pending_g: bool,
pending_z: bool,
has_number_prefix: bool,
has_error_popup: bool,
has_file_browser: bool,
) -> Action {
match event {
Event::Key(key_event) => {
if key_event.kind != KeyEventKind::Press {
return Action::None;
}
handle_key_event(
key_event,
mode,
show_help,
pending_g,
pending_z,
has_number_prefix,
has_error_popup,
has_file_browser,
)
}
Event::Resize(width, height) => Action::Resize(width, height),
_ => Action::None,
}
}
fn handle_key_event(key: KeyEvent, mode: &AppMode, show_help: bool, pending_g: bool, pending_z: bool, has_number_prefix: bool, has_error_popup: bool, has_file_browser: bool) -> Action {
if has_error_popup {
return Action::DismissErrorPopup;
}
if has_file_browser {
return match key.code {
KeyCode::Up | KeyCode::Char('k') => Action::FileBrowserUp,
KeyCode::Down | KeyCode::Char('j') => Action::FileBrowserDown,
KeyCode::Enter | KeyCode::Char('l') => Action::FileBrowserSelect,
KeyCode::Backspace | KeyCode::Char('h') => Action::FileBrowserParent,
KeyCode::Char('a') => Action::FileBrowserToggleAll,
KeyCode::Esc | KeyCode::Char('q') => Action::FileBrowserQuit,
_ => Action::None,
};
}
if show_help {
return match key.code {
KeyCode::Right | KeyCode::Char('l') | KeyCode::Tab => Action::HelpNextTab,
KeyCode::Left | KeyCode::Char('h') | KeyCode::BackTab => Action::HelpPrevTab,
_ => Action::DismissHelp,
};
}
if pending_g {
return handle_g_command(key);
}
if pending_z {
return handle_z_command(key);
}
match mode {
AppMode::Normal => handle_normal_mode(key, has_number_prefix),
AppMode::Command(_) => handle_command_mode(key),
AppMode::Search(_) | AppMode::SearchBackward(_) => handle_search_mode(key),
AppMode::TranslationSettings => handle_translation_settings_mode(key),
}
}
fn handle_g_command(key: KeyEvent) -> Action {
match key.code {
KeyCode::Char('0') => Action::GotoFirstVisibleColumn,
KeyCode::Char('m') => Action::GotoMiddleVisibleColumn,
KeyCode::Char('$') => Action::GotoLastVisibleColumn,
_ => Action::None, }
}
fn handle_z_command(key: KeyEvent) -> Action {
match key.code {
KeyCode::Char('H') => Action::HalfPageLeft,
KeyCode::Char('L') => Action::HalfPageRight,
_ => Action::None, }
}
fn handle_normal_mode(key: KeyEvent, has_number_prefix: bool) -> Action {
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
return Action::Quit;
}
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('f') {
return Action::EnterSearchMode;
}
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('u') {
return Action::HalfPageUp;
}
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('d') {
return Action::HalfPageDown;
}
if (key.modifiers.contains(KeyModifiers::CONTROL) || key.modifiers.contains(KeyModifiers::SHIFT))
&& key.code == KeyCode::Left {
return Action::HalfPageLeft;
}
if (key.modifiers.contains(KeyModifiers::CONTROL) || key.modifiers.contains(KeyModifiers::SHIFT))
&& key.code == KeyCode::Right {
return Action::HalfPageRight;
}
if (key.modifiers.contains(KeyModifiers::CONTROL) || key.modifiers.contains(KeyModifiers::SHIFT))
&& key.code == KeyCode::Up {
return Action::PageUp;
}
if (key.modifiers.contains(KeyModifiers::CONTROL) || key.modifiers.contains(KeyModifiers::SHIFT))
&& key.code == KeyCode::Down {
return Action::PageDown;
}
match key.code {
KeyCode::Char('j') => Action::MoveDown,
KeyCode::Char('k') => Action::MoveUp,
KeyCode::Char('l') => Action::MoveRight,
KeyCode::Char('h') => Action::MoveLeft,
KeyCode::Char('w') => Action::WordForward,
KeyCode::Char('b') => Action::WordBackward,
KeyCode::Char('e') => Action::WordEnd,
KeyCode::Up => Action::MoveUp,
KeyCode::Down => Action::MoveDown,
KeyCode::Right => Action::MoveRight,
KeyCode::Left => Action::MoveLeft,
KeyCode::Char('0') if !has_number_prefix => Action::GotoFirstColumn,
KeyCode::Char('0') => Action::AccumulateDigit('0'),
KeyCode::Char('$') => Action::GotoLastColumn,
KeyCode::Home => Action::GotoFirstColumn,
KeyCode::End => Action::GotoLastColumn,
KeyCode::PageUp => Action::PageUp,
KeyCode::PageDown => Action::PageDown,
KeyCode::Char('g') => Action::PendingG,
KeyCode::Char('z') => Action::PendingZ,
KeyCode::Char(c @ '1'..='9') => Action::AccumulateDigit(c),
KeyCode::Char('|') => Action::ExecuteGotoColumn,
KeyCode::Char(':') => Action::EnterCommandMode,
KeyCode::Char('/') => Action::EnterSearchMode,
KeyCode::Char('?') => Action::EnterSearchBackward,
KeyCode::Char('n') => Action::FindNext,
KeyCode::Char('N') => Action::FindPrevious,
_ => Action::None,
}
}
fn handle_command_mode(key: KeyEvent) -> Action {
match key.code {
KeyCode::Enter => Action::ExecuteCommand,
KeyCode::Esc => Action::CancelCommand,
KeyCode::Backspace => Action::CommandBackspace,
KeyCode::Char(c) => Action::CommandChar(c),
_ => Action::None,
}
}
fn handle_search_mode(key: KeyEvent) -> Action {
match key.code {
KeyCode::Enter => Action::ExecuteSearch,
KeyCode::Esc => Action::CancelSearch,
KeyCode::Backspace => Action::SearchBackspace,
KeyCode::Char(c) => Action::SearchChar(c),
_ => Action::None,
}
}
fn handle_translation_settings_mode(key: KeyEvent) -> Action {
match key.code {
KeyCode::Char('j') | KeyCode::Down => Action::TranslationDown,
KeyCode::Char('k') | KeyCode::Up => Action::TranslationUp,
KeyCode::Char('h') | KeyCode::Left => Action::TranslationFrameLeft,
KeyCode::Char('l') | KeyCode::Right => Action::TranslationFrameRight,
KeyCode::Char('1') => Action::TranslationFrameLeft, KeyCode::Char('2') => Action::TranslationFrameRight,
KeyCode::Char('3') => Action::TranslationFrameRight,
KeyCode::Enter => Action::TranslationConfirm,
KeyCode::Esc | KeyCode::Char('q') => Action::TranslationCancel,
_ => Action::None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ActionResult {
Continue,
StartTranslation,
LoadFile(std::path::PathBuf),
}
pub fn apply_action(state: &mut AppState, action: Action) -> ActionResult {
match action {
Action::None => {}
Action::Quit => {
state.should_quit = true;
}
Action::MoveUp => {
state.move_up();
}
Action::MoveDown => {
state.move_down();
}
Action::MoveLeft => {
state.move_left();
}
Action::MoveRight => {
state.move_right();
}
Action::EnterCommandMode => {
state.enter_command_mode();
}
Action::CommandChar(c) => {
state.command_input(c);
}
Action::ExecuteCommand => {
if state.execute_command() {
return ActionResult::StartTranslation;
}
}
Action::CancelCommand => {
state.cancel_command();
}
Action::CommandBackspace => {
state.command_backspace();
}
Action::Resize(_, _) => {
}
Action::EnterSearchMode => {
state.enter_search_mode(false);
}
Action::EnterSearchBackward => {
state.enter_search_mode(true);
}
Action::SearchChar(c) => {
state.search_input(c);
}
Action::ExecuteSearch => {
state.execute_search();
}
Action::CancelSearch => {
state.cancel_search();
}
Action::SearchBackspace => {
state.search_backspace();
}
Action::FindNext => {
state.find_next();
}
Action::FindPrevious => {
state.find_previous();
}
Action::DismissHelp => {
state.dismiss_help();
}
Action::HelpNextTab => {
state.help_next_tab();
}
Action::HelpPrevTab => {
state.help_prev_tab();
}
Action::GotoFirstColumn => {
state.goto_first_column();
}
Action::GotoLastColumn => {
state.goto_last_column();
}
Action::GotoFirstVisibleColumn => {
state.goto_first_visible_column();
}
Action::GotoMiddleVisibleColumn => {
state.goto_middle_visible_column();
}
Action::GotoLastVisibleColumn => {
state.goto_last_visible_column();
}
Action::GotoColumn(col) => {
state.goto_column(col);
}
Action::PendingG => {
state.set_pending_g();
}
Action::AccumulateDigit(c) => {
state.accumulate_digit(c);
}
Action::ExecuteGotoColumn => {
state.execute_goto_column();
}
Action::TranslationUp => {
state.translation_settings_up();
}
Action::TranslationDown => {
state.translation_settings_down();
}
Action::TranslationFrameLeft => {
state.translation_settings_frame_left();
}
Action::TranslationFrameRight => {
state.translation_settings_frame_right();
}
Action::TranslationConfirm => {
if state.confirm_translation_settings() {
return ActionResult::StartTranslation;
}
}
Action::StartTranslation => {
if state.should_start_translation() {
return ActionResult::StartTranslation;
}
}
Action::TranslationCancel => {
state.cancel_translation_settings();
}
Action::HalfPageUp => {
state.half_page_up();
}
Action::HalfPageDown => {
state.half_page_down();
}
Action::HalfPageLeft => {
state.half_page_left();
}
Action::HalfPageRight => {
state.half_page_right();
}
Action::PageUp => {
state.page_up();
}
Action::PageDown => {
state.page_down();
}
Action::PendingZ => {
state.set_pending_z();
}
Action::WordForward => {
state.word_forward();
}
Action::WordBackward => {
state.word_backward();
}
Action::WordEnd => {
state.word_end();
}
Action::DismissErrorPopup => {
state.dismiss_error_popup();
}
Action::FileBrowserUp => {
state.file_browser_up();
}
Action::FileBrowserDown => {
state.file_browser_down();
}
Action::FileBrowserSelect => {
if let Some(path) = state.file_browser_select() {
return ActionResult::LoadFile(path);
}
}
Action::FileBrowserParent => {
state.file_browser_parent();
}
Action::FileBrowserToggleAll => {
state.file_browser_toggle_show_all();
}
Action::FileBrowserQuit => {
state.file_browser_quit();
}
}
ActionResult::Continue
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normal_mode_navigation() {
let mode = AppMode::Normal;
let key = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::MoveLeft);
let key = KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::MoveDown);
let key = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::MoveUp);
let key = KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::MoveRight);
}
#[test]
fn test_enter_command_mode() {
let mode = AppMode::Normal;
let key = KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::EnterCommandMode);
}
#[test]
fn test_command_mode_input() {
let mode = AppMode::Command(String::new());
let key = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::CommandChar('q'));
let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::ExecuteCommand);
let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::CancelCommand);
}
#[test]
fn test_ctrl_c_quit() {
let mode = AppMode::Normal;
let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::Quit);
}
#[test]
fn test_search_mode_keys() {
let mode = AppMode::Normal;
let key = KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::EnterSearchMode);
let key = KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::EnterSearchBackward);
let key = KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::FindNext);
let key = KeyEvent::new(KeyCode::Char('N'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::FindPrevious);
}
#[test]
fn test_search_mode_input() {
let mode = AppMode::Search(String::new());
let key = KeyEvent::new(KeyCode::Char('A'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::SearchChar('A'));
let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::ExecuteSearch);
let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::CancelSearch);
let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::SearchBackspace);
}
#[test]
fn test_help_navigation() {
let mode = AppMode::Normal;
let key = KeyEvent::new(KeyCode::Right, KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, true, false, false, false, false, false), Action::HelpNextTab);
let key = KeyEvent::new(KeyCode::Left, KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, true, false, false, false, false, false), Action::HelpPrevTab);
let key = KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, true, false, false, false, false, false), Action::HelpNextTab);
let key = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, true, false, false, false, false, false), Action::HelpPrevTab);
let key = KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, true, false, false, false, false, false), Action::HelpNextTab);
let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, true, false, false, false, false, false), Action::DismissHelp);
let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, true, false, false, false, false, false), Action::DismissHelp);
}
#[test]
fn test_jump_navigation() {
let mode = AppMode::Normal;
let key = KeyEvent::new(KeyCode::Char('0'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::GotoFirstColumn);
let key = KeyEvent::new(KeyCode::Char('0'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, true, false, false), Action::AccumulateDigit('0'));
let key = KeyEvent::new(KeyCode::Char('$'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::GotoLastColumn);
let key = KeyEvent::new(KeyCode::Home, KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::GotoFirstColumn);
let key = KeyEvent::new(KeyCode::End, KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::GotoLastColumn);
}
#[test]
fn test_g_commands() {
let mode = AppMode::Normal;
let key = KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::PendingG);
let key = KeyEvent::new(KeyCode::Char('0'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, true, false, false, false, false), Action::GotoFirstVisibleColumn);
let key = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, true, false, false, false, false), Action::GotoMiddleVisibleColumn);
let key = KeyEvent::new(KeyCode::Char('$'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, true, false, false, false, false), Action::GotoLastVisibleColumn);
let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, true, false, false, false, false), Action::None);
}
#[test]
fn test_z_commands() {
let mode = AppMode::Normal;
let key = KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::PendingZ);
let key = KeyEvent::new(KeyCode::Char('H'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, true, false, false, false), Action::HalfPageLeft);
let key = KeyEvent::new(KeyCode::Char('L'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, true, false, false, false), Action::HalfPageRight);
let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, true, false, false, false), Action::None);
}
#[test]
fn test_number_prefix() {
let mode = AppMode::Normal;
let key = KeyEvent::new(KeyCode::Char('5'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::AccumulateDigit('5'));
let key = KeyEvent::new(KeyCode::Char('|'), KeyModifiers::NONE);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::ExecuteGotoColumn);
}
#[test]
fn test_modified_arrows_half_page() {
let mode = AppMode::Normal;
let key = KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::HalfPageLeft);
let key = KeyEvent::new(KeyCode::Right, KeyModifiers::CONTROL);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::HalfPageRight);
let key = KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::HalfPageLeft);
let key = KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::HalfPageRight);
let key = KeyEvent::new(KeyCode::Up, KeyModifiers::SHIFT);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::PageUp);
let key = KeyEvent::new(KeyCode::Down, KeyModifiers::SHIFT);
assert_eq!(handle_key_event(key, &mode, false, false, false, false, false, false), Action::PageDown);
}
}