#![allow(
dead_code,
reason = "unused when the mock-loader feature selects the DLL backend instead"
)]
use super::keycodes::{self, KeyCode};
use crate::keys::{Modifier, Token};
use crate::options::{KeyMap, Options};
use crate::{Keys, Point};
use objc2_core_foundation::CGPoint;
use objc2_core_graphics::{
CGEvent, CGEventField, CGEventFlags, CGEventSource, CGEventSourceStateID, CGEventTapLocation,
CGEventType, CGMouseButton,
};
use std::time::Duration;
const TAP: CGEventTapLocation = CGEventTapLocation::HIDEventTap;
fn source() -> Option<objc2_core_foundation::CFRetained<CGEventSource>> {
CGEventSource::new(CGEventSourceStateID::HIDSystemState)
}
#[derive(Default)]
struct Held {
codes: Vec<KeyCode>,
}
impl Held {
fn press(&mut self, code: KeyCode) {
if !self.codes.contains(&code) {
post_key(code, true, self.flags());
self.codes.push(code);
}
}
fn release(&mut self, code: KeyCode) {
if let Some(i) = self.codes.iter().position(|c| *c == code) {
self.codes.remove(i);
post_key(code, false, self.flags());
}
}
fn flags(&self) -> CGEventFlags {
let mut f = CGEventFlags::empty();
for code in &self.codes {
f |= match *code {
keycodes::VK_COMMAND => CGEventFlags::MaskCommand,
keycodes::VK_SHIFT | keycodes::VK_RIGHT_SHIFT => CGEventFlags::MaskShift,
keycodes::VK_OPTION | keycodes::VK_RIGHT_OPTION => CGEventFlags::MaskAlternate,
keycodes::VK_CONTROL | keycodes::VK_RIGHT_CONTROL => CGEventFlags::MaskControl,
_ => CGEventFlags::empty(),
};
}
f
}
#[cfg(test)]
fn is_empty(&self) -> bool {
self.codes.is_empty()
}
}
impl Drop for Held {
fn drop(&mut self) {
while let Some(code) = self.codes.pop() {
post_key(code, false, self.flags());
}
}
}
fn post_key(code: KeyCode, down: bool, flags: CGEventFlags) {
let Some(src) = source() else { return };
let Some(event) = CGEvent::new_keyboard_event(Some(&src), code, down) else {
return;
};
CGEvent::set_flags(Some(&event), flags);
CGEvent::post(TAP, Some(&event));
}
fn post_char(c: char, flags: CGEventFlags) {
let Some(src) = source() else { return };
let mut utf16 = [0u16; 2];
let encoded = c.encode_utf16(&mut utf16);
for down in [true, false] {
let Some(event) = CGEvent::new_keyboard_event(Some(&src), 0, down) else {
return;
};
unsafe {
CGEvent::keyboard_set_unicode_string(
Some(&event),
encoded.len() as u64,
encoded.as_ptr(),
);
}
CGEvent::set_flags(Some(&event), flags);
CGEvent::post(TAP, Some(&event));
}
}
fn modifier_code(name: &str, map: KeyMap) -> Option<KeyCode> {
let base = keycodes::lookup(name)?;
Some(match (map, base) {
(KeyMap::PortableShortcuts, keycodes::VK_CONTROL) => keycodes::VK_COMMAND,
(KeyMap::PortableShortcuts, keycodes::VK_COMMAND) => keycodes::VK_CONTROL,
_ => base,
})
}
fn is_shortcut(flags: CGEventFlags) -> bool {
flags.intersects(
CGEventFlags::MaskCommand | CGEventFlags::MaskControl | CGEventFlags::MaskAlternate,
)
}
pub(crate) fn send(keys: &Keys, options: &Options) -> crate::Result<()> {
let tokens = keys.tokens()?;
let map = options.key_map;
let mut held = Held::default();
let mut pending: Vec<KeyCode> = Vec::new();
for token in &tokens {
match token {
Token::Modifier(m) => {
let name = match m {
Modifier::Ctrl => "CTRL",
Modifier::Shift => "SHIFT",
Modifier::Alt => "ALT",
Modifier::Win => "LWIN",
};
if let Some(code) = modifier_code(name, map) {
pending.push(code);
}
}
Token::Char(c) => {
for code in &pending {
held.press(*code);
}
let flags = held.flags();
if is_shortcut(flags) {
let Some(code) = keycodes::ansi(*c) else {
return Err(crate::Error::UnsupportedKey {
key: format!("{c} as part of a shortcut"),
platform: "macOS",
});
};
post_key(code, true, flags);
std::thread::sleep(options.send_key_down_delay);
post_key(code, false, flags);
} else {
post_char(*c, flags);
}
for code in std::mem::take(&mut pending) {
held.release(code);
}
std::thread::sleep(options.send_key_delay);
}
Token::Named { name, repeat, hold } => {
let upper = name.to_ascii_uppercase();
if let Some(code) = upper
.strip_suffix("DOWN")
.and_then(|base| modifier_code(base, map))
{
held.press(code);
continue;
}
if let Some(code) = upper
.strip_suffix("UP")
.and_then(|base| modifier_code(base, map))
{
held.release(code);
continue;
}
let Some(code) = keycodes::lookup(&upper) else {
return Err(crate::Error::UnsupportedKey {
key: upper,
platform: "macOS",
});
};
match hold {
Some(true) => held.press(code),
Some(false) => held.release(code),
None => {
for code in &pending {
held.press(*code);
}
for _ in 0..*repeat {
post_key(code, true, held.flags());
std::thread::sleep(options.send_key_down_delay);
post_key(code, false, held.flags());
std::thread::sleep(options.send_key_delay);
}
for code in std::mem::take(&mut pending) {
held.release(code);
}
}
}
}
}
}
Ok(())
}
fn cg_point(p: Point) -> CGPoint {
CGPoint {
x: f64::from(p.x),
y: f64::from(p.y),
}
}
fn button_events(button: &str) -> (CGMouseButton, CGEventType, CGEventType, CGEventType) {
match button {
"right" | "secondary" => (
CGMouseButton::Right,
CGEventType::RightMouseDown,
CGEventType::RightMouseUp,
CGEventType::RightMouseDragged,
),
"middle" => (
CGMouseButton::Center,
CGEventType::OtherMouseDown,
CGEventType::OtherMouseUp,
CGEventType::OtherMouseDragged,
),
_ => (
CGMouseButton::Left,
CGEventType::LeftMouseDown,
CGEventType::LeftMouseUp,
CGEventType::LeftMouseDragged,
),
}
}
fn post_mouse(kind: CGEventType, at: Point, button: CGMouseButton, click_state: Option<i64>) {
let Some(src) = source() else { return };
let Some(event) = CGEvent::new_mouse_event(Some(&src), kind, cg_point(at), button) else {
return;
};
if let Some(n) = click_state {
CGEvent::set_integer_value_field(Some(&event), CGEventField::MouseEventClickState, n);
}
CGEvent::post(TAP, Some(&event));
}
pub(crate) fn mouse_get_pos() -> crate::Result<Point> {
let Some(src) = source() else {
return Ok(Point::ORIGIN);
};
let p = CGEvent::new(Some(&src))
.map(|e| CGEvent::location(Some(&e)))
.unwrap_or(CGPoint { x: 0.0, y: 0.0 });
Ok(Point::new(p.x as i32, p.y as i32))
}
pub(crate) fn mouse_move(to: Point, speed: Option<crate::options::Speed>) -> crate::Result<()> {
let steps = speed.map_or(0, |s| s.get()).max(0);
if steps > 0 {
let from = mouse_get_pos()?;
for i in 1..steps {
let t = f64::from(i) / f64::from(steps);
let x = f64::from(from.x) + (f64::from(to.x - from.x) * t);
let y = f64::from(from.y) + (f64::from(to.y - from.y) * t);
post_mouse(
CGEventType::MouseMoved,
Point::new(x as i32, y as i32),
CGMouseButton::Left,
None,
);
std::thread::sleep(Duration::from_millis(1));
}
}
post_mouse(CGEventType::MouseMoved, to, CGMouseButton::Left, None);
Ok(())
}
pub(crate) fn mouse_click(
button: &str,
at: Point,
clicks: u32,
speed: Option<crate::options::Speed>,
options: &Options,
) -> crate::Result<()> {
let (cg_button, down, up, _) = button_events(button);
mouse_move(at, speed)?;
for n in 1..=clicks.max(1) {
let state = Some(i64::from(n));
post_mouse(down, at, cg_button, state);
std::thread::sleep(options.mouse_click_down_delay);
post_mouse(up, at, cg_button, state);
std::thread::sleep(options.mouse_click_delay);
}
Ok(())
}
pub(crate) fn mouse_down(button: &str) -> crate::Result<()> {
let (cg_button, down, _, _) = button_events(button);
let at = mouse_get_pos()?;
post_mouse(down, at, cg_button, Some(1));
Ok(())
}
pub(crate) fn mouse_up(button: &str) -> crate::Result<()> {
let (cg_button, _, up, _) = button_events(button);
let at = mouse_get_pos()?;
post_mouse(up, at, cg_button, Some(1));
Ok(())
}
pub(crate) fn mouse_click_drag(
button: &str,
from: Point,
to: Point,
speed: Option<crate::options::Speed>,
options: &Options,
) -> crate::Result<()> {
let (cg_button, down, up, dragged) = button_events(button);
mouse_move(from, speed)?;
post_mouse(down, from, cg_button, Some(1));
std::thread::sleep(options.mouse_click_drag_delay);
let steps = speed.map_or(10, |s| s.get()).max(1);
for i in 1..=steps {
let t = f64::from(i) / f64::from(steps);
let x = f64::from(from.x) + (f64::from(to.x - from.x) * t);
let y = f64::from(from.y) + (f64::from(to.y - from.y) * t);
post_mouse(dragged, Point::new(x as i32, y as i32), cg_button, Some(1));
std::thread::sleep(Duration::from_millis(5));
}
post_mouse(up, to, cg_button, Some(1));
Ok(())
}
pub(crate) fn mouse_wheel(direction: &str, clicks: u32) -> crate::Result<()> {
let Some(src) = source() else { return Ok(()) };
let amount = if direction.eq_ignore_ascii_case("down") {
-1
} else {
1
};
for _ in 0..clicks.max(1) {
let Some(event) = CGEvent::new_scroll_wheel_event2(
Some(&src),
objc2_core_graphics::CGScrollEventUnit::Line,
1,
amount,
0,
0,
) else {
return Ok(());
};
CGEvent::post(TAP, Some(&event));
std::thread::sleep(Duration::from_millis(20));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keys;
#[test]
#[ignore = "moves the real cursor"]
fn the_cursor_actually_goes_where_it_is_sent() {
let start = mouse_get_pos().expect("reading the cursor position");
for target in [Point::new(300, 200), Point::new(700, 500)] {
mouse_move(target, Some(crate::options::Speed::INSTANT)).unwrap();
std::thread::sleep(Duration::from_millis(120));
let got = mouse_get_pos().unwrap();
assert_eq!(got, target, "asked for {target:?}, cursor reports {got:?}");
}
mouse_move(start, Some(crate::options::Speed::INSTANT)).unwrap();
}
#[test]
#[ignore = "types into the focused application"]
fn accented_text_types_into_the_focused_app() {
let text = "Ünïcödé ãõç — 1.234,56";
println!("typing into the focused app in 3s: {text}");
std::thread::sleep(Duration::from_secs(3));
send(&keys::Keys::text(text), &Options::default()).unwrap();
}
#[test]
fn the_held_set_tracks_presses_and_releases() {
let mut h = Held::default();
assert!(h.is_empty());
h.press(keycodes::VK_COMMAND);
assert!(!h.is_empty());
assert!(h.flags().contains(CGEventFlags::MaskCommand));
h.press(keycodes::VK_COMMAND);
assert_eq!(h.codes.len(), 1);
h.release(keycodes::VK_COMMAND);
assert!(h.is_empty());
assert!(!h.flags().contains(CGEventFlags::MaskCommand));
}
#[test]
fn releasing_something_never_pressed_is_harmless() {
let mut h = Held::default();
h.release(keycodes::VK_SHIFT);
assert!(h.is_empty());
}
#[test]
fn flags_accumulate_across_several_modifiers() {
let mut h = Held::default();
h.press(keycodes::VK_COMMAND);
h.press(keycodes::VK_SHIFT);
let f = h.flags();
assert!(f.contains(CGEventFlags::MaskCommand));
assert!(f.contains(CGEventFlags::MaskShift));
assert!(!f.contains(CGEventFlags::MaskControl));
}
#[test]
fn the_default_key_map_leaves_control_as_control() {
assert_eq!(
modifier_code("CTRL", KeyMap::AsWritten),
Some(keycodes::VK_CONTROL)
);
assert_eq!(
modifier_code("LWIN", KeyMap::AsWritten),
Some(keycodes::VK_COMMAND)
);
}
#[test]
fn portable_shortcuts_swap_control_and_command() {
assert_eq!(
modifier_code("CTRL", KeyMap::PortableShortcuts),
Some(keycodes::VK_COMMAND)
);
assert_eq!(
modifier_code("LWIN", KeyMap::PortableShortcuts),
Some(keycodes::VK_CONTROL)
);
assert_eq!(
modifier_code("ALT", KeyMap::PortableShortcuts),
modifier_code("ALT", KeyMap::AsWritten)
);
}
#[test]
fn an_unsupported_key_is_an_error_rather_than_silence() {
let err = send(
&keys::Keys::parse("{PRINTSCREEN}").unwrap(),
&Options::default(),
)
.unwrap_err();
assert!(
matches!(
err,
crate::Error::UnsupportedKey {
platform: "macOS",
..
}
),
"{err:?}"
);
}
#[test]
fn a_failure_mid_sequence_leaves_no_modifier_held() {
let mut h = Held::default();
h.press(keycodes::VK_COMMAND);
h.press(keycodes::VK_SHIFT);
assert_eq!(h.codes.len(), 2);
drop(h);
let err = send(
&keys::Keys::parse("{CTRLDOWN}{PRINTSCREEN}{CTRLUP}").unwrap(),
&Options::default(),
);
assert!(err.is_err(), "the unsupported key must still fail");
}
}