First implementation of espanso-inject on macOS

This commit is contained in:
Federico Terzi 2021-02-12 16:58:05 +01:00
parent e3e1ad720f
commit e0bf94013d
6 changed files with 394 additions and 11 deletions

View File

@ -42,6 +42,8 @@ pub trait Injector {
fn send_key_combination(&self, keys: &[keys::Key], delay: i32) -> Result<()>; fn send_key_combination(&self, keys: &[keys::Key], delay: i32) -> Result<()>;
} }
#[allow(dead_code)]
pub struct InjectorOptions { pub struct InjectorOptions {
// Only relevant in Linux systems // Only relevant in Linux systems
use_evdev: bool, use_evdev: bool,
@ -62,7 +64,7 @@ pub fn get_injector(_options: InjectorOptions) -> impl Injector {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn get_injector(_options: InjectorOptions) -> impl Injector { pub fn get_injector(_options: InjectorOptions) -> impl Injector {
// TODO mac::MacInjector::new()
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Vec<i32>> {
let mut virtual_keys: Vec<i32> = 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]
);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#ifndef ESPANSO_INJECT_H
#define ESPANSO_INJECT_H
#include <stdint.h>
// 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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include "native.h"
#include <string.h>
#import <Foundation/Foundation.h>
#include <vector>
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 <UniChar> 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<vkey_count; i++) {
CGEventRef keydown;
keydown = CGEventCreateKeyboardEvent(NULL, vkey_array[i], true);
CGEventPost(kCGHIDEventTap, keydown);
CFRelease(keydown);
usleep(udelay);
CGEventRef keyup;
keyup = CGEventCreateKeyboardEvent(NULL, vkey_array[i], false);
CGEventPost(kCGHIDEventTap, keyup);
CFRelease(keyup);
usleep(udelay);
}
free(vkey_array);
});
}
void inject_vkeys_combination(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) {
// First send the presses
for (int i = 0; i < vkey_count; i++)
{
CGEventRef keydown;
keydown = CGEventCreateKeyboardEvent(NULL, vkey_array[i], true);
CGEventPost(kCGHIDEventTap, keydown);
CFRelease(keydown);
usleep(udelay);
}
// Then the releases
for (int i = (vkey_count - 1); i >= 0; i--)
{
CGEventRef keyup;
keyup = CGEventCreateKeyboardEvent(NULL, vkey_array[i], false);
CGEventPost(kCGHIDEventTap, keyup);
CFRelease(keyup);
usleep(udelay);
}
free(vkey_array);
});
}

View File

@ -0,0 +1,94 @@
use crate::keys::Key;
pub fn convert_key_to_vkey(key: &Key) -> Option<i32> {
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),
}
}

View File

@ -1,5 +1,5 @@
use espanso_detect::event::{InputEvent, Status}; 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 espanso_ui::{event::UIEvent::*, icons::TrayIcon, menu::*};
use simplelog::{CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode}; 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 { // 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 {
// show_icon: true, // show_icon: true,
// icon_paths: &icon_paths, // 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(); eventloop.initialize();
let handle = std::thread::spawn(move || { 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::x11::X11Source::new();
//let mut source = espanso_detect::mac::CocoaSource::new(); let mut source = espanso_detect::mac::CocoaSource::new();
source.initialize(); source.initialize();
source.eventloop(Box::new(move |event: InputEvent| { source.eventloop(Box::new(move |event: InputEvent| {
let injector = get_injector(Default::default()); let injector = get_injector(Default::default());
@ -64,6 +64,7 @@ fn main() {
//remote.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled); //remote.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled);
//remote.show_notification("Espanso is running!"); //remote.show_notification("Espanso is running!");
injector.send_string("hey guys"); injector.send_string("hey guys");
//injector.send_key_combination(&[keys::Key::Meta, keys::Key::V], 2);
} }
} }
} }