use bitflags::bitflags;
use std::fmt;
bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Modifiers: u8 {
const CTRL = 0b0001;
const SHIFT = 0b0010;
const ALT = 0b0100;
const SUPER = 0b1000;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KeyChord {
pub modifiers: Modifiers,
pub key: Key,
}
impl KeyChord {
pub const fn new(modifiers: Modifiers, key: Key) -> Self {
Self { modifiers, key }
}
pub const fn plain(key: Key) -> Self {
Self {
modifiers: Modifiers::empty(),
key,
}
}
pub const fn char(c: char) -> Self {
Self::plain(Key::Char(c))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Key {
Char(char),
Named(NamedKey),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NamedKey {
Esc,
CR,
Tab,
BackTab,
BS,
Space,
Up,
Down,
Left,
Right,
Home,
End,
PageUp,
PageDown,
Insert,
Delete,
F(u8),
Leader,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ParseError {
#[error("unknown key name <{0}>")]
UnknownNamed(String),
#[error("unclosed `<` in key notation")]
UnclosedBracket,
#[error("empty `<...>` block")]
EmptyBracket,
#[error("modifier `<{0}->` with no following key")]
DanglingModifier(String),
#[error("expected exactly one chord, got {0}")]
ExpectedOneChord(usize),
}
pub fn parse_keys(input: &str) -> Result<Vec<KeyChord>, ParseError> {
let mut out = Vec::new();
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
if c.is_ascii_whitespace() {
continue;
}
if c == '<' {
let mut name = String::new();
let mut closed = false;
for nc in chars.by_ref() {
if nc == '>' {
closed = true;
break;
}
name.push(nc);
}
if !closed {
return Err(ParseError::UnclosedBracket);
}
if name.is_empty() {
return Err(ParseError::EmptyBracket);
}
out.push(parse_named(&name)?);
} else {
out.push(parse_char(c));
}
}
Ok(out)
}
pub fn parse_key(input: &str) -> Result<KeyChord, ParseError> {
let chords = parse_keys(input)?;
if chords.len() != 1 {
return Err(ParseError::ExpectedOneChord(chords.len()));
}
Ok(chords.into_iter().next().expect("len checked above"))
}
fn parse_char(c: char) -> KeyChord {
let mut mods = Modifiers::empty();
if c.is_ascii_uppercase() {
mods |= Modifiers::SHIFT;
}
KeyChord {
modifiers: mods,
key: Key::Char(c),
}
}
fn parse_named(raw: &str) -> Result<KeyChord, ParseError> {
if raw.eq_ignore_ascii_case("lt") {
return Ok(KeyChord::char('<'));
}
let (mods, tail) = parse_modifiers(raw);
if tail.is_empty() {
return Err(ParseError::DanglingModifier(raw.to_string()));
}
let key = if tail.eq_ignore_ascii_case("leader") {
Key::Named(NamedKey::Leader)
} else if tail.eq_ignore_ascii_case("space") {
Key::Char(' ')
} else if tail.chars().count() == 1 {
let ch = tail.chars().next().expect("len checked above");
let ch = if mods.contains(Modifiers::CTRL) {
ch.to_ascii_lowercase()
} else if mods.contains(Modifiers::SHIFT) && ch.is_ascii_alphabetic() {
ch.to_ascii_uppercase()
} else {
ch
};
Key::Char(ch)
} else {
Key::Named(parse_named_key(tail)?)
};
Ok(KeyChord {
modifiers: mods,
key,
})
}
fn parse_modifiers(raw: &str) -> (Modifiers, &str) {
let mut mods = Modifiers::empty();
let mut tail = raw;
loop {
let lower_prefix = tail.get(..2).map(str::to_ascii_lowercase);
match lower_prefix.as_deref() {
Some("c-") => {
mods |= Modifiers::CTRL;
tail = &tail[2..];
}
Some("s-") => {
mods |= Modifiers::SHIFT;
tail = &tail[2..];
}
Some("m-") | Some("a-") => {
mods |= Modifiers::ALT;
tail = &tail[2..];
}
Some("d-") => {
mods |= Modifiers::SUPER;
tail = &tail[2..];
}
_ => return (mods, tail),
}
}
}
fn parse_named_key(name: &str) -> Result<NamedKey, ParseError> {
let n = name.to_ascii_lowercase();
Ok(match n.as_str() {
"esc" | "escape" => NamedKey::Esc,
"cr" | "enter" | "return" => NamedKey::CR,
"bs" | "backspace" => NamedKey::BS,
"tab" => NamedKey::Tab,
"backtab" => NamedKey::BackTab,
"space" => NamedKey::Space,
"up" => NamedKey::Up,
"down" => NamedKey::Down,
"left" => NamedKey::Left,
"right" => NamedKey::Right,
"home" => NamedKey::Home,
"end" => NamedKey::End,
"pageup" | "pgup" => NamedKey::PageUp,
"pagedown" | "pgdn" => NamedKey::PageDown,
"insert" | "ins" => NamedKey::Insert,
"delete" | "del" => NamedKey::Delete,
s if s.starts_with('f') => {
let num: u8 = s[1..]
.parse()
.map_err(|_| ParseError::UnknownNamed(name.to_string()))?;
if !(1..=12).contains(&num) {
return Err(ParseError::UnknownNamed(name.to_string()));
}
NamedKey::F(num)
}
_ => return Err(ParseError::UnknownNamed(name.to_string())),
})
}
impl fmt::Display for Modifiers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
bitflags::parser::to_writer(self, f)
}
}
impl fmt::Display for NamedKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
NamedKey::Esc => "Esc",
NamedKey::CR => "CR",
NamedKey::Tab => "Tab",
NamedKey::BackTab => "S-Tab",
NamedKey::BS => "BS",
NamedKey::Space => "Space",
NamedKey::Up => "Up",
NamedKey::Down => "Down",
NamedKey::Left => "Left",
NamedKey::Right => "Right",
NamedKey::Home => "Home",
NamedKey::End => "End",
NamedKey::PageUp => "PageUp",
NamedKey::PageDown => "PageDown",
NamedKey::Insert => "Insert",
NamedKey::Delete => "Delete",
NamedKey::F(n) => return write!(f, "F{n}"),
NamedKey::Leader => "leader",
};
f.write_str(s)
}
}
impl fmt::Display for KeyChord {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mods = self.modifiers;
if mods.is_empty() {
return match self.key {
Key::Char(c) => write!(f, "{c}"),
Key::Named(n) => write!(f, "<{n}>"),
};
}
if mods == Modifiers::SHIFT
&& let Key::Char(c) = self.key
&& c.is_ascii_uppercase()
{
return write!(f, "{c}");
}
write!(f, "<")?;
if mods.contains(Modifiers::CTRL) {
write!(f, "C-")?;
}
if mods.contains(Modifiers::SHIFT) {
write!(f, "S-")?;
}
if mods.contains(Modifiers::ALT) {
write!(f, "A-")?;
}
if mods.contains(Modifiers::SUPER) {
write!(f, "D-")?;
}
match self.key {
Key::Char(c) => write!(f, "{c}>"),
Key::Named(n) => write!(f, "{n}>"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn k(ch: char) -> KeyChord {
KeyChord::char(ch)
}
fn shift_k(ch: char) -> KeyChord {
KeyChord {
key: Key::Char(ch),
modifiers: Modifiers::SHIFT,
}
}
fn ctrl(ch: char) -> KeyChord {
KeyChord {
key: Key::Char(ch),
modifiers: Modifiers::CTRL,
}
}
#[test]
fn empty_input() {
assert_eq!(parse_keys("").unwrap(), vec![]);
}
#[test]
fn bare_chars() {
assert_eq!(parse_keys("gT").unwrap(), vec![k('g'), shift_k('T')]);
}
#[test]
fn ctrl_w_v() {
assert_eq!(parse_keys("<C-w>v").unwrap(), vec![ctrl('w'), k('v')]);
}
#[test]
fn ctrl_shift_tab() {
let chords = parse_keys("<C-S-Tab>").unwrap();
assert_eq!(chords.len(), 1);
assert_eq!(chords[0].key, Key::Named(NamedKey::Tab));
assert!(chords[0].modifiers.contains(Modifiers::CTRL));
assert!(chords[0].modifiers.contains(Modifiers::SHIFT));
}
#[test]
fn shift_tab_alias() {
let a = parse_keys("<S-Tab>").unwrap();
let b = parse_keys("<BackTab>").unwrap();
assert_eq!(a[0].key, Key::Named(NamedKey::Tab));
assert!(a[0].modifiers.contains(Modifiers::SHIFT));
assert_eq!(b[0].key, Key::Named(NamedKey::BackTab));
}
#[test]
fn meta_alias_for_alt() {
let m = parse_keys("<M-x>").unwrap();
let a = parse_keys("<A-x>").unwrap();
assert_eq!(m, a);
assert!(m[0].modifiers.contains(Modifiers::ALT));
}
#[test]
fn leader_is_abstract() {
let chords = parse_keys("<leader>n").unwrap();
assert_eq!(chords[0].key, Key::Named(NamedKey::Leader));
assert_eq!(chords[1], k('n'));
}
#[test]
fn space_special() {
let chords = parse_keys("<Space>x").unwrap();
assert_eq!(chords[0].key, Key::Char(' '));
assert_eq!(chords[1], k('x'));
}
#[test]
fn esc_aliases() {
for s in ["<Esc>", "<escape>", "<ESC>"] {
assert_eq!(
parse_keys(s).unwrap(),
vec![KeyChord::plain(Key::Named(NamedKey::Esc))],
"alias {s}"
);
}
}
#[test]
fn enter_cr_alias() {
let cr = parse_keys("<CR>").unwrap();
let enter = parse_keys("<Enter>").unwrap();
let ret = parse_keys("<Return>").unwrap();
assert_eq!(cr, enter);
assert_eq!(cr, ret);
}
#[test]
fn bs_aliases() {
let bs = parse_keys("<BS>").unwrap();
let bksp = parse_keys("<Backspace>").unwrap();
assert_eq!(bs, bksp);
assert_eq!(bs[0].key, Key::Named(NamedKey::BS));
}
#[test]
fn lt_escape() {
let chords = parse_keys("<lt>").unwrap();
assert_eq!(chords, vec![k('<')]);
}
#[test]
fn function_keys() {
for n in 1..=12u8 {
let s = format!("<F{n}>");
let chords = parse_keys(&s).unwrap();
assert_eq!(chords[0].key, Key::Named(NamedKey::F(n)));
}
}
#[test]
fn function_key_out_of_range_errors() {
assert!(matches!(
parse_keys("<F13>"),
Err(ParseError::UnknownNamed(_))
));
assert!(matches!(
parse_keys("<F0>"),
Err(ParseError::UnknownNamed(_))
));
}
#[test]
fn unclosed_bracket_errors() {
assert_eq!(parse_keys("<C-w").unwrap_err(), ParseError::UnclosedBracket);
}
#[test]
fn dangling_modifier_errors() {
assert!(matches!(
parse_keys("<C->"),
Err(ParseError::DanglingModifier(_))
));
}
#[test]
fn empty_bracket_errors() {
assert!(matches!(parse_keys("<>"), Err(ParseError::EmptyBracket)));
}
#[test]
fn unknown_named_errors() {
match parse_keys("<NoSuchKey>") {
Err(ParseError::UnknownNamed(name)) => {
assert!(name.eq_ignore_ascii_case("nosuchkey"));
}
other => panic!("expected UnknownNamed, got {other:?}"),
}
}
#[test]
fn whitespace_ignored() {
let chords = parse_keys("<C-w> v ").unwrap();
assert_eq!(chords, vec![ctrl('w'), k('v')]);
}
#[test]
fn modifier_order_independent() {
let a = parse_keys("<C-S-Tab>").unwrap();
let b = parse_keys("<S-C-Tab>").unwrap();
assert_eq!(a, b);
}
#[test]
fn shift_letter_normalises_to_uppercase() {
let a = parse_keys("<S-a>").unwrap();
let b = parse_keys("<S-A>").unwrap();
assert_eq!(a, b);
assert_eq!(a[0].key, Key::Char('A'));
assert!(a[0].modifiers.contains(Modifiers::SHIFT));
}
#[test]
fn ctrl_letter_is_case_insensitive() {
let a = parse_keys("<C-A>").unwrap();
let b = parse_keys("<C-a>").unwrap();
assert_eq!(a, b);
assert_eq!(a[0].key, Key::Char('a'));
}
#[test]
fn punctuation_passes_through() {
let chords = parse_keys(":/?,;").unwrap();
assert_eq!(chords, vec![k(':'), k('/'), k('?'), k(','), k(';')]);
}
#[test]
fn embedded_literals_split_per_char() {
let chords = parse_keys("Hello").unwrap();
assert_eq!(chords.len(), 5);
assert_eq!(chords[0], shift_k('H'));
assert_eq!(chords[1], k('e'));
assert_eq!(chords[4], k('o'));
}
#[test]
fn register_prefix_quote_a() {
let chords = parse_keys("\"ay").unwrap();
assert_eq!(chords, vec![k('"'), k('a'), k('y')]);
}
#[test]
fn parse_key_single() {
let kc = parse_key("<C-w>").unwrap();
assert_eq!(kc, ctrl('w'));
}
#[test]
fn parse_key_residual_errors() {
assert!(matches!(
parse_key("<C-w>v"),
Err(ParseError::ExpectedOneChord(2))
));
assert!(matches!(
parse_key(""),
Err(ParseError::ExpectedOneChord(0))
));
}
}