keyboard-codes 0.2.0

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

/// Standard key mapping table (string -> key -> multi-platform codes)
/// Structure: (key_string, key_enum, windows_code, linux_code, macos_code)
pub const STANDARD_KEY_MAPPINGS: &[(&str, Key, usize, usize, usize)] = &[
    ("Escape", Key::Escape, 0x1B, 1, 53),
    ("Enter", Key::Enter, 0x0D, 28, 36),
    ("Tab", Key::Tab, 0x09, 15, 48),
    ("Backspace", Key::Backspace, 0x08, 14, 51),
    ("Space", Key::Space, 0x20, 57, 49),
    ("Insert", Key::Insert, 0x2D, 110, 114),
    ("Delete", Key::Delete, 0x2E, 111, 117),
    ("Home", Key::Home, 0x24, 102, 115),
    ("End", Key::End, 0x23, 107, 119),
    ("PageUp", Key::PageUp, 0x21, 104, 116),
    ("PageDown", Key::PageDown, 0x22, 109, 121),
    ("ArrowLeft", Key::ArrowLeft, 0x25, 105, 123),
    ("ArrowUp", Key::ArrowUp, 0x26, 103, 126),
    ("ArrowRight", Key::ArrowRight, 0x27, 106, 124),
    ("ArrowDown", Key::ArrowDown, 0x28, 108, 125),
    ("A", Key::A, 0x41, 30, 0),
    ("B", Key::B, 0x42, 48, 11),
    ("C", Key::C, 0x43, 46, 8),
    ("D", Key::D, 0x44, 32, 2),
    ("E", Key::E, 0x45, 18, 14),
    ("F", Key::F, 0x46, 33, 3),
    ("G", Key::G, 0x47, 34, 5),
    ("H", Key::H, 0x48, 35, 4),
    ("I", Key::I, 0x49, 23, 34),
    ("J", Key::J, 0x4A, 36, 38),
    ("K", Key::K, 0x4B, 37, 40),
    ("L", Key::L, 0x4C, 38, 37),
    ("M", Key::M, 0x4D, 50, 46),
    ("N", Key::N, 0x4E, 49, 45),
    ("O", Key::O, 0x4F, 24, 31),
    ("P", Key::P, 0x50, 25, 35),
    ("Q", Key::Q, 0x51, 16, 12),
    ("R", Key::R, 0x52, 19, 15),
    ("S", Key::S, 0x53, 31, 1),
    ("T", Key::T, 0x54, 20, 17),
    ("U", Key::U, 0x55, 22, 32),
    ("V", Key::V, 0x56, 47, 9),
    ("W", Key::W, 0x57, 17, 13),
    ("X", Key::X, 0x58, 45, 7),
    ("Y", Key::Y, 0x59, 21, 16),
    ("Z", Key::Z, 0x5A, 44, 6),
    ("D0", Key::D0, 0x30, 11, 29),
    ("D1", Key::D1, 0x31, 2, 18),
    ("D2", Key::D2, 0x32, 3, 19),
    ("D3", Key::D3, 0x33, 4, 20),
    ("D4", Key::D4, 0x34, 5, 21),
    ("D5", Key::D5, 0x35, 6, 23),
    ("D6", Key::D6, 0x36, 7, 22),
    ("D7", Key::D7, 0x37, 8, 26),
    ("D8", Key::D8, 0x38, 9, 28),
    ("D9", Key::D9, 0x39, 10, 25),
    ("Num0", Key::Num0, 0x60, 82, 82),
    ("Num1", Key::Num1, 0x61, 79, 83),
    ("Num2", Key::Num2, 0x62, 80, 84),
    ("Num3", Key::Num3, 0x63, 81, 85),
    ("Num4", Key::Num4, 0x64, 75, 86),
    ("Num5", Key::Num5, 0x65, 76, 87),
    ("Num6", Key::Num6, 0x66, 77, 88),
    ("Num7", Key::Num7, 0x67, 71, 89),
    ("Num8", Key::Num8, 0x68, 72, 91),
    ("Num9", Key::Num9, 0x69, 73, 92),
    ("NumMultiply", Key::NumMultiply, 0x6A, 55, 67),
    ("NumAdd", Key::NumAdd, 0x6B, 78, 69),
    ("NumSubtract", Key::NumSubtract, 0x6D, 74, 78),
    ("NumDivide", Key::NumDivide, 0x6F, 53, 75),
    ("NumDecimal", Key::NumDecimal, 0x6E, 83, 65),
    ("F1", Key::F1, 0x70, 59, 122),
    ("F2", Key::F2, 0x71, 60, 120),
    ("F3", Key::F3, 0x72, 61, 99),
    ("F4", Key::F4, 0x73, 62, 118),
    ("F5", Key::F5, 0x74, 63, 96),
    ("F6", Key::F6, 0x75, 64, 97),
    ("F7", Key::F7, 0x76, 65, 98),
    ("F8", Key::F8, 0x77, 66, 100),
    ("F9", Key::F9, 0x78, 67, 101),
    ("F10", Key::F10, 0x79, 68, 109),
    ("F11", Key::F11, 0x7A, 87, 103),
    ("F12", Key::F12, 0x7B, 88, 111),
    ("F13", Key::F13, 0x7C, 183, 104),
    ("F14", Key::F14, 0x7D, 184, 105),
    ("F15", Key::F15, 0x7E, 185, 106),
    ("F16", Key::F16, 0x7F, 186, 107),
    ("F17", Key::F17, 0x80, 187, 108),
    ("F18", Key::F18, 0x81, 188, 110),
    ("F19", Key::F19, 0x82, 189, 112),
    ("F20", Key::F20, 0x83, 190, 113),
    ("F21", Key::F21, 0x84, 191, 102),
    ("F22", Key::F22, 0x85, 192, 121),
    ("F23", Key::F23, 0x86, 193, 122),
    ("F24", Key::F24, 0x87, 194, 123),
    ("CapsLock", Key::CapsLock, 0x14, 58, 57),
    ("NumLock", Key::NumLock, 0x90, 69, 71),
    ("ScrollLock", Key::ScrollLock, 0x91, 70, 72),
    ("Pause", Key::Pause, 0x13, 119, 117),
    ("Apps", Key::Apps, 0x5D, 135, 110),
    ("Sleep", Key::Sleep, 0x5F, 142, 142),
    ("MediaPlayPause", Key::MediaPlayPause, 0xB3, 164, 163),
    ("MediaStop", Key::MediaStop, 0xB2, 165, 165),
    ("MediaNext", Key::MediaNext, 0xB0, 163, 171),
    ("MediaPrevious", Key::MediaPrevious, 0xB1, 166, 172),
    ("VolumeUp", Key::VolumeUp, 0xAF, 123, 126),
    ("VolumeDown", Key::VolumeDown, 0xAE, 122, 125),
    ("VolumeMute", Key::VolumeMute, 0xAD, 121, 127),
    ("BrowserBack", Key::BrowserBack, 0xA6, 166, 178),
    ("BrowserForward", Key::BrowserForward, 0xA7, 167, 179),
    ("BrowserRefresh", Key::BrowserRefresh, 0xA8, 168, 177),
    ("BrowserHome", Key::BrowserHome, 0xAC, 178, 174),
];

