keyboard-codes 0.2.0

Cross-platform keyboard key code mapping and conversion
Documentation
use crate::error::KeyParseError;
use crate::{Key, Modifier};

/// Represents a parsed keyboard shortcut
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Shortcut {
    /// Modifier keys in the shortcut
    pub modifiers: Vec<Modifier>,
    /// The main key in the shortcut
    pub key: Key,
}

impl Shortcut {
    /// Create a new shortcut
    pub fn new(modifiers: Vec<Modifier>, key: Key) -> Self {
        Self { modifiers, key }
    }

    /// Check if the shortcut has no modifiers
    pub fn is_simple(&self) -> bool {
        self.modifiers.is_empty()
    }

    /// Convert to string representation
    ///
    /// This returns the string representation of the shortcut in the format
    /// "Modifier1+Modifier2+Key" or just "Key" if there are no modifiers.
    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())
    }
}

/// Parse a shortcut combination with alias and case-insensitive support
/// Format: [modifier1+][modifier2+]...+key (e.g., "ctrl+shift+a", "win+enter")
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();

    // Validate format: at least one modifier and one key (length >= 2)
    if parts.len() < 2 {
        return Err(KeyParseError::InvalidShortcutFormat(format!(
            "shortcut must contain at least one modifier and one key: {}",
            shortcut
        )));
    }

    // Parse key part (last element)
    let key_part = parts
        .last()
        .cloned()
        .ok_or_else(|| KeyParseError::InvalidShortcutFormat(shortcut.to_string()))?;

    // Use alias-aware key parsing
    let normalized_key = super::normalize_key_name(key_part);
    let key = crate::mapping::standard::parse_key_ignore_case(normalized_key)?;

    // Parse modifier parts (all elements except the last)
    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))
}

/// Parse shortcut with flexible separator support (supports '+', '-', and space)
pub fn parse_shortcut_flexible(shortcut: &str) -> Result<Shortcut, KeyParseError> {
    if shortcut.is_empty() {
        return Err(KeyParseError::InvalidShortcutFormat(
            "empty shortcut".to_string(),
        ));
    }

    // Replace common separators with '+' for consistent parsing
    let normalized = shortcut.replace(['-', ' '], "+");
    parse_shortcut_with_aliases(&normalized)
}

/// Parse single key or shortcut (auto-detection)
pub fn parse_input(input: &str) -> Result<Shortcut, KeyParseError> {
    if input.is_empty() {
        return Err(KeyParseError::InvalidShortcutFormat(
            "empty input".to_string(),
        ));
    }

    // If contains separators, parse as shortcut
    if input.contains('+') || input.contains('-') || input.contains(' ') {
        parse_shortcut_flexible(input)
    } else {
        // Otherwise parse as single key
        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))
    }
}

/// Parse multiple shortcuts separated by commas
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() {
        // Single key
        let shortcut = parse_input("a").unwrap();
        assert!(shortcut.is_simple());
        assert_eq!(shortcut.key, Key::A);

        // Single key with alias
        let shortcut = parse_input("esc").unwrap();
        assert!(shortcut.is_simple());
        assert_eq!(shortcut.key, Key::Escape);

        // Shortcut
        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");
    }
}