use std::collections::HashMap;
use ftui_core::event::{KeyCode, Modifiers, MouseEventKind};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InputEvent {
Key(KeyCode, Modifiers),
Mouse(MouseEventKind, u16, u16),
Resize(u16, u16),
Action(KeyAction),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KeyAction {
Quit,
TogglePalette,
NextScreen,
PrevScreen,
ToggleHelp,
CycleTheme,
Dismiss,
Up,
Down,
Left,
Right,
PageUp,
PageDown,
Home,
End,
Confirm,
Delete,
Copy,
Custom(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct KeyBinding {
pub key: String,
pub modifiers: Vec<String>,
pub action: KeyAction,
}
pub struct Keymap {
bindings: HashMap<(KeyCode, Modifiers), KeyAction>,
}
impl Keymap {
#[must_use]
pub fn default_bindings() -> Self {
let mut bindings = HashMap::new();
bindings.insert((KeyCode::Char('q'), Modifiers::NONE), KeyAction::Quit);
bindings.insert((KeyCode::Char('c'), Modifiers::CTRL), KeyAction::Quit);
bindings.insert(
(KeyCode::Char('p'), Modifiers::CTRL),
KeyAction::TogglePalette,
);
bindings.insert(
(KeyCode::Char(':'), Modifiers::NONE),
KeyAction::TogglePalette,
);
bindings.insert((KeyCode::Tab, Modifiers::NONE), KeyAction::NextScreen);
bindings.insert((KeyCode::BackTab, Modifiers::SHIFT), KeyAction::PrevScreen);
bindings.insert((KeyCode::Char('?'), Modifiers::NONE), KeyAction::ToggleHelp);
bindings.insert((KeyCode::F(1), Modifiers::NONE), KeyAction::ToggleHelp);
bindings.insert((KeyCode::Escape, Modifiers::NONE), KeyAction::Dismiss);
bindings.insert((KeyCode::Up, Modifiers::NONE), KeyAction::Up);
bindings.insert((KeyCode::Down, Modifiers::NONE), KeyAction::Down);
bindings.insert((KeyCode::Left, Modifiers::NONE), KeyAction::Left);
bindings.insert((KeyCode::Right, Modifiers::NONE), KeyAction::Right);
bindings.insert((KeyCode::Char('k'), Modifiers::NONE), KeyAction::Up);
bindings.insert((KeyCode::Char('j'), Modifiers::NONE), KeyAction::Down);
bindings.insert((KeyCode::Char('h'), Modifiers::NONE), KeyAction::Left);
bindings.insert((KeyCode::Char('l'), Modifiers::NONE), KeyAction::Right);
bindings.insert((KeyCode::PageUp, Modifiers::NONE), KeyAction::PageUp);
bindings.insert((KeyCode::PageDown, Modifiers::NONE), KeyAction::PageDown);
bindings.insert((KeyCode::Home, Modifiers::NONE), KeyAction::Home);
bindings.insert((KeyCode::End, Modifiers::NONE), KeyAction::End);
bindings.insert((KeyCode::Char('t'), Modifiers::CTRL), KeyAction::CycleTheme);
bindings.insert((KeyCode::Enter, Modifiers::NONE), KeyAction::Confirm);
bindings.insert((KeyCode::Backspace, Modifiers::NONE), KeyAction::Delete);
bindings.insert((KeyCode::Char('y'), Modifiers::CTRL), KeyAction::Copy);
Self { bindings }
}
#[must_use]
pub fn resolve(&self, key: KeyCode, modifiers: Modifiers) -> Option<&KeyAction> {
self.bindings.get(&(key, modifiers))
}
pub fn bind(&mut self, key: KeyCode, modifiers: Modifiers, action: KeyAction) {
self.bindings.insert((key, modifiers), action);
}
pub fn unbind(&mut self, key: KeyCode, modifiers: Modifiers) {
self.bindings.remove(&(key, modifiers));
}
#[must_use]
pub fn len(&self) -> usize {
self.bindings.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.bindings.is_empty()
}
}
impl Default for Keymap {
fn default() -> Self {
Self::default_bindings()
}
}
#[cfg(test)]
mod tests {
use ftui_core::event::{KeyCode, Modifiers};
use super::*;
#[test]
fn default_keymap_has_bindings() {
let keymap = Keymap::default_bindings();
assert!(!keymap.is_empty());
assert!(keymap.len() > 15);
}
#[test]
fn resolve_quit_q() {
let keymap = Keymap::default_bindings();
let action = keymap.resolve(KeyCode::Char('q'), Modifiers::NONE);
assert_eq!(action, Some(&KeyAction::Quit));
}
#[test]
fn resolve_quit_ctrl_c() {
let keymap = Keymap::default_bindings();
let action = keymap.resolve(KeyCode::Char('c'), Modifiers::CTRL);
assert_eq!(action, Some(&KeyAction::Quit));
}
#[test]
fn resolve_palette_ctrl_p() {
let keymap = Keymap::default_bindings();
let action = keymap.resolve(KeyCode::Char('p'), Modifiers::CTRL);
assert_eq!(action, Some(&KeyAction::TogglePalette));
}
#[test]
fn resolve_vim_movement() {
let keymap = Keymap::default_bindings();
assert_eq!(
keymap.resolve(KeyCode::Char('j'), Modifiers::NONE),
Some(&KeyAction::Down)
);
assert_eq!(
keymap.resolve(KeyCode::Char('k'), Modifiers::NONE),
Some(&KeyAction::Up)
);
}
#[test]
fn resolve_unknown_returns_none() {
let keymap = Keymap::default_bindings();
assert!(
keymap
.resolve(KeyCode::Char('z'), Modifiers::NONE)
.is_none()
);
}
#[test]
fn custom_binding() {
let mut keymap = Keymap::default_bindings();
keymap.bind(
KeyCode::Char('s'),
Modifiers::CTRL,
KeyAction::Custom("save".to_string()),
);
let action = keymap.resolve(KeyCode::Char('s'), Modifiers::CTRL);
assert_eq!(action, Some(&KeyAction::Custom("save".to_string())));
}
#[test]
fn unbind_removes_binding() {
let mut keymap = Keymap::default_bindings();
assert!(
keymap
.resolve(KeyCode::Char('q'), Modifiers::NONE)
.is_some()
);
keymap.unbind(KeyCode::Char('q'), Modifiers::NONE);
assert!(
keymap
.resolve(KeyCode::Char('q'), Modifiers::NONE)
.is_none()
);
}
#[test]
fn key_action_serde_roundtrip() {
for action in [
KeyAction::Quit,
KeyAction::TogglePalette,
KeyAction::CycleTheme,
KeyAction::Up,
KeyAction::Custom("test".to_string()),
] {
let json = serde_json::to_string(&action).unwrap();
let decoded: KeyAction = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, action);
}
}
}