use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Key {
pub sym: String,
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
}
impl Key {
pub fn plain(sym: impl Into<String>) -> Self {
Self {
sym: sym.into(),
ctrl: false,
alt: false,
shift: false,
}
}
pub fn is_count_digit(&self) -> bool {
!self.ctrl
&& !self.alt
&& self.sym.len() == 1
&& self.sym.chars().next().is_some_and(|c| c.is_ascii_digit())
}
}
impl fmt::Display for Key {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.ctrl && !self.alt && !self.shift {
return write!(f, "{}", self.sym);
}
let mut parts = Vec::new();
if self.ctrl {
parts.push("C");
}
if self.alt {
parts.push("A");
}
if self.shift {
parts.push("S");
}
write!(f, "<{}-{}>", parts.join("-"), self.sym)
}
}
pub fn display_sequence(keys: &[Key]) -> String {
keys.iter().map(|k| k.to_string()).collect()
}
pub fn parse_key_string(s: &str) -> Vec<Key> {
let mut keys = Vec::new();
let mut chars = s.chars().peekable();
while let Some(&c) = chars.peek() {
if c == '<' {
chars.next();
let mut inner = String::new();
for ch in chars.by_ref() {
if ch == '>' {
break;
}
inner.push(ch);
}
let parts: Vec<&str> = inner.split('-').collect();
let (mods, name) = parts.split_at(parts.len().saturating_sub(1));
let mut key = match name.first().and_then(|n| canonical_name(n)) {
Some(sym) => Key::plain(sym),
None => continue,
};
for &m in mods {
match m {
"C" => key.ctrl = true,
"A" => key.alt = true,
"S" => key.shift = true,
_ => {}
}
}
keys.push(key);
} else {
chars.next();
keys.push(Key::plain(c.to_string()));
}
}
keys
}
pub fn canonical_name(name: &str) -> Option<String> {
let sym = match name.to_lowercase().as_str() {
"escape" | "esc" => "Escape",
"return" | "enter" | "cr" => "Return",
"tab" => "Tab",
"space" => "space",
"backspace" | "bs" => "BackSpace",
"delete" | "del" => "Delete",
"insert" => "Insert",
"up" => "Up",
"down" => "Down",
"left" => "Left",
"right" => "Right",
"pgup" | "pageup" => "PgUp",
"pgdown" | "pagedown" => "PgDown",
"home" => "Home",
"end" => "End",
"f1" => "F1",
"f2" => "F2",
"f3" => "F3",
"f4" => "F4",
"f5" => "F5",
"f6" => "F6",
"f7" => "F7",
"f8" => "F8",
"f9" => "F9",
"f10" => "F10",
"f11" => "F11",
"f12" => "F12",
_ => {
if name.chars().count() == 1 {
return Some(name.to_string());
}
return None;
}
};
Some(sym.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_plain_sequence() {
let keys = parse_key_string("gg");
assert_eq!(keys, vec![Key::plain("g"), Key::plain("g")]);
}
#[test]
fn parse_ctrl() {
let keys = parse_key_string("<C-f>");
assert_eq!(keys.len(), 1);
assert!(keys[0].ctrl);
assert_eq!(keys[0].sym, "f");
}
#[test]
fn parse_alt_digit() {
let keys = parse_key_string("<A-1>");
assert_eq!(keys.len(), 1);
assert!(keys[0].alt);
assert_eq!(keys[0].sym, "1");
}
#[test]
fn parse_named_in_mods() {
let keys = parse_key_string("<C-Tab>");
assert_eq!(keys.len(), 1);
assert!(keys[0].ctrl);
assert_eq!(keys[0].sym, "Tab");
}
#[test]
fn uppercase_preserved() {
let keys = parse_key_string("G");
assert_eq!(keys, vec![Key::plain("G")]);
}
#[test]
fn count_digit_detection() {
assert!(Key::plain("5").is_count_digit());
assert!(!Key::plain("j").is_count_digit());
let alt5 = Key {
sym: "5".into(),
alt: true,
ctrl: false,
shift: false,
};
assert!(!alt5.is_count_digit());
}
#[test]
fn display_roundtrip() {
assert_eq!(Key::plain("j").to_string(), "j");
assert_eq!(
Key {
sym: "f".into(),
ctrl: true,
alt: false,
shift: false
}
.to_string(),
"<C-f>"
);
}
}