use crate::error::KeyParseError;
use crate::{Key, Modifier};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Shortcut {
pub modifiers: Vec<Modifier>,
pub key: Key,
}
impl Shortcut {
pub fn new(modifiers: Vec<Modifier>, key: Key) -> Self {
Self { modifiers, key }
}
pub fn is_simple(&self) -> bool {
self.modifiers.is_empty()
}
pub fn as_string(&self) -> String {
if self.modifiers.is_empty() {
self.key.to_string()
} else {
let mods: Vec<String> = self.modifiers.iter().map(|m| m.to_string()).collect();
format!("{}+{}", mods.join("+"), self.key)
}
}
}
impl std::fmt::Display for Shortcut {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_string())
}
}
pub fn parse_shortcut_with_aliases(shortcut: &str) -> Result<Shortcut, KeyParseError> {
if shortcut.is_empty() {
return Err(KeyParseError::InvalidShortcutFormat(
"empty shortcut".to_string(),
));
}
let parts: Vec<&str> = shortcut.split('+').map(|s| s.trim()).collect();
if parts.len() < 2 {
return Err(KeyParseError::InvalidShortcutFormat(format!(
"shortcut must contain at least one modifier and one key: {}",
shortcut
)));
}
let key_part = parts
.last()
.cloned()
.ok_or_else(|| KeyParseError::InvalidShortcutFormat(shortcut.to_string()))?;
let normalized_key = super::normalize_key_name(key_part);
let key = crate::mapping::standard::parse_key_ignore_case(normalized_key)?;
let modifiers: Vec<Modifier> = parts[0..parts.len() - 1]
.iter()
.map(|&m| super::parse_modifier_with_aliases(m))
.collect::<Result<_, _>>()?;
Ok(Shortcut::new(modifiers, key))
}
pub fn parse_shortcut_flexible(shortcut: &str) -> Result<Shortcut, KeyParseError> {
if shortcut.is_empty() {
return Err(KeyParseError::InvalidShortcutFormat(
"empty shortcut".to_string(),
));
}
let normalized = shortcut.replace(['-', ' '], "+");
parse_shortcut_with_aliases(&normalized)
}
pub fn parse_input(input: &str) -> Result<Shortcut, KeyParseError> {
if input.is_empty() {
return Err(KeyParseError::InvalidShortcutFormat(
"empty input".to_string(),
));
}
if input.contains('+') || input.contains('-') || input.contains(' ') {
parse_shortcut_flexible(input)
} else {
let normalized_key = super::normalize_key_name(input);
let key = crate::mapping::standard::parse_key_ignore_case(normalized_key)?;
Ok(Shortcut::new(Vec::new(), key))
}
}
pub fn parse_shortcut_sequence(sequence: &str) -> Result<Vec<Shortcut>, KeyParseError> {
sequence
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(parse_shortcut_flexible)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_shortcut_with_aliases() {
let shortcut = parse_shortcut_with_aliases("ctrl+shift+a").unwrap();
assert_eq!(shortcut.modifiers, vec![Modifier::Control, Modifier::Shift]);
assert_eq!(shortcut.key, Key::A);
let shortcut = parse_shortcut_with_aliases("cmd+q").unwrap();
assert_eq!(shortcut.modifiers, vec![Modifier::Meta]);
assert_eq!(shortcut.key, Key::Q);
let shortcut = parse_shortcut_with_aliases("ctrl+alt+del").unwrap();
assert_eq!(shortcut.modifiers, vec![Modifier::Control, Modifier::Alt]);
assert_eq!(shortcut.key, Key::Delete);
}
#[test]
fn test_parse_shortcut_flexible() {
let shortcut = parse_shortcut_flexible("ctrl-shift-a").unwrap();
assert_eq!(shortcut.modifiers, vec![Modifier::Control, Modifier::Shift]);
assert_eq!(shortcut.key, Key::A);
let shortcut = parse_shortcut_flexible("cmd alt delete").unwrap();
assert_eq!(shortcut.modifiers, vec![Modifier::Meta, Modifier::Alt]);
assert_eq!(shortcut.key, Key::Delete);
}
#[test]
fn test_parse_input() {
let shortcut = parse_input("a").unwrap();
assert!(shortcut.is_simple());
assert_eq!(shortcut.key, Key::A);
let shortcut = parse_input("esc").unwrap();
assert!(shortcut.is_simple());
assert_eq!(shortcut.key, Key::Escape);
let shortcut = parse_input("ctrl+a").unwrap();
assert_eq!(shortcut.modifiers, vec![Modifier::Control]);
assert_eq!(shortcut.key, Key::A);
}
#[test]
fn test_parse_shortcut_sequence() {
let shortcuts = parse_shortcut_sequence("ctrl+a, cmd+q, shift-enter").unwrap();
assert_eq!(shortcuts.len(), 3);
assert_eq!(shortcuts[0].modifiers, vec![Modifier::Control]);
assert_eq!(shortcuts[0].key, Key::A);
assert_eq!(shortcuts[1].modifiers, vec![Modifier::Meta]);
assert_eq!(shortcuts[1].key, Key::Q);
assert_eq!(shortcuts[2].modifiers, vec![Modifier::Shift]);
assert_eq!(shortcuts[2].key, Key::Enter);
}
#[test]
fn test_shortcut_display() {
let shortcut = Shortcut::new(vec![Modifier::Control, Modifier::Shift], Key::A);
assert_eq!(shortcut.to_string(), "Control+Shift+A");
let simple = Shortcut::new(Vec::new(), Key::Enter);
assert_eq!(simple.to_string(), "Enter");
}
#[test]
fn test_shortcut_as_string() {
let shortcut = Shortcut::new(vec![Modifier::Control, Modifier::Shift], Key::A);
assert_eq!(shortcut.as_string(), "Control+Shift+A");
}
}