From e0bf94013dc6e059964d5a41e84aa78d20f92174 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Fri, 12 Feb 2021 16:58:05 +0100 Subject: [PATCH] First implementation of espanso-inject on macOS --- espanso-inject/src/lib.rs | 4 +- espanso-inject/src/mac/mod.rs | 109 ++++++++++++++++++++++ espanso-inject/src/mac/native.h | 35 +++++++ espanso-inject/src/mac/native.mm | 142 +++++++++++++++++++++++++++++ espanso-inject/src/mac/raw_keys.rs | 94 +++++++++++++++++++ espanso/src/main.rs | 21 +++-- 6 files changed, 394 insertions(+), 11 deletions(-) create mode 100644 espanso-inject/src/mac/mod.rs create mode 100644 espanso-inject/src/mac/native.h create mode 100644 espanso-inject/src/mac/native.mm create mode 100644 espanso-inject/src/mac/raw_keys.rs diff --git a/espanso-inject/src/lib.rs b/espanso-inject/src/lib.rs index f9ad7d9..4e44980 100644 --- a/espanso-inject/src/lib.rs +++ b/espanso-inject/src/lib.rs @@ -42,6 +42,8 @@ pub trait Injector { fn send_key_combination(&self, keys: &[keys::Key], delay: i32) -> Result<()>; } + +#[allow(dead_code)] pub struct InjectorOptions { // Only relevant in Linux systems use_evdev: bool, @@ -62,7 +64,7 @@ pub fn get_injector(_options: InjectorOptions) -> impl Injector { #[cfg(target_os = "macos")] pub fn get_injector(_options: InjectorOptions) -> impl Injector { - // TODO + mac::MacInjector::new() } #[cfg(target_os = "linux")] diff --git a/espanso-inject/src/mac/mod.rs b/espanso-inject/src/mac/mod.rs new file mode 100644 index 0000000..551e981 --- /dev/null +++ b/espanso-inject/src/mac/mod.rs @@ -0,0 +1,109 @@ +/* + * 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 . + */ + +mod raw_keys; + +use std::{ffi::CString, os::raw::c_char}; + +use log::error; +use raw_keys::convert_key_to_vkey; + +use anyhow::Result; +use thiserror::Error; + +use crate::{keys, Injector}; + +#[allow(improper_ctypes)] +#[link(name = "espansoinject", kind = "static")] +extern "C" { + pub fn inject_string(string: *const c_char); + pub fn inject_separate_vkeys(vkey_array: *const i32, vkey_count: i32, delay: i32); + pub fn inject_vkeys_combination(vkey_array: *const i32, vkey_count: i32, delay: i32); +} + +pub struct MacInjector {} + +#[allow(clippy::new_without_default)] +impl MacInjector { + pub fn new() -> Self { + Self {} + } + + pub fn convert_to_vk_array(keys: &[keys::Key]) -> Result> { + let mut virtual_keys: Vec = Vec::new(); + for key in keys.iter() { + let vk = convert_key_to_vkey(key); + if let Some(vk) = vk { + virtual_keys.push(vk) + } else { + return Err(MacInjectorError::MappingFailure(key.clone()).into()); + } + } + Ok(virtual_keys) + } +} + +impl Injector for MacInjector { + fn send_string(&self, string: &str) -> Result<()> { + let c_string = CString::new(string)?; + unsafe { + inject_string(c_string.as_ptr()); + } + Ok(()) + } + + fn send_keys(&self, keys: &[keys::Key], delay: i32) -> Result<()> { + let virtual_keys = Self::convert_to_vk_array(keys)?; + + unsafe { + inject_separate_vkeys(virtual_keys.as_ptr(), virtual_keys.len() as i32, delay); + } + + Ok(()) + } + + fn send_key_combination(&self, keys: &[keys::Key], delay: i32) -> Result<()> { + let virtual_keys = Self::convert_to_vk_array(keys)?; + + unsafe { + inject_vkeys_combination(virtual_keys.as_ptr(), virtual_keys.len() as i32, delay); + } + + Ok(()) + } +} + +#[derive(Error, Debug)] +pub enum MacInjectorError { + #[error("missing vkey mapping for key `{0}`")] + MappingFailure(keys::Key), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn convert_raw_to_virtual_key_array() { + assert_eq!( + MacInjector::convert_to_vk_array(&[keys::Key::Alt, keys::Key::V]).unwrap(), + vec![0x3A, 0x09] + ); + } +} diff --git a/espanso-inject/src/mac/native.h b/espanso-inject/src/mac/native.h new file mode 100644 index 0000000..6d52913 --- /dev/null +++ b/espanso-inject/src/mac/native.h @@ -0,0 +1,35 @@ +/* + * 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 . + */ + +#ifndef ESPANSO_INJECT_H +#define ESPANSO_INJECT_H + +#include + +// Inject a complete string using the KEYEVENTF_UNICODE flag +extern "C" void inject_string(char * string); + +// Send a sequence of vkey presses and releases +extern "C" void inject_separate_vkeys(int32_t *vkey_array, int32_t vkey_count, int32_t delay); + +// Send a combination of vkeys, first pressing all the vkeys and then releasing +// This is needed for keyboard shortcuts, for example. +extern "C" void inject_vkeys_combination(int32_t *vkey_array, int32_t vkey_count, int32_t delay); + +#endif //ESPANSO_INJECT_H \ No newline at end of file diff --git a/espanso-inject/src/mac/native.mm b/espanso-inject/src/mac/native.mm new file mode 100644 index 0000000..ea05bcb --- /dev/null +++ b/espanso-inject/src/mac/native.mm @@ -0,0 +1,142 @@ +/* + * 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 . + */ + +#include "native.h" +#include +#import +#include + +void inject_string(char *string) +{ + char * stringCopy = strdup(string); + dispatch_async(dispatch_get_main_queue(), ^(void) { + // Convert the c string to a UniChar array as required by the CGEventKeyboardSetUnicodeString method + NSString *nsString = [NSString stringWithUTF8String:stringCopy]; + CFStringRef cfString = (__bridge CFStringRef) nsString; + std::vector buffer(nsString.length); + CFStringGetCharacters(cfString, CFRangeMake(0, nsString.length), buffer.data()); + + free(stringCopy); + + // Send the event + + // Check if the shift key is down, and if so, release it + // To see why: https://github.com/federico-terzi/espanso/issues/279 + if (CGEventSourceKeyState(kCGEventSourceStateHIDSystemState, 0x38)) { + CGEventRef e2 = CGEventCreateKeyboardEvent(NULL, 0x38, false); + CGEventPost(kCGHIDEventTap, e2); + CFRelease(e2); + + usleep(2000); + } + + // Because of a bug ( or undocumented limit ) of the CGEventKeyboardSetUnicodeString method + // the string gets truncated after 20 characters, so we need to send multiple events. + + int i = 0; + while (i < buffer.size()) { + int chunk_size = 20; + if ((i+chunk_size) > buffer.size()) { + chunk_size = buffer.size() - i; + } + + UniChar * offset_buffer = buffer.data() + i; + CGEventRef e = CGEventCreateKeyboardEvent(NULL, 0x31, true); + CGEventKeyboardSetUnicodeString(e, chunk_size, offset_buffer); + CGEventPost(kCGHIDEventTap, e); + CFRelease(e); + + usleep(2000); + + // Some applications require an explicit release of the space key + // For more information: https://github.com/federico-terzi/espanso/issues/159 + CGEventRef e2 = CGEventCreateKeyboardEvent(NULL, 0x31, false); + CGEventPost(kCGHIDEventTap, e2); + CFRelease(e2); + + usleep(2000); + + i += chunk_size; + } + }); +} + +void inject_separate_vkeys(int32_t *_vkey_array, int32_t vkey_count, int32_t delay) +{ + long udelay = delay * 1000; + + // Create an heap allocated copy of the array, so that it doesn't get freed within the block + int32_t *vkey_array = (int32_t*)malloc(sizeof(int32_t)*vkey_count); + memcpy(vkey_array, _vkey_array, sizeof(int32_t)*vkey_count); + + dispatch_async(dispatch_get_main_queue(), ^(void) { + for (int i = 0; i= 0; i--) + { + CGEventRef keyup; + keyup = CGEventCreateKeyboardEvent(NULL, vkey_array[i], false); + CGEventPost(kCGHIDEventTap, keyup); + CFRelease(keyup); + + usleep(udelay); + } + + free(vkey_array); + }); +} diff --git a/espanso-inject/src/mac/raw_keys.rs b/espanso-inject/src/mac/raw_keys.rs new file mode 100644 index 0000000..663632d --- /dev/null +++ b/espanso-inject/src/mac/raw_keys.rs @@ -0,0 +1,94 @@ +use crate::keys::Key; + +pub fn convert_key_to_vkey(key: &Key) -> Option { + match key { + Key::Alt => Some(0x3A), + Key::CapsLock => Some(0x39), + Key::Control => Some(0x3B), + Key::Meta => Some(0x37), + Key::NumLock => None, + Key::Shift => Some(0x38), + Key::Enter => Some(0x24), + Key::Tab => Some(0x30), + Key::Space => Some(0x31), + Key::ArrowDown => Some(0x7D), + Key::ArrowLeft => Some(0x7B), + Key::ArrowRight => Some(0x7C), + Key::ArrowUp => Some(0x7E), + Key::End => Some(0x77), + Key::Home => Some(0x73), + Key::PageDown => Some(0x79), + Key::PageUp => Some(0x74), + Key::Escape => Some(0x35), + Key::Backspace => Some(0x33), + Key::Insert => None, + Key::Delete => Some(0x75), + Key::F1 => Some(0x7A), + Key::F2 => Some(0x78), + Key::F3 => Some(0x63), + Key::F4 => Some(0x76), + Key::F5 => Some(0x60), + Key::F6 => Some(0x61), + Key::F7 => Some(0x62), + Key::F8 => Some(0x64), + Key::F9 => Some(0x65), + Key::F10 => Some(0x6D), + Key::F11 => Some(0x67), + Key::F12 => Some(0x6F), + Key::F13 => Some(0x69), + Key::F14 => Some(0x6B), + Key::F15 => Some(0x71), + Key::F16 => Some(0x6A), + Key::F17 => Some(0x40), + Key::F18 => Some(0x4F), + Key::F19 => Some(0x50), + Key::F20 => Some(0x5A), + Key::A => Some(0x00), + Key::B => Some(0x0B), + Key::C => Some(0x08), + Key::D => Some(0x02), + Key::E => Some(0x0E), + Key::F => Some(0x03), + Key::G => Some(0x05), + Key::H => Some(0x04), + Key::I => Some(0x22), + Key::J => Some(0x26), + Key::K => Some(0x28), + Key::L => Some(0x25), + Key::M => Some(0x2E), + Key::N => Some(0x2D), + Key::O => Some(0x1F), + Key::P => Some(0x23), + Key::Q => Some(0x0C), + Key::R => Some(0x0F), + Key::S => Some(0x01), + Key::T => Some(0x11), + Key::U => Some(0x20), + Key::V => Some(0x09), + Key::W => Some(0x0D), + Key::X => Some(0x07), + Key::Y => Some(0x10), + Key::Z => Some(0x06), + Key::N0 => Some(0x1D), + Key::N1 => Some(0x12), + Key::N2 => Some(0x13), + Key::N3 => Some(0x14), + Key::N4 => Some(0x15), + Key::N5 => Some(0x17), + Key::N6 => Some(0x16), + Key::N7 => Some(0x1A), + Key::N8 => Some(0x1C), + Key::N9 => Some(0x19), + Key::Numpad0 => Some(0x52), + Key::Numpad1 => Some(0x53), + Key::Numpad2 => Some(0x54), + Key::Numpad3 => Some(0x55), + Key::Numpad4 => Some(0x56), + Key::Numpad5 => Some(0x57), + Key::Numpad6 => Some(0x58), + Key::Numpad7 => Some(0x59), + Key::Numpad8 => Some(0x5B), + Key::Numpad9 => Some(0x5C), + Key::Raw(code) => Some(*code), + } +} diff --git a/espanso/src/main.rs b/espanso/src/main.rs index 65a4bc7..9f77312 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -1,5 +1,5 @@ use espanso_detect::event::{InputEvent, Status}; -use espanso_inject::{get_injector, Injector}; +use espanso_inject::{get_injector, Injector, keys}; use espanso_ui::{event::UIEvent::*, icons::TrayIcon, menu::*}; use simplelog::{CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode}; @@ -36,23 +36,23 @@ fn main() { ), ]; - let (remote, mut eventloop) = espanso_ui::win32::create(espanso_ui::win32::Win32UIOptions { - show_icon: true, - icon_paths: &icon_paths, - notification_icon_path: r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreensmall.png" - .to_string(), - }); - // let (remote, mut eventloop) = espanso_ui::mac::create(MacUIOptions { + // let (remote, mut eventloop) = espanso_ui::win32::create(espanso_ui::win32::Win32UIOptions { // show_icon: true, // icon_paths: &icon_paths, + // notification_icon_path: r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreensmall.png" + // .to_string(), // }); + let (remote, mut eventloop) = espanso_ui::mac::create(espanso_ui::mac::MacUIOptions { + show_icon: true, + icon_paths: &icon_paths, + }); eventloop.initialize(); let handle = std::thread::spawn(move || { - let mut source = espanso_detect::win32::Win32Source::new(); + //let mut source = espanso_detect::win32::Win32Source::new(); //let mut source = espanso_detect::x11::X11Source::new(); - //let mut source = espanso_detect::mac::CocoaSource::new(); + let mut source = espanso_detect::mac::CocoaSource::new(); source.initialize(); source.eventloop(Box::new(move |event: InputEvent| { let injector = get_injector(Default::default()); @@ -64,6 +64,7 @@ fn main() { //remote.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled); //remote.show_notification("Espanso is running!"); injector.send_string("hey guys"); + //injector.send_key_combination(&[keys::Key::Meta, keys::Key::V], 2); } } }