fix(natives): prevented macos input suppression

Configured Quartz event sources to permit local hardware events in both suppression states and replaced Enigo-backed global posting with the configured source.

Added a macOS regression test for the event-source suppression settings.

Fixes #7872
This commit is contained in:
roboomp
2026-08-07 03:02:49 +00:00
parent 3a8591a8af
commit 62b007295a
3 changed files with 259 additions and 179 deletions
+1 -22
View File
@@ -60,7 +60,7 @@ impl KeyName {
matches!(self, Self::Ctrl | Self::Alt | Self::Shift | Self::Meta)
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
#[cfg(target_os = "windows")]
pub(crate) const fn to_enigo(self) -> enigo::Key {
use enigo::Key;
match self {
@@ -74,10 +74,7 @@ impl KeyName {
Self::Space => Key::Space,
Self::Backspace => Key::Backspace,
Self::Delete => Key::Delete,
#[cfg(target_os = "windows")]
Self::Insert => Key::Insert,
#[cfg(target_os = "macos")]
Self::Insert => Key::Other(0x72),
Self::Home => Key::Home,
Self::End => Key::End,
Self::PageUp => Key::PageUp,
@@ -87,14 +84,8 @@ impl KeyName {
Self::Left => Key::LeftArrow,
Self::Right => Key::RightArrow,
Self::CapsLock => Key::CapsLock,
#[cfg(target_os = "windows")]
Self::NumLock => Key::Numlock,
#[cfg(target_os = "macos")]
Self::NumLock => Key::Other(0x47),
#[cfg(target_os = "windows")]
Self::PrintScreen => Key::PrintScr,
#[cfg(target_os = "macos")]
Self::PrintScreen => Key::Other(0x69),
Self::F1 => Key::F1,
Self::F2 => Key::F2,
Self::F3 => Key::F3,
@@ -115,22 +106,10 @@ impl KeyName {
Self::F18 => Key::F18,
Self::F19 => Key::F19,
Self::F20 => Key::F20,
#[cfg(target_os = "windows")]
Self::F21 => Key::F21,
#[cfg(target_os = "windows")]
Self::F22 => Key::F22,
#[cfg(target_os = "windows")]
Self::F23 => Key::F23,
#[cfg(target_os = "windows")]
Self::F24 => Key::F24,
#[cfg(target_os = "macos")]
Self::F21 => Key::Other(0x6e),
#[cfg(target_os = "macos")]
Self::F22 => Key::Other(0x6f),
#[cfg(target_os = "macos")]
Self::F23 => Key::Other(0x70),
#[cfg(target_os = "macos")]
Self::F24 => Key::Other(0x71),
Self::Char(character) => Key::Unicode(character),
}
}
+254 -157
View File
@@ -4,11 +4,15 @@ use std::{
};
use core_graphics::{
event::{CGEvent, CGEventFlags, CGEventType, CGMouseButton, ScrollEventUnit},
event::{
CGEvent, CGEventFlags, CGEventTapLocation, CGEventType, CGMouseButton, EventField,
ScrollEventUnit,
},
event_source::{CGEventSource, CGEventSourceStateID},
geometry::CGPoint,
sys::CGEventSourceRef,
};
use enigo::{Axis, Button, Coordinate, Direction, Enigo, Keyboard, Mouse, Settings};
use foreign_types::ForeignType;
use super::{
super::{
@@ -23,22 +27,12 @@ use super::{
};
pub(super) struct MacInput {
global: Enigo,
source: CGEventSource,
}
impl MacInput {
pub(super) fn new() -> CoreResult<Self> {
let settings = Settings { open_prompt_to_get_permissions: false, ..Settings::default() };
let global = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| Enigo::new(&settings)))
.map_err(|_| {
DesktopError::input_failed("Quartz native input initialization failed unexpectedly")
})?
.map_err(|error| {
DesktopError::input_failed(format!(
"Quartz native input initialization failed: {error}"
))
})?;
Ok(Self { global })
Ok(Self { source: source()? })
}
pub(super) fn pointer(
@@ -49,7 +43,7 @@ impl MacInput {
capture: &MacCapture,
) -> CoreResult<()> {
match target {
Target::Desktop => global_pointer(&mut self.global, event),
Target::Desktop => global_pointer(&self.source, event),
Target::Window(id) => {
let window = capture.window(id)?;
let (pid, wid) = window_identity(&window)?;
@@ -59,10 +53,10 @@ impl MacInput {
if !window.focused {
skylight::activate_without_raise(pid, wid)?;
}
background_pointer(pid, wid, &window, event)
background_pointer(&self.source, pid, wid, &window, event)
},
DeliveryMode::Foreground => {
skylight::with_foreground(pid, wid, || global_pointer(&mut self.global, event))
skylight::with_foreground(pid, wid, || global_pointer(&self.source, event))
},
}
},
@@ -77,7 +71,7 @@ impl MacInput {
capture: &MacCapture,
) -> CoreResult<()> {
match target {
Target::Desktop => self.global.text(text).map_err(input_error),
Target::Desktop => global_type(&self.source, text),
Target::Window(id) => {
let window = capture.window(id)?;
let (pid, wid) = window_identity(&window)?;
@@ -85,11 +79,11 @@ impl MacInput {
DeliveryMode::Background => {
background_guard(&window, "keyboard", None)?;
prepare_background_keys(&window, pid, wid, capture)?;
background_type(pid, text)
background_type(&self.source, pid, text)
},
DeliveryMode::Foreground => skylight::with_foreground(pid, wid, || {
let _ = ax::prepare_foreground_input(&window);
self.global.text(text).map_err(input_error)
global_type(&self.source, text)
}),
}
},
@@ -104,7 +98,7 @@ impl MacInput {
capture: &MacCapture,
) -> CoreResult<()> {
match target {
Target::Desktop => global_chord(&mut self.global, keys),
Target::Desktop => global_chord(&self.source, keys),
Target::Window(id) => {
let window = capture.window(id)?;
let (pid, wid) = window_identity(&window)?;
@@ -112,11 +106,11 @@ impl MacInput {
DeliveryMode::Background => {
background_guard(&window, "keyboard", None)?;
prepare_background_keys(&window, pid, wid, capture)?;
background_chord(pid, keys)
background_chord(&self.source, pid, keys)
},
DeliveryMode::Foreground => skylight::with_foreground(pid, wid, || {
let _ = ax::prepare_foreground_input(&window);
global_chord(&mut self.global, keys)
global_chord(&self.source, keys)
}),
}
},
@@ -218,9 +212,48 @@ fn background_guard(
Ok(())
}
const LOCAL_EVENT_FILTER: u32 = 0x01 | 0x02 | 0x04;
const SUPPRESSION_INTERVAL: u32 = 0;
const REMOTE_MOUSE_DRAG: u32 = 1;
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
#[link_name = "CGEventSourceSetLocalEventsSuppressionInterval"]
fn set_local_events_suppression_interval(source: CGEventSourceRef, seconds: f64);
#[link_name = "CGEventSourceSetLocalEventsFilterDuringSuppressionState"]
fn set_local_events_filter_during_suppression_state(
source: CGEventSourceRef,
filter: u32,
state: u32,
);
#[cfg(test)]
#[link_name = "CGEventSourceGetLocalEventsSuppressionInterval"]
fn get_local_events_suppression_interval(source: CGEventSourceRef) -> f64;
#[cfg(test)]
#[link_name = "CGEventSourceGetLocalEventsFilterDuringSuppressionState"]
fn get_local_events_filter_during_suppression_state(source: CGEventSourceRef, state: u32)
-> u32;
}
fn source() -> CoreResult<CGEventSource> {
CGEventSource::new(CGEventSourceStateID::HIDSystemState)
.map_err(|()| DesktopError::input_failed("failed to create a Quartz input event source"))
let source = CGEventSource::new(CGEventSourceStateID::HIDSystemState)
.map_err(|()| DesktopError::input_failed("failed to create a Quartz input event source"))?;
// SAFETY: `source` is a live CGEventSource and both setters accept these
// documented masks/states.
unsafe {
set_local_events_suppression_interval(source.as_ptr(), 0.0);
set_local_events_filter_during_suppression_state(
source.as_ptr(),
LOCAL_EVENT_FILTER,
SUPPRESSION_INTERVAL,
);
set_local_events_filter_during_suppression_state(
source.as_ptr(),
LOCAL_EVENT_FILTER,
REMOTE_MOUSE_DRAG,
);
}
Ok(source)
}
fn modifier_flags(modifiers: Modifiers) -> CGEventFlags {
@@ -269,6 +302,7 @@ const fn button_types(
}
fn background_pointer(
source: &CGEventSource,
pid: libc::pid_t,
wid: u32,
window: &DesktopWindow,
@@ -276,7 +310,7 @@ fn background_pointer(
) -> CoreResult<()> {
match event {
PointerEvent::Click { x, y, button, count, modifiers } => {
background_click(pid, wid, window, x, y, button, count, modifiers)
background_click(source, pid, wid, window, x, y, button, count, modifiers)
},
PointerEvent::Move { x, y } => {
let group = click_group_id();
@@ -284,7 +318,7 @@ fn background_pointer(
pid,
wid,
window,
source()?,
source.clone(),
CGEventType::MouseMoved,
CGMouseButton::Left,
x,
@@ -297,13 +331,16 @@ fn background_pointer(
)
},
PointerEvent::Drag { path, button, modifiers } => {
background_drag(pid, wid, window, &path, button, modifiers)
background_drag(source, pid, wid, window, &path, button, modifiers)
},
PointerEvent::Scroll { x, y, dx, dy } => {
background_scroll(source, pid, wid, window, x, y, dx, dy)
},
PointerEvent::Scroll { x, y, dx, dy } => background_scroll(pid, wid, window, x, y, dx, dy),
}
}
fn background_click(
source: &CGEventSource,
pid: libc::pid_t,
wid: u32,
window: &DesktopWindow,
@@ -313,11 +350,10 @@ fn background_click(
count: u32,
modifiers: Modifiers,
) -> CoreResult<()> {
let source = source()?;
let group = click_group_id();
let (cg_button, down, up, _, number) = button_types(button);
let flags = modifier_flags(modifiers);
pointer_prologue(pid, wid, window, &source, x, y, group, flags)?;
pointer_prologue(pid, wid, window, source, x, y, group, flags)?;
for click_state in 1..=count.max(1) {
post_mouse(
pid,
@@ -419,6 +455,7 @@ fn pointer_prologue(
}
fn background_drag(
source: &CGEventSource,
pid: libc::pid_t,
wid: u32,
window: &DesktopWindow,
@@ -432,11 +469,10 @@ fn background_drag(
if path.len() < 2 {
return Err(DesktopError::input_failed("drag path must contain at least two points"));
}
let source = source()?;
let group = click_group_id();
let (cg_button, down, up, dragged, number) = button_types(button);
let flags = modifier_flags(modifiers);
pointer_prologue(pid, wid, window, &source, start_x, start_y, group, flags)?;
pointer_prologue(pid, wid, window, source, start_x, start_y, group, flags)?;
post_mouse(
pid,
wid,
@@ -474,7 +510,21 @@ fn background_drag(
let &(end_x, end_y) = path
.last()
.ok_or_else(|| DesktopError::input_failed("drag path is empty"))?;
post_mouse(pid, wid, window, source, up, cg_button, end_x, end_y, 3, 1, number, group, flags)?;
post_mouse(
pid,
wid,
window,
source.clone(),
up,
cg_button,
end_x,
end_y,
3,
1,
number,
group,
flags,
)?;
Ok(())
}
@@ -512,6 +562,7 @@ fn post_mouse(
}
fn background_scroll(
source: &CGEventSource,
pid: libc::pid_t,
wid: u32,
window: &DesktopWindow,
@@ -521,7 +572,6 @@ fn background_scroll(
dy: f64,
) -> CoreResult<()> {
let group = click_group_id();
let source = source()?;
post_mouse(
pid,
wid,
@@ -540,8 +590,9 @@ fn background_scroll(
thread::sleep(Duration::from_millis(15));
let wheel_x = finite_i32(dx, "horizontal scroll delta")?;
let wheel_y = finite_i32(dy, "vertical scroll delta")?;
let event = CGEvent::new_scroll_event(source, ScrollEventUnit::PIXEL, 2, wheel_y, wheel_x, 0)
.map_err(|()| DesktopError::input_failed("failed to create a Quartz scroll event"))?;
let event =
CGEvent::new_scroll_event(source.clone(), ScrollEventUnit::PIXEL, 2, wheel_y, wheel_x, 0)
.map_err(|()| DesktopError::input_failed("failed to create a Quartz scroll event"))?;
event.set_location(CGPoint::new(x, y));
skylight::stamp_event(
&event,
@@ -564,8 +615,19 @@ fn click_group_id() -> i64 {
.into()
}
fn background_type(pid: libc::pid_t, text: &str) -> CoreResult<()> {
let source = source()?;
fn background_type(source: &CGEventSource, pid: libc::pid_t, text: &str) -> CoreResult<()> {
type_text(source, text, |event| skylight::post_keyboard(pid, event))
}
fn global_type(source: &CGEventSource, text: &str) -> CoreResult<()> {
type_text(source, text, post_global)
}
fn type_text(
source: &CGEventSource,
text: &str,
mut post: impl FnMut(&CGEvent) -> CoreResult<()>,
) -> CoreResult<()> {
for character in text.chars() {
let value = character.to_string();
for down in [true, false] {
@@ -573,28 +635,39 @@ fn background_type(pid: libc::pid_t, text: &str) -> CoreResult<()> {
.map_err(|()| DesktopError::input_failed("failed to create a Quartz keyboard event"))?;
event.set_string(&value);
event.set_flags(CGEventFlags::CGEventFlagNull);
skylight::post_keyboard(pid, &event)?;
post(&event)?;
thread::sleep(Duration::from_millis(8));
}
}
Ok(())
}
fn background_chord(pid: libc::pid_t, keys: &[KeyName]) -> CoreResult<()> {
fn background_chord(source: &CGEventSource, pid: libc::pid_t, keys: &[KeyName]) -> CoreResult<()> {
key_chord(source, keys, |event| skylight::post_keyboard(pid, event))
}
fn global_chord(source: &CGEventSource, keys: &[KeyName]) -> CoreResult<()> {
key_chord(source, keys, post_global)
}
fn key_chord(
source: &CGEventSource,
keys: &[KeyName],
mut post: impl FnMut(&CGEvent) -> CoreResult<()>,
) -> CoreResult<()> {
if keys.is_empty() {
return Err(DesktopError::invalid_key("key chord must not be empty"));
}
let source = source()?;
let mut active = Modifiers::default();
for &key in keys {
update_modifier(&mut active, key, true);
post_key(pid, &source, key, true, modifier_flags(active))?;
post_key(source, key, true, modifier_flags(active), &mut post)?;
thread::sleep(Duration::from_millis(8));
}
let mut first_error = None;
for &key in keys.iter().rev() {
update_modifier(&mut active, key, false);
if let Err(error) = post_key(pid, &source, key, false, modifier_flags(active))
if let Err(error) = post_key(source, key, false, modifier_flags(active), &mut post)
&& first_error.is_none()
{
first_error = Some(error);
@@ -605,17 +678,17 @@ fn background_chord(pid: libc::pid_t, keys: &[KeyName]) -> CoreResult<()> {
}
fn post_key(
pid: libc::pid_t,
source: &CGEventSource,
key: KeyName,
down: bool,
flags: CGEventFlags,
post: &mut impl FnMut(&CGEvent) -> CoreResult<()>,
) -> CoreResult<()> {
let code = key_code(key)?;
let event = CGEvent::new_keyboard_event(source.clone(), code, down)
.map_err(|()| DesktopError::input_failed("failed to create a Quartz keyboard event"))?;
event.set_flags(flags);
skylight::post_keyboard(pid, &event)
post(&event)
}
const fn update_modifier(modifiers: &mut Modifiers, key: KeyName, down: bool) {
@@ -740,128 +813,142 @@ fn char_key_code(character: char) -> CoreResult<u16> {
Ok(code)
}
fn global_pointer(input: &mut Enigo, event: PointerEvent) -> CoreResult<()> {
fn global_pointer(source: &CGEventSource, event: PointerEvent) -> CoreResult<()> {
match event {
PointerEvent::Click { x, y, button, count, modifiers } => {
move_global(input, x, y)?;
with_global_modifiers(input, modifiers, |input| {
for _ in 0..count.max(1) {
input
.button(enigo_button(button), Direction::Click)
.map_err(input_error)?;
post_global_mouse(
source,
CGEventType::MouseMoved,
CGMouseButton::Left,
x,
y,
0,
0,
CGEventFlags::CGEventFlagNull,
)?;
let (cg_button, down, up, _, number) = button_types(button);
let flags = modifier_flags(modifiers);
for click_state in 1..=count.max(1) {
post_global_mouse(
source,
down,
cg_button,
x,
y,
i64::from(click_state),
number,
flags,
)?;
thread::sleep(Duration::from_millis(1));
post_global_mouse(source, up, cg_button, x, y, i64::from(click_state), number, flags)?;
if click_state < count.max(1) {
thread::sleep(Duration::from_millis(80));
}
Ok(())
})
}
Ok(())
},
PointerEvent::Move { x, y } => move_global(input, x, y),
PointerEvent::Move { x, y } => post_global_mouse(
source,
CGEventType::MouseMoved,
CGMouseButton::Left,
x,
y,
0,
0,
CGEventFlags::CGEventFlagNull,
),
PointerEvent::Drag { path, button, modifiers } => {
let Some(&(x, y)) = path.first() else {
let Some(&(start_x, start_y)) = path.first() else {
return Err(DesktopError::input_failed("drag path must contain at least two points"));
};
if path.len() < 2 {
return Err(DesktopError::input_failed("drag path must contain at least two points"));
}
move_global(input, x, y)?;
with_global_modifiers(input, modifiers, |input| {
input
.button(enigo_button(button), Direction::Press)
.map_err(input_error)?;
let drag = path[1..]
.iter()
.try_for_each(|&(x, y)| move_global(input, x, y));
let release = input
.button(enigo_button(button), Direction::Release)
.map_err(input_error);
drag.and(release)
})
let (cg_button, down, up, dragged, number) = button_types(button);
let flags = modifier_flags(modifiers);
post_global_mouse(
source,
CGEventType::MouseMoved,
CGMouseButton::Left,
start_x,
start_y,
0,
0,
CGEventFlags::CGEventFlagNull,
)?;
post_global_mouse(source, down, cg_button, start_x, start_y, 1, number, flags)?;
for &(x, y) in &path[1..] {
thread::sleep(Duration::from_millis(16));
post_global_mouse(source, dragged, cg_button, x, y, 1, number, flags)?;
}
thread::sleep(Duration::from_millis(50));
let &(end_x, end_y) = path
.last()
.ok_or_else(|| DesktopError::input_failed("drag path is empty"))?;
post_global_mouse(source, up, cg_button, end_x, end_y, 1, number, flags)
},
PointerEvent::Scroll { x, y, dx, dy } => {
move_global(input, x, y)?;
let horizontal = finite_i32(dx, "horizontal scroll delta")?;
let vertical = finite_i32(dy, "vertical scroll delta")?;
if horizontal != 0 {
input
.scroll(horizontal, Axis::Horizontal)
.map_err(input_error)?;
}
if vertical != 0 {
input
.scroll(vertical, Axis::Vertical)
.map_err(input_error)?;
}
Ok(())
post_global_mouse(
source,
CGEventType::MouseMoved,
CGMouseButton::Left,
x,
y,
0,
0,
CGEventFlags::CGEventFlagNull,
)?;
let wheel_x = finite_i32(dx, "horizontal scroll delta")?;
let wheel_y = finite_i32(dy, "vertical scroll delta")?;
let event = CGEvent::new_scroll_event(
source.clone(),
ScrollEventUnit::PIXEL,
2,
wheel_y,
wheel_x,
0,
)
.map_err(|()| DesktopError::input_failed("failed to create a Quartz scroll event"))?;
event.set_location(point(x, y)?);
post_global(&event)
},
}
}
fn global_chord(input: &mut Enigo, keys: &[KeyName]) -> CoreResult<()> {
if keys.is_empty() {
return Err(DesktopError::invalid_key("key chord must not be empty"));
}
if keys.len() == 1 {
return input
.key(keys[0].to_enigo(), Direction::Click)
.map_err(input_error);
}
let mut pressed = Vec::with_capacity(keys.len());
for &key in keys {
if let Err(error) = input.key(key.to_enigo(), Direction::Press) {
for &held in pressed.iter().rev() {
let _ = input.key(held, Direction::Release);
}
return Err(input_error(error));
}
pressed.push(key.to_enigo());
}
let mut first_error = None;
for key in pressed.into_iter().rev() {
if let Err(error) = input.key(key, Direction::Release)
&& first_error.is_none()
{
first_error = Some(input_error(error));
}
}
first_error.map_or(Ok(()), Err)
}
fn with_global_modifiers(
input: &mut Enigo,
modifiers: Modifiers,
action: impl FnOnce(&mut Enigo) -> CoreResult<()>,
#[allow(
clippy::too_many_arguments,
reason = "the parameters are the native CGEvent fields stamped together"
)]
fn post_global_mouse(
source: &CGEventSource,
event_type: CGEventType,
button: CGMouseButton,
x: f64,
y: f64,
click_state: i64,
button_number: i64,
flags: CGEventFlags,
) -> CoreResult<()> {
let requested = [
(modifiers.ctrl, KeyName::Ctrl),
(modifiers.alt, KeyName::Alt),
(modifiers.shift, KeyName::Shift),
(modifiers.meta, KeyName::Meta),
];
let mut pressed = Vec::new();
for (_, key) in requested.into_iter().filter(|(enabled, _)| *enabled) {
let key = key.to_enigo();
if let Err(error) = input.key(key, Direction::Press) {
for &held in pressed.iter().rev() {
let _ = input.key(held, Direction::Release);
}
return Err(input_error(error));
}
pressed.push(key);
let event = CGEvent::new_mouse_event(source.clone(), event_type, point(x, y)?, button)
.map_err(|()| DesktopError::input_failed("failed to create a Quartz pointer event"))?;
event.set_integer_value_field(EventField::MOUSE_EVENT_CLICK_STATE, click_state);
if button_number != 0 {
event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, button_number);
}
let result = action(input);
let mut release = Ok(());
for key in pressed.into_iter().rev() {
if let Err(error) = input.key(key, Direction::Release)
&& release.is_ok()
{
release = Err(input_error(error));
}
}
result.and(release)
event.set_flags(flags);
post_global(&event)
}
fn move_global(input: &mut Enigo, x: f64, y: f64) -> CoreResult<()> {
input
.move_mouse(finite_i32(x, "x coordinate")?, finite_i32(y, "y coordinate")?, Coordinate::Abs)
.map_err(input_error)
fn post_global(event: &CGEvent) -> CoreResult<()> {
event.post(CGEventTapLocation::HID);
Ok(())
}
fn point(x: f64, y: f64) -> CoreResult<CGPoint> {
Ok(CGPoint::new(
f64::from(finite_i32(x, "x coordinate")?),
f64::from(finite_i32(y, "y coordinate")?),
))
}
fn finite_i32(value: f64, name: &str) -> CoreResult<i32> {
@@ -873,14 +960,24 @@ fn finite_i32(value: f64, name: &str) -> CoreResult<i32> {
Ok(value.round() as i32)
}
const fn enigo_button(button: MouseButton) -> Button {
match button {
MouseButton::Left => Button::Left,
MouseButton::Right => Button::Right,
MouseButton::Middle => Button::Middle,
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_source_never_suppresses_local_input() {
let source = source().expect("Quartz event source");
// SAFETY: `source` remains live for both CoreGraphics getter calls.
unsafe {
assert_eq!(get_local_events_suppression_interval(source.as_ptr()), 0.0);
assert_eq!(
get_local_events_filter_during_suppression_state(source.as_ptr(), SUPPRESSION_INTERVAL,),
LOCAL_EVENT_FILTER,
);
assert_eq!(
get_local_events_filter_during_suppression_state(source.as_ptr(), REMOTE_MOUSE_DRAG,),
LOCAL_EVENT_FILTER,
);
}
}
}
fn input_error(error: impl std::fmt::Display) -> DesktopError {
DesktopError::input_failed(format!("native input failed: {error}"))
}