Skip to content

Commit

Permalink
Compile core_graphics only in Mac
Browse files Browse the repository at this point in the history
  • Loading branch information
xitanggg committed Jul 5, 2024
1 parent 6ee867d commit 7a7d9b7
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 89 deletions.
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ crate-type = ["cdylib"]

[dependencies]
arboard = "3.3.0"
core-graphics = "0.23.2"
enigo = "0.2.0-rc2"
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
napi = { version = "2.12.2", default-features = false, features = ["napi4"] }
napi-derive = "2.12.2"

[target.'cfg(target_os = "macos")'.dependencies]
core-graphics = "0.23.2"

[build-dependencies]
napi-build = "2.0.1"

Expand Down
181 changes: 93 additions & 88 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,15 @@ extern crate napi_derive;
use arboard::{ Clipboard, ImageData };
use enigo::{ Direction::{ Click, Press, Release }, Enigo, Key, Keyboard, Settings };
use std::{ thread, time::Duration };
#[cfg(target_os = "macos")]
use core_graphics::{
event::{
CGEvent,
CGEventTapLocation,
KeyCode,
CGEventFlags,
},
event::{ CGEvent, CGEventTapLocation, KeyCode, CGEventFlags },
event_source::{ CGEventSource, CGEventSourceStateID },
};

#[cfg(target_os = "macos")]
static DEFAULT_MAC_CG_EVENT_WAIT_TIME_MS: u64 = 20;

static DEFAULT_PASTE_WAIT_TIME_MS: u32 = 30;

