use core::fmt;
use core::str::FromStr;
use super::{Key, KeyInput, Modifiers};
impl fmt::Display for KeyInput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mods = self.modifiers();
if mods.contains(Modifiers::CTRL) {
f.write_str("ctrl+")?;
}
if mods.contains(Modifiers::ALT) {
f.write_str("alt+")?;
}
if mods.contains(Modifiers::SHIFT) {
f.write_str("shift+")?;
}
if mods.contains(Modifiers::SUPER) {
f.write_str("super+")?;
}
match self.key() {
Key::Char(c) => write!(f, "{c}"),
Key::F(n) => write!(f, "f{n}"),
Key::Enter => f.write_str("enter"),
Key::Esc => f.write_str("esc"),
Key::Tab => f.write_str("tab"),
Key::Backspace => f.write_str("backspace"),
Key::Delete => f.write_str("delete"),
Key::Insert => f.write_str("insert"),
Key::Home => f.write_str("home"),
Key::End => f.write_str("end"),
Key::PageUp => f.write_str("pageup"),
Key::PageDown => f.write_str("pagedown"),
Key::Up => f.write_str("up"),
Key::Down => f.write_str("down"),
Key::Left => f.write_str("left"),
Key::Right => f.write_str("right"),
}
}
}
impl FromStr for KeyInput {
type Err = ParseKeyInputError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err(ParseKeyInputError::new(s, ErrorKind::Empty));
}
let mut rest = s;
let mut mods = Modifiers::NONE;
while let Some(idx) = rest.find('+') {
let token = &rest[..idx];
if let Some(m) = parse_modifier(token) {
mods |= m;
rest = &rest[idx + 1..];
} else {
break;
}
}
let key =
parse_key(rest).ok_or_else(|| ParseKeyInputError::new(s, ErrorKind::InvalidKey))?;
Ok(KeyInput::normalized(key, mods))
}
}
fn parse_modifier(token: &str) -> Option<Modifiers> {
match token.to_ascii_lowercase().as_str() {
"ctrl" | "control" => Some(Modifiers::CTRL),
"alt" | "opt" | "option" => Some(Modifiers::ALT),
"shift" => Some(Modifiers::SHIFT),
"cmd" | "super" | "win" | "meta" => Some(Modifiers::SUPER),
_ => None,
}
}
fn parse_key(token: &str) -> Option<Key> {
if token.is_empty() {
return None;
}
let lower = token.to_ascii_lowercase();
let named = match lower.as_str() {
"tab" => Some(Key::Tab),
"enter" | "return" => Some(Key::Enter),
"esc" | "escape" => Some(Key::Esc),
"space" => Some(Key::Char(' ')),
"backspace" => Some(Key::Backspace),
"delete" | "del" => Some(Key::Delete),
"insert" | "ins" => Some(Key::Insert),
"home" => Some(Key::Home),
"end" => Some(Key::End),
"pageup" | "pgup" => Some(Key::PageUp),
"pagedown" | "pgdn" => Some(Key::PageDown),
"up" => Some(Key::Up),
"down" => Some(Key::Down),
"left" => Some(Key::Left),
"right" => Some(Key::Right),
_ => None,
};
if let Some(key) = named {
return Some(key);
}
if let Some(rest) = lower.strip_prefix('f') {
if let Ok(n) = rest.parse::<u8>() {
if (1..=24).contains(&n) {
return Some(Key::F(n));
}
}
}
let mut chars = token.chars();
let c = chars.next()?;
if chars.next().is_some() {
return None;
}
Some(Key::Char(c))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseKeyInputError {
input: String,
kind: ErrorKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ErrorKind {
Empty,
InvalidKey,
}
impl ParseKeyInputError {
fn new(input: &str, kind: ErrorKind) -> Self {
ParseKeyInputError {
input: input.to_string(),
kind,
}
}
#[must_use]
pub fn input(&self) -> &str {
&self.input
}
}
impl fmt::Display for ParseKeyInputError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
ErrorKind::Empty => f.write_str("empty key string"),
ErrorKind::InvalidKey => write!(f, "invalid key string: {:?}", self.input),
}
}
}
impl std::error::Error for ParseKeyInputError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_round_trips_through_from_str() {
let cases = [
KeyInput::new(Key::Char('a'), Modifiers::NONE),
KeyInput::new(Key::Char('A'), Modifiers::NONE),
KeyInput::new(Key::Char('1'), Modifiers::SUPER),
KeyInput::new(Key::Char('s'), Modifiers::CTRL | Modifiers::SHIFT),
KeyInput::new(Key::Char('あ'), Modifiers::CTRL),
KeyInput::new(Key::Char(' '), Modifiers::NONE),
KeyInput::new(Key::Char('+'), Modifiers::CTRL),
KeyInput::new(Key::F(1), Modifiers::NONE),
KeyInput::new(Key::F(12), Modifiers::SHIFT),
KeyInput::new(Key::Tab, Modifiers::SHIFT),
KeyInput::new(Key::Esc, Modifiers::NONE),
KeyInput::new(Key::Up, Modifiers::CTRL | Modifiers::ALT),
KeyInput::new(
Key::Enter,
Modifiers::CTRL | Modifiers::ALT | Modifiers::SHIFT | Modifiers::SUPER,
),
];
for k in cases {
let rendered = k.to_string();
assert_eq!(
rendered.parse::<KeyInput>(),
Ok(k),
"round trip via {rendered:?}"
);
}
}
#[test]
fn display_uses_canonical_modifier_order_and_names() {
let k = KeyInput::new(Key::Char('a'), Modifiers::SUPER | Modifiers::CTRL);
assert_eq!(k.to_string(), "ctrl+super+a");
assert_eq!(KeyInput::new(Key::F(1), Modifiers::NONE).to_string(), "f1");
assert_eq!(
KeyInput::new(Key::Tab, Modifiers::SHIFT).to_string(),
"shift+tab"
);
}
#[test]
fn from_str_accepts_aliases_case_insensitively() {
let ctrl_a = KeyInput::new(Key::Char('a'), Modifiers::CTRL);
assert_eq!("ctrl+a".parse::<KeyInput>().unwrap(), ctrl_a);
assert_eq!("CTRL+a".parse::<KeyInput>().unwrap(), ctrl_a);
assert_eq!("control+a".parse::<KeyInput>().unwrap(), ctrl_a);
assert_eq!(
"cmd+1".parse::<KeyInput>().unwrap(),
"super+1".parse::<KeyInput>().unwrap()
);
}
#[test]
fn from_str_shares_shift_normalization() {
let a = KeyInput::new(Key::Char('a'), Modifiers::NONE);
assert_eq!("shift+a".parse::<KeyInput>().unwrap(), a);
assert_eq!("a".parse::<KeyInput>().unwrap(), a);
}
#[test]
fn from_str_keeps_shift_with_other_modifier() {
let cmd_shift_s = "cmd+shift+s".parse::<KeyInput>().unwrap();
assert_eq!(
cmd_shift_s,
KeyInput::new(Key::Char('s'), Modifiers::SUPER | Modifiers::SHIFT)
);
assert_ne!(cmd_shift_s, "cmd+s".parse::<KeyInput>().unwrap());
assert_ne!(
"ctrl+shift+s".parse::<KeyInput>().unwrap(),
"ctrl+s".parse::<KeyInput>().unwrap()
);
}
#[test]
fn from_str_handles_literal_plus_as_key() {
assert_eq!(
"ctrl++".parse::<KeyInput>().unwrap(),
KeyInput::new(Key::Char('+'), Modifiers::CTRL)
);
assert_eq!(
"+".parse::<KeyInput>().unwrap(),
KeyInput::new(Key::Char('+'), Modifiers::NONE)
);
}
#[test]
fn from_str_parses_named_and_function_keys() {
assert_eq!("f1".parse::<KeyInput>().unwrap().key(), Key::F(1));
assert_eq!("up".parse::<KeyInput>().unwrap().key(), Key::Up);
assert_eq!("esc".parse::<KeyInput>().unwrap().key(), Key::Esc);
assert_eq!("escape".parse::<KeyInput>().unwrap().key(), Key::Esc);
}
#[test]
fn from_str_rejects_invalid_input() {
assert!("".parse::<KeyInput>().is_err());
assert!("ctrl".parse::<KeyInput>().is_err());
assert!("hyper+a".parse::<KeyInput>().is_err());
assert!("ctrl+xyz".parse::<KeyInput>().is_err());
}
}