use std::fmt;
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use serde::{
de::{self, Visitor},
Deserialize, Deserializer,
};
use crate::{command::Command, config::ConfigError};
#[derive(Clone, Debug, PartialEq, Deserialize)]
pub(crate) struct KeyBinding {
pub key: Key,
pub command: Command,
}
impl From<(Key, Command)> for KeyBinding {
fn from((key, command): (Key, Command)) -> Self {
Self::new(key, command)
}
}
impl KeyBinding {
pub const fn new(key: Key, command: Command) -> Self {
Self { key, command }
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Keystroke {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
impl Keystroke {
pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
Self { code, modifiers }
}
}
impl From<KeyEvent> for Keystroke {
fn from(value: KeyEvent) -> Self {
Self::from((value.code, value.modifiers))
}
}
impl From<KeyCode> for Keystroke {
fn from(code: KeyCode) -> Self {
Keystroke::from((code, KeyModifiers::NONE))
}
}
impl From<(KeyCode, KeyModifiers)> for Keystroke {
fn from((code, mut modifiers): (KeyCode, KeyModifiers)) -> Self {
let code = match code {
KeyCode::Char(ch) if ch.is_uppercase() => {
modifiers.insert(KeyModifiers::SHIFT);
code
}
KeyCode::Char(ch)
if modifiers.contains(KeyModifiers::SHIFT) && ch.is_ascii_lowercase() =>
{
KeyCode::Char(ch.to_ascii_uppercase())
}
_ => code,
};
Self { code, modifiers }
}
}
impl From<(char, KeyModifiers)> for Keystroke {
fn from((c, modifiers): (char, KeyModifiers)) -> Self {
Keystroke::from((KeyCode::Char(c), modifiers))
}
}
impl From<&KeyEvent> for Keystroke {
fn from(event: &KeyEvent) -> Self {
Self::from((event.code, event.modifiers))
}
}
impl fmt::Display for Keystroke {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let code = self.code.to_string().replace(" ", "_");
let modifiers = match self.code {
KeyCode::Char(ch) if ch.is_uppercase() => self.modifiers - KeyModifiers::SHIFT,
_ => self.modifiers,
};
if modifiers.is_empty() {
write!(f, "{code}")
} else {
write!(f, "{}-{code}", modifiers.to_string().to_ascii_lowercase())
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Key {
Single(Keystroke),
Chord(Vec<Keystroke>),
}
impl fmt::Display for Key {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Key::Single(key) => key.fmt(f),
Key::Chord(keys) => keys.iter().try_for_each(|key| key.fmt(f)),
}
}
}
impl Key {
pub const CTRL_C: Key = Key::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
Key::Single(Keystroke::new(code, modifiers))
}
pub fn chord(iter: impl IntoIterator<Item = Keystroke>) -> Self {
Key::Chord(iter.into_iter().collect())
}
fn into_keystrokes(self) -> Vec<Keystroke> {
match self {
Key::Single(keystroke) => vec![keystroke],
Key::Chord(keystrokes) => keystrokes,
}
}
}
impl From<KeyEvent> for Key {
fn from(value: KeyEvent) -> Self {
Self::Single(Keystroke::from(value))
}
}
impl From<KeyCode> for Key {
fn from(value: KeyCode) -> Self {
Self::Single(Keystroke::from(value))
}
}
impl From<(KeyCode, KeyModifiers)> for Key {
fn from(value: (KeyCode, KeyModifiers)) -> Self {
Self::Single(Keystroke::from(value))
}
}
impl From<char> for Key {
fn from(value: char) -> Self {
Self::from(KeyCode::Char(value))
}
}
impl From<(char, KeyModifiers)> for Key {
fn from(value: (char, KeyModifiers)) -> Self {
Self::Single(Keystroke::from(value))
}
}
impl From<Keystroke> for Key {
fn from(value: Keystroke) -> Self {
Self::Single(value)
}
}
impl FromIterator<Keystroke> for Key {
fn from_iter<T: IntoIterator<Item = Keystroke>>(iter: T) -> Self {
Key::chord(iter)
}
}
impl From<Vec<Keystroke>> for Key {
fn from(value: Vec<Keystroke>) -> Self {
Key::from_iter(value)
}
}
impl<'de> Deserialize<'de> for Key {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(KeyVisitor)
}
}
struct KeyVisitor;
impl Visitor<'_> for KeyVisitor {
type Value = Key;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a single key (\"a\"), named key (\"esc\"), modified key (\"ctrl+x\"), or key sequence (\"gg\", \"<space>f\")")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
let mut keystrokes = parse_keystrokes(value).map_err(de::Error::custom)?;
Ok(match keystrokes.len() {
1 => Key::Single(keystrokes.remove(0)),
_ => Key::Chord(keystrokes),
})
}
}
fn parse_keystrokes(input: &str) -> Result<Vec<Keystroke>, ConfigError> {
let mut keystrokes = Vec::new();
let mut rest = input;
while !rest.is_empty() {
match rest.strip_prefix('<') {
Some(after) => {
let (name, tail) = after.split_once('>').ok_or_else(|| {
ConfigError::InvalidKeybinding(format!("unterminated `<` in {input:?}"))
})?;
match parse_segment(name)?.as_slice() {
[single] if !name.is_empty() => keystrokes.push(single.clone()),
_ => {
return Err(ConfigError::InvalidKeybinding(format!(
"`<{name}>` is not a single key"
)))
}
}
rest = tail;
}
None => {
let bare = rest.split('<').next().unwrap_or(rest);
keystrokes.extend(parse_segment(bare)?);
rest = &rest[bare.len()..];
}
}
}
Ok(keystrokes)
}
fn parse_segment(segment: &str) -> Result<Vec<Keystroke>, ConfigError> {
let mut parts = segment.split('+');
let code = parts
.next_back()
.ok_or_else(|| ConfigError::UnknownKeyCode(segment.to_string()))?;
let modifiers = parts.try_fold(KeyModifiers::NONE, |modifiers, part| {
parse_modifiers(&part.to_lowercase()).map(|parsed| modifiers | parsed)
})?;
Ok(parse_key(code, modifiers)?.into_keystrokes())
}
fn parse_key(code: &str, modifiers: KeyModifiers) -> Result<Key, ConfigError> {
if code.is_empty() {
return Ok(Key::from((KeyCode::Null, modifiers)));
}
let key_code = match code {
"esc" => KeyCode::Esc,
"space" => KeyCode::Char(' '),
"backspace" => KeyCode::Backspace,
"backtab" => KeyCode::BackTab,
"delete" => KeyCode::Delete,
"down" => KeyCode::Down,
"end" => KeyCode::End,
"enter" => KeyCode::Enter,
"home" => KeyCode::Home,
"insert" => KeyCode::Insert,
"lt" => KeyCode::Char('<'),
"gt" => KeyCode::Char('>'),
"left" => KeyCode::Left,
"page_down" => KeyCode::PageDown,
"page_up" => KeyCode::PageUp,
"right" => KeyCode::Right,
"tab" => KeyCode::Tab,
"up" => KeyCode::Up,
c if c.chars().count() == 1 => c
.chars()
.next()
.map(KeyCode::Char)
.ok_or_else(|| ConfigError::UnknownKeyCode(c.to_string()))?,
c if c.starts_with('f') => c[1..]
.parse::<u8>()
.map(KeyCode::F)
.map_err(|_| ConfigError::UnknownKeyCode(c.to_string()))?,
c => {
return Ok(Key::chord(
c.chars().map(KeyCode::Char).map(Keystroke::from),
))
}
};
Ok(Key::from((key_code, modifiers)))
}
fn parse_modifiers(modifiers: &str) -> Result<KeyModifiers, ConfigError> {
if modifiers.is_empty() {
return Ok(KeyModifiers::NONE);
}
match modifiers {
"alt" => Ok(KeyModifiers::ALT),
"ctrl" | "control" => Ok(KeyModifiers::CONTROL),
"hyper" => Ok(KeyModifiers::HYPER),
"meta" => Ok(KeyModifiers::META),
"shift" => Ok(KeyModifiers::SHIFT),
"super" => Ok(KeyModifiers::SUPER),
_ => Err(ConfigError::UnknownKeyModifiers(modifiers.to_string())),
}
}
impl de::Error for ConfigError {
fn custom<T>(msg: T) -> Self
where
T: fmt::Display,
{
ConfigError::InvalidKeybinding(msg.to_string())
}
}
#[cfg(test)]
mod tests {
use ratatui::crossterm::event::{KeyCode, KeyModifiers};
use serde::de::IntoDeserializer;
use super::*;
fn key_from_str(s: &str) -> Result<Key, ConfigError> {
Key::deserialize(s.into_deserializer())
}
#[test]
fn test_named_keys() {
let cases = [
("esc", Key::from(KeyCode::Esc)),
("enter", Key::from(KeyCode::Enter)),
("space", Key::from(KeyCode::Char(' '))),
("backspace", Key::from(KeyCode::Backspace)),
("backtab", Key::from(KeyCode::BackTab)),
("delete", Key::from(KeyCode::Delete)),
("tab", Key::from(KeyCode::Tab)),
("up", Key::from(KeyCode::Up)),
("down", Key::from(KeyCode::Down)),
("left", Key::from(KeyCode::Left)),
("right", Key::from(KeyCode::Right)),
("home", Key::from(KeyCode::Home)),
("end", Key::from(KeyCode::End)),
("page_up", Key::from(KeyCode::PageUp)),
("page_down", Key::from(KeyCode::PageDown)),
("insert", Key::from(KeyCode::Insert)),
];
cases.into_iter().for_each(|(input, expected)| {
assert_eq!(key_from_str(input).unwrap(), expected, "input: {input:?}");
});
}
#[test]
fn test_single_char_keys() {
let cases = [
("a", Key::from('a')),
("z", Key::from('z')),
("A", Key::from('A')),
("0", Key::from('0')),
("?", Key::from('?')),
("/", Key::from('/')),
(":", Key::from(':')),
];
cases.into_iter().for_each(|(input, expected)| {
assert_eq!(key_from_str(input).unwrap(), expected, "input: {input:?}");
});
}
#[test]
fn test_function_keys() {
let cases = [
("f1", Key::from(KeyCode::F(1))),
("f5", Key::from(KeyCode::F(5))),
("f12", Key::from(KeyCode::F(12))),
];
cases.into_iter().for_each(|(input, expected)| {
assert_eq!(key_from_str(input).unwrap(), expected, "input: {input:?}");
});
}
#[test]
fn test_modified_keys() {
let cases = [
("ctrl+c", Key::from(('c', KeyModifiers::CONTROL))),
("control+c", Key::from(('c', KeyModifiers::CONTROL))),
("alt+x", Key::from(('x', KeyModifiers::ALT))),
("shift+a", Key::from(('a', KeyModifiers::SHIFT))),
(
"ctrl+shift+k",
Key::from((
KeyCode::Char('k'),
KeyModifiers::CONTROL | KeyModifiers::SHIFT,
)),
),
(
"ctrl+enter",
Key::from((KeyCode::Enter, KeyModifiers::CONTROL)),
),
("alt+esc", Key::from((KeyCode::Esc, KeyModifiers::ALT))),
("ctrl+f5", Key::from((KeyCode::F(5), KeyModifiers::CONTROL))),
];
cases.into_iter().for_each(|(input, expected)| {
assert_eq!(key_from_str(input).unwrap(), expected, "input: {input:?}");
});
}
#[test]
fn test_key_sequences() {
let cases: &[(&str, &[Keystroke])] = &[
(
"gg",
&[
Keystroke::from(KeyCode::Char('g')),
Keystroke::from(KeyCode::Char('g')),
],
),
(
"gG",
&[
Keystroke::from(KeyCode::Char('g')),
Keystroke::from(KeyCode::Char('G')),
],
),
(
"crn",
&[
Keystroke::from(KeyCode::Char('c')),
Keystroke::from(KeyCode::Char('r')),
Keystroke::from(KeyCode::Char('n')),
],
),
];
cases.iter().for_each(|(input, expected_keys)| {
let key = key_from_str(input).unwrap();
match key {
Key::Chord(keys) => assert_eq!(keys, *expected_keys, "input: {input:?}"),
Key::Single(_) => panic!("Expected sequence for {input:?}, got plain key"),
}
});
}
#[test]
fn test_named_key_sequences() {
let space = Keystroke::from(KeyCode::Char(' '));
let cases: &[(&str, &[Keystroke])] = &[
(
"<space>f",
&[space.clone(), Keystroke::from(KeyCode::Char('f'))],
),
("<space><space>", &[space.clone(), space.clone()]),
(
"<enter>x",
&[
Keystroke::from(KeyCode::Enter),
Keystroke::from(KeyCode::Char('x')),
],
),
(
"g<space>",
&[Keystroke::from(KeyCode::Char('g')), space.clone()],
),
(
"<ctrl+space>f",
&[
Keystroke::from((KeyCode::Char(' '), KeyModifiers::CONTROL)),
Keystroke::from(KeyCode::Char('f')),
],
),
];
cases.iter().for_each(
|(input, expected_keys)| match key_from_str(input).unwrap() {
Key::Chord(keys) => assert_eq!(keys, *expected_keys, "input: {input:?}"),
Key::Single(_) => panic!("Expected sequence for {input:?}, got plain key"),
},
);
}
#[test]
fn test_named_group_single_key() {
assert_eq!(
key_from_str("<space>").unwrap(),
key_from_str("space").unwrap()
);
assert_eq!(key_from_str("<esc>").unwrap(), key_from_str("esc").unwrap());
assert_eq!(key_from_str("<lt>").unwrap(), Key::from('<'));
assert_eq!(key_from_str("<gt>").unwrap(), Key::from('>'));
}
#[test]
fn test_invalid_keys() {
let cases = [
"unknown_modifier+c",
"badmod+x",
"f999",
"<space",
"<>",
"<unknown_key>",
];
cases.into_iter().for_each(|input| {
assert!(key_from_str(input).is_err(), "Expected error for {input:?}");
});
}
#[test]
fn test_keystroke_display() {
let cases = [
(Keystroke::new(KeyCode::Char('a'), KeyModifiers::NONE), "a"),
(
Keystroke::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
"control-c",
),
(Keystroke::from(KeyCode::Char('G')), "G"),
(
Keystroke::from((KeyCode::Char('G'), KeyModifiers::CONTROL)),
"control-G",
),
];
cases.into_iter().for_each(|(key, expected)| {
assert_eq!(key.to_string(), expected, "key: {key:?}");
});
}
#[test]
fn test_key_sequence_display() {
let keys = [
Keystroke::from(KeyCode::Char('g')),
Keystroke::from(KeyCode::Char('G')),
];
assert_eq!(Key::chord(keys).to_string(), "gG");
}
#[test]
fn test_uppercase_implies_shift() {
let upper = key_from_str("G").unwrap();
assert_eq!(
upper,
Key::Single(Keystroke::new(KeyCode::Char('G'), KeyModifiers::SHIFT))
);
let seq = key_from_str("gG").unwrap();
assert_eq!(
seq,
Key::chord([
Keystroke::new(KeyCode::Char('g'), KeyModifiers::NONE),
Keystroke::new(KeyCode::Char('G'), KeyModifiers::SHIFT),
])
);
}
}