use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
const MAX_LINE_NUM: u32 = 999_999;
pub(super) struct InputAccumulator {
count: Option<u32>,
}
impl InputAccumulator {
pub(super) fn new() -> Self {
Self { count: None }
}
fn push_digit(&mut self, d: u32) -> bool {
let current = self.count.unwrap_or(0);
let new = current.saturating_mul(10).saturating_add(d);
if new > MAX_LINE_NUM {
return false; }
self.count = Some(new);
true
}
fn take(&mut self) -> Option<u32> {
self.count.take()
}
pub(super) fn peek(&self) -> Option<u32> {
self.count
}
pub(super) fn reset(&mut self) {
self.count = None;
}
pub(super) fn is_active(&self) -> bool {
self.count.is_some()
}
}
pub(super) enum Action {
Quit,
ScrollDown(u32),
ScrollUp(u32),
HalfPageDown(u32),
HalfPageUp(u32),
JumpToTop,
JumpToBottom,
JumpToLine(u32),
YankExact(u32),
YankExactPrompt,
YankBlock(u32),
YankBlockPrompt,
OpenUrl(u32),
OpenUrlPrompt,
EnterUrlPicker,
EnterToc,
EnterSearch,
EnterCommand,
SearchNextMatch,
SearchPrevMatch,
GoBack,
CancelInput,
Digit,
}
pub(super) fn map_key_event(key: KeyEvent, acc: &mut InputAccumulator) -> Option<Action> {
let KeyEvent {
code, modifiers, ..
} = key;
match (code, modifiers) {
(KeyCode::Char('q'), _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => Some(Action::Quit),
(KeyCode::Esc, _) => {
acc.reset();
Some(Action::CancelInput)
}
(KeyCode::Char(c @ '0'..='9'), KeyModifiers::NONE) => {
let d = c as u32 - '0' as u32;
acc.push_digit(d);
Some(Action::Digit)
}
(KeyCode::Char('j'), _) | (KeyCode::Down, _) => {
let count = acc.take().unwrap_or(1);
Some(Action::ScrollDown(count))
}
(KeyCode::Char('k'), _) | (KeyCode::Up, _) => {
let count = acc.take().unwrap_or(1);
Some(Action::ScrollUp(count))
}
(KeyCode::Char('d'), _) => {
let count = acc.take().unwrap_or(1);
Some(Action::HalfPageDown(count))
}
(KeyCode::Char('u'), _) => {
let count = acc.take().unwrap_or(1);
Some(Action::HalfPageUp(count))
}
(KeyCode::Char('g'), _) => match acc.take() {
None => Some(Action::JumpToTop),
Some(n) => Some(Action::JumpToLine(n)),
},
(KeyCode::Char('G'), _) => match acc.take() {
None => Some(Action::JumpToBottom),
Some(n) => Some(Action::JumpToLine(n)),
},
(KeyCode::Char('y'), _) => match acc.take() {
None => Some(Action::YankExactPrompt),
Some(n) => Some(Action::YankExact(n)),
},
(KeyCode::Char('Y'), _) => match acc.take() {
None => Some(Action::YankBlockPrompt),
Some(n) => Some(Action::YankBlock(n)),
},
(KeyCode::Char('o'), KeyModifiers::CONTROL) => {
acc.reset();
Some(Action::GoBack)
}
(KeyCode::Char('o'), _) => match acc.take() {
None => Some(Action::OpenUrlPrompt),
Some(n) => Some(Action::OpenUrl(n)),
},
(KeyCode::Char('O'), _) => {
acc.reset();
Some(Action::EnterUrlPicker)
}
(KeyCode::Char('t'), KeyModifiers::NONE) => {
acc.reset();
Some(Action::EnterToc)
}
(KeyCode::Char('/'), _) => {
acc.reset();
Some(Action::EnterSearch)
}
(KeyCode::Char(':'), _) => {
acc.reset();
Some(Action::EnterCommand)
}
(KeyCode::Char('n'), KeyModifiers::NONE) => {
acc.reset();
Some(Action::SearchNextMatch)
}
(KeyCode::Char('N'), KeyModifiers::SHIFT) => {
acc.reset();
Some(Action::SearchPrevMatch)
}
_ => {
if acc.is_active() {
acc.reset();
Some(Action::CancelInput)
} else {
None
}
}
}
}
pub(super) enum SearchAction {
Type(char),
Backspace,
SelectNext,
SelectPrev,
SelectIndex(usize),
Confirm,
Cancel,
}
pub(super) enum CommandAction {
Type(char),
Backspace,
Execute,
Cancel,
}
pub(super) fn map_command_key(key: KeyEvent) -> Option<CommandAction> {
let KeyEvent {
code, modifiers, ..
} = key;
match (code, modifiers) {
(KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
Some(CommandAction::Cancel)
}
(KeyCode::Enter, _) => Some(CommandAction::Execute),
(KeyCode::Backspace, _) => Some(CommandAction::Backspace),
(KeyCode::Char(c), _) => Some(CommandAction::Type(c)),
_ => None,
}
}
pub(super) enum TocAction {
SelectNext,
SelectPrev,
Confirm,
Cancel,
Quit,
}
pub(super) fn map_toc_key(key: KeyEvent) -> Option<TocAction> {
let KeyEvent {
code, modifiers, ..
} = key;
match (code, modifiers) {
(KeyCode::Char('q'), _) => Some(TocAction::Quit),
(KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => Some(TocAction::Cancel),
(KeyCode::Enter, _) => Some(TocAction::Confirm),
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) => {
Some(TocAction::SelectNext)
}
(KeyCode::Char('k'), KeyModifiers::NONE) | (KeyCode::Up, _) => Some(TocAction::SelectPrev),
_ => None,
}
}
pub(super) enum UrlAction {
SelectNext,
SelectPrev,
Confirm,
Cancel,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum LogAction {
ScrollDown,
ScrollUp,
JumpToTop,
JumpToBottom,
EnterSearch,
SearchNext,
SearchPrev,
Type(char),
Backspace,
Yank,
Cancel,
}
pub(super) fn map_log_key(key: KeyEvent) -> Option<LogAction> {
let KeyEvent {
code, modifiers, ..
} = key;
match (code, modifiers) {
(KeyCode::Char('q'), _)
| (KeyCode::Esc, _)
| (KeyCode::Char('c'), KeyModifiers::CONTROL) => Some(LogAction::Cancel),
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) => {
Some(LogAction::ScrollDown)
}
(KeyCode::Char('k'), KeyModifiers::NONE) | (KeyCode::Up, _) => Some(LogAction::ScrollUp),
(KeyCode::Char('g'), KeyModifiers::NONE) => Some(LogAction::JumpToTop),
(KeyCode::Char('G'), _) => Some(LogAction::JumpToBottom),
(KeyCode::Char('/'), _) => Some(LogAction::EnterSearch),
(KeyCode::Char('n'), KeyModifiers::NONE) => Some(LogAction::SearchNext),
(KeyCode::Char('N'), _) => Some(LogAction::SearchPrev),
(KeyCode::Char('y'), KeyModifiers::NONE) => Some(LogAction::Yank),
(KeyCode::Backspace, _) => Some(LogAction::Backspace),
(KeyCode::Char(c), _) => Some(LogAction::Type(c)),
_ => None,
}
}
pub(super) fn map_url_key(key: KeyEvent) -> Option<UrlAction> {
let KeyEvent {
code, modifiers, ..
} = key;
match (code, modifiers) {
(KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => Some(UrlAction::Cancel),
(KeyCode::Enter, _) => Some(UrlAction::Confirm),
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) => {
Some(UrlAction::SelectNext)
}
(KeyCode::Char('k'), KeyModifiers::NONE) | (KeyCode::Up, _) => Some(UrlAction::SelectPrev),
_ => None,
}
}
pub(super) fn map_search_key(key: KeyEvent) -> Option<SearchAction> {
let KeyEvent {
code, modifiers, ..
} = key;
match (code, modifiers) {
(KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
Some(SearchAction::Cancel)
}
(KeyCode::Enter, _) => Some(SearchAction::Confirm),
(KeyCode::Backspace, _) => Some(SearchAction::Backspace),
(KeyCode::Down, _) => Some(SearchAction::SelectNext),
(KeyCode::Up, _) => Some(SearchAction::SelectPrev),
(KeyCode::Char(c @ '1'..='9'), KeyModifiers::ALT) => {
Some(SearchAction::SelectIndex((c as u8 - b'1') as usize))
}
(KeyCode::Char(c), _) => Some(SearchAction::Type(c)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyEventKind, KeyEventState};
fn key(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent {
code,
modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}
}
fn simple_key(code: KeyCode) -> KeyEvent {
key(code, KeyModifiers::NONE)
}
#[test]
fn test_5j_scroll_down() {
let mut acc = InputAccumulator::new();
let a = map_key_event(simple_key(KeyCode::Char('5')), &mut acc);
assert!(matches!(a, Some(Action::Digit)));
let a = map_key_event(simple_key(KeyCode::Char('j')), &mut acc);
assert!(matches!(a, Some(Action::ScrollDown(5))));
}
#[test]
fn test_g_without_prefix_jumps_top() {
let mut acc = InputAccumulator::new();
let a = map_key_event(simple_key(KeyCode::Char('g')), &mut acc);
assert!(matches!(a, Some(Action::JumpToTop)));
}
#[test]
fn test_56g_jumps_to_line() {
let mut acc = InputAccumulator::new();
map_key_event(simple_key(KeyCode::Char('5')), &mut acc);
map_key_event(simple_key(KeyCode::Char('6')), &mut acc);
let a = map_key_event(simple_key(KeyCode::Char('g')), &mut acc);
assert!(matches!(a, Some(Action::JumpToLine(56))));
}
#[test]
fn test_q_quits() {
let mut acc = InputAccumulator::new();
let a = map_key_event(simple_key(KeyCode::Char('q')), &mut acc);
assert!(matches!(a, Some(Action::Quit)));
}
#[test]
fn test_ctrl_c_quits() {
let mut acc = InputAccumulator::new();
let a = map_key_event(key(KeyCode::Char('c'), KeyModifiers::CONTROL), &mut acc);
assert!(matches!(a, Some(Action::Quit)));
}
#[test]
fn test_esc_cancels_input() {
let mut acc = InputAccumulator::new();
map_key_event(simple_key(KeyCode::Char('5')), &mut acc);
assert!(acc.is_active());
let a = map_key_event(simple_key(KeyCode::Esc), &mut acc);
assert!(matches!(a, Some(Action::CancelInput)));
assert!(!acc.is_active());
}
#[test]
fn test_unknown_key_returns_none() {
let mut acc = InputAccumulator::new();
let a = map_key_event(simple_key(KeyCode::Char('x')), &mut acc);
assert!(a.is_none());
}
#[test]
fn test_unknown_key_with_accumulator_cancels_input() {
let mut acc = InputAccumulator::new();
map_key_event(simple_key(KeyCode::Char('5')), &mut acc);
assert!(acc.is_active());
let a = map_key_event(simple_key(KeyCode::Char('x')), &mut acc);
assert!(matches!(a, Some(Action::CancelInput)));
assert!(!acc.is_active());
}
#[test]
fn test_yank_with_prefix() {
let mut acc = InputAccumulator::new();
map_key_event(simple_key(KeyCode::Char('3')), &mut acc);
let a = map_key_event(simple_key(KeyCode::Char('y')), &mut acc);
assert!(matches!(a, Some(Action::YankExact(3))));
}
#[test]
fn test_yank_without_prefix() {
let mut acc = InputAccumulator::new();
let a = map_key_event(simple_key(KeyCode::Char('y')), &mut acc);
assert!(matches!(a, Some(Action::YankExactPrompt)));
}
#[test]
fn test_big_g_bottom() {
let mut acc = InputAccumulator::new();
let a = map_key_event(key(KeyCode::Char('G'), KeyModifiers::SHIFT), &mut acc);
assert!(matches!(a, Some(Action::JumpToBottom)));
}
#[test]
fn test_ctrl_o_goes_back() {
let mut acc = InputAccumulator::new();
let a = map_key_event(key(KeyCode::Char('o'), KeyModifiers::CONTROL), &mut acc);
assert!(matches!(a, Some(Action::GoBack)));
}
#[test]
fn test_ctrl_o_resets_accumulator() {
let mut acc = InputAccumulator::new();
map_key_event(simple_key(KeyCode::Char('5')), &mut acc);
assert!(acc.is_active());
map_key_event(key(KeyCode::Char('o'), KeyModifiers::CONTROL), &mut acc);
assert!(!acc.is_active());
}
#[test]
fn test_o_open_url_prompt() {
let mut acc = InputAccumulator::new();
let a = map_key_event(simple_key(KeyCode::Char('o')), &mut acc);
assert!(matches!(a, Some(Action::OpenUrlPrompt)));
}
#[test]
fn test_5o_open_url() {
let mut acc = InputAccumulator::new();
map_key_event(simple_key(KeyCode::Char('5')), &mut acc);
let a = map_key_event(simple_key(KeyCode::Char('o')), &mut acc);
assert!(matches!(a, Some(Action::OpenUrl(5))));
}
#[test]
fn test_big_o_enters_url_picker() {
let mut acc = InputAccumulator::new();
let a = map_key_event(key(KeyCode::Char('O'), KeyModifiers::SHIFT), &mut acc);
assert!(matches!(a, Some(Action::EnterUrlPicker)));
}
#[test]
fn test_slash_enters_search() {
let mut acc = InputAccumulator::new();
let a = map_key_event(simple_key(KeyCode::Char('/')), &mut acc);
assert!(matches!(a, Some(Action::EnterSearch)));
}
#[test]
fn test_slash_resets_accumulator() {
let mut acc = InputAccumulator::new();
map_key_event(simple_key(KeyCode::Char('5')), &mut acc);
assert!(acc.is_active());
map_key_event(simple_key(KeyCode::Char('/')), &mut acc);
assert!(!acc.is_active());
}
#[test]
fn test_n_search_next() {
let mut acc = InputAccumulator::new();
let a = map_key_event(simple_key(KeyCode::Char('n')), &mut acc);
assert!(matches!(a, Some(Action::SearchNextMatch)));
}
#[test]
fn test_big_n_search_prev() {
let mut acc = InputAccumulator::new();
let a = map_key_event(key(KeyCode::Char('N'), KeyModifiers::SHIFT), &mut acc);
assert!(matches!(a, Some(Action::SearchPrevMatch)));
}
#[test]
fn test_search_type_char() {
let a = map_search_key(simple_key(KeyCode::Char('a')));
assert!(matches!(a, Some(SearchAction::Type('a'))));
}
#[test]
fn test_search_backspace() {
let a = map_search_key(simple_key(KeyCode::Backspace));
assert!(matches!(a, Some(SearchAction::Backspace)));
}
#[test]
fn test_search_select_next_down() {
let a = map_search_key(simple_key(KeyCode::Down));
assert!(matches!(a, Some(SearchAction::SelectNext)));
}
#[test]
fn test_search_select_prev_up() {
let a = map_search_key(simple_key(KeyCode::Up));
assert!(matches!(a, Some(SearchAction::SelectPrev)));
}
#[test]
fn test_search_confirm() {
let a = map_search_key(simple_key(KeyCode::Enter));
assert!(matches!(a, Some(SearchAction::Confirm)));
}
#[test]
fn test_search_cancel_esc() {
let a = map_search_key(simple_key(KeyCode::Esc));
assert!(matches!(a, Some(SearchAction::Cancel)));
}
#[test]
fn test_search_cancel_ctrl_c() {
let a = map_search_key(key(KeyCode::Char('c'), KeyModifiers::CONTROL));
assert!(matches!(a, Some(SearchAction::Cancel)));
}
#[test]
fn test_search_unknown_returns_none() {
let a = map_search_key(simple_key(KeyCode::Tab));
assert!(a.is_none());
}
#[test]
fn test_search_alt_digit_selects_index() {
let e = key(KeyCode::Char('1'), KeyModifiers::ALT);
assert!(matches!(
map_search_key(e),
Some(SearchAction::SelectIndex(0))
));
let e = key(KeyCode::Char('9'), KeyModifiers::ALT);
assert!(matches!(
map_search_key(e),
Some(SearchAction::SelectIndex(8))
));
}
#[test]
fn test_search_alt_digit_not_plain_digit() {
let e = key(KeyCode::Char('1'), KeyModifiers::NONE);
assert!(matches!(map_search_key(e), Some(SearchAction::Type('1'))));
}
#[test]
fn test_t_enters_toc() {
let mut acc = InputAccumulator::new();
let a = map_key_event(simple_key(KeyCode::Char('t')), &mut acc);
assert!(matches!(a, Some(Action::EnterToc)));
}
#[test]
fn test_t_resets_accumulator() {
let mut acc = InputAccumulator::new();
map_key_event(simple_key(KeyCode::Char('5')), &mut acc);
assert!(acc.is_active());
map_key_event(simple_key(KeyCode::Char('t')), &mut acc);
assert!(!acc.is_active());
}
#[test]
fn test_toc_select_next_j() {
let a = map_toc_key(simple_key(KeyCode::Char('j')));
assert!(matches!(a, Some(TocAction::SelectNext)));
}
#[test]
fn test_toc_select_prev_k() {
let a = map_toc_key(simple_key(KeyCode::Char('k')));
assert!(matches!(a, Some(TocAction::SelectPrev)));
}
#[test]
fn test_toc_confirm() {
let a = map_toc_key(simple_key(KeyCode::Enter));
assert!(matches!(a, Some(TocAction::Confirm)));
}
#[test]
fn test_toc_cancel_esc() {
let a = map_toc_key(simple_key(KeyCode::Esc));
assert!(matches!(a, Some(TocAction::Cancel)));
}
#[test]
fn test_toc_cancel_ctrl_c() {
let a = map_toc_key(key(KeyCode::Char('c'), KeyModifiers::CONTROL));
assert!(matches!(a, Some(TocAction::Cancel)));
}
#[test]
fn test_toc_quit() {
let a = map_toc_key(simple_key(KeyCode::Char('q')));
assert!(matches!(a, Some(TocAction::Quit)));
}
#[test]
fn test_toc_unknown_returns_none() {
let a = map_toc_key(simple_key(KeyCode::Tab));
assert!(a.is_none());
}
#[test]
fn test_colon_enters_command() {
let mut acc = InputAccumulator::new();
let a = map_key_event(simple_key(KeyCode::Char(':')), &mut acc);
assert!(matches!(a, Some(Action::EnterCommand)));
}
#[test]
fn test_command_type_char() {
let a = map_command_key(simple_key(KeyCode::Char('r')));
assert!(matches!(a, Some(CommandAction::Type('r'))));
}
#[test]
fn test_command_backspace() {
let a = map_command_key(simple_key(KeyCode::Backspace));
assert!(matches!(a, Some(CommandAction::Backspace)));
}
#[test]
fn test_command_execute() {
let a = map_command_key(simple_key(KeyCode::Enter));
assert!(matches!(a, Some(CommandAction::Execute)));
}
#[test]
fn test_command_cancel_esc() {
let a = map_command_key(simple_key(KeyCode::Esc));
assert!(matches!(a, Some(CommandAction::Cancel)));
}
#[test]
fn test_command_cancel_ctrl_c() {
let a = map_command_key(key(KeyCode::Char('c'), KeyModifiers::CONTROL));
assert!(matches!(a, Some(CommandAction::Cancel)));
}
#[test]
fn log_scroll_down() {
assert_eq!(
map_log_key(simple_key(KeyCode::Char('j'))),
Some(LogAction::ScrollDown)
);
assert_eq!(
map_log_key(simple_key(KeyCode::Down)),
Some(LogAction::ScrollDown)
);
}
#[test]
fn log_scroll_up() {
assert_eq!(
map_log_key(simple_key(KeyCode::Char('k'))),
Some(LogAction::ScrollUp)
);
assert_eq!(
map_log_key(simple_key(KeyCode::Up)),
Some(LogAction::ScrollUp)
);
}
#[test]
fn log_top_bottom() {
assert_eq!(
map_log_key(simple_key(KeyCode::Char('g'))),
Some(LogAction::JumpToTop)
);
assert_eq!(
map_log_key(key(KeyCode::Char('G'), KeyModifiers::SHIFT)),
Some(LogAction::JumpToBottom)
);
}
#[test]
fn log_search() {
assert_eq!(
map_log_key(simple_key(KeyCode::Char('/'))),
Some(LogAction::EnterSearch)
);
}
#[test]
fn log_search_nav() {
assert_eq!(
map_log_key(simple_key(KeyCode::Char('n'))),
Some(LogAction::SearchNext)
);
assert_eq!(
map_log_key(key(KeyCode::Char('N'), KeyModifiers::SHIFT)),
Some(LogAction::SearchPrev)
);
}
#[test]
fn log_yank() {
assert_eq!(
map_log_key(simple_key(KeyCode::Char('y'))),
Some(LogAction::Yank)
);
}
#[test]
fn log_cancel() {
assert_eq!(
map_log_key(simple_key(KeyCode::Char('q'))),
Some(LogAction::Cancel)
);
assert_eq!(
map_log_key(simple_key(KeyCode::Esc)),
Some(LogAction::Cancel)
);
}
#[test]
fn log_type_in_search() {
assert_eq!(
map_log_key(simple_key(KeyCode::Char('a'))),
Some(LogAction::Type('a'))
);
assert_eq!(
map_log_key(simple_key(KeyCode::Backspace)),
Some(LogAction::Backspace)
);
}
}