use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyAction {
Up,
Down,
PageUp,
PageDown,
Home,
End,
Left,
Right,
Submit,
Cancel,
Toggle,
Backspace,
Delete,
WordLeft,
WordRight,
DeleteWord,
ClearInput,
Hotkey(char),
Search,
Refresh,
Quit,
Custom(String),
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyHelpMode {
Menu,
MenuWithToggle,
Input,
Info,
Waiting,
Confirm,
Table,
}
#[derive(Debug, Clone)]
pub struct KeyMap {
pub up: Vec<KeyCode>,
pub down: Vec<KeyCode>,
pub page_up: Vec<KeyCode>,
pub page_down: Vec<KeyCode>,
pub home: Vec<KeyCode>,
pub end: Vec<KeyCode>,
pub left: Vec<KeyCode>,
pub right: Vec<KeyCode>,
pub submit: Vec<KeyCode>,
pub cancel: Vec<KeyCode>,
pub toggle: Vec<KeyCode>,
pub delete: Vec<KeyCode>,
pub backspace: Vec<KeyCode>,
pub allow_quit_on_q: bool,
pub custom: Vec<(KeyCode, KeyModifiers, KeyAction)>,
}
impl Default for KeyMap {
fn default() -> Self {
Self {
up: vec![KeyCode::Up, KeyCode::Char('k')],
down: vec![KeyCode::Down, KeyCode::Char('j')],
page_up: vec![KeyCode::PageUp],
page_down: vec![KeyCode::PageDown],
home: vec![KeyCode::Home],
end: vec![KeyCode::End],
left: vec![KeyCode::Left],
right: vec![KeyCode::Right],
submit: vec![KeyCode::Enter],
cancel: vec![KeyCode::Esc],
toggle: vec![KeyCode::Char(' ')],
delete: vec![KeyCode::Delete],
backspace: vec![KeyCode::Backspace],
allow_quit_on_q: true,
custom: Vec::new(),
}
}
}
impl KeyMap {
pub fn menu_default(allow_quit_on_q: bool) -> Self {
Self {
allow_quit_on_q,
right: vec![KeyCode::Right],
left: vec![KeyCode::Left],
..Default::default()
}
}
pub fn with_custom(
mut self,
code: KeyCode,
modifiers: KeyModifiers,
action: KeyAction,
) -> Self {
self.custom.push((code, modifiers, action));
self
}
pub fn resolve(&self, event: &KeyEvent) -> KeyAction {
if event.kind != KeyEventKind::Press {
return KeyAction::None;
}
if (event.modifiers.contains(KeyModifiers::CONTROL)
&& (event.code == KeyCode::Char('c')
|| event.code == KeyCode::Char('C')
|| event.code == KeyCode::Char('d')))
|| event.code == KeyCode::Char('\x03')
|| event.code == KeyCode::Char('\x04')
{
return KeyAction::Quit;
}
for (c, m, act) in &self.custom {
if event.code == *c && event.modifiers == *m {
return act.clone();
}
}
if event.modifiers.contains(KeyModifiers::CONTROL) {
match event.code {
KeyCode::Char('w')
| KeyCode::Char('W')
| KeyCode::Backspace
| KeyCode::Char('\x08')
| KeyCode::Char('\x7f') => return KeyAction::DeleteWord,
KeyCode::Char('u') | KeyCode::Char('U') => return KeyAction::ClearInput,
KeyCode::Left => return KeyAction::WordLeft,
KeyCode::Right => return KeyAction::WordRight,
_ => {}
}
}
if event.modifiers.contains(KeyModifiers::ALT) {
match event.code {
KeyCode::Backspace | KeyCode::Char('\x08') | KeyCode::Char('\x7f') => {
return KeyAction::DeleteWord
}
KeyCode::Char('b') | KeyCode::Char('B') => return KeyAction::WordLeft,
KeyCode::Char('f') | KeyCode::Char('F') => return KeyAction::WordRight,
_ => {}
}
}
if self.allow_quit_on_q
&& event.modifiers.is_empty()
&& (event.code == KeyCode::Char('q') || event.code == KeyCode::Char('Q'))
{
return KeyAction::Quit;
}
if self.up.contains(&event.code) {
return KeyAction::Up;
}
if self.down.contains(&event.code) {
return KeyAction::Down;
}
if self.page_up.contains(&event.code) {
return KeyAction::PageUp;
}
if self.page_down.contains(&event.code) {
return KeyAction::PageDown;
}
if self.home.contains(&event.code) {
return KeyAction::Home;
}
if self.end.contains(&event.code) {
return KeyAction::End;
}
if self.submit.contains(&event.code) {
return KeyAction::Submit;
}
if self.cancel.contains(&event.code) {
return KeyAction::Cancel;
}
if self.toggle.contains(&event.code) {
return KeyAction::Toggle;
}
if self.delete.contains(&event.code) {
return KeyAction::Delete;
}
if self.backspace.contains(&event.code) {
return KeyAction::Backspace;
}
if self.left.contains(&event.code) {
return KeyAction::Left;
}
if self.right.contains(&event.code) {
return KeyAction::Right;
}
if let KeyCode::Char(c) = event.code {
if event.modifiers.is_empty() || event.modifiers == KeyModifiers::SHIFT {
return KeyAction::Hotkey(c);
}
}
KeyAction::None
}
pub fn footer_help_text(&self, mode: KeyHelpMode) -> String {
let q_part = if self.allow_quit_on_q {
" | [q] Exit"
} else {
""
};
match mode {
KeyHelpMode::Menu => {
format!(
"[↑/↓/j/k] Move | [Enter/→] Select | [Esc/←] Back{}",
q_part
)
}
KeyHelpMode::MenuWithToggle => {
format!(
"[↑/↓] Move | [Space] Toggle | [Enter] Confirm | [Esc] Cancel{}",
q_part
)
}
KeyHelpMode::Input => {
"[Enter] Confirm | [Esc] Cancel | [Ctrl+W] Delete Word".to_string()
}
KeyHelpMode::Info => {
format!("[↑/↓/PgUp/PgDn] Scroll | [Enter/Esc] Dismiss{}", q_part)
}
KeyHelpMode::Waiting => "[Esc] Cancel | [Ctrl+C] Abort".to_string(),
KeyHelpMode::Confirm => {
"[←/→/Tab] Select | [Enter/y/n] Confirm | [Esc] Cancel".to_string()
}
KeyHelpMode::Table => {
format!(
"[↑/↓] Select Row | [PgUp/PgDn] Page | [Enter] Open | [Esc] Back{}",
q_part
)
}
}
}
pub fn footer_help_items(&self, mode: KeyHelpMode) -> Vec<String> {
let mut items = match mode {
KeyHelpMode::Menu => vec![
"[↑/↓/j/k] Move".to_string(),
"[Enter/→] Select".to_string(),
"[Esc/←] Back".to_string(),
],
KeyHelpMode::MenuWithToggle => vec![
"[↑/↓] Move".to_string(),
"[Space] Toggle".to_string(),
"[Enter] Confirm".to_string(),
"[Esc] Cancel".to_string(),
],
KeyHelpMode::Input => vec![
"[Enter] Confirm".to_string(),
"[Esc] Cancel".to_string(),
"[Ctrl+W] Delete Word".to_string(),
],
KeyHelpMode::Info => vec![
"[↑/↓/PgUp/PgDn] Scroll".to_string(),
"[Enter/Esc] Dismiss".to_string(),
],
KeyHelpMode::Waiting => vec!["[Esc] Cancel".to_string(), "[Ctrl+C] Abort".to_string()],
KeyHelpMode::Confirm => vec![
"[←/→/Tab] Select".to_string(),
"[Enter/y/n] Confirm".to_string(),
"[Esc] Cancel".to_string(),
],
KeyHelpMode::Table => vec![
"[↑/↓] Select Row".to_string(),
"[PgUp/PgDn] Page".to_string(),
"[Enter] Open".to_string(),
"[Esc] Back".to_string(),
],
};
if self.allow_quit_on_q
&& (mode == KeyHelpMode::Menu
|| mode == KeyHelpMode::MenuWithToggle
|| mode == KeyHelpMode::Info
|| mode == KeyHelpMode::Table)
{
items.push("[q] Exit".to_string());
}
items
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_mapping() {
let km = KeyMap::default();
let up_event = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
assert_eq!(km.resolve(&up_event), KeyAction::Up);
let k_event = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE);
assert_eq!(km.resolve(&k_event), KeyAction::Up);
let enter_event = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
assert_eq!(km.resolve(&enter_event), KeyAction::Submit);
let esc_event = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(km.resolve(&esc_event), KeyAction::Cancel);
let ctrl_c = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
assert_eq!(km.resolve(&ctrl_c), KeyAction::Quit);
let hotkey_1 = KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE);
assert_eq!(km.resolve(&hotkey_1), KeyAction::Hotkey('1'));
let ctrl_w = KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL);
assert_eq!(km.resolve(&ctrl_w), KeyAction::DeleteWord);
}
#[test]
fn test_footer_help_text() {
let km = KeyMap::default();
let menu_help = km.footer_help_text(KeyHelpMode::Menu);
assert!(menu_help.contains("Move"));
assert!(menu_help.contains("Select"));
assert!(menu_help.contains("Back"));
assert!(menu_help.contains("Exit"));
}
}