/// Standard modifier key mapping table
pub const STANDARD_MODIFIER_MAPPINGS: &[(&str, Modifier, usize, usize, usize)] = &[
    ("Alt", Modifier::Alt, 0x12, 56, 61),
    ("Control", Modifier::Control, 0x11, 29, 59),
    ("Shift", Modifier::Shift, 0x10, 42, 60),
    ("Meta", Modifier::Meta, 0x5B, 125, 55),
    ("LeftAlt", Modifier::LeftAlt, 0xA4, 56, 61),
    ("RightAlt", Modifier::RightAlt, 0xA5, 100, 61),
    ("LeftControl", Modifier::LeftControl, 0xA2, 29, 59),
    ("RightControl", Modifier::RightControl, 0xA3, 97, 59),
    ("LeftShift", Modifier::LeftShift, 0xA0, 42, 60),
    ("RightShift", Modifier::RightShift, 0xA1, 54, 60),
    ("LeftMeta", Modifier::LeftMeta, 0x5B, 125, 55),
    ("RightMeta", Modifier::RightMeta, 0x5C, 126, 55),
];

/// Parse a key from a string using standard mappings (case-sensitive)
pub fn parse_key_from_str(s: &str) -> Result<Key, KeyParseError> {
    STANDARD_KEY_MAPPINGS
        .iter()
        .find(|(name, _, _, _, _)| *name == s)
        .map(|(_, key, _, _, _)| *key)
        .ok_or_else(|| KeyParseError::UnknownKey(s.to_string()))
}

