#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ChordError {
#[error("chord {0:?} names no key — expected something like \"cmd+s\" or \"enter\"")]
Empty(String),
}
pub const NAMED_KEYS: &[&str] = &[
"cmd",
"command",
"meta",
"super",
"ctrl",
"control",
"alt",
"option",
"opt",
"shift",
"tab",
"enter",
"return",
"esc",
"escape",
"space",
"backspace",
"delete",
"up",
"down",
"left",
"right",
];
pub fn split(chord: &str) -> Result<(Vec<&str>, &str), ChordError> {
let mut parts: Vec<&str> = chord
.split('+')
.map(str::trim)
.filter(|part| !part.is_empty())
.collect();
let key = parts
.pop()
.ok_or_else(|| ChordError::Empty(chord.to_string()))?;
Ok((parts, key))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chords_split_into_modifiers_and_a_key() {
let (modifiers, key) = split("cmd+shift+s").expect("valid");
assert_eq!(modifiers, vec!["cmd", "shift"]);
assert_eq!(key, "s");
}
#[test]
fn a_bare_key_has_no_modifiers() {
let (modifiers, key) = split("enter").expect("valid");
assert!(modifiers.is_empty());
assert_eq!(key, "enter");
}
#[test]
fn whitespace_around_chord_parts_is_tolerated() {
let (modifiers, key) = split("cmd + s").expect("valid");
assert_eq!(modifiers, vec!["cmd"]);
assert_eq!(key, "s");
}
#[test]
fn an_empty_chord_is_an_error() {
assert!(split("").is_err());
assert!(split("+").is_err());
assert!(split(" ").is_err());
}
#[test]
fn the_error_quotes_the_chord_it_could_not_read() {
let error = split("+").expect_err("empty");
assert!(error.to_string().contains("\"+\""), "{error}");
}
#[test]
fn every_named_key_is_lowercase_and_listed_once() {
for name in NAMED_KEYS {
assert_eq!(*name, name.to_ascii_lowercase(), "{name}");
assert!(!name.is_empty());
assert_eq!(
NAMED_KEYS.iter().filter(|other| *other == name).count(),
1,
"{name} is listed more than once"
);
}
}
#[test]
fn no_named_key_contains_the_separator() {
for name in NAMED_KEYS {
let (modifiers, key) = split(name).expect("a bare name is a valid chord");
assert!(modifiers.is_empty(), "{name}");
assert_eq!(key, *name);
}
}
}