/// Insert the given text at the current cursor position.
Expand All @@ -25,17 +23,17 @@ static DEFAULT_PASTE_WAIT_TIME_MS: u32 = 30;
///
/// ##### Arguments
/// * `text` - Text to be inserted
/// * `insertWithPaste` - An optional boolean that sets whether to insert text with the paste method.
/// * `insertWithPaste` - An optional boolean that sets whether to insert text with the paste method.
/// Default to false. (Setting true to use the paste method is useful to bypass
/// some limitations in the default insert method. For example, the default
/// some limitations in the default insert method. For example, the default
/// insert method may not work for some apps, and in Mac, it doesn't work
/// when certain key, such as Cmd, is pressed during insert.)
/// * `arrowKeyToClickBeforeInsert` - An optional string that sets which arrow key to click before
/// inserting text. Can be either "left" or "right". Default to None.
/// * `pasteWaitTimeMs` - An optional number that sets how long to wait after performing the paste
/// operation before restoring the previous clipboard state. Default to 30ms.
/// `pasteWaitTimeMs` is only used when using the paste method, i.e. when
/// `insertWithPaste` is set to true. (Beware of setting this value too low,
/// `insertWithPaste` is set to true. (Beware of setting this value too low,
/// as it may end up pasting the previous clipboard text/image)
#[napi]
pub fn insert_text(
Expand All @@ -54,7 +52,7 @@ pub fn insert_text(
let insert_with_paste = insert_with_paste.unwrap_or(false);
if !insert_with_paste {
// Insert text using default method
// Note: This may not work for some apps, and in Mac, it doesn't work when
// Note: This may not work for some apps, and in Mac, it doesn't work when
// certain key, such as Cmd, is pressed during insert (https://github.com/enigo-rs/enigo/issues/297)
enigo.text(&text).unwrap();
} else {
Expand All @@ -64,12 +62,12 @@ pub fn insert_text(
// 1. Save clipboard existing text or image
let clipboard_text = clipboard.get_text().unwrap_or(String::new());
let clipboard_image = clipboard
.get_image()
.unwrap_or(ImageData { width: 0, height: 0, bytes: [].as_ref().into() });
.get_image()
.unwrap_or(ImageData { width: 0, height: 0, bytes: [].as_ref().into() });

// 2. Clear clipboard
clipboard.clear().unwrap();

// 3. Set text to be inserted to clipboard
clipboard.set_text(&text).unwrap();

Expand Down Expand Up @@ -103,9 +101,7 @@ pub fn insert_text(
/// * `arrowKeyToClickBeforePaste` - An optional string that sets which arrow key to click before
/// pasting. Can be either "left" or "right". Default to None.
#[napi]
pub fn paste(
arrow_key_to_click_before_paste: Option<String>,
){
pub fn paste(arrow_key_to_click_before_paste: Option<String>) {
let mut enigo = Enigo::new(&Settings::default()).unwrap();

let arrow_key = arrow_key_to_click_before_paste.unwrap_or(String::new());
Expand All @@ -116,87 +112,96 @@ pub fn paste(
_paste(&mut enigo);
}

/// Simulate arrow key click (left or right)
/// Simulate arrow key click (left or right) - Mac
#[cfg(target_os = "macos")]
fn _click_arrow_key(enigo: &mut Enigo, arrow_key: String) {
if cfg!(target_os = "macos") {
let arrow_key_code = if arrow_key == "left" {
KeyCode::LEFT_ARROW
} else {
KeyCode::RIGHT_ARROW
};

let event_source_state_id = CGEventSourceStateID::CombinedSessionState;
let event_source = CGEventSource::new(event_source_state_id).unwrap();
let event_tap_location = CGEventTapLocation::HID;

let press_arrow_key_event = CGEvent::new_keyboard_event(
event_source.clone(),
arrow_key_code,
true
).unwrap();
press_arrow_key_event.post(event_tap_location);

let release_arrow_key_event = CGEvent::new_keyboard_event(
event_source.clone(),
arrow_key_code,
false
).unwrap();
release_arrow_key_event.post(event_tap_location);
thread::sleep(Duration::from_millis(DEFAULT_MAC_CG_EVENT_WAIT_TIME_MS));
let arrow_key_code = if arrow_key == "left" {
KeyCode::LEFT_ARROW
} else {
let key = if arrow_key == "left" { Key::LeftArrow } else { Key::RightArrow };
enigo.key(key, Click).unwrap();
}
KeyCode::RIGHT_ARROW
};

let event_source_state_id = CGEventSourceStateID::CombinedSessionState;
let event_source = CGEventSource::new(event_source_state_id).unwrap();
let event_tap_location = CGEventTapLocation::HID;

let press_arrow_key_event = CGEvent::new_keyboard_event(
event_source.clone(),
arrow_key_code,
true
).unwrap();
press_arrow_key_event.post(event_tap_location);

let release_arrow_key_event = CGEvent::new_keyboard_event(
event_source.clone(),
arrow_key_code,
false
).unwrap();
release_arrow_key_event.post(event_tap_location);
thread::sleep(Duration::from_millis(DEFAULT_MAC_CG_EVENT_WAIT_TIME_MS));
}

/// Simulate arrow key click (left or right) - Windows
#[cfg(not(target_os = "macos"))]
fn _click_arrow_key(enigo: &mut Enigo, arrow_key: String) {
let key = if arrow_key == "left" { Key::LeftArrow } else { Key::RightArrow };
enigo.key(key, Click).unwrap();
}

// Define CG key code for "v" key
// Reference: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.13.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L206
#[cfg(target_os = "macos")]
static V_KEY_CODE: u16 = 0x09;

/// Simulate Ctrl+ V (Cmd + V in Mac) keyboard input to perform paste
///
/// Windows calls into Enigo to simulate keyboard input. But for MacOS, it calls into
/// Mac's Core Graphics CGEvent libary directly to work around 2 issues with Enigo's current
/// implementation, which casues additional delay (https://github.com/enigo-rs/enigo/issues/105)
/// Simulate Ctrl+ V (Cmd + V in Mac) keyboard input to perform paste - Mac
///
/// Windows calls into Enigo to simulate keyboard input. But for Mac, it calls into
/// Mac's Core Graphics CGEvent library directly to work around 2 issues with Enigo's current
/// implementation, which causes additional delay (https://github.com/enigo-rs/enigo/issues/105)
/// and subjects to mouse movement/keyboard interruption (https://github.com/enigo-rs/enigo/issues/201).
/// Calling into CGEvent and setting event flag solves both issues.
#[cfg(target_os = "macos")]
fn _paste(enigo: &mut Enigo) {
if cfg!(target_os = "macos") {
// Implementation reference: https://stackoverflow.com/questions/2008126/cgeventpost-possible-bug-when-simulating-keyboard-events

// Event source state id reference: https://developer.apple.com/documentation/coregraphics/cgeventsourcestateid
let event_source_state_id = CGEventSourceStateID::CombinedSessionState;
let event_source = CGEventSource::new(event_source_state_id).unwrap();
// Event tap location reference: https://developer.apple.com/documentation/coregraphics/cgeventtaplocation
let event_tap_location = CGEventTapLocation::HID;

let press_cmd_v_event = CGEvent::new_keyboard_event(
event_source.clone(),
V_KEY_CODE,
true
).unwrap();
press_cmd_v_event.set_flags(CGEventFlags::CGEventFlagCommand); // Set flags to Cmd
press_cmd_v_event.post(event_tap_location);

let release_v_event = CGEvent::new_keyboard_event(
event_source.clone(),
V_KEY_CODE,
false
).unwrap();
release_v_event.set_flags(CGEventFlags::CGEventFlagNull); // Reset flags to null
release_v_event.post(event_tap_location);

// Release Cmd Key for completeness. May or may not be necessary
// given Apple's documentation is not clear on this.
let release_cmd_event = CGEvent::new_keyboard_event(
event_source.clone(),
KeyCode::COMMAND,
false
).unwrap();
release_cmd_event.post(event_tap_location);
} else {
enigo.key(Key::Control, Press).unwrap();
enigo.key(Key::Unicode('v'), Click).unwrap();
enigo.key(Key::Control, Release).unwrap();
}
// Implementation reference: https://stackoverflow.com/questions/2008126/cgeventpost-possible-bug-when-simulating-keyboard-events

// Event source state id reference: https://developer.apple.com/documentation/coregraphics/cgeventsourcestateid
let event_source_state_id = CGEventSourceStateID::CombinedSessionState;
let event_source = CGEventSource::new(event_source_state_id).unwrap();
// Event tap location reference: https://developer.apple.com/documentation/coregraphics/cgeventtaplocation
let event_tap_location = CGEventTapLocation::HID;

let press_cmd_v_event = CGEvent::new_keyboard_event(
event_source.clone(),
V_KEY_CODE,
true
).unwrap();
press_cmd_v_event.set_flags(CGEventFlags::CGEventFlagCommand); // Set flags to Cmd
press_cmd_v_event.post(event_tap_location);

let release_v_event = CGEvent::new_keyboard_event(
event_source.clone(),
V_KEY_CODE,
false
).unwrap();
release_v_event.set_flags(CGEventFlags::CGEventFlagNull); // Reset flags to null
release_v_event.post(event_tap_location);

// Release Cmd Key for completeness. May or may not be necessary
// given Apple's documentation is not clear on this.
let release_cmd_event = CGEvent::new_keyboard_event(
event_source.clone(),
KeyCode::COMMAND,
false
).unwrap();
release_cmd_event.post(event_tap_location);
}

/// Simulate Ctrl+ V (Cmd + V in Mac) keyboard input to perform paste - Windows
///
/// Windows calls into Enigo to simulate keyboard input
#[cfg(not(target_os = "macos"))]
fn _paste(enigo: &mut Enigo) {
enigo.key(Key::Control, Press).unwrap();
enigo.key(Key::Unicode('v'), Click).unwrap();
enigo.key(Key::Control, Release).unwrap();
}

0 comments on commit 7a7d9b7

Please sign in to comment.