use crate::core::keymap::{KeyInput, Keymap, UNBIND_ACTION};
use std::str::FromStr;
#[derive(Debug, Clone)]
pub enum KeymapEntry {
Preset(String),
Binding { key: String, action: String },
}
fn expand_preset(name: &str) -> Result<Vec<(&'static str, &'static str)>, String> {
match name {
"nav" => Ok(vec![
("j", "Nav.MoveDown"),
("Down", "Nav.MoveDown"),
("k", "Nav.MoveUp"),
("Up", "Nav.MoveUp"),
("Ctrl+d", "Nav.HalfPageDown"),
("Ctrl+u", "Nav.HalfPageUp"),
("g", "Nav.JumpTop"),
("G", "Nav.JumpBottom"),
]),
"search" => Ok(vec![
("/", "Search.Start"),
("n", "Search.Next"),
("N", "Search.Prev"),
]),
_ => Err(format!("Unknown preset: {name:?}")),
}
}
pub fn build_keymap<A>(entries: &[KeymapEntry]) -> Result<Keymap<A>, String>
where
A: Clone + FromStr<Err = String>,
{
let mut preset_pairs: Vec<(String, String)> = Vec::new();
let mut explicit_pairs: Vec<(String, String)> = Vec::new();
for entry in entries {
match entry {
KeymapEntry::Preset(name) => {
for (key, action) in expand_preset(name)? {
preset_pairs.push((key.to_string(), action.to_string()));
}
}
KeymapEntry::Binding { key, action } => {
explicit_pairs.push((key.clone(), action.clone()));
}
}
}
let mut km = Keymap::new();
for (key_str, action_str) in preset_pairs.iter().chain(explicit_pairs.iter()) {
let ki: KeyInput = key_str
.parse()
.map_err(|e| format!("Invalid key {key_str:?}: {e}"))?;
if action_str == UNBIND_ACTION {
km = km.unbind(ki.code, ki.modifiers);
continue;
}
let action: A = action_str
.parse()
.map_err(|e| format!("Invalid action {action_str:?}: {e}"))?;
km = km.bind(ki.code, ki.modifiers, action);
}
Ok(km)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::keymap::{NavAction, SearchAction};
use crate::git::panes::file_tree::FileTreeAction;
use crossterm::event::KeyEvent;
fn key_event(s: &str) -> KeyEvent {
let ki: KeyInput = s.parse().unwrap();
KeyEvent::new(ki.code, ki.modifiers)
}
#[test]
fn preset_nav_expands() {
let entries = vec![KeymapEntry::Preset("nav".into())];
let km: Keymap<FileTreeAction> = build_keymap(&entries).unwrap();
assert!(matches!(
km.lookup(key_event("j")),
Some(FileTreeAction::Nav(NavAction::MoveDown))
));
assert!(matches!(
km.lookup(key_event("G")),
Some(FileTreeAction::Nav(NavAction::JumpBottom))
));
}
#[test]
fn explicit_overrides_preset() {
let entries = vec![
KeymapEntry::Preset("nav".into()),
KeymapEntry::Binding {
key: "j".into(),
action: "ToggleDir".into(),
},
];
let km: Keymap<FileTreeAction> = build_keymap(&entries).unwrap();
assert!(matches!(
km.lookup(key_event("j")),
Some(FileTreeAction::ToggleDir)
));
}
#[test]
fn preset_search_expands() {
let entries = vec![KeymapEntry::Preset("search".into())];
let km: Keymap<FileTreeAction> = build_keymap(&entries).unwrap();
assert!(matches!(
km.lookup(key_event("/")),
Some(FileTreeAction::Search(SearchAction::Start))
));
assert!(matches!(
km.lookup(key_event("n")),
Some(FileTreeAction::Search(SearchAction::Next))
));
assert!(matches!(
km.lookup(key_event("N")),
Some(FileTreeAction::Search(SearchAction::Prev))
));
}
#[test]
fn none_unbinds_key() {
let entries = vec![
KeymapEntry::Preset("nav".into()),
KeymapEntry::Binding {
key: "j".into(),
action: "None".into(),
},
];
let km: Keymap<FileTreeAction> = build_keymap(&entries).unwrap();
assert!(km.lookup(key_event("j")).is_none());
assert!(km.lookup(key_event("k")).is_some());
assert!(
!km.help_entries().iter().any(|(k, _)| k.contains('j')),
"unbound key must not appear in help"
);
}
#[test]
fn unknown_preset_fails() {
let entries = vec![KeymapEntry::Preset("unknown_preset".into())];
let result: Result<Keymap<FileTreeAction>, _> = build_keymap(&entries);
assert!(result.is_err());
}
#[test]
fn invalid_key_fails() {
let entries = vec![KeymapEntry::Binding {
key: "NotAKey".into(),
action: "ToggleDir".into(),
}];
let result: Result<Keymap<FileTreeAction>, _> = build_keymap(&entries);
assert!(result.is_err());
}
#[test]
fn invalid_action_fails() {
let entries = vec![KeymapEntry::Binding {
key: "j".into(),
action: "NoSuchAction".into(),
}];
let result: Result<Keymap<FileTreeAction>, _> = build_keymap(&entries);
assert!(result.is_err());
}
}