Add hotkey detection implementation to Windows

This commit is contained in:
Federico Terzi 2021-03-14 18:24:37 +01:00
parent 89805a0248
commit 474eae69d5
8 changed files with 220 additions and 113 deletions

View File

@ -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"

View File

@ -415,94 +415,89 @@ impl ShortcutKey {
#[cfg(target_os = "windows")]
pub fn to_code(&self) -> Option<u32> {
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)
}

View File

@ -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<ShortcutKey>,
}
// TODO: test all methods
impl HotKey {
pub fn new(id: i32, shortcut: &str) -> Result<Self> {
let tokens: Vec<String> = 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<String> = 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());
}
}
}

View File

@ -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<Box<dyn Source>> {
pub fn get_source(options: SourceCreationOptions) -> Result<Box<dyn Source>> {
info!("using Win32Source");
Ok(Box::new(win32::Win32Source::new()))
Ok(Box::new(win32::Win32Source::new(&options.hotkeys)))
}
#[cfg(target_os = "macos")]

View File

@ -17,16 +17,16 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
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<SourceCallback>,
hotkeys: Vec<HotKey>,
}
#[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<RawHotKey> {
let key_code = hk.key.to_code()?;
let code: Result<u32, _> = 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<RawInputEvent> for Option<InputEvent> {
return Some(InputEvent::Mouse(MouseEvent { button, status }));
}
}
// Hotkey events
INPUT_EVENT_TYPE_HOTKEY => {
return Some(InputEvent::HotKey(HotKeyEvent {
hotkey_id: raw.key_code
}))
}
_ => {}
}

View File

@ -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)

View File

@ -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);

View File

@ -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();