diff --git a/crates/livesplit-hotkey/src/key_code.rs b/crates/livesplit-hotkey/src/key_code.rs index 36c0e993..96af5d0e 100644 --- a/crates/livesplit-hotkey/src/key_code.rs +++ b/crates/livesplit-hotkey/src/key_code.rs @@ -1,3 +1,4 @@ +use alloc::borrow::Cow; use core::str::FromStr; // Based on @@ -261,6 +262,20 @@ pub enum KeyCode { ZoomToggle, } +#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone, serde::Serialize, serde::Deserialize)] +pub enum KeyCodeClass { + WritingSystem, + Functional, + ControlPad, + ArrowPad, + Numpad, + Function, + Media, + Legacy, + Gamepad, + NonStandard, +} + impl KeyCode { /// Resolve the KeyCode according to the standard US layout. pub fn as_str(self) -> &'static str { @@ -482,6 +497,88 @@ impl KeyCode { ZoomToggle => "Zoom Toggle", } } + + pub fn classify(self) -> KeyCodeClass { + use self::KeyCode::*; + match self { + // Writing System Keys + Backquote | Backslash | Backspace | BracketLeft | BracketRight | Comma | Digit0 + | Digit1 | Digit2 | Digit3 | Digit4 | Digit5 | Digit6 | Digit7 | Digit8 | Digit9 + | Equal | IntlBackslash | IntlRo | IntlYen | KeyA | KeyB | KeyC | KeyD | KeyE + | KeyF | KeyG | KeyH | KeyI | KeyJ | KeyK | KeyL | KeyM | KeyN | KeyO | KeyP | KeyQ + | KeyR | KeyS | KeyT | KeyU | KeyV | KeyW | KeyX | KeyY | KeyZ | Minus | Period + | Quote | Semicolon | Slash => KeyCodeClass::WritingSystem, + + // Functional Keys + AltLeft | AltRight | CapsLock | ContextMenu | ControlLeft | ControlRight | Enter + | MetaLeft | MetaRight | ShiftLeft | ShiftRight | Space | Tab | Convert | KanaMode + | Lang1 | Lang2 | Lang3 | Lang4 | Lang5 | NonConvert => KeyCodeClass::Functional, + + // Control Pad Section + Delete | End | Help | Home | Insert | PageDown | PageUp => KeyCodeClass::ControlPad, + + // Arrow Pad Section + ArrowDown | ArrowLeft | ArrowRight | ArrowUp => KeyCodeClass::ArrowPad, + + // Numpad Section + NumLock | Numpad0 | Numpad1 | Numpad2 | Numpad3 | Numpad4 | Numpad5 | Numpad6 + | Numpad7 | Numpad8 | Numpad9 | NumpadAdd | NumpadBackspace | NumpadClear + | NumpadClearEntry | NumpadComma | NumpadDecimal | NumpadDivide | NumpadEnter + | NumpadEqual | NumpadHash | NumpadMemoryAdd | NumpadMemoryClear + | NumpadMemoryRecall | NumpadMemoryStore | NumpadMemorySubtract | NumpadMultiply + | NumpadParenLeft | NumpadParenRight | NumpadStar | NumpadSubtract => { + KeyCodeClass::Numpad + } + + // Function Section + Escape | F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 | F9 | F10 | F11 | F12 | F13 | F14 + | F15 | F16 | F17 | F18 | F19 | F20 | F21 | F22 | F23 | F24 | Fn | FnLock + | PrintScreen | ScrollLock | Pause => KeyCodeClass::Function, + + // Media Keys + BrowserBack | BrowserFavorites | BrowserForward | BrowserHome | BrowserRefresh + | BrowserSearch | BrowserStop | Eject | LaunchApp1 | LaunchApp2 | LaunchMail + | MediaPlayPause | MediaSelect | MediaStop | MediaTrackNext | MediaTrackPrevious + | Power | Sleep | AudioVolumeDown | AudioVolumeMute | AudioVolumeUp | WakeUp => { + KeyCodeClass::Media + } + + // Legacy, Non-Standard and Special Keys + Again | Copy | Cut | Find | Open | Paste | Props | Select | Undo => { + KeyCodeClass::Legacy + } + + // Gamepad Keys + Gamepad0 | Gamepad1 | Gamepad2 | Gamepad3 | Gamepad4 | Gamepad5 | Gamepad6 + | Gamepad7 | Gamepad8 | Gamepad9 | Gamepad10 | Gamepad11 | Gamepad12 | Gamepad13 + | Gamepad14 | Gamepad15 | Gamepad16 | Gamepad17 | Gamepad18 | Gamepad19 => { + KeyCodeClass::Gamepad + } + + // Browser specific Keys + BrightnessDown | BrightnessUp | DisplayToggleIntExt | KeyboardLayoutSelect + | LaunchAssistant | LaunchControlPanel | LaunchScreenSaver | MailForward + | MailReply | MailSend | MediaFastForward | MediaPause | MediaPlay | MediaRecord + | MediaRewind | PrivacyScreenToggle | SelectTask | ShowAllWindows | ZoomToggle => { + KeyCodeClass::NonStandard + } + } + } + + pub fn resolve(self) -> Cow<'static, str> { + let class = self.classify(); + if class == KeyCodeClass::WritingSystem { + if let Some(resolved) = crate::platform::try_resolve(self) { + let uppercase = if resolved != "ß" { + resolved.to_uppercase() + } else { + resolved + }; + return uppercase.into(); + } + } + self.as_str().into() + } } impl FromStr for KeyCode { diff --git a/crates/livesplit-hotkey/src/lib.rs b/crates/livesplit-hotkey/src/lib.rs index 7df74241..42fffef0 100644 --- a/crates/livesplit-hotkey/src/lib.rs +++ b/crates/livesplit-hotkey/src/lib.rs @@ -2,37 +2,39 @@ #![recursion_limit = "1024"] #![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; + cfg_if::cfg_if! { if #[cfg(not(feature = "std"))] { mod other; - pub use self::other::*; + use self::other as platform; } else if #[cfg(windows)] { mod windows; - pub use self::windows::*; + use self::windows as platform; } else if #[cfg(target_os = "linux")] { mod linux; - pub use self::linux::*; + use self::linux as platform; } else if #[cfg(target_os = "macos")] { mod macos; - pub use self::macos::*; + use self::macos as platform; } else if #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] { cfg_if::cfg_if! { if #[cfg(feature = "wasm-web")] { mod wasm_web; - pub use self::wasm_web::*; + use self::wasm_web as platform; } else { mod wasm_unknown; - pub use self::wasm_unknown::*; + use self::wasm_unknown as platform; } } } else { mod other; - pub use self::other::*; + use self::other as platform; } } mod key_code; -pub use self::key_code::*; +pub use self::{key_code::*, platform::*}; #[cfg(test)] mod tests { @@ -56,4 +58,21 @@ mod tests { thread::sleep(Duration::from_secs(5)); hook.unregister(KeyCode::Numpad1).unwrap(); } + + #[test] + fn resolve() { + // Based on German keyboard layout. + println!("ß: {}", KeyCode::Minus.resolve()); + println!("ü: {}", KeyCode::BracketLeft.resolve()); + println!("#: {}", KeyCode::Backslash.resolve()); + println!("+: {}", KeyCode::BracketRight.resolve()); + println!("z: {}", KeyCode::KeyY.resolve()); + println!("^: {}", KeyCode::Backquote.resolve()); + println!("<: {}", KeyCode::IntlBackslash.resolve()); + println!("Yen: {}", KeyCode::IntlYen.resolve()); + println!("Enter: {}", KeyCode::Enter.resolve()); + println!("Space: {}", KeyCode::Space.resolve()); + println!("Tab: {}", KeyCode::Tab.resolve()); + println!("Numpad0: {}", KeyCode::Numpad0.resolve()); + } } diff --git a/crates/livesplit-hotkey/src/linux/mod.rs b/crates/livesplit-hotkey/src/linux/mod.rs index c44e833b..281a2210 100644 --- a/crates/livesplit-hotkey/src/linux/mod.rs +++ b/crates/livesplit-hotkey/src/linux/mod.rs @@ -421,3 +421,7 @@ impl Hook { future.value().ok_or(Error::ThreadStopped)? } } + +pub(crate) fn try_resolve(_key_code: KeyCode) -> Option { + None +} diff --git a/crates/livesplit-hotkey/src/macos/mod.rs b/crates/livesplit-hotkey/src/macos/mod.rs index 98f7e1e6..f3d8826c 100644 --- a/crates/livesplit-hotkey/src/macos/mod.rs +++ b/crates/livesplit-hotkey/src/macos/mod.rs @@ -292,3 +292,7 @@ unsafe extern "C" fn callback( } event } + +pub(crate) fn try_resolve(_key_code: KeyCode) -> Option { + None +} diff --git a/crates/livesplit-hotkey/src/other/mod.rs b/crates/livesplit-hotkey/src/other/mod.rs index d07ae8bf..463b19a6 100644 --- a/crates/livesplit-hotkey/src/other/mod.rs +++ b/crates/livesplit-hotkey/src/other/mod.rs @@ -1,4 +1,5 @@ use crate::KeyCode; +use alloc::string::String; #[derive(Debug, snafu::Snafu)] pub enum Error {} @@ -23,3 +24,7 @@ impl Hook { Ok(()) } } + +pub(crate) fn try_resolve(_key_code: KeyCode) -> Option { + None +} diff --git a/crates/livesplit-hotkey/src/wasm_unknown/mod.rs b/crates/livesplit-hotkey/src/wasm_unknown/mod.rs index 321685b1..b258e68b 100644 --- a/crates/livesplit-hotkey/src/wasm_unknown/mod.rs +++ b/crates/livesplit-hotkey/src/wasm_unknown/mod.rs @@ -91,3 +91,7 @@ impl Hook { } } } + +pub(crate) fn try_resolve(_key_code: KeyCode) -> Option { + None +} diff --git a/crates/livesplit-hotkey/src/wasm_web/mod.rs b/crates/livesplit-hotkey/src/wasm_web/mod.rs index b876ae4b..1c729b34 100644 --- a/crates/livesplit-hotkey/src/wasm_web/mod.rs +++ b/crates/livesplit-hotkey/src/wasm_web/mod.rs @@ -3,7 +3,6 @@ use wasm_bindgen::{prelude::*, JsCast}; use web_sys::{window, Gamepad, GamepadButton, KeyboardEvent}; use std::{ - array, cell::Cell, collections::hash_map::{Entry, HashMap}, sync::{Arc, Mutex}, @@ -82,10 +81,7 @@ impl Hook { }) as Box); window - .add_event_listener_with_callback( - "keydown", - keyboard_callback.as_ref().unchecked_ref(), - ) + .add_event_listener_with_callback("keydown", keyboard_callback.as_ref().unchecked_ref()) .map_err(|_| Error::FailedToCreateHook)?; let hotkey_map = hotkeys.clone(); @@ -162,3 +158,7 @@ impl Hook { } } } + +pub(crate) fn try_resolve(_key_code: KeyCode) -> Option { + None +} diff --git a/crates/livesplit-hotkey/src/windows/mod.rs b/crates/livesplit-hotkey/src/windows/mod.rs index a9f72919..a3999801 100644 --- a/crates/livesplit-hotkey/src/windows/mod.rs +++ b/crates/livesplit-hotkey/src/windows/mod.rs @@ -20,8 +20,9 @@ use winapi::{ libloaderapi::GetModuleHandleW, processthreadsapi::GetCurrentThreadId, winuser::{ - CallNextHookEx, GetMessageW, PostThreadMessageW, SetWindowsHookExW, - UnhookWindowsHookEx, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, WH_KEYBOARD_LL, WM_KEYDOWN, + CallNextHookEx, GetMessageW, MapVirtualKeyW, PostThreadMessageW, SetWindowsHookExW, + UnhookWindowsHookEx, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MAPVK_VK_TO_CHAR, + MAPVK_VSC_TO_VK_EX, WH_KEYBOARD_LL, WM_KEYDOWN, }, }, }; @@ -346,3 +347,78 @@ impl Hook { } } } + +pub(crate) fn try_resolve(key_code: KeyCode) -> Option { + use self::KeyCode::*; + let scan_code = match key_code { + Backquote => 0x0029, + Backslash => 0x002B, + Backspace => 0x000E, + BracketLeft => 0x001A, + BracketRight => 0x001B, + Comma => 0x0033, + Digit1 => 0x0002, + Digit2 => 0x0003, + Digit3 => 0x0004, + Digit4 => 0x0005, + Digit5 => 0x0006, + Digit6 => 0x0007, + Digit7 => 0x0008, + Digit8 => 0x0009, + Digit9 => 0x000A, + Digit0 => 0x000B, + Equal => 0x000D, + IntlBackslash => 0x0056, + IntlRo => 0x0073, + IntlYen => 0x007D, + KeyA => 0x001E, + KeyB => 0x0030, + KeyC => 0x002E, + KeyD => 0x0020, + KeyE => 0x0012, + KeyF => 0x0021, + KeyG => 0x0022, + KeyH => 0x0023, + KeyI => 0x0017, + KeyJ => 0x0024, + KeyK => 0x0025, + KeyL => 0x0026, + KeyM => 0x0032, + KeyN => 0x0031, + KeyO => 0x0018, + KeyP => 0x0019, + KeyQ => 0x0010, + KeyR => 0x0013, + KeyS => 0x001F, + KeyT => 0x0014, + KeyU => 0x0016, + KeyV => 0x002F, + KeyW => 0x0011, + KeyX => 0x002D, + KeyY => 0x0015, + KeyZ => 0x002C, + Minus => 0x000C, + Period => 0x0034, + Quote => 0x0028, + Semicolon => 0x0027, + Slash => 0x0035, + _ => return None, + }; + + let virtual_key_code = unsafe { MapVirtualKeyW(scan_code, MAPVK_VSC_TO_VK_EX) }; + if virtual_key_code == 0 { + return None; + } + + let mapped_char = unsafe { MapVirtualKeyW(virtual_key_code, MAPVK_VK_TO_CHAR) }; + if mapped_char == 0 { + return None; + } + + // Dead keys (diacritics) are indicated by setting the top bit of the return + // value. + const TOP_BIT_MASK: u32 = u32::MAX >> 1; + let char = mapped_char & TOP_BIT_MASK; + + Some(char::from_u32(char)?.to_string()) +} diff --git a/src/settings/image/mod.rs b/src/settings/image/mod.rs index 3b70714f..869a6d86 100644 --- a/src/settings/image/mod.rs +++ b/src/settings/image/mod.rs @@ -1,6 +1,9 @@ use crate::platform::prelude::*; use base64::{display::Base64Display, STANDARD}; -use core::{ops::Deref, sync::atomic::{AtomicUsize, Ordering}}; +use core::{ + ops::Deref, + sync::atomic::{AtomicUsize, Ordering}, +}; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; #[cfg(test)]