use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
use super::app::{ActionVerb, InputMode, KeyPress};
#[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),
_ => None,
};
}
if mode == InputMode::Text {
return match key.code {
KeyCode::Char(typed) if !key.modifiers.contains(KeyModifiers::ALT) => {
Some(KeyPress::TextChar(typed))
}
KeyCode::Backspace => Some(KeyPress::TextBackspace),
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::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('e') => Some(KeyPress::Edit),
KeyCode::Char('h') => Some(KeyPress::Help),
KeyCode::Char(' ') => Some(KeyPress::Cycle),
KeyCode::Char('d') => Some(KeyPress::ListRemove),
KeyCode::Char('K') => Some(KeyPress::ListMoveUp),
KeyCode::Char('J') => Some(KeyPress::ListMoveDown),
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,
}
}
#[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))
}
#[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::ListRemove)
);
assert_eq!(
map_key(&key(KeyCode::Char('K')), InputMode::Normal),
Some(KeyPress::ListMoveUp)
);
assert_eq!(
map_key(&key(KeyCode::Char('J')), InputMode::Normal),
Some(KeyPress::ListMoveDown)
);
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 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), None);
}
#[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);
}
#[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), None);
}
#[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)
);
}
}