use std::collections::BTreeMap;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use serde::{Deserialize, Serialize};
const MAX_FUNCTION_KEY: u8 = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyChord {
code: KeyCode,
ctrl: bool,
alt: bool,
shift: bool,
}
impl KeyChord {
#[must_use]
pub fn parse(text: &str) -> Option<KeyChord> {
let parts: Vec<&str> = text
.split('+')
.map(str::trim)
.filter(|part| !part.is_empty())
.collect();
let (code_token, modifiers) = parts.split_last()?;
let mut chord = KeyChord {
code: code_from_token(code_token)?,
ctrl: false,
alt: false,
shift: false,
};
for modifier in modifiers {
match modifier.to_ascii_lowercase().as_str() {
"ctrl" | "control" => chord.ctrl = true,
"alt" | "option" => chord.alt = true,
"shift" => chord.shift = true,
_ => return None,
}
}
Some(chord)
}
#[must_use]
pub fn from_key(key: KeyEvent) -> Self {
KeyChord {
code: key.code,
ctrl: key.modifiers.contains(KeyModifiers::CONTROL),
alt: key.modifiers.contains(KeyModifiers::ALT),
shift: !matches!(key.code, KeyCode::Char(_))
&& key.modifiers.contains(KeyModifiers::SHIFT),
}
}
#[must_use]
pub fn matches(&self, key: &KeyEvent) -> bool {
if self.code != key.code
|| self.ctrl != key.modifiers.contains(KeyModifiers::CONTROL)
|| self.alt != key.modifiers.contains(KeyModifiers::ALT)
{
return false;
}
if matches!(self.code, KeyCode::Char(_)) {
return true;
}
self.shift == key.modifiers.contains(KeyModifiers::SHIFT)
}
#[must_use]
pub fn display(&self) -> String {
let mut text = String::new();
if self.ctrl {
text.push_str("ctrl+");
}
if self.alt {
text.push_str("alt+");
}
if self.shift && !matches!(self.code, KeyCode::Char(_)) {
text.push_str("shift+");
}
text.push_str(&token_for_code(self.code));
text
}
#[must_use]
pub fn code(&self) -> KeyCode {
self.code
}
#[must_use]
pub fn to_key(&self) -> KeyEvent {
let mut modifiers = KeyModifiers::NONE;
if self.ctrl {
modifiers |= KeyModifiers::CONTROL;
}
if self.alt {
modifiers |= KeyModifiers::ALT;
}
if self.shift {
modifiers |= KeyModifiers::SHIFT;
}
KeyEvent::new(self.code, modifiers)
}
}
pub trait Action: Copy + Eq + 'static {
fn all() -> impl Iterator<Item = Self> + Clone;
fn config_name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn default_keys(&self) -> &'static [&'static str];
fn overlaps(&self, other: &Self) -> bool {
let _ = other;
true
}
fn from_config_name(name: &str) -> Option<Self> {
Self::all().find(|action| action.config_name() == name)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum KeyBinding {
One(String),
Many(Vec<String>),
}
impl KeyBinding {
#[must_use]
pub fn into_keys(self) -> Vec<String> {
match self {
KeyBinding::One(key) => vec![key],
KeyBinding::Many(keys) => keys,
}
}
}
#[derive(Debug, Clone)]
pub struct Conflict<A> {
pub key: String,
pub action: A,
pub claimed_by: A,
}
#[derive(Debug, Clone)]
pub struct Keymap<A> {
entries: Vec<(KeyChord, A)>,
conflicts: Vec<Conflict<A>>,
}
impl<A: Action> Default for Keymap<A> {
fn default() -> Self {
Self::from_overrides(&BTreeMap::new())
}
}
impl<A: Action> Keymap<A> {
#[must_use]
pub fn from_overrides(overrides: &BTreeMap<String, Vec<String>>) -> Self {
Self::for_actions(A::all(), overrides)
}
#[must_use]
pub fn for_actions(
actions: impl IntoIterator<Item = A>,
overrides: &BTreeMap<String, Vec<String>>,
) -> Self {
let mut map = Keymap {
entries: Vec::new(),
conflicts: Vec::new(),
};
for action in actions {
for key in override_keys(overrides, action) {
map.bind(action, &key);
}
}
map
}
#[must_use]
pub fn action_for(&self, key: &KeyEvent) -> Option<A> {
self.action_for_where(key, |_| true)
}
#[must_use]
pub fn action_for_where(
&self,
key: &KeyEvent,
allow: impl Fn(&A) -> bool,
) -> Option<A> {
self.entries
.iter()
.find(|(chord, action)| allow(action) && chord.matches(key))
.map(|(_, action)| *action)
}
#[must_use]
pub fn keys_for(&self, action: A) -> Vec<String> {
self.entries
.iter()
.filter(|(_, bound)| *bound == action)
.map(|(chord, _)| chord.display())
.collect()
}
#[must_use]
pub fn conflicts(&self) -> &[Conflict<A>] {
&self.conflicts
}
#[must_use]
pub fn hints(&self, actions: &[A]) -> Vec<(String, String)> {
actions
.iter()
.filter_map(|&action| {
let keys = self.keys_for(action).join("/");
if keys.is_empty() {
return None;
}
Some((keys, action.description().to_string()))
})
.collect()
}
fn bind(&mut self, action: A, key: &str) {
let Some(chord) = KeyChord::parse(key) else {
log::warn!("invalid key '{key}' for '{}'", action.config_name());
return;
};
let owner = self.entries.iter().find(|(existing, owner)| {
*existing == chord && owner.overlaps(&action)
});
if let Some((_, owner)) = owner {
log::warn!(
"key '{}' for '{}' is already bound to '{}', ignoring",
chord.display(),
action.config_name(),
owner.config_name(),
);
self.conflicts.push(Conflict {
key: chord.display(),
action,
claimed_by: *owner,
});
return;
}
self.entries.push((chord, action));
}
}
pub fn warn_unknown<A: Action>(overrides: &BTreeMap<String, Vec<String>>) {
for name in overrides.keys() {
if A::from_config_name(name).is_none() {
log::warn!("unknown key action '{name}' in config, ignoring");
}
}
}
fn override_keys<A: Action>(
overrides: &BTreeMap<String, Vec<String>>,
action: A,
) -> Vec<String> {
overrides
.get(action.config_name())
.cloned()
.unwrap_or_else(|| {
action
.default_keys()
.iter()
.map(|key| (*key).to_string())
.collect()
})
}
fn code_from_token(token: &str) -> Option<KeyCode> {
let lower = token.to_ascii_lowercase();
let code = match lower.as_str() {
"enter" | "return" => KeyCode::Enter,
"esc" | "escape" => KeyCode::Esc,
"tab" => KeyCode::Tab,
"backtab" => KeyCode::BackTab,
"space" => KeyCode::Char(' '),
"backspace" => KeyCode::Backspace,
"insert" | "ins" => KeyCode::Insert,
"up" => KeyCode::Up,
"down" => KeyCode::Down,
"left" => KeyCode::Left,
"right" => KeyCode::Right,
"pgup" | "pageup" => KeyCode::PageUp,
"pgdn" | "pgdown" | "pagedown" => KeyCode::PageDown,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"del" | "delete" => KeyCode::Delete,
_ => return function_or_char(token, &lower),
};
Some(code)
}
fn function_or_char(token: &str, lower: &str) -> Option<KeyCode> {
if let Some(digits) = lower.strip_prefix('f')
&& let Ok(number) = digits.parse::<u8>()
&& (1..=MAX_FUNCTION_KEY).contains(&number)
{
return Some(KeyCode::F(number));
}
let mut chars = token.chars();
let first = chars.next()?;
if chars.next().is_some() {
return None;
}
Some(KeyCode::Char(first))
}
fn token_for_code(code: KeyCode) -> String {
match code {
KeyCode::Char(' ') => "space".to_string(),
KeyCode::Char(ch) => ch.to_string(),
KeyCode::Enter => "enter".to_string(),
KeyCode::Esc => "esc".to_string(),
KeyCode::Tab => "tab".to_string(),
KeyCode::BackTab => "backtab".to_string(),
KeyCode::Backspace => "backspace".to_string(),
KeyCode::Insert => "insert".to_string(),
KeyCode::Up => "up".to_string(),
KeyCode::Down => "down".to_string(),
KeyCode::Left => "left".to_string(),
KeyCode::Right => "right".to_string(),
KeyCode::PageUp => "pgup".to_string(),
KeyCode::PageDown => "pgdn".to_string(),
KeyCode::Home => "home".to_string(),
KeyCode::End => "end".to_string(),
KeyCode::Delete => "del".to_string(),
KeyCode::F(number) => format!("f{number}"),
_ => "?".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Act {
Up,
Down,
Quit,
Extend,
}
impl Action for Act {
fn all() -> impl Iterator<Item = Self> + Clone {
[Act::Up, Act::Down, Act::Quit, Act::Extend].into_iter()
}
fn config_name(&self) -> &'static str {
match self {
Act::Up => "up",
Act::Down => "down",
Act::Quit => "quit",
Act::Extend => "extend",
}
}
fn description(&self) -> &'static str {
"test action"
}
fn default_keys(&self) -> &'static [&'static str] {
match self {
Act::Up => &["up", "k"],
Act::Down => &["down", "j"],
Act::Quit => &["ctrl+q", "q"],
Act::Extend => &["shift+up"],
}
}
}
fn event(code: KeyCode, mods: KeyModifiers) -> KeyEvent {
KeyEvent::new(code, mods)
}
fn chord(text: &str) -> KeyChord {
KeyChord::parse(text).expect("a valid literal chord")
}
#[test]
fn parses_modifiers_keys_and_function_keys() {
assert_eq!(chord("a").display(), "a");
assert_eq!(chord("G").display(), "G");
assert_eq!(chord("ctrl+q").display(), "ctrl+q");
assert_eq!(chord("control+q").display(), "ctrl+q");
assert_eq!(chord("alt+up").display(), "alt+up");
assert_eq!(chord("option+up").display(), "alt+up");
assert_eq!(chord("shift+left").display(), "shift+left");
assert_eq!(chord("f2").display(), "f2");
assert_eq!(chord("pgup").display(), "pgup");
assert_eq!(chord("pageup").display(), "pgup");
assert_eq!(chord("space").display(), "space");
assert_eq!(chord("ctrl+alt+del").display(), "ctrl+alt+del");
}
#[test]
fn rejects_an_unknown_modifier_or_a_word_key() {
assert!(KeyChord::parse("hyper+a").is_none());
assert!(KeyChord::parse("cmd+a").is_none());
assert!(KeyChord::parse("arrows").is_none());
assert!(KeyChord::parse("f13").is_none());
assert!(KeyChord::parse("").is_none());
}
#[test]
fn chords_round_trip_through_parse_and_display() {
for text in [
"a",
"G",
"ctrl+q",
"alt+up",
"shift+left",
"f2",
"pgup",
"pgdn",
"space",
"enter",
"esc",
"tab",
"del",
"home",
"end",
"backspace",
"ctrl+alt+left",
] {
let parsed = chord(text);
assert_eq!(parsed.display(), text, "{text} must round-trip");
assert_eq!(chord(&parsed.display()), parsed);
}
}
#[test]
fn altgr_does_not_trigger_a_ctrl_chord() {
let ctrl_q = chord("ctrl+q");
assert!(
ctrl_q.matches(&event(KeyCode::Char('q'), KeyModifiers::CONTROL))
);
assert!(!ctrl_q.matches(&event(
KeyCode::Char('q'),
KeyModifiers::CONTROL | KeyModifiers::ALT,
)));
}
#[test]
fn a_plain_chord_does_not_match_a_modified_key() {
let plain = chord("q");
assert!(plain.matches(&event(KeyCode::Char('q'), KeyModifiers::NONE)));
assert!(
!plain.matches(&event(KeyCode::Char('q'), KeyModifiers::CONTROL))
);
assert!(!plain.matches(&event(KeyCode::Char('q'), KeyModifiers::ALT)));
}
#[test]
fn shift_distinguishes_non_character_chords() {
let plain = chord("left");
let shifted = chord("shift+left");
assert!(plain.matches(&event(KeyCode::Left, KeyModifiers::NONE)));
assert!(!plain.matches(&event(KeyCode::Left, KeyModifiers::SHIFT)));
assert!(shifted.matches(&event(KeyCode::Left, KeyModifiers::SHIFT)));
assert!(!shifted.matches(&event(KeyCode::Left, KeyModifiers::NONE)));
}
#[test]
fn shift_is_ignored_for_a_character_chord() {
let upper = chord("G");
assert!(upper.matches(&event(KeyCode::Char('G'), KeyModifiers::SHIFT)));
assert!(upper.matches(&event(KeyCode::Char('G'), KeyModifiers::NONE)));
assert!(!upper.matches(&event(KeyCode::Char('g'), KeyModifiers::NONE)));
}
#[test]
fn defaults_resolve_every_action() {
let map: Keymap<Act> = Keymap::from_overrides(&BTreeMap::new());
assert!(map.conflicts().is_empty());
assert_eq!(
map.action_for(&event(KeyCode::Char('k'), KeyModifiers::NONE)),
Some(Act::Up)
);
assert_eq!(
map.action_for(&event(KeyCode::Char('q'), KeyModifiers::CONTROL)),
Some(Act::Quit)
);
assert_eq!(map.keys_for(Act::Up), vec!["up", "k"]);
assert_eq!(
map.action_for(&event(KeyCode::Char('z'), KeyModifiers::NONE)),
None
);
}
#[test]
fn a_shifted_arrow_resolves_to_its_own_action() {
let map: Keymap<Act> = Keymap::from_overrides(&BTreeMap::new());
assert_eq!(
map.action_for(&event(KeyCode::Up, KeyModifiers::NONE)),
Some(Act::Up)
);
assert_eq!(
map.action_for(&event(KeyCode::Up, KeyModifiers::SHIFT)),
Some(Act::Extend)
);
}
#[test]
fn an_override_replaces_the_defaults() {
let mut overrides = BTreeMap::new();
overrides.insert("up".to_string(), vec!["ctrl+p".to_string()]);
let map: Keymap<Act> = Keymap::from_overrides(&overrides);
assert_eq!(
map.action_for(&event(KeyCode::Char('p'), KeyModifiers::CONTROL)),
Some(Act::Up)
);
assert_eq!(
map.action_for(&event(KeyCode::Char('k'), KeyModifiers::NONE)),
None
);
}
#[test]
fn a_contested_key_keeps_the_earlier_action_and_is_reported() {
let mut overrides = BTreeMap::new();
overrides.insert("down".to_string(), vec!["k".to_string()]);
let map: Keymap<Act> = Keymap::from_overrides(&overrides);
assert_eq!(
map.action_for(&event(KeyCode::Char('k'), KeyModifiers::NONE)),
Some(Act::Up)
);
let conflicts = map.conflicts();
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0].key, "k");
assert_eq!(conflicts[0].action, Act::Down);
assert_eq!(conflicts[0].claimed_by, Act::Up);
}
#[test]
fn an_unparseable_override_is_skipped_not_fatal() {
let mut overrides = BTreeMap::new();
overrides.insert(
"quit".to_string(),
vec!["nonsense+x".to_string(), "f4".to_string()],
);
let map: Keymap<Act> = Keymap::from_overrides(&overrides);
assert_eq!(
map.action_for(&event(KeyCode::F(4), KeyModifiers::NONE)),
Some(Act::Quit)
);
}
#[test]
fn action_for_where_narrows_the_lookup() {
let mut overrides = BTreeMap::new();
overrides.insert("up".to_string(), vec!["x".to_string()]);
let map: Keymap<Act> = Keymap::from_overrides(&overrides);
let key = event(KeyCode::Char('x'), KeyModifiers::NONE);
assert_eq!(map.action_for(&key), Some(Act::Up));
assert_eq!(map.action_for_where(&key, |a| *a != Act::Up), None);
}
#[test]
fn for_actions_restricts_the_map_to_a_subset() {
let map: Keymap<Act> =
Keymap::for_actions([Act::Quit], &BTreeMap::new());
assert_eq!(
map.action_for(&event(KeyCode::Char('q'), KeyModifiers::NONE)),
Some(Act::Quit)
);
assert_eq!(
map.action_for(&event(KeyCode::Char('k'), KeyModifiers::NONE)),
None
);
}
#[test]
fn to_key_round_trips_and_always_matches() {
for text in ["a", "G", "ctrl+q", "alt+up", "shift+left", "f2", "space"]
{
let parsed = chord(text);
let key = parsed.to_key();
assert!(parsed.matches(&key), "{text} must match its own key");
assert_eq!(KeyChord::from_key(key), parsed, "{text} round-trips");
}
}
#[test]
fn hints_join_several_keys_and_skip_unbound_actions() {
let mut overrides = BTreeMap::new();
overrides.insert("quit".to_string(), Vec::new());
let map: Keymap<Act> = Keymap::from_overrides(&overrides);
assert_eq!(
map.hints(&[Act::Up, Act::Quit]),
vec![("up/k".to_string(), "test action".to_string())]
);
}
#[test]
fn from_config_name_resolves_every_action() {
for action in Act::all() {
assert_eq!(
Act::from_config_name(action.config_name()),
Some(action)
);
}
assert_eq!(Act::from_config_name("nope"), None);
}
#[test]
fn a_key_binding_reads_both_the_scalar_and_the_list_form() {
assert_eq!(
KeyBinding::One("ctrl+s".to_string()).into_keys(),
vec!["ctrl+s".to_string()]
);
assert_eq!(
KeyBinding::Many(vec!["a".to_string(), "b".to_string()])
.into_keys(),
vec!["a".to_string(), "b".to_string()]
);
}
}