From 89805a0248151eef3a37c29f215da6925f2f9e40 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 14 Mar 2021 15:50:54 +0100 Subject: [PATCH] First draft of hotkey support on macOS --- Cargo.lock | 21 +- espanso-detect/Cargo.toml | 1 + espanso-detect/build.rs | 1 + espanso-detect/src/event.rs | 6 + espanso-detect/src/hotkey/keys.rs | 545 ++++++++++++++++++++++++++++++ espanso-detect/src/hotkey/mod.rs | 127 +++++++ espanso-detect/src/lib.rs | 15 +- espanso-detect/src/mac/mod.rs | 89 ++++- espanso-detect/src/mac/native.h | 16 +- espanso-detect/src/mac/native.mm | 44 ++- espanso-inject/src/keys.rs | 4 +- espanso/src/main.rs | 15 +- 12 files changed, 842 insertions(+), 42 deletions(-) create mode 100644 espanso-detect/src/hotkey/keys.rs create mode 100644 espanso-detect/src/hotkey/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 46c544a..d061d3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,6 +260,7 @@ dependencies = [ "lazycell", "libc", "log", + "regex", "scopeguard", "thiserror", "widestring", @@ -521,12 +522,6 @@ dependencies = [ "objc", ] -[[package]] -name = "once_cell" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" - [[package]] name = "pkg-config" version = "0.3.19" @@ -613,14 +608,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.4.3" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +checksum = "54fd1046a3107eb58f42de31d656fee6853e5d276c455fd943742dce89fc3dd3" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] [[package]] @@ -802,15 +796,6 @@ dependencies = [ "syn 1.0.60", ] -[[package]] -name = "thread_local" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" -dependencies = [ - "once_cell", -] - [[package]] name = "time" version = "0.1.44" diff --git a/espanso-detect/Cargo.toml b/espanso-detect/Cargo.toml index fe5c145..4f78968 100644 --- a/espanso-detect/Cargo.toml +++ b/espanso-detect/Cargo.toml @@ -15,6 +15,7 @@ log = "0.4.14" lazycell = "1.3.0" anyhow = "1.0.38" thiserror = "1.0.23" +regex = "1.4.3" [target.'cfg(windows)'.dependencies] widestring = "0.4.3" diff --git a/espanso-detect/build.rs b/espanso-detect/build.rs index d125928..1622a4b 100644 --- a/espanso-detect/build.rs +++ b/espanso-detect/build.rs @@ -75,6 +75,7 @@ fn cc_config() { println!("cargo:rustc-link-lib=dylib=c++"); println!("cargo:rustc-link-lib=static=espansodetect"); println!("cargo:rustc-link-lib=framework=Cocoa"); + println!("cargo:rustc-link-lib=framework=Carbon"); } fn main() { diff --git a/espanso-detect/src/event.rs b/espanso-detect/src/event.rs index 482c69d..a1135cf 100644 --- a/espanso-detect/src/event.rs +++ b/espanso-detect/src/event.rs @@ -24,6 +24,7 @@ use enum_as_inner::EnumAsInner; pub enum InputEvent { Mouse(MouseEvent), Keyboard(KeyboardEvent), + HotKey(HotKeyEvent), } #[derive(Debug, PartialEq)] @@ -121,3 +122,8 @@ pub enum Key { // Other keys, includes the raw code provided by the operating system Other(i32), } + +#[derive(Debug, PartialEq)] +pub struct HotKeyEvent { + pub hotkey_id: i32, +} \ No newline at end of file diff --git a/espanso-detect/src/hotkey/keys.rs b/espanso-detect/src/hotkey/keys.rs new file mode 100644 index 0000000..456cbc3 --- /dev/null +++ b/espanso-detect/src/hotkey/keys.rs @@ -0,0 +1,545 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use std::fmt::Display; + +use regex::Regex; + +lazy_static! { + static ref RAW_PARSER: Regex = Regex::new(r"^RAW\((\d+)\)$").unwrap(); +} + +#[derive(Debug, PartialEq, Clone)] +pub enum ShortcutKey { + Alt, + Control, + Meta, + Shift, + + Enter, + Tab, + Space, + Insert, + + // Navigation + ArrowDown, + ArrowLeft, + ArrowRight, + ArrowUp, + End, + Home, + PageDown, + PageUp, + + // Function ShortcutKeys + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + F13, + F14, + F15, + F16, + F17, + F18, + F19, + F20, + + // Alphabet + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, + + // Numbers + N0, + N1, + N2, + N3, + N4, + N5, + N6, + N7, + N8, + N9, + + // Numpad + Numpad0, + Numpad1, + Numpad2, + Numpad3, + Numpad4, + Numpad5, + Numpad6, + Numpad7, + Numpad8, + Numpad9, + + // Specify the raw platform-specific virtual key code. + Raw(u32), +} + +impl Display for ShortcutKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + ShortcutKey::Alt => write!(f, "ALT"), + ShortcutKey::Control => write!(f, "CTRL"), + ShortcutKey::Meta => write!(f, "META"), + ShortcutKey::Shift => write!(f, "SHIFT"), + ShortcutKey::Enter => write!(f, "ENTER"), + ShortcutKey::Tab => write!(f, "TAB"), + ShortcutKey::Space => write!(f, "SPACE"), + ShortcutKey::Insert => write!(f, "INSERT"), + ShortcutKey::ArrowDown => write!(f, "DOWN"), + ShortcutKey::ArrowLeft => write!(f, "LEFT"), + ShortcutKey::ArrowRight => write!(f, "RIGHT"), + ShortcutKey::ArrowUp => write!(f, "UP"), + ShortcutKey::End => write!(f, "END"), + ShortcutKey::Home => write!(f, "HOME"), + ShortcutKey::PageDown => write!(f, "PAGEDOWN"), + ShortcutKey::PageUp => write!(f, "PAGEUP"), + ShortcutKey::F1 => write!(f, "F1"), + ShortcutKey::F2 => write!(f, "F2"), + ShortcutKey::F3 => write!(f, "F3"), + ShortcutKey::F4 => write!(f, "F4"), + ShortcutKey::F5 => write!(f, "F5"), + ShortcutKey::F6 => write!(f, "F6"), + ShortcutKey::F7 => write!(f, "F7"), + ShortcutKey::F8 => write!(f, "F8"), + ShortcutKey::F9 => write!(f, "F9"), + ShortcutKey::F10 => write!(f, "F10"), + ShortcutKey::F11 => write!(f, "F11"), + ShortcutKey::F12 => write!(f, "F12"), + ShortcutKey::F13 => write!(f, "F13"), + ShortcutKey::F14 => write!(f, "F14"), + ShortcutKey::F15 => write!(f, "F15"), + ShortcutKey::F16 => write!(f, "F16"), + ShortcutKey::F17 => write!(f, "F17"), + ShortcutKey::F18 => write!(f, "F18"), + ShortcutKey::F19 => write!(f, "F19"), + ShortcutKey::F20 => write!(f, "F20"), + ShortcutKey::A => write!(f, "A"), + ShortcutKey::B => write!(f, "B"), + ShortcutKey::C => write!(f, "C"), + ShortcutKey::D => write!(f, "D"), + ShortcutKey::E => write!(f, "E"), + ShortcutKey::F => write!(f, "F"), + ShortcutKey::G => write!(f, "G"), + ShortcutKey::H => write!(f, "H"), + ShortcutKey::I => write!(f, "I"), + ShortcutKey::J => write!(f, "J"), + ShortcutKey::K => write!(f, "K"), + ShortcutKey::L => write!(f, "L"), + ShortcutKey::M => write!(f, "M"), + ShortcutKey::N => write!(f, "N"), + ShortcutKey::O => write!(f, "O"), + ShortcutKey::P => write!(f, "P"), + ShortcutKey::Q => write!(f, "Q"), + ShortcutKey::R => write!(f, "R"), + ShortcutKey::S => write!(f, "S"), + ShortcutKey::T => write!(f, "T"), + ShortcutKey::U => write!(f, "U"), + ShortcutKey::V => write!(f, "V"), + ShortcutKey::W => write!(f, "W"), + ShortcutKey::X => write!(f, "X"), + ShortcutKey::Y => write!(f, "Y"), + ShortcutKey::Z => write!(f, "Z"), + ShortcutKey::N0 => write!(f, "0"), + ShortcutKey::N1 => write!(f, "1"), + ShortcutKey::N2 => write!(f, "2"), + ShortcutKey::N3 => write!(f, "3"), + ShortcutKey::N4 => write!(f, "4"), + ShortcutKey::N5 => write!(f, "5"), + ShortcutKey::N6 => write!(f, "6"), + ShortcutKey::N7 => write!(f, "7"), + ShortcutKey::N8 => write!(f, "8"), + ShortcutKey::N9 => write!(f, "9"), + ShortcutKey::Numpad0 => write!(f, "NUMPAD0"), + ShortcutKey::Numpad1 => write!(f, "NUMPAD1"), + ShortcutKey::Numpad2 => write!(f, "NUMPAD2"), + ShortcutKey::Numpad3 => write!(f, "NUMPAD3"), + ShortcutKey::Numpad4 => write!(f, "NUMPAD4"), + ShortcutKey::Numpad5 => write!(f, "NUMPAD5"), + ShortcutKey::Numpad6 => write!(f, "NUMPAD6"), + ShortcutKey::Numpad7 => write!(f, "NUMPAD7"), + ShortcutKey::Numpad8 => write!(f, "NUMPAD8"), + ShortcutKey::Numpad9 => write!(f, "NUMPAD9"), + ShortcutKey::Raw(code) => write!(f, "RAW({})", code), + } + } +} + +impl ShortcutKey { + pub fn parse(key: &str) -> Option { + let parsed = match key { + "ALT" | "OPTION" => Some(ShortcutKey::Alt), + "CTRL" => Some(ShortcutKey::Control), + "META" | "CMD" => Some(ShortcutKey::Meta), + "SHIFT" => Some(ShortcutKey::Shift), + "ENTER" => Some(ShortcutKey::Enter), + "TAB" => Some(ShortcutKey::Tab), + "SPACE" => Some(ShortcutKey::Space), + "INSERT" => Some(ShortcutKey::Insert), + "DOWN" => Some(ShortcutKey::ArrowDown), + "LEFT" => Some(ShortcutKey::ArrowLeft), + "RIGHT" => Some(ShortcutKey::ArrowRight), + "UP" => Some(ShortcutKey::ArrowUp), + "END" => Some(ShortcutKey::End), + "HOME" => Some(ShortcutKey::Home), + "PAGEDOWN" => Some(ShortcutKey::PageDown), + "PAGEUP" => Some(ShortcutKey::PageUp), + "F1" => Some(ShortcutKey::F1), + "F2" => Some(ShortcutKey::F2), + "F3" => Some(ShortcutKey::F3), + "F4" => Some(ShortcutKey::F4), + "F5" => Some(ShortcutKey::F5), + "F6" => Some(ShortcutKey::F6), + "F7" => Some(ShortcutKey::F7), + "F8" => Some(ShortcutKey::F8), + "F9" => Some(ShortcutKey::F9), + "F10" => Some(ShortcutKey::F10), + "F11" => Some(ShortcutKey::F11), + "F12" => Some(ShortcutKey::F12), + "F13" => Some(ShortcutKey::F13), + "F14" => Some(ShortcutKey::F14), + "F15" => Some(ShortcutKey::F15), + "F16" => Some(ShortcutKey::F16), + "F17" => Some(ShortcutKey::F17), + "F18" => Some(ShortcutKey::F18), + "F19" => Some(ShortcutKey::F19), + "F20" => Some(ShortcutKey::F20), + "A" => Some(ShortcutKey::A), + "B" => Some(ShortcutKey::B), + "C" => Some(ShortcutKey::C), + "D" => Some(ShortcutKey::D), + "E" => Some(ShortcutKey::E), + "F" => Some(ShortcutKey::F), + "G" => Some(ShortcutKey::G), + "H" => Some(ShortcutKey::H), + "I" => Some(ShortcutKey::I), + "J" => Some(ShortcutKey::J), + "K" => Some(ShortcutKey::K), + "L" => Some(ShortcutKey::L), + "M" => Some(ShortcutKey::M), + "N" => Some(ShortcutKey::N), + "O" => Some(ShortcutKey::O), + "P" => Some(ShortcutKey::P), + "Q" => Some(ShortcutKey::Q), + "R" => Some(ShortcutKey::R), + "S" => Some(ShortcutKey::S), + "T" => Some(ShortcutKey::T), + "U" => Some(ShortcutKey::U), + "V" => Some(ShortcutKey::V), + "W" => Some(ShortcutKey::W), + "X" => Some(ShortcutKey::X), + "Y" => Some(ShortcutKey::Y), + "Z" => Some(ShortcutKey::Z), + "0" => Some(ShortcutKey::N0), + "1" => Some(ShortcutKey::N1), + "2" => Some(ShortcutKey::N2), + "3" => Some(ShortcutKey::N3), + "4" => Some(ShortcutKey::N4), + "5" => Some(ShortcutKey::N5), + "6" => Some(ShortcutKey::N6), + "7" => Some(ShortcutKey::N7), + "8" => Some(ShortcutKey::N8), + "9" => Some(ShortcutKey::N9), + "NUMPAD0" => Some(ShortcutKey::Numpad0), + "NUMPAD1" => Some(ShortcutKey::Numpad1), + "NUMPAD2" => Some(ShortcutKey::Numpad2), + "NUMPAD3" => Some(ShortcutKey::Numpad3), + "NUMPAD4" => Some(ShortcutKey::Numpad4), + "NUMPAD5" => Some(ShortcutKey::Numpad5), + "NUMPAD6" => Some(ShortcutKey::Numpad6), + "NUMPAD7" => Some(ShortcutKey::Numpad7), + "NUMPAD8" => Some(ShortcutKey::Numpad8), + "NUMPAD9" => Some(ShortcutKey::Numpad9), + _ => None, + }; + + if parsed.is_none() { + // Attempt to parse raw ShortcutKeys + if RAW_PARSER.is_match(key) { + if let Some(caps) = RAW_PARSER.captures(key) { + let code_str = caps.get(1).map_or("", |m| m.as_str()); + let code = code_str.parse::(); + if let Ok(code) = code { + return Some(ShortcutKey::Raw(code)); + } + } + } + } + + parsed + } + + // macOS keycodes + + #[cfg(target_os = "macos")] + pub fn to_code(&self) -> Option { + match self { + ShortcutKey::Alt => Some(0x3A), + ShortcutKey::Control => Some(0x3B), + ShortcutKey::Meta => Some(0x37), + ShortcutKey::Shift => Some(0x38), + ShortcutKey::Enter => Some(0x24), + ShortcutKey::Tab => Some(0x30), + ShortcutKey::Space => Some(0x31), + ShortcutKey::ArrowDown => Some(0x7D), + ShortcutKey::ArrowLeft => Some(0x7B), + ShortcutKey::ArrowRight => Some(0x7C), + ShortcutKey::ArrowUp => Some(0x7E), + ShortcutKey::End => Some(0x77), + ShortcutKey::Home => Some(0x73), + ShortcutKey::PageDown => Some(0x79), + ShortcutKey::PageUp => Some(0x74), + ShortcutKey::Insert => None, + ShortcutKey::F1 => Some(0x7A), + ShortcutKey::F2 => Some(0x78), + ShortcutKey::F3 => Some(0x63), + ShortcutKey::F4 => Some(0x76), + ShortcutKey::F5 => Some(0x60), + ShortcutKey::F6 => Some(0x61), + ShortcutKey::F7 => Some(0x62), + ShortcutKey::F8 => Some(0x64), + ShortcutKey::F9 => Some(0x65), + ShortcutKey::F10 => Some(0x6D), + ShortcutKey::F11 => Some(0x67), + ShortcutKey::F12 => Some(0x6F), + ShortcutKey::F13 => Some(0x69), + ShortcutKey::F14 => Some(0x6B), + ShortcutKey::F15 => Some(0x71), + ShortcutKey::F16 => Some(0x6A), + ShortcutKey::F17 => Some(0x40), + ShortcutKey::F18 => Some(0x4F), + ShortcutKey::F19 => Some(0x50), + ShortcutKey::F20 => Some(0x5A), + ShortcutKey::A => Some(0x00), + ShortcutKey::B => Some(0x0B), + ShortcutKey::C => Some(0x08), + ShortcutKey::D => Some(0x02), + ShortcutKey::E => Some(0x0E), + ShortcutKey::F => Some(0x03), + ShortcutKey::G => Some(0x05), + ShortcutKey::H => Some(0x04), + ShortcutKey::I => Some(0x22), + ShortcutKey::J => Some(0x26), + ShortcutKey::K => Some(0x28), + ShortcutKey::L => Some(0x25), + ShortcutKey::M => Some(0x2E), + ShortcutKey::N => Some(0x2D), + ShortcutKey::O => Some(0x1F), + ShortcutKey::P => Some(0x23), + ShortcutKey::Q => Some(0x0C), + ShortcutKey::R => Some(0x0F), + ShortcutKey::S => Some(0x01), + ShortcutKey::T => Some(0x11), + ShortcutKey::U => Some(0x20), + ShortcutKey::V => Some(0x09), + ShortcutKey::W => Some(0x0D), + ShortcutKey::X => Some(0x07), + ShortcutKey::Y => Some(0x10), + ShortcutKey::Z => Some(0x06), + ShortcutKey::N0 => Some(0x1D), + ShortcutKey::N1 => Some(0x12), + ShortcutKey::N2 => Some(0x13), + ShortcutKey::N3 => Some(0x14), + ShortcutKey::N4 => Some(0x15), + ShortcutKey::N5 => Some(0x17), + ShortcutKey::N6 => Some(0x16), + ShortcutKey::N7 => Some(0x1A), + ShortcutKey::N8 => Some(0x1C), + ShortcutKey::N9 => Some(0x19), + ShortcutKey::Numpad0 => Some(0x52), + ShortcutKey::Numpad1 => Some(0x53), + ShortcutKey::Numpad2 => Some(0x54), + ShortcutKey::Numpad3 => Some(0x55), + ShortcutKey::Numpad4 => Some(0x56), + ShortcutKey::Numpad5 => Some(0x57), + ShortcutKey::Numpad6 => Some(0x58), + ShortcutKey::Numpad7 => Some(0x59), + ShortcutKey::Numpad8 => Some(0x5B), + ShortcutKey::Numpad9 => Some(0x5C), + ShortcutKey::Raw(code) => Some(*code), + } + } + + // Windows key codes + + #[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, + }; + Some(vkey) + } + + #[cfg(target_os = "linux")] + pub fn to_code(&self) -> Option { + None // Not supported on Linux + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_works_correctly() { + assert!(matches!( + ShortcutKey::parse("ALT").unwrap(), + ShortcutKey::Alt + )); + assert!(matches!( + ShortcutKey::parse("META").unwrap(), + ShortcutKey::Meta + )); + assert!(matches!( + ShortcutKey::parse("CMD").unwrap(), + ShortcutKey::Meta + )); + assert!(matches!( + ShortcutKey::parse("RAW(1234)").unwrap(), + ShortcutKey::Raw(1234) + )); + } + + #[test] + fn parse_invalid_keys() { + assert!(ShortcutKey::parse("INVALID").is_none()); + assert!(ShortcutKey::parse("RAW(a)").is_none()); + } +} diff --git a/espanso-detect/src/hotkey/mod.rs b/espanso-detect/src/hotkey/mod.rs new file mode 100644 index 0000000..2bd2694 --- /dev/null +++ b/espanso-detect/src/hotkey/mod.rs @@ -0,0 +1,127 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +pub mod keys; + + +use anyhow::Result; +use keys::ShortcutKey; +use thiserror::Error; + +static MODIFIERS: &[ShortcutKey; 4] = &[ShortcutKey::Control, ShortcutKey::Alt, ShortcutKey::Shift, ShortcutKey::Meta]; + +#[derive(Debug, PartialEq, Clone)] +pub struct HotKey { + pub id: i32, + pub key: ShortcutKey, + pub modifiers: Vec, +} + +// TODO: test all methods +impl HotKey { + pub fn new(id: i32, shortcut: &str) -> Result { + let tokens: Vec = shortcut + .split('+') + .map(|token| token.trim().to_uppercase()) + .collect(); + + let mut modifiers = Vec::new(); + let mut main_key = None; + for token in tokens { + let key = ShortcutKey::parse(&token); + match key { + Some(key) => { + if MODIFIERS.contains(&key) { + modifiers.push(key) + } else { + main_key = Some(key) + } + } + None => return Err(HotKeyError::InvalidKey(token).into()), + }; + } + + if modifiers.is_empty() || main_key.is_none() { + return Err(HotKeyError::InvalidShortcut(shortcut.to_string()).into()); + } + + Ok(Self { + id, + modifiers, + key: main_key.unwrap(), + }) + } + + pub(crate) fn has_ctrl(&self) -> bool { + self.modifiers.contains(&ShortcutKey::Control) + } + + pub(crate) fn has_meta(&self) -> bool { + self.modifiers.contains(&ShortcutKey::Meta) + } + + pub(crate) fn has_alt(&self) -> bool { + self.modifiers.contains(&ShortcutKey::Alt) + } + + pub(crate) fn has_shift(&self) -> bool { + self.modifiers.contains(&ShortcutKey::Shift) + } +} + +#[derive(Error, Debug)] +pub enum HotKeyError { + #[error("invalid hotkey shortcut, `{0}` is not a valid key")] + InvalidKey(String), + + #[error("invalid hotkey shortcut `{0}`")] + InvalidShortcut(String), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[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!(HotKey::new(3, "invalid").is_err()); + } + + #[test] + fn modifiers_detected_correcty() { + assert!(HotKey::new(1, "CTRL+V").unwrap().has_ctrl()); + assert!(HotKey::new(1, "ALT + V").unwrap().has_alt()); + assert!(HotKey::new(1, "CMD + V").unwrap().has_meta()); + assert!(HotKey::new(1, "SHIFT+ V").unwrap().has_shift()); + + assert!(!HotKey::new(1, "SHIFT+ V").unwrap().has_ctrl()); + 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 0143cee..b0c4b89 100644 --- a/espanso-detect/src/lib.rs +++ b/espanso-detect/src/lib.rs @@ -18,9 +18,11 @@ */ use anyhow::Result; +use hotkey::HotKey; use log::info; pub mod event; +pub mod hotkey; #[cfg(target_os = "windows")] pub mod win32; @@ -49,11 +51,15 @@ pub trait Source { #[allow(dead_code)] pub struct SourceCreationOptions { // Only relevant in X11 Linux systems, use the EVDEV backend instead of X11. - use_evdev: bool, + pub use_evdev: bool, // Can be used to overwrite the keymap configuration // used by espanso to inject key presses. - evdev_keyboard_rmlvo: Option, + pub evdev_keyboard_rmlvo: Option, + + // List of global hotkeys the detection module has to register + // NOTE: Hotkeys are ignored on Linux + pub hotkeys: Vec, } // This struct identifies the keyboard layout that @@ -73,6 +79,7 @@ impl Default for SourceCreationOptions { Self { use_evdev: false, evdev_keyboard_rmlvo: None, + hotkeys: Vec::new(), } } } @@ -84,9 +91,9 @@ pub fn get_source(_options: SourceCreationOptions) -> Result> { } #[cfg(target_os = "macos")] -pub fn get_source(_options: SourceCreationOptions) -> Result> { +pub fn get_source(options: SourceCreationOptions) -> Result> { info!("using CocoaSource"); - Ok(Box::new(mac::CocoaSource::new())) + Ok(Box::new(mac::CocoaSource::new(&options.hotkeys))) } #[cfg(target_os = "linux")] diff --git a/espanso-detect/src/mac/mod.rs b/espanso-detect/src/mac/mod.rs index e5e5f50..ab997ee 100644 --- a/espanso-detect/src/mac/mod.rs +++ b/espanso-detect/src/mac/mod.rs @@ -17,13 +17,10 @@ * along with espanso. If not, see . */ -use std::{ - ffi::CStr, - sync::{ +use std::{convert::TryInto, ffi::CStr, sync::{ mpsc::{channel, Receiver, Sender}, Arc, Mutex, - }, -}; + }}; use lazycell::LazyCell; use log::{error, trace, warn}; @@ -31,13 +28,14 @@ use log::{error, trace, warn}; use anyhow::Result; use thiserror::Error; -use crate::event::Variant::*; -use crate::event::{InputEvent, Key, KeyboardEvent, Variant}; +use crate::event::{HotKeyEvent, InputEvent, Key, KeyboardEvent, Variant}; use crate::event::{Key::*, MouseButton, MouseEvent}; use crate::{event::Status::*, Source, SourceCallback}; +use crate::{event::Variant::*, hotkey::HotKey}; 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; @@ -58,10 +56,26 @@ pub struct RawInputEvent { pub status: i32, } +#[repr(C)] +pub struct RawHotKey { + pub id: i32, + pub code: u16, + pub flags: u32, +} + +#[repr(C)] +pub struct RawInitializationOptions { + pub hotkeys: *const RawHotKey, + pub hotkeys_count: i32, +} + #[allow(improper_ctypes)] #[link(name = "espansodetect", kind = "static")] extern "C" { - pub fn detect_initialize(callback: extern "C" fn(event: RawInputEvent)); + pub fn detect_initialize( + callback: extern "C" fn(event: RawInputEvent), + options: RawInitializationOptions, + ); } lazy_static! { @@ -88,13 +102,15 @@ extern "C" fn native_callback(raw_event: RawInputEvent) { pub struct CocoaSource { receiver: LazyCell>, + hotkeys: Vec, } #[allow(clippy::new_without_default)] impl CocoaSource { - pub fn new() -> CocoaSource { + pub fn new(hotkeys: &[HotKey]) -> CocoaSource { Self { receiver: LazyCell::new(), + hotkeys: hotkeys.to_vec(), } } } @@ -111,7 +127,24 @@ impl Source for CocoaSource { *lock = Some(sender); } - unsafe { detect_initialize(native_callback) }; + // Generate the options + let hotkeys: Vec = self + .hotkeys + .iter() + .filter_map(|hk| { + let raw = convert_hotkey_to_raw(&hk); + if raw.is_none() { + error!("unable to register hotkey: {:?}", hk); + } + raw + }) + .collect(); + let options = RawInitializationOptions { + hotkeys: hotkeys.as_ptr(), + hotkeys_count: hotkeys.len() as i32, + }; + + unsafe { detect_initialize(native_callback, options) }; if self.receiver.fill(receiver).is_err() { error!("Unable to set CocoaSource receiver"); @@ -156,6 +189,35 @@ impl Drop for CocoaSource { } } +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 = 0; + if hk.has_ctrl() { + flags |= 1 << 12; + } + if hk.has_alt() { + flags |= 1 << 11; + } + if hk.has_meta() { + flags |= 1 << 8; + } + if hk.has_shift() { + flags |= 1 << 9; + } + + 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 CocoaSourceError { #[error("unknown error")] @@ -213,6 +275,13 @@ impl From for Option { return Some(InputEvent::Mouse(MouseEvent { button, status })); } } + // HOTKEYS + INPUT_EVENT_TYPE_HOTKEY => { + let id = raw.key_code; + return Some(InputEvent::HotKey(HotKeyEvent { + hotkey_id: id, + })) + } _ => {} } diff --git a/espanso-detect/src/mac/native.h b/espanso-detect/src/mac/native.h index 0a2e664..9be5357 100644 --- a/espanso-detect/src/mac/native.h +++ b/espanso-detect/src/mac/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 @@ -45,7 +46,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 if mouse_event. + // Hotkey ID in case of hotkeys int32_t key_code; // Pressed or Released status @@ -54,8 +56,18 @@ typedef struct { typedef void (*EventCallback)(InputEvent data); +typedef struct { + int32_t hk_id; + uint16_t key_code; + uint32_t flags; +} HotKey; + +typedef struct { + HotKey *hotkeys; + int32_t hotkeys_count; +} InitializeOptions; // Initialize the event global monitor -extern "C" void * detect_initialize(EventCallback callback); +extern "C" void * detect_initialize(EventCallback callback, InitializeOptions options); #endif //ESPANSO_DETECT_H \ No newline at end of file diff --git a/espanso-detect/src/mac/native.mm b/espanso-detect/src/mac/native.mm index 7b1a8b8..3605be9 100644 --- a/espanso-detect/src/mac/native.mm +++ b/espanso-detect/src/mac/native.mm @@ -28,8 +28,35 @@ const unsigned long long FLAGS = NSEventMaskKeyDown | NSEventMaskKeyUp | NSEvent NSEventMaskLeftMouseUp | NSEventMaskRightMouseDown | NSEventMaskRightMouseUp | NSEventMaskOtherMouseDown | NSEventMaskOtherMouseUp; -void * detect_initialize(EventCallback callback) { +OSStatus hotkey_event_handler(EventHandlerCallRef _next, EventRef evt, void *userData); + +void * detect_initialize(EventCallback callback, InitializeOptions options) { + HotKey * hotkeys_clone = (HotKey*) malloc(sizeof(HotKey) * options.hotkeys_count); + memcpy(hotkeys_clone, options.hotkeys, sizeof(HotKey) * options.hotkeys_count); + dispatch_async(dispatch_get_main_queue(), ^(void) { + // Setup hotkeys + if (options.hotkeys_count > 0) { + EventHotKeyRef hotkey_ref; + EventHotKeyID hotkey_id; + hotkey_id.signature='htk1'; + + EventTypeSpec eventType; + eventType.eventClass = kEventClassKeyboard; + eventType.eventKind = kEventHotKeyPressed; + + InstallApplicationEventHandler(&hotkey_event_handler, 1, &eventType, (void*)callback, NULL); + + for (int i = 0; i Option { let parsed = match key { - "ALT" => Some(Key::Alt), + "ALT" | "OPTION" => Some(Key::Alt), "CAPSLOCK" => Some(Key::CapsLock), "CTRL" => Some(Key::Control), - "META" => Some(Key::Meta), + "META" | "CMD" => Some(Key::Meta), "NUMLOCK" => Some(Key::NumLock), "SHIFT" => Some(Key::Shift), "ENTER" => Some(Key::Enter), diff --git a/espanso/src/main.rs b/espanso/src/main.rs index 232ea62..84e71af 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -1,9 +1,6 @@ use std::time::Duration; -use espanso_detect::{ - event::{InputEvent, Status}, - get_source, -}; +use espanso_detect::{SourceCreationOptions, event::{InputEvent, Status}, get_source, hotkey::HotKey}; use espanso_inject::{get_injector, keys, Injector}; use espanso_ui::{event::UIEvent::*, icons::TrayIcon, menu::*}; use simplelog::{CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode}; @@ -63,7 +60,13 @@ fn main() { let handle = std::thread::spawn(move || { let injector = get_injector(Default::default()).unwrap(); - let mut source = get_source(Default::default()).unwrap(); + let mut source = get_source(SourceCreationOptions { + hotkeys: vec![ + HotKey::new(1, "OPTION+SPACE").unwrap(), + HotKey::new(2, "CMD+OPTION+3").unwrap(), + ], + ..Default::default() + }).unwrap(); source.initialize().unwrap(); source .eventloop(Box::new(move |event: InputEvent| { @@ -81,6 +84,8 @@ fn main() { //injector.send_key_combination(&[keys::Key::Control, keys::Key::V], Default::default()).unwrap(); } } + InputEvent::HotKey(_) => { + } } })) .unwrap();