/// Parse a modifier from a string using standard mappings (case-sensitive)
pub fn parse_modifier_from_str(s: &str) -> Result<Modifier, KeyParseError> {
    STANDARD_MODIFIER_MAPPINGS
        .iter()
        .find(|(name, _, _, _, _)| *name == s)
        .map(|(_, modifier, _, _, _)| *modifier)
        .ok_or_else(|| KeyParseError::UnknownModifier(s.to_string()))
}

/// Parse a key from a string with case-insensitive support
pub fn parse_key_ignore_case(s: &str) -> Result<Key, KeyParseError> {
    STANDARD_KEY_MAPPINGS
        .iter()
        .find(|(name, _, _, _, _)| name.eq_ignore_ascii_case(s))
        .map(|(_, key, _, _, _)| *key)
        .ok_or_else(|| KeyParseError::UnknownKey(s.to_string()))
}

/// Parse a modifier from a string with case-insensitive support
pub fn parse_modifier_ignore_case(s: &str) -> Result<Modifier, KeyParseError> {
    STANDARD_MODIFIER_MAPPINGS
        .iter()
        .find(|(name, _, _, _, _)| name.eq_ignore_ascii_case(s))
        .map(|(_, modifier, _, _, _)| *modifier)
        .ok_or_else(|| KeyParseError::UnknownModifier(s.to_string()))
}

/// Convert a key to a platform-specific code using standard mappings
pub fn key_to_code(key: Key, platform: Platform) -> usize {
    STANDARD_KEY_MAPPINGS
        .iter()
        .find(|(_, k, _, _, _)| k == &key)
        .map(|(_, _, win, linux, mac)| match platform {
            Platform::Windows => *win,
            Platform::Linux => *linux,
            Platform::MacOS => *mac,
        })
        .unwrap_or_else(|| panic!("Unsupported key in standard mapping: {:?}", key))
}

/// Convert a modifier to a platform-specific code using standard mappings
pub fn modifier_to_code(modifier: Modifier, platform: Platform) -> usize {
    STANDARD_MODIFIER_MAPPINGS
        .iter()
        .find(|(_, m, _, _, _)| m == &modifier)
        .map(|(_, _, win, linux, mac)| match platform {
            Platform::Windows => *win,
            Platform::Linux => *linux,
            Platform::MacOS => *mac,
        })
        .unwrap_or_else(|| panic!("Unsupported modifier in standard mapping: {:?}", modifier))
}

/// Parse a key from a platform-specific code using standard mappings
pub fn key_from_code(code: usize, platform: Platform) -> Option<Key> {
    STANDARD_KEY_MAPPINGS
        .iter()
        .find(|(_, _, win, linux, mac)| match platform {
            Platform::Windows => *win == code,
            Platform::Linux => *linux == code,
            Platform::MacOS => *mac == code,
        })
        .map(|(_, key, _, _, _)| *key)
}

