diff --git a/Cargo.lock b/Cargo.lock index 0280887..d4c363c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,7 @@ dependencies = [ "anyhow", "cc", "enum-as-inner", + "lazy_static", "lazycell", "libc", "log", diff --git a/espanso-detect/Cargo.toml b/espanso-detect/Cargo.toml index ab5bc5e..317b4a1 100644 --- a/espanso-detect/Cargo.toml +++ b/espanso-detect/Cargo.toml @@ -18,6 +18,9 @@ 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/build.rs b/espanso-detect/build.rs index 5ae16b0..6fade5d 100644 --- a/espanso-detect/build.rs +++ b/espanso-detect/build.rs @@ -59,7 +59,16 @@ fn cc_config() { #[cfg(target_os = "macos")] fn cc_config() { - // TODO + println!("cargo:rerun-if-changed=src/mac/native.mm"); + println!("cargo:rerun-if-changed=src/mac/native.h"); + cc::Build::new() + .cpp(true) + .include("src/mac/native.h") + .file("src/mac/native.mm") + .compile("espansodetect"); + println!("cargo:rustc-link-lib=dylib=c++"); + println!("cargo:rustc-link-lib=static=espansodetect"); + println!("cargo:rustc-link-lib=framework=Cocoa"); } fn main() { diff --git a/espanso-detect/src/evdev/mod.rs b/espanso-detect/src/evdev/mod.rs index 7b2dc49..9273f9d 100644 --- a/espanso-detect/src/evdev/mod.rs +++ b/espanso-detect/src/evdev/mod.rs @@ -227,6 +227,9 @@ fn key_sym_to_key(key_sym: i32) -> (Key, Option) { 0xFF56 => (PageDown, None), 0xFF55 => (PageUp, None), + // UI + 0xFF1B => (Escape, None), + // Editing keys 0xFF08 => (Backspace, None), diff --git a/espanso-detect/src/event.rs b/espanso-detect/src/event.rs index c110e7a..482c69d 100644 --- a/espanso-detect/src/event.rs +++ b/espanso-detect/src/event.rs @@ -90,6 +90,9 @@ pub enum Key { PageDown, PageUp, + // UI + Escape, + // Editing keys Backspace, diff --git a/espanso-detect/src/lib.rs b/espanso-detect/src/lib.rs index 6c94591..f12253d 100644 --- a/espanso-detect/src/lib.rs +++ b/espanso-detect/src/lib.rs @@ -27,3 +27,10 @@ pub mod x11; #[cfg(target_os = "linux")] pub mod evdev; + +#[cfg(target_os = "macos")] +pub mod mac; + +#[cfg(target_os = "macos")] +#[macro_use] +extern crate lazy_static; \ No newline at end of file diff --git a/espanso-detect/src/mac/mod.rs b/espanso-detect/src/mac/mod.rs new file mode 100644 index 0000000..bb88fdd --- /dev/null +++ b/espanso-detect/src/mac/mod.rs @@ -0,0 +1,343 @@ +/* + * 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::{ffi::{CStr}, sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, Mutex, + }}; + +use lazycell::LazyCell; +use log::{error, trace, warn}; + +use anyhow::Result; +use thiserror::Error; + +use crate::event::Status::*; +use crate::event::Variant::*; +use crate::event::{InputEvent, Key, KeyboardEvent, Variant}; +use crate::event::{Key::*, MouseButton, MouseEvent}; + +const INPUT_EVENT_TYPE_KEYBOARD: i32 = 1; +const INPUT_EVENT_TYPE_MOUSE: i32 = 2; + +const INPUT_STATUS_PRESSED: i32 = 1; +const INPUT_STATUS_RELEASED: i32 = 2; + +const INPUT_MOUSE_LEFT_BUTTON: i32 = 1; +const INPUT_MOUSE_RIGHT_BUTTON: i32 = 2; +const INPUT_MOUSE_MIDDLE_BUTTON: i32 = 3; + +// Take a look at the native.h header file for an explanation of the fields +#[repr(C)] +pub struct RawInputEvent { + pub event_type: i32, + + pub buffer: [u8; 24], + pub buffer_len: i32, + + pub key_code: i32, + pub status: i32, +} + +#[allow(improper_ctypes)] +#[link(name = "espansodetect", kind = "static")] +extern "C" { + pub fn detect_initialize(callback: extern "C" fn(event: RawInputEvent)); +} + +lazy_static! { + static ref CURRENT_SENDER: Arc>>> = Arc::new(Mutex::new(None)); +} + +extern "C" fn native_callback(raw_event: RawInputEvent) { + let lock = CURRENT_SENDER + .lock() + .expect("unable to acquire CocoaSource sender lock"); + if let Some(sender) = lock.as_ref() { + let event: Option = raw_event.into(); + if let Some(event) = event { + if let Err(error) = sender.send(event) { + error!("Unable to send event to Cocoa Sender: {}", error); + } + } else { + trace!("Unable to convert raw event to input event"); + } + } else { + warn!("Lost raw event, as Cocoa Sender is not available"); + } +} + +pub type CocoaSourceCallback = Box; +pub struct CocoaSource { + receiver: LazyCell>, +} + +#[allow(clippy::new_without_default)] +impl CocoaSource { + pub fn new() -> CocoaSource { + Self { + receiver: LazyCell::new(), + } + } + + pub fn initialize(&mut self) -> Result<()> { + let (sender, receiver) = channel(); + + // Set the global sender + { + let mut lock = CURRENT_SENDER + .lock() + .expect("unable to acquire CocoaSource sender lock during initialization"); + *lock = Some(sender); + } + + unsafe { detect_initialize(native_callback) }; + + if self.receiver.fill(receiver).is_err() { + error!("Unable to set CocoaSource receiver"); + return Err(CocoaSourceError::Unknown().into()); + } + + Ok(()) + } + + pub fn eventloop(&self, event_callback: CocoaSourceCallback) { + if let Some(receiver) = self.receiver.borrow() { + loop { + let event = receiver.recv(); + match event { + Ok(event) => { + event_callback(event); + } + Err(error) => { + error!("CocoaSource receiver reported error: {}", error); + break; + } + } + } + } else { + panic!("Unable to start event loop if CocoaSource receiver is null"); + } + } +} + +impl Drop for CocoaSource { + fn drop(&mut self) { + // Reset the global sender + { + let mut lock = CURRENT_SENDER + .lock() + .expect("unable to acquire CocoaSource sender lock during initialization"); + *lock = None; + } + } +} + +#[derive(Error, Debug)] +pub enum CocoaSourceError { + #[error("unknown error")] + Unknown(), +} + +impl From for Option { + fn from(raw: RawInputEvent) -> Option { + let status = match raw.status { + INPUT_STATUS_RELEASED => Released, + INPUT_STATUS_PRESSED => Pressed, + _ => Pressed, + }; + + match raw.event_type { + // Keyboard events + INPUT_EVENT_TYPE_KEYBOARD => { + let (key, variant) = key_code_to_key(raw.key_code); + + let value = if raw.buffer_len > 0 { + let raw_string_result = CStr::from_bytes_with_nul(&raw.buffer[..((raw.buffer_len + 1) as usize)]); + match raw_string_result { + Ok(c_string) => { + let string_result = c_string.to_str(); + match string_result { + Ok(value) => Some(value.to_string()), + Err(err) => { + warn!("CocoaSource event utf8 conversion error: {}", err); + None + } + } + } + Err(err) => { + trace!("Received malformed event buffer: {}", err); + None + } + } + } else { + None + }; + + return Some(InputEvent::Keyboard(KeyboardEvent { + key, + value, + status, + variant, + })); + } + // Mouse events + INPUT_EVENT_TYPE_MOUSE => { + let button = raw_to_mouse_button(raw.key_code); + + if let Some(button) = button { + return Some(InputEvent::Mouse(MouseEvent { button, status })); + } + } + _ => {} + } + + None + } +} + +// Mappings from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values +fn key_code_to_key(key_code: i32) -> (Key, Option) { + match key_code { + // Modifiers + 0x3A => (Alt, Some(Left)), + 0x3D => (Alt, Some(Right)), + 0x39 => (CapsLock, None), // TODO + 0x3B => (Control, Some(Left)), + 0x3E => (Control, Some(Right)), + 0x37 => (Meta, Some(Left)), + 0x36 => (Meta, Some(Right)), + 0x38 => (Shift, Some(Left)), + 0x3C => (Shift, Some(Right)), + + // Whitespace + 0x24 => (Enter, None), + 0x30 => (Tab, None), + 0x31 => (Space, None), + + // Navigation + 0x7D => (ArrowDown, None), + 0x7B => (ArrowLeft, None), + 0x7C => (ArrowRight, None), + 0x7E => (ArrowUp, None), + 0x77 => (End, None), + 0x73 => (Home, None), + 0x79 => (PageDown, None), + 0x74 => (PageUp, None), + + // UI + 0x35 => (Escape, None), + + // Editing keys + 0x33 => (Backspace, None), + + // Function keys + 0x7A => (F1, None), + 0x78 => (F2, None), + 0x63 => (F3, None), + 0x76 => (F4, None), + 0x60 => (F5, None), + 0x61 => (F6, None), + 0x62 => (F7, None), + 0x64 => (F8, None), + 0x65 => (F9, None), + 0x6D => (F10, None), + 0x67 => (F11, None), + 0x6F => (F12, None), + 0x69 => (F13, None), + 0x6B => (F14, None), + 0x71 => (F15, None), + 0x6A => (F16, None), + 0x40 => (F17, None), + 0x4F => (F18, None), + 0x50 => (F19, None), + 0x5A => (F20, None), + + // Other keys, includes the raw code provided by the operating system + _ => (Other(key_code), None), + } +} + +fn raw_to_mouse_button(raw: i32) -> Option { + match raw { + INPUT_MOUSE_LEFT_BUTTON => Some(MouseButton::Left), + INPUT_MOUSE_RIGHT_BUTTON => Some(MouseButton::Right), + INPUT_MOUSE_MIDDLE_BUTTON => Some(MouseButton::Middle), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use std::ffi::CString; + + use super::*; + + fn default_raw_input_event() -> RawInputEvent { + RawInputEvent { + event_type: INPUT_EVENT_TYPE_KEYBOARD, + buffer: [0; 24], + buffer_len: 0, + key_code: 0, + status: INPUT_STATUS_PRESSED, + } + } + + #[test] + fn raw_to_input_event_keyboard_works_correctly() { + let c_string = CString::new("k".to_string()).unwrap(); + let mut buffer: [u8; 24] = [0; 24]; + buffer[..1].copy_from_slice(c_string.as_bytes()); + + let mut raw = default_raw_input_event(); + raw.buffer = buffer; + raw.buffer_len = 1; + raw.status = INPUT_STATUS_RELEASED; + raw.key_code = 40; + + let result: Option = raw.into(); + assert_eq!( + result.unwrap(), + InputEvent::Keyboard(KeyboardEvent { + key: Other(40), + status: Released, + value: Some("k".to_string()), + variant: None, + }) + ); + } + + #[test] + fn raw_to_input_event_mouse_works_correctly() { + let mut raw = default_raw_input_event(); + raw.event_type = INPUT_EVENT_TYPE_MOUSE; + raw.status = INPUT_STATUS_RELEASED; + raw.key_code = INPUT_MOUSE_RIGHT_BUTTON; + + let result: Option = raw.into(); + assert_eq!( + result.unwrap(), + InputEvent::Mouse(MouseEvent { + status: Released, + button: MouseButton::Right, + }) + ); + } +} + diff --git a/espanso-detect/src/mac/native.h b/espanso-detect/src/mac/native.h new file mode 100644 index 0000000..0a2e664 --- /dev/null +++ b/espanso-detect/src/mac/native.h @@ -0,0 +1,61 @@ +/* + * 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_DETECT_H +#define ESPANSO_DETECT_H + +#include + +#define INPUT_EVENT_TYPE_KEYBOARD 1 +#define INPUT_EVENT_TYPE_MOUSE 2 + +#define INPUT_STATUS_PRESSED 1 +#define INPUT_STATUS_RELEASED 2 + +#define INPUT_LEFT_VARIANT 1 +#define INPUT_RIGHT_VARIANT 2 + +#define INPUT_MOUSE_LEFT_BUTTON 1 +#define INPUT_MOUSE_RIGHT_BUTTON 2 +#define INPUT_MOUSE_MIDDLE_BUTTON 3 + +typedef struct { + // Keyboard or Mouse event + int32_t event_type; + + // Contains the string corresponding to the key, if any + char buffer[24]; + // Length of the extracted string. Equals 0 if no string is extracted + int32_t buffer_len; + + // Virtual key code of the pressed key in case of keyboard events + // Mouse button code otherwise. + int32_t key_code; + + // Pressed or Released status + int32_t status; +} InputEvent; + +typedef void (*EventCallback)(InputEvent data); + + +// Initialize the event global monitor +extern "C" void * detect_initialize(EventCallback callback); + +#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 new file mode 100644 index 0000000..7b1a8b8 --- /dev/null +++ b/espanso-detect/src/mac/native.mm @@ -0,0 +1,78 @@ +/* + * 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" +#import +#import +#include + +#include + +const unsigned long long FLAGS = NSEventMaskKeyDown | NSEventMaskKeyUp | NSEventMaskFlagsChanged | NSEventMaskLeftMouseDown | + NSEventMaskLeftMouseUp | NSEventMaskRightMouseDown | NSEventMaskRightMouseUp | + NSEventMaskOtherMouseDown | NSEventMaskOtherMouseUp; + +void * detect_initialize(EventCallback callback) { + dispatch_async(dispatch_get_main_queue(), ^(void) { + [NSEvent addGlobalMonitorForEventsMatchingMask:FLAGS handler:^(NSEvent *event){ + InputEvent inputEvent = {}; + if (event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp ) { + inputEvent.event_type = INPUT_EVENT_TYPE_KEYBOARD; + inputEvent.status = (event.type == NSEventTypeKeyDown) ? INPUT_STATUS_PRESSED : INPUT_STATUS_RELEASED; + inputEvent.key_code = event.keyCode; + + const char *chars = [event.characters UTF8String]; + strncpy(inputEvent.buffer, chars, 23); + inputEvent.buffer_len = event.characters.length; + + callback(inputEvent); + }else if (event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeRightMouseDown || event.type == NSEventTypeOtherMouseDown || + event.type == NSEventTypeLeftMouseUp || event.type == NSEventTypeRightMouseUp || event.type == NSEventTypeOtherMouseUp) { + inputEvent.event_type = INPUT_EVENT_TYPE_MOUSE; + inputEvent.status = (event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeRightMouseDown || + event.type == NSEventTypeOtherMouseDown) ? INPUT_STATUS_PRESSED : INPUT_STATUS_RELEASED; + if (event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeLeftMouseUp) { + inputEvent.key_code = INPUT_MOUSE_LEFT_BUTTON; + } else if (event.type == NSEventTypeRightMouseDown || event.type == NSEventTypeRightMouseUp) { + inputEvent.key_code = INPUT_MOUSE_RIGHT_BUTTON; + } else if (event.type == NSEventTypeOtherMouseDown || event.type == NSEventTypeOtherMouseUp) { + inputEvent.key_code = INPUT_MOUSE_MIDDLE_BUTTON; + } + + callback(inputEvent); + }else{ + // Modifier keys (SHIFT, CTRL, ecc) are handled as a separate case on macOS + inputEvent.event_type = INPUT_EVENT_TYPE_KEYBOARD; + inputEvent.key_code = event.keyCode; + + // To determine whether these keys are pressed or released, we have to analyze each case + if (event.keyCode == kVK_Shift || event.keyCode == kVK_RightShift) { + inputEvent.status = (([event modifierFlags] & NSEventModifierFlagShift) == 0) ? INPUT_STATUS_RELEASED : INPUT_STATUS_PRESSED; + } else if (event.keyCode == kVK_Command || event.keyCode == kVK_RightCommand) { + inputEvent.status = (([event modifierFlags] & NSEventModifierFlagCommand) == 0) ? INPUT_STATUS_RELEASED : INPUT_STATUS_PRESSED; + } else if (event.keyCode == kVK_Control || event.keyCode == kVK_RightControl) { + inputEvent.status = (([event modifierFlags] & NSEventModifierFlagControl) == 0) ? INPUT_STATUS_RELEASED : INPUT_STATUS_PRESSED; + } else if (event.keyCode == kVK_Option || event.keyCode == kVK_RightOption) { + inputEvent.status = (([event modifierFlags] & NSEventModifierFlagOption) == 0) ? INPUT_STATUS_RELEASED : INPUT_STATUS_PRESSED; + } + callback(inputEvent); + } + }]; + }); +} \ No newline at end of file diff --git a/espanso-detect/src/win32/mod.rs b/espanso-detect/src/win32/mod.rs index 878e9a9..b10bab9 100644 --- a/espanso-detect/src/win32/mod.rs +++ b/espanso-detect/src/win32/mod.rs @@ -260,6 +260,9 @@ fn key_code_to_key(key_code: i32) -> (Key, Option) { 0x22 => (PageDown, None), 0x21 => (PageUp, None), + // UI + 0x1B => (Escape, None), + // Editing keys 0x08 => (Backspace, None), diff --git a/espanso-detect/src/x11/mod.rs b/espanso-detect/src/x11/mod.rs index 01c9704..9c9498a 100644 --- a/espanso-detect/src/x11/mod.rs +++ b/espanso-detect/src/x11/mod.rs @@ -260,6 +260,9 @@ fn key_sym_to_key(key_sym: i32) -> (Key, Option) { 0xFF56 => (PageDown, None), 0xFF55 => (PageUp, None), + // UI keys + 0xFF1B => (Escape, None), + // Editing keys 0xFF08 => (Backspace, None), diff --git a/espanso/src/main.rs b/espanso/src/main.rs index 9ad23df..0c60ef2 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -46,26 +46,27 @@ fn main() { icon_paths: &icon_paths, }); - let handle = std::thread::spawn(move || { + eventloop.initialize(); + let handle = std::thread::spawn(move || { //let mut source = espanso_detect::win32::Win32Source::new(); //let mut source = espanso_detect::x11::X11Source::new(); - // source.initialize(); - // source.eventloop(Box::new(move |event: InputEvent| { - // println!("ev {:?}", event); - // match event { - // InputEvent::Mouse(_) => {} - // InputEvent::Keyboard(evt) => { - // if evt.key == espanso_detect::event::Key::Shift && evt.status == Status::Pressed { - // //remote.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled); - // remote.show_notification("Espanso is running!"); - // } - // } - // } - // })); + let mut source = espanso_detect::mac::CocoaSource::new(); + source.initialize(); + source.eventloop(Box::new(move |event: InputEvent| { + println!("ev {:?}", event); + match event { + InputEvent::Mouse(_) => {} + InputEvent::Keyboard(evt) => { + if evt.key == espanso_detect::event::Key::Shift && evt.status == Status::Pressed { + //remote.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled); + //remote.show_notification("Espanso is running!"); + } + } + } + })); }); - eventloop.initialize(); eventloop.run(Box::new(move |event| { println!("ui {:?}", event); let menu = Menu::from(vec![