Skip to content

Commit

Permalink
Implement Resolving Hotkeys on Windows
Browse files Browse the repository at this point in the history
This allows resolving a `KeyCode` to the key on the user's current
keyboard layout. This is useful for visualizing the hotkeys in a user
interface. For now this is only the implementation for Windows. On all
other platforms it falls back to the US keyboard layout.
  • Loading branch information
CryZe committed Oct 24, 2021
1 parent 04f0391 commit a202f83
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 15 deletions.
99 changes: 99 additions & 0 deletions crates/livesplit-hotkey/src/key_code.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use alloc::borrow::Cow;
#[cfg(not(feature = "std"))]
use alloc::string::String;
use core::str::FromStr;

// Based on
Expand Down Expand Up @@ -261,6 +264,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 {
Expand Down Expand Up @@ -482,6 +499,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 {
Expand Down
35 changes: 27 additions & 8 deletions crates/livesplit-hotkey/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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());
}
}
4 changes: 4 additions & 0 deletions crates/livesplit-hotkey/src/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,7 @@ impl Hook {
future.value().ok_or(Error::ThreadStopped)?
}
}

pub(crate) fn try_resolve(_key_code: KeyCode) -> Option<String> {
None
}
4 changes: 4 additions & 0 deletions crates/livesplit-hotkey/src/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,7 @@ unsafe extern "C" fn callback(
}
event
}

pub(crate) fn try_resolve(_key_code: KeyCode) -> Option<String> {
None
}
5 changes: 5 additions & 0 deletions crates/livesplit-hotkey/src/other/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::KeyCode;
use alloc::string::String;

#[derive(Debug, snafu::Snafu)]
pub enum Error {}
Expand All @@ -23,3 +24,7 @@ impl Hook {
Ok(())
}
}

pub(crate) fn try_resolve(_key_code: KeyCode) -> Option<String> {
None
}
4 changes: 4 additions & 0 deletions crates/livesplit-hotkey/src/wasm_unknown/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,7 @@ impl Hook {
}
}
}

pub(crate) fn try_resolve(_key_code: KeyCode) -> Option<String> {
None
}
10 changes: 5 additions & 5 deletions crates/livesplit-hotkey/src/wasm_web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -82,10 +81,7 @@ impl Hook {
}) as Box<dyn FnMut(KeyboardEvent)>);

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();
Expand Down Expand Up @@ -162,3 +158,7 @@ impl Hook {
}
}
}

pub(crate) fn try_resolve(_key_code: KeyCode) -> Option<String> {
None
}
80 changes: 78 additions & 2 deletions crates/livesplit-hotkey/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
};
Expand Down Expand Up @@ -346,3 +347,78 @@ impl Hook {
}
}
}

pub(crate) fn try_resolve(key_code: KeyCode) -> Option<String> {
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())
}

0 comments on commit a202f83

Please sign in to comment.