/// Parse a modifier from a platform-specific code using standard mappings
pub fn modifier_from_code(code: usize, platform: Platform) -> Option<Modifier> {
    STANDARD_MODIFIER_MAPPINGS
        .iter()
        .find(|(_, _, win, linux, mac)| match platform {
            Platform::Windows => *win == code,
            Platform::Linux => *linux == code,
            Platform::MacOS => *mac == code,
        })
        .map(|(_, modifier, _, _, _)| *modifier)
}

// Implement KeyCodeMapper for Key
impl crate::types::KeyCodeMapper for Key {
    fn to_code(&self, platform: Platform) -> usize {
        key_to_code(*self, platform)
    }

    fn from_code(code: usize, platform: Platform) -> Option<Self> {
        key_from_code(code, platform)
    }
}

// Implement KeyCodeMapper for Modifier
impl crate::types::KeyCodeMapper for Modifier {
    fn to_code(&self, platform: Platform) -> usize {
        modifier_to_code(*self, platform)
    }

    fn from_code(code: usize, platform: Platform) -> Option<Self> {
        modifier_from_code(code, platform)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_key_from_str() {
        assert_eq!(parse_key_from_str("Escape").unwrap(), Key::Escape);
        assert_eq!(parse_key_from_str("A").unwrap(), Key::A);
        assert!(parse_key_from_str("UnknownKey").is_err());
    }

    #[test]
    fn test_parse_modifier_from_str() {
        assert_eq!(parse_modifier_from_str("Shift").unwrap(), Modifier::Shift);
        assert_eq!(
            parse_modifier_from_str("Control").unwrap(),
            Modifier::Control
        );
        assert!(parse_modifier_from_str("UnknownModifier").is_err());
    }

    #[test]
    fn test_parse_key_ignore_case() {
        assert_eq!(parse_key_ignore_case("escape").unwrap(), Key::Escape);
        assert_eq!(parse_key_ignore_case("ESCAPE").unwrap(), Key::Escape);
        assert_eq!(parse_key_ignore_case("Escape").unwrap(), Key::Escape);
        assert_eq!(parse_key_ignore_case("a").unwrap(), Key::A);
        assert_eq!(parse_key_ignore_case("A").unwrap(), Key::A);
    }

    #[test]
    fn test_parse_modifier_ignore_case() {
        assert_eq!(
            parse_modifier_ignore_case("shift").unwrap(),
            Modifier::Shift
        );
        assert_eq!(
            parse_modifier_ignore_case("SHIFT").unwrap(),
            Modifier::Shift
        );
        assert_eq!(
            parse_modifier_ignore_case("Shift").unwrap(),
            Modifier::Shift
        );
        assert_eq!(
            parse_modifier_ignore_case("control").unwrap(),
            Modifier::Control
        );
        assert_eq!(
            parse_modifier_ignore_case("CONTROL").unwrap(),
            Modifier::Control
        );
    }

    #[test]
    fn test_key_to_code() {
        assert_eq!(key_to_code(Key::Enter, Platform::Windows), 0x0D);
        assert_eq!(key_to_code(Key::Enter, Platform::Linux), 28);
        assert_eq!(key_to_code(Key::Enter, Platform::MacOS), 36);
    }

    #[test]
    fn test_key_from_code() {
        assert_eq!(key_from_code(0x1B, Platform::Windows), Some(Key::Escape));
        assert_eq!(key_from_code(1, Platform::Linux), Some(Key::Escape));
        assert_eq!(key_from_code(53, Platform::MacOS), Some(Key::Escape));
    }

    #[test]
    fn test_key_code_mapper_impl() {
        use crate::types::KeyCodeMapper;

        let key = Key::Enter;
        assert_eq!(key.to_code(Platform::Windows), 0x0D);
        assert_eq!(Key::from_code(0x0D, Platform::Windows), Some(Key::Enter));
    }
}