use std::collections::HashMap;
use std::fmt;
use crate::event::{Key, KeyCode};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Chord {
pub code: KeyCode,
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
}
impl Chord {
pub fn new(code: KeyCode) -> Self {
Self {
code,
ctrl: false,
alt: false,
shift: false,
}
}
pub fn from_key(key: Key) -> Self {
let shift = if matches!(key.code, KeyCode::Char(_)) {
false
} else {
key.shift
};
Self {
code: key.code,
ctrl: key.ctrl,
alt: key.alt,
shift,
}
}
pub fn parse(spec: &str) -> Result<Self, KeyParseError> {
let spec = spec.trim();
if spec.is_empty() {
return Err(KeyParseError::new(spec));
}
let mut ctrl = false;
let mut alt = false;
let mut shift = false;
let mut key_token: Option<&str> = None;
let segments = split_plus(spec);
let last = segments.len().saturating_sub(1);
for (index, segment) in segments.iter().enumerate() {
if index == last {
key_token = Some(segment);
break;
}
match segment.to_ascii_lowercase().as_str() {
"ctrl" | "control" => ctrl = true,
"alt" | "option" | "opt" | "meta" => alt = true,
"shift" => shift = true,
_ => return Err(KeyParseError::new(spec)),
}
}
let key_token = key_token.ok_or_else(|| KeyParseError::new(spec))?;
let mut code = parse_key_code(key_token).ok_or_else(|| KeyParseError::new(spec))?;
if matches!(code, KeyCode::Char(_)) {
shift = false;
}
if code == KeyCode::Tab && shift {
code = KeyCode::BackTab;
shift = false;
}
Ok(Self {
code,
ctrl,
alt,
shift,
})
}
pub fn display(&self) -> String {
let mut out = String::new();
if self.ctrl {
out.push_str("Ctrl+");
}
if self.alt {
out.push_str("Alt+");
}
if self.shift {
out.push_str("Shift+");
}
out.push_str(&key_code_label(self.code));
out
}
}
fn split_plus(spec: &str) -> Vec<&str> {
if spec == "+" {
return vec!["+"];
}
if let Some(prefix) = spec.strip_suffix('+') {
let mut parts: Vec<&str> = prefix.split('+').filter(|s| !s.is_empty()).collect();
parts.push("+");
return parts;
}
spec.split('+').collect()
}
fn parse_key_code(token: &str) -> Option<KeyCode> {
let mut chars = token.chars();
if let (Some(c), None) = (chars.next(), chars.next()) {
return Some(KeyCode::Char(c));
}
Some(match token.to_ascii_lowercase().as_str() {
"enter" | "return" | "cr" => KeyCode::Enter,
"esc" | "escape" => KeyCode::Esc,
"tab" => KeyCode::Tab,
"backtab" => KeyCode::BackTab,
"backspace" | "bs" => KeyCode::Backspace,
"delete" | "del" => KeyCode::Delete,
"space" | "spc" => KeyCode::Char(' '),
"up" => KeyCode::Up,
"down" => KeyCode::Down,
"left" => KeyCode::Left,
"right" => KeyCode::Right,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"pageup" | "pgup" => KeyCode::PageUp,
"pagedown" | "pgdn" => KeyCode::PageDown,
_ => return None,
})
}
fn key_code_label(code: KeyCode) -> String {
match code {
KeyCode::Char(' ') => "Space".to_string(),
KeyCode::Char(c) if c.is_ascii_alphabetic() => c.to_ascii_uppercase().to_string(),
KeyCode::Char(c) => c.to_string(),
KeyCode::Enter => "Enter".to_string(),
KeyCode::Esc => "Esc".to_string(),
KeyCode::Backspace => "Backspace".to_string(),
KeyCode::Delete => "Delete".to_string(),
KeyCode::Tab => "Tab".to_string(),
KeyCode::BackTab => "Shift+Tab".to_string(),
KeyCode::Up => "Up".to_string(),
KeyCode::Down => "Down".to_string(),
KeyCode::Left => "Left".to_string(),
KeyCode::Right => "Right".to_string(),
KeyCode::Home => "Home".to_string(),
KeyCode::End => "End".to_string(),
KeyCode::PageUp => "PageUp".to_string(),
KeyCode::PageDown => "PageDown".to_string(),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct KeyParseError {
spec: String,
}
impl KeyParseError {
fn new(spec: &str) -> Self {
Self {
spec: spec.to_string(),
}
}
}
impl fmt::Display for KeyParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid key spec: {:?}", self.spec)
}
}
impl std::error::Error for KeyParseError {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct KeySequence(Vec<Chord>);
impl KeySequence {
pub fn parse(spec: &str) -> Result<Self, KeyParseError> {
let chords = spec
.split_whitespace()
.map(Chord::parse)
.collect::<Result<Vec<_>, _>>()?;
if chords.is_empty() {
return Err(KeyParseError::new(spec));
}
Ok(Self(chords))
}
pub fn chords(&self) -> &[Chord] {
&self.0
}
pub fn display(&self) -> String {
self.0
.iter()
.map(Chord::display)
.collect::<Vec<_>>()
.join(" ")
}
}
#[derive(Clone, Debug)]
pub struct Binding<C> {
sequence: KeySequence,
command: C,
label: Option<String>,
}
#[derive(Clone, Debug)]
pub struct Layer<C> {
name: String,
priority: i32,
when: Vec<(String, String)>,
bindings: Vec<Binding<C>>,
}
impl<C> Layer<C> {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
priority: 0,
when: Vec::new(),
bindings: Vec::new(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
pub fn when(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.when.push((key.into(), value.into()));
self
}
pub fn bind(self, keys: &str, command: C) -> Self {
self.try_bind(keys, command)
.unwrap_or_else(|err| panic!("{err}"))
}
pub fn bind_labeled(self, keys: &str, label: impl Into<String>, command: C) -> Self {
self.try_bind_labeled(keys, Some(label.into()), command)
.unwrap_or_else(|err| panic!("{err}"))
}
pub fn try_bind(self, keys: &str, command: C) -> Result<Self, KeyParseError> {
self.try_bind_labeled(keys, None, command)
}
fn try_bind_labeled(
mut self,
keys: &str,
label: Option<String>,
command: C,
) -> Result<Self, KeyParseError> {
let sequence = KeySequence::parse(keys)?;
self.bindings.push(Binding {
sequence,
command,
label,
});
Ok(self)
}
fn is_active(&self, data: &HashMap<String, String>) -> bool {
self.when
.iter()
.all(|(key, value)| data.get(key).map(String::as_str) == Some(value.as_str()))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Dispatch<C> {
Command(C),
Pending,
Unmatched,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Hint<C> {
pub keys: String,
pub label: Option<String>,
pub command: C,
}
#[derive(Clone, Debug, Default)]
pub struct Keymap<C> {
layers: Vec<Layer<C>>,
data: HashMap<String, String>,
pending: Vec<Chord>,
}
impl<C: Clone> Keymap<C> {
pub fn new() -> Self {
Self {
layers: Vec::new(),
data: HashMap::new(),
pending: Vec::new(),
}
}
pub fn layer(mut self, layer: Layer<C>) -> Self {
self.layers.push(layer);
self
}
pub fn add_layer(&mut self, layer: Layer<C>) {
self.layers.push(layer);
}
pub fn set_data(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.data.insert(key.into(), value.into());
self.pending.clear();
}
pub fn data(&self, key: &str) -> Option<&str> {
self.data.get(key).map(String::as_str)
}
pub fn pending(&self) -> &[Chord] {
&self.pending
}
pub fn reset(&mut self) {
self.pending.clear();
}
pub fn dispatch(&mut self, key: Key) -> Dispatch<C> {
let chord = Chord::from_key(key);
self.pending.push(chord);
let (exact, prefix) = self.resolve(&self.pending);
if let Some(command) = exact {
self.pending.clear();
return Dispatch::Command(command);
}
if prefix {
return Dispatch::Pending;
}
self.pending.clear();
let single = [chord];
let (exact, prefix) = self.resolve(&single);
if let Some(command) = exact {
return Dispatch::Command(command);
}
if prefix {
self.pending.push(chord);
return Dispatch::Pending;
}
Dispatch::Unmatched
}
fn resolve(&self, candidate: &[Chord]) -> (Option<C>, bool) {
let mut best: Option<(i32, &C)> = None;
let mut prefix = false;
for layer in &self.layers {
if !layer.is_active(&self.data) {
continue;
}
for binding in &layer.bindings {
let seq = binding.sequence.chords();
if seq == candidate {
if best.is_none_or(|(p, _)| layer.priority > p) {
best = Some((layer.priority, &binding.command));
}
} else if seq.len() > candidate.len() && seq.starts_with(candidate) {
prefix = true;
}
}
}
(best.map(|(_, command)| command.clone()), prefix)
}
pub fn hints(&self) -> Vec<Hint<C>> {
let mut layers: Vec<&Layer<C>> = self
.layers
.iter()
.filter(|l| l.is_active(&self.data))
.collect();
layers.sort_by(|a, b| b.priority.cmp(&a.priority));
layers
.iter()
.flat_map(|layer| layer.bindings.iter())
.map(|binding| Hint {
keys: binding.sequence.display(),
label: binding.label.clone(),
command: binding.command.clone(),
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{Key, KeyCode};
#[derive(Clone, Debug, PartialEq, Eq)]
enum Action {
Search,
Quit,
Top,
Bottom,
Close,
}
fn key(code: KeyCode) -> Key {
Key::new(code)
}
fn ctrl(c: char) -> Key {
Key {
code: KeyCode::Char(c),
ctrl: true,
alt: false,
shift: false,
}
}
#[test]
fn parses_modifier_chords() {
assert_eq!(
Chord::parse("ctrl+r").unwrap(),
Chord {
code: KeyCode::Char('r'),
ctrl: true,
alt: false,
shift: false,
}
);
assert_eq!(
Chord::parse("alt+shift+tab").unwrap(),
Chord {
code: KeyCode::BackTab,
ctrl: false,
alt: true,
shift: false,
}
);
}
#[test]
fn parses_named_and_literal_keys() {
assert_eq!(Chord::parse("enter").unwrap().code, KeyCode::Enter);
assert_eq!(Chord::parse("space").unwrap().code, KeyCode::Char(' '));
assert_eq!(Chord::parse("?").unwrap().code, KeyCode::Char('?'));
assert_eq!(
Chord::parse("ctrl++").unwrap(),
Chord {
code: KeyCode::Char('+'),
ctrl: true,
alt: false,
shift: false,
}
);
}
#[test]
fn char_chords_ignore_shift() {
assert!(!Chord::parse("shift+?").unwrap().shift);
let from = Chord::from_key(Key {
code: KeyCode::Char('?'),
ctrl: false,
alt: false,
shift: true,
});
assert!(!from.shift);
}
#[test]
fn rejects_garbage_specs() {
assert!(Chord::parse("").is_err());
assert!(Chord::parse("bogus+r").is_err());
assert!(KeySequence::parse(" ").is_err());
}
#[test]
fn display_is_human_readable() {
assert_eq!(Chord::parse("ctrl+r").unwrap().display(), "Ctrl+R");
assert_eq!(Chord::parse("space").unwrap().display(), "Space");
assert_eq!(Chord::parse("enter").unwrap().display(), "Enter");
assert_eq!(
KeySequence::parse("ctrl+x s").unwrap().display(),
"Ctrl+X S"
);
}
#[test]
fn dispatches_single_chords() {
let mut keymap = Keymap::new().layer(
Layer::new("global")
.bind("ctrl+r", Action::Search)
.bind("ctrl+d", Action::Quit),
);
assert_eq!(
keymap.dispatch(ctrl('r')),
Dispatch::Command(Action::Search)
);
assert_eq!(keymap.dispatch(ctrl('d')), Dispatch::Command(Action::Quit));
assert_eq!(keymap.dispatch(ctrl('z')), Dispatch::Unmatched);
}
#[test]
fn resolves_multi_stroke_sequences() {
let mut keymap = Keymap::new().layer(
Layer::new("nav")
.bind("g g", Action::Top)
.bind("g e", Action::Bottom),
);
assert_eq!(keymap.dispatch(key(KeyCode::Char('g'))), Dispatch::Pending);
assert_eq!(keymap.pending().len(), 1);
assert_eq!(
keymap.dispatch(key(KeyCode::Char('g'))),
Dispatch::Command(Action::Top)
);
assert!(keymap.pending().is_empty());
assert_eq!(keymap.dispatch(key(KeyCode::Char('g'))), Dispatch::Pending);
assert_eq!(
keymap.dispatch(key(KeyCode::Char('e'))),
Dispatch::Command(Action::Bottom)
);
}
#[test]
fn dead_end_sequence_retries_final_stroke() {
let mut keymap = Keymap::new().layer(
Layer::new("nav")
.bind("g g", Action::Top)
.bind("ctrl+r", Action::Search),
);
assert_eq!(keymap.dispatch(key(KeyCode::Char('g'))), Dispatch::Pending);
assert_eq!(
keymap.dispatch(ctrl('r')),
Dispatch::Command(Action::Search)
);
assert!(keymap.pending().is_empty());
}
#[test]
fn exact_match_wins_over_prefix() {
let mut keymap = Keymap::new().layer(
Layer::new("nav")
.bind("g", Action::Bottom)
.bind("g g", Action::Top),
);
assert_eq!(
keymap.dispatch(key(KeyCode::Char('g'))),
Dispatch::Command(Action::Bottom)
);
}
#[test]
fn mode_gated_layers_activate_on_data() {
let mut keymap = Keymap::new()
.layer(Layer::new("global").bind("ctrl+d", Action::Quit))
.layer(
Layer::new("panel")
.when("mode", "panel")
.bind("q", Action::Close),
);
assert_eq!(
keymap.dispatch(key(KeyCode::Char('q'))),
Dispatch::Unmatched
);
keymap.set_data("mode", "panel");
assert_eq!(
keymap.dispatch(key(KeyCode::Char('q'))),
Dispatch::Command(Action::Close)
);
assert_eq!(keymap.dispatch(ctrl('d')), Dispatch::Command(Action::Quit));
}
#[test]
fn higher_priority_layer_wins_conflicts() {
let mut keymap = Keymap::new()
.layer(Layer::new("base").priority(0).bind("x", Action::Quit))
.layer(Layer::new("over").priority(10).bind("x", Action::Close));
assert_eq!(
keymap.dispatch(key(KeyCode::Char('x'))),
Dispatch::Command(Action::Close)
);
}
#[test]
fn changing_mode_clears_pending() {
let mut keymap = Keymap::new().layer(Layer::new("nav").bind("g g", Action::Top));
assert_eq!(keymap.dispatch(key(KeyCode::Char('g'))), Dispatch::Pending);
keymap.set_data("mode", "other");
assert!(keymap.pending().is_empty());
}
#[test]
fn hints_list_active_bindings_by_priority() {
let keymap = Keymap::new()
.layer(Layer::new("global").priority(0).bind_labeled(
"ctrl+r",
"search",
Action::Search,
))
.layer(
Layer::new("panel")
.priority(10)
.when("mode", "panel")
.bind_labeled("q", "close", Action::Close),
);
let hints = keymap.hints();
assert_eq!(hints.len(), 1);
assert_eq!(hints[0].keys, "Ctrl+R");
assert_eq!(hints[0].label.as_deref(), Some("search"));
assert_eq!(hints[0].command, Action::Search);
let mut keymap = keymap;
keymap.set_data("mode", "panel");
let hints = keymap.hints();
assert_eq!(hints.len(), 2);
assert_eq!(hints[0].command, Action::Close);
assert_eq!(hints[1].command, Action::Search);
}
}