mod action;
mod defaults;
pub use action::Action;
use crossterm::event::KeyEvent;
use keybinds::Keybinds;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, str::FromStr};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum KeybindingMode {
Normal,
Help,
ThemePicker,
Interactive,
InteractiveTable,
LinkFollow,
LinkSearch,
Search,
DocSearch,
CommandPalette,
CellEdit,
ConfirmDialog,
FilePicker,
FileSearch,
}
impl KeybindingMode {
pub fn display_name(&self) -> &'static str {
match self {
KeybindingMode::Normal => "Normal",
KeybindingMode::Help => "Help",
KeybindingMode::ThemePicker => "Theme Picker",
KeybindingMode::Interactive => "Interactive",
KeybindingMode::InteractiveTable => "Table Navigation",
KeybindingMode::LinkFollow => "Link Follow",
KeybindingMode::LinkSearch => "Link Search",
KeybindingMode::Search => "Search",
KeybindingMode::DocSearch => "Doc Search",
KeybindingMode::CommandPalette => "Command Palette",
KeybindingMode::CellEdit => "Cell Edit",
KeybindingMode::ConfirmDialog => "Confirm",
KeybindingMode::FilePicker => "File Picker",
KeybindingMode::FileSearch => "File Search",
}
}
}
#[derive(Debug)]
pub struct Keybindings {
bindings: HashMap<KeybindingMode, Keybinds<Action>>,
}
impl Default for Keybindings {
fn default() -> Self {
defaults::default_keybindings()
}
}
impl Clone for Keybindings {
fn clone(&self) -> Self {
Self {
bindings: self.bindings.clone(),
}
}
}
impl Keybindings {
pub fn new() -> Self {
Self {
bindings: HashMap::new(),
}
}
fn from_config(config: &KeybindingsConfig) -> Result<Self, keybinds::Error> {
let mut def = Keybindings::default();
for (mode, bindings) in &mut def.bindings {
let mut binding_vec = std::mem::take(bindings).into_vec();
if let Some(config_bindings) = config.0.get(mode) {
for (config_key, config_action) in config_bindings {
let config_seq = keybinds::KeySeq::from_str(config_key)?;
if let Some(existing) = binding_vec.iter_mut().find(|b| b.seq == config_seq) {
existing.action = *config_action;
} else if *config_action != Action::Noop {
binding_vec.push(keybinds::Keybind::new(config_seq, *config_action));
}
}
binding_vec.retain(|b| b.action != Action::Noop);
}
*bindings = keybinds::Keybinds::new(binding_vec);
}
Ok(def)
}
pub fn dispatch(&mut self, mode: KeybindingMode, event: KeyEvent) -> Option<Action> {
self.bindings
.get_mut(&mode)
.and_then(|kb| kb.dispatch(event).copied())
}
pub fn is_sequence_ongoing(&self, mode: KeybindingMode) -> bool {
self.bindings
.get(&mode)
.map(|kb| kb.is_ongoing())
.unwrap_or(false)
}
pub fn reset_sequences(&mut self) {
for kb in self.bindings.values_mut() {
kb.reset();
}
}
pub fn get_mode_keybinds(&self, mode: KeybindingMode) -> Option<&Keybinds<Action>> {
self.bindings.get(&mode)
}
pub fn bind(
&mut self,
mode: KeybindingMode,
key_sequence: &str,
action: Action,
) -> Result<(), keybinds::Error> {
self.bindings
.entry(mode)
.or_default()
.bind(key_sequence, action)
}
pub fn keys_for_action(&self, mode: KeybindingMode, action: Action) -> Vec<String> {
self.bindings
.get(&mode)
.map(|kb| {
kb.as_slice()
.iter()
.filter(|bind| bind.action == action)
.map(|bind| format_key_sequence(&bind.seq))
.collect()
})
.unwrap_or_default()
}
pub fn help_entries(&self, mode: KeybindingMode) -> Vec<(Action, Vec<String>)> {
let mut action_keys: HashMap<Action, Vec<String>> = HashMap::new();
if let Some(kb) = self.bindings.get(&mode) {
for bind in kb.as_slice().iter().filter(|b| b.action != Action::Noop) {
let key_str = format_key_sequence(&bind.seq);
action_keys.entry(bind.action).or_default().push(key_str);
}
}
let mut entries: Vec<_> = action_keys.into_iter().collect();
entries.sort_by(|a, b| {
a.0.category()
.cmp(b.0.category())
.then(a.0.description().cmp(b.0.description()))
});
entries
}
}
fn format_key_sequence(seq: &keybinds::KeySeq) -> String {
seq.as_slice()
.iter()
.map(format_key_input)
.collect::<Vec<_>>()
.join(" ")
}
fn format_key_input(input: &keybinds::KeyInput) -> String {
let mut parts = Vec::new();
let mods = input.mods();
if mods.contains(keybinds::Mods::CTRL) {
parts.push("C");
}
if mods.contains(keybinds::Mods::ALT) {
parts.push("A");
}
if mods.contains(keybinds::Mods::SHIFT) {
parts.push("S");
}
let key_str = format_key(input.key());
parts.push(&key_str);
if parts.len() == 1 {
key_str
} else {
parts.join("-")
}
}
fn format_key(key: keybinds::Key) -> String {
use keybinds::Key;
match key {
Key::Char(' ') => "Spc".to_string(),
Key::Char(c) => c.to_string(),
Key::Enter => "Ret".to_string(),
Key::Esc => "Esc".to_string(),
Key::Tab => "Tab".to_string(),
Key::Backspace => "BS".to_string(),
Key::Delete => "Del".to_string(),
Key::Up => "↑".to_string(),
Key::Down => "↓".to_string(),
Key::Left => "←".to_string(),
Key::Right => "→".to_string(),
Key::PageUp => "PgU".to_string(),
Key::PageDown => "PgD".to_string(),
Key::Home => "Home".to_string(),
Key::End => "End".to_string(),
Key::F1 => "F1".to_string(),
Key::F2 => "F2".to_string(),
Key::F3 => "F3".to_string(),
Key::F4 => "F4".to_string(),
Key::F5 => "F5".to_string(),
Key::F6 => "F6".to_string(),
Key::F7 => "F7".to_string(),
Key::F8 => "F8".to_string(),
Key::F9 => "F9".to_string(),
Key::F10 => "F10".to_string(),
Key::F11 => "F11".to_string(),
Key::F12 => "F12".to_string(),
_ => "?".to_string(),
}
}
pub fn format_key_compact(key: &str) -> String {
key.to_string()
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct KeybindingsConfig(pub HashMap<KeybindingMode, HashMap<String, Action>>);
impl KeybindingsConfig {
pub fn to_keybindings(&self) -> Keybindings {
Keybindings::from_config(self).unwrap_or_default()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyEventKind, KeyEventState, KeyModifiers};
fn make_key_event(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent {
code,
modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}
}
#[test]
fn test_default_keybindings_exist() {
let mut kb = Keybindings::default();
assert!(
kb.dispatch(
KeybindingMode::Normal,
make_key_event(KeyCode::Char('j'), KeyModifiers::NONE)
)
.is_some()
);
assert!(
kb.dispatch(
KeybindingMode::Normal,
make_key_event(KeyCode::Char('k'), KeyModifiers::NONE)
)
.is_some()
);
assert!(
kb.dispatch(
KeybindingMode::Normal,
make_key_event(KeyCode::Char('q'), KeyModifiers::NONE)
)
.is_some()
);
}
#[test]
fn test_dispatch() {
let mut kb = Keybindings::default();
let action = kb.dispatch(
KeybindingMode::Normal,
make_key_event(KeyCode::Char('j'), KeyModifiers::NONE),
);
assert_eq!(action, Some(Action::Next));
let action = kb.dispatch(
KeybindingMode::Normal,
make_key_event(KeyCode::Char('x'), KeyModifiers::NONE),
);
assert!(action.is_none() || action == Some(Action::Next)); }
#[test]
fn test_keys_for_action() {
let kb = Keybindings::default();
let keys = kb.keys_for_action(KeybindingMode::Normal, Action::Next);
assert!(!keys.is_empty()); }
#[test]
fn test_user_config_overrides_defaults() {
let mut config_map = HashMap::new();
let mut normal_bindings = HashMap::new();
normal_bindings.insert("j".to_string(), Action::Last);
config_map.insert(KeybindingMode::Normal, normal_bindings);
let config = KeybindingsConfig(config_map);
let mut kb = config.to_keybindings();
let action = kb.dispatch(
KeybindingMode::Normal,
make_key_event(KeyCode::Char('j'), KeyModifiers::NONE),
);
assert_eq!(
action,
Some(Action::Last),
"User binding must override default"
);
}
#[test]
fn test_noop_unbinds_key() {
let mut config_map = HashMap::new();
let mut normal_bindings = HashMap::new();
normal_bindings.insert("j".to_string(), Action::Noop);
config_map.insert(KeybindingMode::Normal, normal_bindings);
let config = KeybindingsConfig(config_map);
let mut kb = config.to_keybindings();
let action = kb.dispatch(
KeybindingMode::Normal,
make_key_event(KeyCode::Char('j'), KeyModifiers::NONE),
);
assert_eq!(
action, None,
"Noop binding should effectively unbind the key"
);
}
#[test]
fn test_clone_preserves_user_config() {
let mut config_map = HashMap::new();
let mut normal_bindings = HashMap::new();
normal_bindings.insert("j".to_string(), Action::Last);
config_map.insert(KeybindingMode::Normal, normal_bindings);
let config = KeybindingsConfig(config_map);
let kb = config.to_keybindings();
let mut cloned = kb.clone();
let action = cloned.dispatch(
KeybindingMode::Normal,
make_key_event(KeyCode::Char('j'), KeyModifiers::NONE),
);
assert_eq!(
action,
Some(Action::Last),
"Clone must preserve user bindings"
);
}
#[test]
fn test_noop_filtered_from_help_entries() {
let mut config_map = HashMap::new();
let mut normal_bindings = HashMap::new();
normal_bindings.insert("j".to_string(), Action::Noop);
config_map.insert(KeybindingMode::Normal, normal_bindings);
let config = KeybindingsConfig(config_map);
let kb = config.to_keybindings();
let entries = kb.help_entries(KeybindingMode::Normal);
assert!(
!entries.iter().any(|(action, _)| *action == Action::Noop),
"Noop should not appear in help entries"
);
}
#[test]
fn test_all_modes_have_bindings() {
let kb = Keybindings::default();
let modes = [
KeybindingMode::Normal,
KeybindingMode::Help,
KeybindingMode::ThemePicker,
KeybindingMode::Interactive,
KeybindingMode::InteractiveTable,
KeybindingMode::LinkFollow,
KeybindingMode::LinkSearch,
KeybindingMode::Search,
KeybindingMode::DocSearch,
KeybindingMode::CommandPalette,
KeybindingMode::ConfirmDialog,
KeybindingMode::CellEdit,
];
for mode in modes {
assert!(
kb.get_mode_keybinds(mode).is_some(),
"Mode {:?} has no bindings",
mode
);
assert!(
!kb.get_mode_keybinds(mode).unwrap().as_slice().is_empty(),
"Mode {:?} has empty bindings",
mode
);
}
}
}