From 474eae69d516bfb398c01f7382e1103b72a11d3f Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 14 Mar 2021 18:24:37 +0100 Subject: [PATCH] Add hotkey detection implementation to Windows --- espanso-detect/Cargo.toml | 4 +- espanso-detect/src/hotkey/keys.rs | 171 ++++++++++++++-------------- espanso-detect/src/hotkey/mod.rs | 50 +++++--- espanso-detect/src/lib.rs | 5 +- espanso-detect/src/win32/mod.rs | 73 +++++++++++- espanso-detect/src/win32/native.cpp | 15 +++ espanso-detect/src/win32/native.h | 13 ++- espanso/src/main.rs | 2 +- 8 files changed, 220 insertions(+), 113 deletions(-) diff --git a/espanso-detect/Cargo.toml b/espanso-detect/Cargo.toml index 4f78968..b043be7 100644 --- a/espanso-detect/Cargo.toml +++ b/espanso-detect/Cargo.toml @@ -16,6 +16,7 @@ lazycell = "1.3.0" anyhow = "1.0.38" thiserror = "1.0.23" regex = "1.4.3" +lazy_static = "1.4.0" [target.'cfg(windows)'.dependencies] widestring = "0.4.3" @@ -24,9 +25,6 @@ widestring = "0.4.3" libc = "0.2.85" scopeguard = "1.1.0" -[target.'cfg(target_os="macos")'.dependencies] -lazy_static = "1.4.0" - [build-dependencies] cc = "1.0.66" diff --git a/espanso-detect/src/hotkey/keys.rs b/espanso-detect/src/hotkey/keys.rs index 456cbc3..f430cfa 100644 --- a/espanso-detect/src/hotkey/keys.rs +++ b/espanso-detect/src/hotkey/keys.rs @@ -415,94 +415,89 @@ impl ShortcutKey { #[cfg(target_os = "windows")] pub fn to_code(&self) -> Option { let vkey = match self { - Key::Alt => 0x12, - Key::CapsLock => 0x14, - Key::Control => 0x11, - Key::Meta => 0x5B, - Key::NumLock => 0x90, - Key::Shift => 0xA0, - Key::Enter => 0x0D, - Key::Tab => 0x09, - Key::Space => 0x20, - Key::ArrowDown => 0x28, - Key::ArrowLeft => 0x25, - Key::ArrowRight => 0x27, - Key::ArrowUp => 0x26, - Key::End => 0x23, - Key::Home => 0x24, - Key::PageDown => 0x22, - Key::PageUp => 0x21, - Key::Escape => 0x1B, - Key::Backspace => 0x08, - Key::Insert => 0x2D, - Key::Delete => 0x2E, - Key::F1 => 0x70, - Key::F2 => 0x71, - Key::F3 => 0x72, - Key::F4 => 0x73, - Key::F5 => 0x74, - Key::F6 => 0x75, - Key::F7 => 0x76, - Key::F8 => 0x77, - Key::F9 => 0x78, - Key::F10 => 0x79, - Key::F11 => 0x7A, - Key::F12 => 0x7B, - Key::F13 => 0x7C, - Key::F14 => 0x7D, - Key::F15 => 0x7E, - Key::F16 => 0x7F, - Key::F17 => 0x80, - Key::F18 => 0x81, - Key::F19 => 0x82, - Key::F20 => 0x83, - Key::A => 0x41, - Key::B => 0x42, - Key::C => 0x43, - Key::D => 0x44, - Key::E => 0x45, - Key::F => 0x46, - Key::G => 0x47, - Key::H => 0x48, - Key::I => 0x49, - Key::J => 0x4A, - Key::K => 0x4B, - Key::L => 0x4C, - Key::M => 0x4D, - Key::N => 0x4E, - Key::O => 0x4F, - Key::P => 0x50, - Key::Q => 0x51, - Key::R => 0x52, - Key::S => 0x53, - Key::T => 0x54, - Key::U => 0x55, - Key::V => 0x56, - Key::W => 0x57, - Key::X => 0x58, - Key::Y => 0x59, - Key::Z => 0x5A, - Key::N0 => 0x30, - Key::N1 => 0x31, - Key::N2 => 0x32, - Key::N3 => 0x33, - Key::N4 => 0x34, - Key::N5 => 0x35, - Key::N6 => 0x36, - Key::N7 => 0x37, - Key::N8 => 0x38, - Key::N9 => 0x39, - Key::Numpad0 => 0x60, - Key::Numpad1 => 0x61, - Key::Numpad2 => 0x62, - Key::Numpad3 => 0x63, - Key::Numpad4 => 0x64, - Key::Numpad5 => 0x65, - Key::Numpad6 => 0x66, - Key::Numpad7 => 0x67, - Key::Numpad8 => 0x68, - Key::Numpad9 => 0x69, - Key::Raw(code) => *code, + ShortcutKey::Alt => 0x12, + ShortcutKey::Control => 0x11, + ShortcutKey::Meta => 0x5B, + ShortcutKey::Shift => 0xA0, + ShortcutKey::Enter => 0x0D, + ShortcutKey::Tab => 0x09, + ShortcutKey::Space => 0x20, + ShortcutKey::ArrowDown => 0x28, + ShortcutKey::ArrowLeft => 0x25, + ShortcutKey::ArrowRight => 0x27, + ShortcutKey::ArrowUp => 0x26, + ShortcutKey::End => 0x23, + ShortcutKey::Home => 0x24, + ShortcutKey::PageDown => 0x22, + ShortcutKey::PageUp => 0x21, + ShortcutKey::Insert => 0x2D, + ShortcutKey::F1 => 0x70, + ShortcutKey::F2 => 0x71, + ShortcutKey::F3 => 0x72, + ShortcutKey::F4 => 0x73, + ShortcutKey::F5 => 0x74, + ShortcutKey::F6 => 0x75, + ShortcutKey::F7 => 0x76, + ShortcutKey::F8 => 0x77, + ShortcutKey::F9 => 0x78, + ShortcutKey::F10 => 0x79, + ShortcutKey::F11 => 0x7A, + ShortcutKey::F12 => 0x7B, + ShortcutKey::F13 => 0x7C, + ShortcutKey::F14 => 0x7D, + ShortcutKey::F15 => 0x7E, + ShortcutKey::F16 => 0x7F, + ShortcutKey::F17 => 0x80, + ShortcutKey::F18 => 0x81, + ShortcutKey::F19 => 0x82, + ShortcutKey::F20 => 0x83, + ShortcutKey::A => 0x41, + ShortcutKey::B => 0x42, + ShortcutKey::C => 0x43, + ShortcutKey::D => 0x44, + ShortcutKey::E => 0x45, + ShortcutKey::F => 0x46, + ShortcutKey::G => 0x47, + ShortcutKey::H => 0x48, + ShortcutKey::I => 0x49, + ShortcutKey::J => 0x4A, + ShortcutKey::K => 0x4B, + ShortcutKey::L => 0x4C, + ShortcutKey::M => 0x4D, + ShortcutKey::N => 0x4E, + ShortcutKey::O => 0x4F, + ShortcutKey::P => 0x50, + ShortcutKey::Q => 0x51, + ShortcutKey::R => 0x52, + ShortcutKey::S => 0x53, + ShortcutKey::T => 0x54, + ShortcutKey::U => 0x55, + ShortcutKey::V => 0x56, + ShortcutKey::W => 0x57, + ShortcutKey::X => 0x58, + ShortcutKey::Y => 0x59, + ShortcutKey::Z => 0x5A, + ShortcutKey::N0 => 0x30, + ShortcutKey::N1 => 0x31, + ShortcutKey::N2 => 0x32, + ShortcutKey::N3 => 0x33, + ShortcutKey::N4 => 0x34, + ShortcutKey::N5 => 0x35, + ShortcutKey::N6 => 0x36, + ShortcutKey::N7 => 0x37, + ShortcutKey::N8 => 0x38, + ShortcutKey::N9 => 0x39, + ShortcutKey::Numpad0 => 0x60, + ShortcutKey::Numpad1 => 0x61, + ShortcutKey::Numpad2 => 0x62, + ShortcutKey::Numpad3 => 0x63, + ShortcutKey::Numpad4 => 0x64, + ShortcutKey::Numpad5 => 0x65, + ShortcutKey::Numpad6 => 0x66, + ShortcutKey::Numpad7 => 0x67, + ShortcutKey::Numpad8 => 0x68, + ShortcutKey::Numpad9 => 0x69, + ShortcutKey::Raw(code) => *code, }; Some(vkey) } diff --git a/espanso-detect/src/hotkey/mod.rs b/espanso-detect/src/hotkey/mod.rs index 2bd2694..415e0a6 100644 --- a/espanso-detect/src/hotkey/mod.rs +++ b/espanso-detect/src/hotkey/mod.rs @@ -19,12 +19,18 @@ pub mod keys; +use std::fmt::Display; use anyhow::Result; use keys::ShortcutKey; use thiserror::Error; -static MODIFIERS: &[ShortcutKey; 4] = &[ShortcutKey::Control, ShortcutKey::Alt, ShortcutKey::Shift, ShortcutKey::Meta]; +static MODIFIERS: &[ShortcutKey; 4] = &[ + ShortcutKey::Control, + ShortcutKey::Alt, + ShortcutKey::Shift, + ShortcutKey::Meta, +]; #[derive(Debug, PartialEq, Clone)] pub struct HotKey { @@ -33,7 +39,6 @@ pub struct HotKey { pub modifiers: Vec, } -// TODO: test all methods impl HotKey { pub fn new(id: i32, shortcut: &str) -> Result { let tokens: Vec = shortcut @@ -85,6 +90,19 @@ impl HotKey { } } +impl Display for HotKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str_modifiers: Vec = self.modifiers.iter().map(|m| m.to_string()).collect(); + let modifiers = str_modifiers.join("+"); + write!( + f, + "{}+{}", + &modifiers, + &self.key + ) + } +} + #[derive(Error, Debug)] pub enum HotKeyError { #[error("invalid hotkey shortcut, `{0}` is not a valid key")] @@ -100,16 +118,22 @@ mod tests { #[test] fn parse_correctly() { - assert_eq!(HotKey::new(1, "CTRL+V").unwrap(), HotKey { - id: 1, - key: ShortcutKey::V, - modifiers: vec![ShortcutKey::Control], - }); - assert_eq!(HotKey::new(2, "SHIFT + Ctrl + v").unwrap(), HotKey { - id: 2, - key: ShortcutKey::V, - modifiers: vec![ShortcutKey::Shift, ShortcutKey::Control], - }); + assert_eq!( + HotKey::new(1, "CTRL+V").unwrap(), + HotKey { + id: 1, + key: ShortcutKey::V, + modifiers: vec![ShortcutKey::Control], + } + ); + assert_eq!( + HotKey::new(2, "SHIFT + Ctrl + v").unwrap(), + HotKey { + id: 2, + key: ShortcutKey::V, + modifiers: vec![ShortcutKey::Shift, ShortcutKey::Control], + } + ); assert!(HotKey::new(3, "invalid").is_err()); } @@ -124,4 +148,4 @@ mod tests { assert!(!HotKey::new(1, "SHIFT+ V").unwrap().has_alt()); assert!(!HotKey::new(1, "SHIFT+ V").unwrap().has_meta()); } -} \ No newline at end of file +} diff --git a/espanso-detect/src/lib.rs b/espanso-detect/src/lib.rs index b0c4b89..117c33b 100644 --- a/espanso-detect/src/lib.rs +++ b/espanso-detect/src/lib.rs @@ -37,7 +37,6 @@ pub mod evdev; #[cfg(target_os = "macos")] pub mod mac; -#[cfg(target_os = "macos")] #[macro_use] extern crate lazy_static; @@ -85,9 +84,9 @@ impl Default for SourceCreationOptions { } #[cfg(target_os = "windows")] -pub fn get_source(_options: SourceCreationOptions) -> Result> { +pub fn get_source(options: SourceCreationOptions) -> Result> { info!("using Win32Source"); - Ok(Box::new(win32::Win32Source::new())) + Ok(Box::new(win32::Win32Source::new(&options.hotkeys))) } #[cfg(target_os = "macos")] diff --git a/espanso-detect/src/win32/mod.rs b/espanso-detect/src/win32/mod.rs index 3378940..0680cd6 100644 --- a/espanso-detect/src/win32/mod.rs +++ b/espanso-detect/src/win32/mod.rs @@ -17,16 +17,16 @@ * along with espanso. If not, see . */ -use std::ffi::c_void; +use std::{convert::TryInto, ffi::c_void}; use lazycell::LazyCell; -use log::{error, trace, warn}; +use log::{debug, error, trace, warn}; use widestring::U16CStr; use anyhow::Result; use thiserror::Error; -use crate::event::Variant::*; +use crate::{event::{HotKeyEvent, Variant::*}, hotkey::{HotKey}}; use crate::event::{InputEvent, Key, KeyboardEvent, Variant}; use crate::event::{Key::*, MouseButton, MouseEvent}; use crate::{event::Status::*, Source, SourceCallback}; @@ -36,6 +36,7 @@ const INPUT_RIGHT_VARIANT: i32 = 2; const INPUT_EVENT_TYPE_KEYBOARD: i32 = 1; const INPUT_EVENT_TYPE_MOUSE: i32 = 2; +const INPUT_EVENT_TYPE_HOTKEY: i32 = 3; const INPUT_STATUS_PRESSED: i32 = 1; const INPUT_STATUS_RELEASED: i32 = 2; @@ -62,10 +63,18 @@ pub struct RawInputEvent { pub status: i32, } +#[repr(C)] +pub struct RawHotKey { + pub id: i32, + pub code: u32, + pub flags: u32, +} + #[allow(improper_ctypes)] #[link(name = "espansodetect", kind = "static")] extern "C" { pub fn detect_initialize(_self: *const Win32Source, error_code: *mut i32) -> *mut c_void; + pub fn detect_register_hotkey(window: *const c_void, hotkey: RawHotKey) -> i32; pub fn detect_eventloop( window: *const c_void, @@ -78,20 +87,24 @@ extern "C" { pub struct Win32Source { handle: *mut c_void, callback: LazyCell, + hotkeys: Vec, } #[allow(clippy::new_without_default)] impl Win32Source { - pub fn new() -> Win32Source { + pub fn new(hotkeys: &[HotKey]) -> Win32Source { Self { handle: std::ptr::null_mut(), callback: LazyCell::new(), + hotkeys: hotkeys.to_vec(), } } } impl Source for Win32Source { fn initialize(&mut self) -> Result<()> { + + let mut error_code = 0; let handle = unsafe { detect_initialize(self as *const Win32Source, &mut error_code) }; @@ -104,6 +117,23 @@ impl Source for Win32Source { return Err(error.into()); } + // Register the hotkeys + self + .hotkeys + .iter() + .for_each(|hk| { + let raw = convert_hotkey_to_raw(&hk); + if let Some(raw_hk) = raw { + if unsafe { detect_register_hotkey(handle, raw_hk) } == 0 { + error!("unable to register hotkey: {}", hk); + } else { + debug!("registered hotkey: {}", hk); + } + } else { + error!("unable to generate raw hotkey mapping: {}", hk); + } + }); + self.handle = handle; Ok(()) @@ -156,6 +186,35 @@ impl Drop for Win32Source { } } +fn convert_hotkey_to_raw(hk: &HotKey) -> Option { + let key_code = hk.key.to_code()?; + let code: Result = key_code.try_into(); + if let Ok(code) = code { + let mut flags = 0x4000; // NOREPEAT flags + if hk.has_ctrl() { + flags |= 0x0002; + } + if hk.has_alt() { + flags |= 0x0001; + } + if hk.has_meta() { + flags |= 0x0008; + } + if hk.has_shift() { + flags |= 0x0004; + } + + Some(RawHotKey { + id: hk.id, + code, + flags, + }) + } else { + error!("unable to generate raw hotkey, the key_code is overflowing"); + None + } +} + #[derive(Error, Debug)] pub enum Win32SourceError { #[error("window registration failed")] @@ -225,6 +284,12 @@ impl From for Option { return Some(InputEvent::Mouse(MouseEvent { button, status })); } } + // Hotkey events + INPUT_EVENT_TYPE_HOTKEY => { + return Some(InputEvent::HotKey(HotKeyEvent { + hotkey_id: raw.key_code + })) + } _ => {} } diff --git a/espanso-detect/src/win32/native.cpp b/espanso-detect/src/win32/native.cpp index 51065ec..04a5ad6 100644 --- a/espanso-detect/src/win32/native.cpp +++ b/espanso-detect/src/win32/native.cpp @@ -75,6 +75,17 @@ LRESULT CALLBACK detect_window_procedure(HWND window, unsigned int msg, WPARAM w SetWindowLongPtrW(window, GWLP_USERDATA, NULL); return 0L; + case WM_HOTKEY: // Hotkeys + { + InputEvent event = {}; + event.event_type = INPUT_EVENT_TYPE_HOTKEY; + event.key_code = (int32_t) wp; + if (variables->rust_instance != NULL && variables->event_callback != NULL) + { + variables->event_callback(variables->rust_instance, event); + } + break; + } case WM_INPUT: // Message relative to the RAW INPUT events { InputEvent event = {}; @@ -322,6 +333,10 @@ void * detect_initialize(void *_self, int32_t *error_code) return window; } +int32_t detect_register_hotkey(void * window, HotKey hotkey) { + return RegisterHotKey((HWND)window, hotkey.hk_id, hotkey.flags, hotkey.key_code); +} + int32_t detect_eventloop(void * window, EventCallback _callback) { if (window) diff --git a/espanso-detect/src/win32/native.h b/espanso-detect/src/win32/native.h index 7ba3f09..6ac73ff 100644 --- a/espanso-detect/src/win32/native.h +++ b/espanso-detect/src/win32/native.h @@ -24,6 +24,7 @@ #define INPUT_EVENT_TYPE_KEYBOARD 1 #define INPUT_EVENT_TYPE_MOUSE 2 +#define INPUT_EVENT_TYPE_HOTKEY 3 #define INPUT_STATUS_PRESSED 1 #define INPUT_STATUS_RELEASED 2 @@ -50,7 +51,8 @@ typedef struct { int32_t buffer_len; // Virtual key code of the pressed key in case of keyboard events - // Mouse button code otherwise. + // Mouse button code for mouse events. + // Hotkey id for hotkey events int32_t key_code; // Left or Right variant @@ -60,12 +62,21 @@ typedef struct { int32_t status; } InputEvent; +typedef struct { + int32_t hk_id; + uint32_t key_code; + uint32_t flags; +} HotKey; + typedef void (*EventCallback)(void * rust_istance, InputEvent data); // Initialize the Raw Input API and the Window. extern "C" void * detect_initialize(void * rust_istance, int32_t *error_code); +// Register the given hotkey, return a non-zero code if successful +extern "C" int32_t detect_register_hotkey(void * window, HotKey hotkey); + // Run the event loop. Blocking call. extern "C" int32_t detect_eventloop(void * window, EventCallback callback); diff --git a/espanso/src/main.rs b/espanso/src/main.rs index 84e71af..89a233e 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -63,7 +63,7 @@ fn main() { let mut source = get_source(SourceCreationOptions { hotkeys: vec![ HotKey::new(1, "OPTION+SPACE").unwrap(), - HotKey::new(2, "CMD+OPTION+3").unwrap(), + HotKey::new(2, "CTRL+OPTION+3").unwrap(), ], ..Default::default() }).unwrap();