use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
use super::app::{ActionVerb, InputMode, KeyPress};
use super::textbox::TextEdit;
#[must_use]
pub fn map_key(event: &Event, mode: InputMode) -> Option<KeyPress> {
let Event::Key(key) = event else {
return None;
};
if key.kind != KeyEventKind::Press {
return None;
}
if key.modifiers.contains(KeyModifiers::CONTROL) {
return match key.code {
KeyCode::Char('c') => Some(KeyPress::Quit),
KeyCode::Char('d') if mode == InputMode::Normal => Some(KeyPress::PageDown),
KeyCode::Char('u') if mode == InputMode::Normal => Some(KeyPress::PageUp),
KeyCode::Char(letter) if mode == InputMode::Text => text_edit_ctrl(letter),
_ => None,
};
}
if mode == InputMode::Text {
let alt = key.modifiers.contains(KeyModifiers::ALT);
return match key.code {
KeyCode::Char('b') if alt => edit(TextEdit::WordLeft),
KeyCode::Char('f') if alt => edit(TextEdit::WordRight),
KeyCode::Left if alt => edit(TextEdit::WordLeft),
KeyCode::Right if alt => edit(TextEdit::WordRight),
KeyCode::Backspace if alt => edit(TextEdit::DeleteWordBack),
KeyCode::Char(typed) if !alt => Some(KeyPress::TextChar(typed)),
KeyCode::Backspace => Some(KeyPress::TextBackspace),
KeyCode::Left => edit(TextEdit::Left),
KeyCode::Right => edit(TextEdit::Right),
KeyCode::Home => edit(TextEdit::Home),
KeyCode::End => edit(TextEdit::End),
KeyCode::Delete => edit(TextEdit::Delete),
KeyCode::Enter => Some(KeyPress::TextApply),
KeyCode::Esc => Some(KeyPress::TextAbandon),
_ => None,
};
}
match key.code {
KeyCode::Char('q') => Some(KeyPress::Quit),
KeyCode::Esc => Some(KeyPress::Escape),
KeyCode::Char('/') => Some(KeyPress::FilterStart),
KeyCode::Tab => Some(KeyPress::NextGroup),
KeyCode::BackTab => Some(KeyPress::PrevGroup),
KeyCode::Char(digit @ '1'..='9') => Some(KeyPress::Group(digit as u8 - b'0')),
KeyCode::Char('u') => Some(KeyPress::Undo),
KeyCode::Char('c') => Some(KeyPress::Continue),
KeyCode::Char('j') | KeyCode::Down => Some(KeyPress::SelectDown),
KeyCode::Char('k') | KeyCode::Up => Some(KeyPress::SelectUp),
KeyCode::Char('g') | KeyCode::Home => Some(KeyPress::SelectFirst),
KeyCode::Char('G') | KeyCode::End => Some(KeyPress::SelectLast),
KeyCode::Char('r') => Some(KeyPress::Refresh),
KeyCode::Char('x') => Some(KeyPress::Action(ActionVerb::Stop)),
KeyCode::Char('R') => Some(KeyPress::Action(ActionVerb::Restart)),
KeyCode::Char('L') => Some(KeyPress::Action(ActionVerb::Reload)),
KeyCode::Char('s') => Some(KeyPress::Settings),
KeyCode::Char('S') => Some(KeyPress::Secrets),
KeyCode::Char('v') => Some(KeyPress::Reveal),
KeyCode::Char('y') => Some(KeyPress::Copy),
KeyCode::Left => Some(KeyPress::TabPrev),
KeyCode::Right => Some(KeyPress::TabNext),
KeyCode::Char('e') => Some(KeyPress::Edit),
KeyCode::Char('h' | '?') => Some(KeyPress::Help),
KeyCode::Char(' ') => Some(KeyPress::Cycle),
KeyCode::Char('d') => Some(KeyPress::Remove),
KeyCode::Char('D') => Some(KeyPress::SecretDelete),
KeyCode::Char('K') => Some(KeyPress::StepUp),
KeyCode::Char('J') => Some(KeyPress::StepDown),
KeyCode::Char('F') => Some(KeyPress::FoldView),
KeyCode::Char('z') => Some(KeyPress::Collapse),
KeyCode::Char('b') => Some(KeyPress::Bleats),
KeyCode::Char('o') => Some(KeyPress::StreamCycle),
KeyCode::Char('m') => Some(KeyPress::LevelCycle),
KeyCode::Char('f') => Some(KeyPress::FollowToggle),
KeyCode::Char('w') => Some(KeyPress::WrapToggle),
KeyCode::Char('n') => Some(KeyPress::MatchNext),
KeyCode::Char('N') => Some(KeyPress::MatchPrev),
KeyCode::Enter => Some(KeyPress::Confirm),
_ => None,
}
}
fn edit(text_edit: TextEdit) -> Option<KeyPress> {
Some(KeyPress::TextEdit(text_edit))
}
fn text_edit_ctrl(letter: char) -> Option<KeyPress> {
match letter {
'a' => edit(TextEdit::Home),
'e' => edit(TextEdit::End),
'w' => edit(TextEdit::DeleteWordBack),
'u' => edit(TextEdit::DeleteToStart),
'k' => edit(TextEdit::DeleteToEnd),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
fn key(code: KeyCode) -> Event {
Event::Key(KeyEvent::new(code, KeyModifiers::NONE))
}
fn press(code: KeyCode) -> Option<KeyPress> {
map_key(&key(code), InputMode::Normal)
}
#[test]
fn every_bound_key_resolves_to_its_press() {
assert_eq!(
map_key(&key(KeyCode::Char('q')), InputMode::Normal),
Some(KeyPress::Quit)
);
assert_eq!(
map_key(&key(KeyCode::Esc), InputMode::Normal),
Some(KeyPress::Escape)
);
assert_eq!(
map_key(&key(KeyCode::Char('j')), InputMode::Normal),
Some(KeyPress::SelectDown)
);
assert_eq!(
map_key(&key(KeyCode::Down), InputMode::Normal),
Some(KeyPress::SelectDown)
);
assert_eq!(
map_key(&key(KeyCode::Char('k')), InputMode::Normal),
Some(KeyPress::SelectUp)
);
assert_eq!(
map_key(&key(KeyCode::Up), InputMode::Normal),
Some(KeyPress::SelectUp)
);
assert_eq!(
map_key(&key(KeyCode::Char('g')), InputMode::Normal),
Some(KeyPress::SelectFirst)
);
assert_eq!(
map_key(&key(KeyCode::Char('G')), InputMode::Normal),
Some(KeyPress::SelectLast)
);
assert_eq!(
map_key(&key(KeyCode::Char('r')), InputMode::Normal),
Some(KeyPress::Refresh)
);
assert_eq!(
map_key(&key(KeyCode::Char('x')), InputMode::Normal),
Some(KeyPress::Action(ActionVerb::Stop))
);
assert_eq!(
map_key(&key(KeyCode::Char('R')), InputMode::Normal),
Some(KeyPress::Action(ActionVerb::Restart))
);
assert_eq!(
map_key(&key(KeyCode::Char('L')), InputMode::Normal),
Some(KeyPress::Action(ActionVerb::Reload))
);
assert_eq!(
map_key(&key(KeyCode::Char('s')), InputMode::Normal),
Some(KeyPress::Settings)
);
assert_eq!(
map_key(&key(KeyCode::Char(' ')), InputMode::Normal),
Some(KeyPress::Cycle)
);
assert_eq!(
map_key(&key(KeyCode::Char('e')), InputMode::Normal),
Some(KeyPress::Edit)
);
assert_eq!(
map_key(&key(KeyCode::Char('E')), InputMode::Normal),
None,
"the config pane is lower-case `e`; `E` is unbound"
);
assert_eq!(
map_key(&key(KeyCode::Char('h')), InputMode::Normal),
Some(KeyPress::Help)
);
assert_eq!(
map_key(&key(KeyCode::Char('d')), InputMode::Normal),
Some(KeyPress::Remove)
);
assert_eq!(
map_key(&key(KeyCode::Char('D')), InputMode::Normal),
Some(KeyPress::SecretDelete)
);
assert_eq!(
map_key(&key(KeyCode::Char('K')), InputMode::Normal),
Some(KeyPress::StepUp)
);
assert_eq!(
map_key(&key(KeyCode::Char('J')), InputMode::Normal),
Some(KeyPress::StepDown)
);
assert_eq!(
map_key(&key(KeyCode::Char('F')), InputMode::Normal),
Some(KeyPress::FoldView)
);
assert_eq!(
map_key(&key(KeyCode::Char('z')), InputMode::Normal),
Some(KeyPress::Collapse)
);
}
#[test]
fn b_opens_the_bleats_pane() {
assert_eq!(
map_key(&key(KeyCode::Char('b')), InputMode::Normal),
Some(KeyPress::Bleats)
);
}
#[test]
fn the_movement_keys_are_unchanged_and_now_mean_selection() {
assert_eq!(
map_key(&key(KeyCode::Char('j')), InputMode::Normal),
Some(KeyPress::SelectDown)
);
assert_eq!(
map_key(&key(KeyCode::Down), InputMode::Normal),
Some(KeyPress::SelectDown)
);
assert_eq!(
map_key(&key(KeyCode::Char('k')), InputMode::Normal),
Some(KeyPress::SelectUp)
);
assert_eq!(
map_key(&key(KeyCode::Up), InputMode::Normal),
Some(KeyPress::SelectUp)
);
assert_eq!(
map_key(&key(KeyCode::Char('g')), InputMode::Normal),
Some(KeyPress::SelectFirst)
);
assert_eq!(
map_key(&key(KeyCode::Home), InputMode::Normal),
Some(KeyPress::SelectFirst)
);
assert_eq!(
map_key(&key(KeyCode::Char('G')), InputMode::Normal),
Some(KeyPress::SelectLast)
);
assert_eq!(
map_key(&key(KeyCode::End), InputMode::Normal),
Some(KeyPress::SelectLast)
);
}
#[test]
fn capital_s_opens_the_secrets_pane_and_lower_s_still_opens_settings() {
assert_eq!(
map_key(&key(KeyCode::Char('S')), InputMode::Normal),
Some(KeyPress::Secrets)
);
assert_eq!(
map_key(&key(KeyCode::Char('s')), InputMode::Normal),
Some(KeyPress::Settings),
"the settings screen keeps its own key"
);
assert_eq!(
map_key(&key(KeyCode::Char('g')), InputMode::Normal),
Some(KeyPress::SelectFirst),
"the frame wanted `g` for secrets; `g` is still SelectFirst"
);
}
#[test]
fn lower_v_is_the_reveal() {
assert_eq!(
map_key(&key(KeyCode::Char('v')), InputMode::Normal),
Some(KeyPress::Reveal)
);
assert_eq!(
map_key(&key(KeyCode::Char('V')), InputMode::Normal),
None,
"one key puts a value on screen, and it is not a shifted one"
);
}
#[test]
fn the_arrow_keys_move_the_environment_tab() {
assert_eq!(
map_key(&key(KeyCode::Left), InputMode::Normal),
Some(KeyPress::TabPrev)
);
assert_eq!(
map_key(&key(KeyCode::Right), InputMode::Normal),
Some(KeyPress::TabNext)
);
assert_eq!(
map_key(&key(KeyCode::Up), InputMode::Normal),
Some(KeyPress::SelectUp),
"the vertical arrows keep the meaning they already have"
);
}
#[test]
fn tab_walks_the_config_pane_groups_and_the_tab_row_does_not_claim_it() {
assert_eq!(
map_key(&key(KeyCode::Tab), InputMode::Normal),
Some(KeyPress::NextGroup)
);
assert_eq!(
map_key(
&Event::Key(KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT)),
InputMode::Normal
),
Some(KeyPress::PrevGroup),
"shift-tab walks back where tab walks forward"
);
assert_eq!(
map_key(&key(KeyCode::Tab), InputMode::Text),
None,
"and neither reaches an open input, where a tab is not a character"
);
}
#[test]
fn ctrl_c_quits_because_raw_mode_swallows_the_signal() {
let event = Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));
assert_eq!(map_key(&event, InputMode::Normal), Some(KeyPress::Quit));
assert_eq!(
map_key(&key(KeyCode::Char('c')), InputMode::Normal),
Some(KeyPress::Continue)
);
}
#[test]
fn only_a_press_counts() {
let mut release = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
release.kind = KeyEventKind::Release;
assert_eq!(map_key(&Event::Key(release), InputMode::Normal), None);
let mut repeat = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
repeat.kind = KeyEventKind::Repeat;
assert_eq!(map_key(&Event::Key(repeat), InputMode::Normal), None);
}
fn modified(code: KeyCode, modifiers: KeyModifiers) -> Event {
Event::Key(KeyEvent::new(code, modifiers))
}
#[test]
fn the_cursor_keys_move_the_cursor_in_text_mode() {
let text = |event: Event| map_key(&event, InputMode::Text);
assert_eq!(text(key(KeyCode::Left)), edit(TextEdit::Left));
assert_eq!(text(key(KeyCode::Right)), edit(TextEdit::Right));
assert_eq!(text(key(KeyCode::Home)), edit(TextEdit::Home));
assert_eq!(text(key(KeyCode::End)), edit(TextEdit::End));
assert_eq!(text(key(KeyCode::Delete)), edit(TextEdit::Delete));
assert_eq!(
text(modified(KeyCode::Char('a'), KeyModifiers::CONTROL)),
edit(TextEdit::Home)
);
assert_eq!(
text(modified(KeyCode::Char('e'), KeyModifiers::CONTROL)),
edit(TextEdit::End)
);
}
#[test]
fn alt_arrows_and_alt_b_and_f_move_by_word_in_text_mode() {
let text = |event: Event| map_key(&event, InputMode::Text);
assert_eq!(
text(modified(KeyCode::Left, KeyModifiers::ALT)),
edit(TextEdit::WordLeft)
);
assert_eq!(
text(modified(KeyCode::Right, KeyModifiers::ALT)),
edit(TextEdit::WordRight)
);
assert_eq!(
text(modified(KeyCode::Char('b'), KeyModifiers::ALT)),
edit(TextEdit::WordLeft)
);
assert_eq!(
text(modified(KeyCode::Char('f'), KeyModifiers::ALT)),
edit(TextEdit::WordRight)
);
}
#[test]
fn the_kill_keys_delete_in_text_mode() {
let text = |event: Event| map_key(&event, InputMode::Text);
assert_eq!(
text(modified(KeyCode::Char('w'), KeyModifiers::CONTROL)),
edit(TextEdit::DeleteWordBack)
);
assert_eq!(
text(modified(KeyCode::Backspace, KeyModifiers::ALT)),
edit(TextEdit::DeleteWordBack)
);
assert_eq!(
text(modified(KeyCode::Char('u'), KeyModifiers::CONTROL)),
edit(TextEdit::DeleteToStart)
);
assert_eq!(
text(modified(KeyCode::Char('k'), KeyModifiers::CONTROL)),
edit(TextEdit::DeleteToEnd)
);
}
#[test]
fn the_edit_keys_keep_their_normal_mode_meaning_or_mean_nothing() {
let normal = |event: Event| map_key(&event, InputMode::Normal);
assert_eq!(normal(key(KeyCode::Left)), Some(KeyPress::TabPrev));
assert_eq!(normal(key(KeyCode::Right)), Some(KeyPress::TabNext));
assert_eq!(normal(key(KeyCode::Home)), Some(KeyPress::SelectFirst));
assert_eq!(normal(key(KeyCode::End)), Some(KeyPress::SelectLast));
assert_eq!(normal(key(KeyCode::Delete)), None);
assert_eq!(
normal(modified(KeyCode::Char('u'), KeyModifiers::CONTROL)),
Some(KeyPress::PageUp)
);
for letter in ['a', 'e', 'w', 'k'] {
assert_eq!(
normal(modified(KeyCode::Char(letter), KeyModifiers::CONTROL)),
None,
"ctrl-{letter}"
);
}
assert_eq!(
normal(modified(KeyCode::Backspace, KeyModifiers::ALT)),
None
);
}
#[test]
fn an_alt_letter_is_not_typed_into_the_box() {
assert_eq!(
map_key(
&modified(KeyCode::Char('x'), KeyModifiers::ALT),
InputMode::Text
),
None
);
}
#[test]
fn a_paste_event_is_no_key() {
assert_eq!(
map_key(&Event::Paste("ab\ncd".to_owned()), InputMode::Text),
None
);
}
#[test]
fn typing_q_while_editing_types_a_letter() {
assert_eq!(
map_key(&key(KeyCode::Char('q')), InputMode::Text),
Some(KeyPress::TextChar('q'))
);
assert_eq!(
map_key(&key(KeyCode::Char('q')), InputMode::Normal),
Some(KeyPress::Quit)
);
}
#[test]
fn the_text_mode_binds_exactly_the_box_s_keys() {
assert_eq!(
map_key(&key(KeyCode::Backspace), InputMode::Text),
Some(KeyPress::TextBackspace)
);
assert_eq!(
map_key(&key(KeyCode::Enter), InputMode::Text),
Some(KeyPress::TextApply)
);
assert_eq!(
map_key(&key(KeyCode::Esc), InputMode::Text),
Some(KeyPress::TextAbandon)
);
let ctrl_c = Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));
assert_eq!(map_key(&ctrl_c, InputMode::Text), Some(KeyPress::Quit));
assert_eq!(map_key(&key(KeyCode::F(5)), InputMode::Text), None);
}
#[test]
fn a_shifted_letter_is_still_a_letter_in_the_box() {
let shifted = Event::Key(KeyEvent::new(KeyCode::Char('W'), KeyModifiers::SHIFT));
assert_eq!(
map_key(&shifted, InputMode::Text),
Some(KeyPress::TextChar('W'))
);
}
#[test]
fn o_and_m_cycle_the_stream_and_level_axes() {
assert_eq!(
map_key(&key(KeyCode::Char('o')), InputMode::Normal),
Some(KeyPress::StreamCycle)
);
assert_eq!(
map_key(&key(KeyCode::Char('m')), InputMode::Normal),
Some(KeyPress::LevelCycle)
);
}
#[test]
fn f_toggles_follow() {
assert_eq!(
map_key(&key(KeyCode::Char('f')), InputMode::Normal),
Some(KeyPress::FollowToggle)
);
}
#[test]
fn ctrl_d_and_ctrl_u_page_only_in_normal_mode() {
let ctrl_d = Event::Key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL));
let ctrl_u = Event::Key(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL));
assert_eq!(
map_key(&ctrl_d, InputMode::Normal),
Some(KeyPress::PageDown)
);
assert_eq!(map_key(&ctrl_u, InputMode::Normal), Some(KeyPress::PageUp));
assert_eq!(map_key(&ctrl_d, InputMode::Text), None);
assert_eq!(
map_key(&ctrl_u, InputMode::Text),
edit(TextEdit::DeleteToStart),
"in a box ctrl-u kills to the start of the line instead"
);
}
#[test]
fn question_mark_opens_the_keymap_and_slash_still_filters() {
assert_eq!(
map_key(&key(KeyCode::Char('?')), InputMode::Normal),
Some(KeyPress::Help)
);
assert_eq!(
map_key(&key(KeyCode::Char('/')), InputMode::Normal),
Some(KeyPress::FilterStart)
);
assert_eq!(
map_key(&key(KeyCode::Char('?')), InputMode::Text),
Some(KeyPress::TextChar('?')),
"a question mark typed into a box is a character"
);
}
#[test]
fn a_shifted_question_mark_still_opens_the_keymap() {
let shifted = Event::Key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::SHIFT));
assert_eq!(map_key(&shifted, InputMode::Normal), Some(KeyPress::Help));
}
#[test]
fn slash_opens_the_filter_in_normal_mode() {
assert_eq!(
map_key(&key(KeyCode::Char('/')), InputMode::Normal),
Some(KeyPress::FilterStart)
);
}
#[test]
fn w_and_n_and_shift_n_are_bound() {
assert_eq!(
map_key(&key(KeyCode::Char('w')), InputMode::Normal),
Some(KeyPress::WrapToggle)
);
assert_eq!(
map_key(&key(KeyCode::Char('n')), InputMode::Normal),
Some(KeyPress::MatchNext)
);
assert_eq!(
map_key(&key(KeyCode::Char('N')), InputMode::Normal),
Some(KeyPress::MatchPrev)
);
}
#[test]
fn tab_asks_for_the_next_group() {
assert_eq!(press(KeyCode::Tab), Some(KeyPress::NextGroup));
}
#[test]
fn the_digits_one_through_nine_jump_to_a_group() {
for (typed, wanted) in [
('1', 1_u8),
('2', 2),
('3', 3),
('4', 4),
('5', 5),
('6', 6),
('7', 7),
('8', 8),
('9', 9),
] {
assert_eq!(press(KeyCode::Char(typed)), Some(KeyPress::Group(wanted)));
}
}
#[test]
fn zero_is_unbound() {
assert_eq!(press(KeyCode::Char('0')), None);
}
#[test]
fn bare_u_undoes_and_ctrl_u_still_pages() {
assert_eq!(press(KeyCode::Char('u')), Some(KeyPress::Undo));
let ctrl_u = Event::Key(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL));
assert_eq!(map_key(&ctrl_u, InputMode::Normal), Some(KeyPress::PageUp));
}
#[test]
fn bare_c_continues() {
assert_eq!(press(KeyCode::Char('c')), Some(KeyPress::Continue));
}
#[test]
fn a_digit_in_text_mode_is_typed() {
let one = Event::Key(KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE));
assert_eq!(
map_key(&one, InputMode::Text),
Some(KeyPress::TextChar('1'))
);
}
#[test]
fn d_removes() {
assert_eq!(press(KeyCode::Char('d')), Some(KeyPress::Remove));
}
#[test]
fn shift_j_and_shift_k_are_steps() {
assert_eq!(
map_key(&key(KeyCode::Char('J')), InputMode::Normal),
Some(KeyPress::StepDown)
);
assert_eq!(
map_key(&key(KeyCode::Char('K')), InputMode::Normal),
Some(KeyPress::StepUp)
);
}
}