use std::collections::{HashMap, HashSet, VecDeque};
use std::ffi::c_void;
use std::ptr;
use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU64, Ordering};
use std::sync::mpsc::{self, Receiver, Sender, SyncSender};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::Duration;
use color_eyre::eyre::{Result, eyre};
use crate::app_settings::{HOTKEY_MODIFIERS_MASK, HotkeyBinding, RuntimeHotkey, RuntimeHotkeys};
use crate::audio::CaptureInstant;
use crate::dictation::{BOTH_SIDES_WINDOW, MINIMUM_HOLD_DURATION};
mod hotkey;
pub(crate) mod physical_keys;
mod tap;
pub use hotkey::{DictationHotkey, HotkeyAction};
use tap::*;
const DOUBLE_TAP_WINDOW: Duration = MINIMUM_HOLD_DURATION;
const ESCAPE_KEY_CODE: u16 = 53;
const GLOBE_KEY_CODE: u16 = 179;
const EVENT_LEFT_MOUSE_DOWN: u32 = 1;
const EVENT_LEFT_MOUSE_UP: u32 = 2;
const EVENT_RIGHT_MOUSE_DOWN: u32 = 3;
const EVENT_RIGHT_MOUSE_UP: u32 = 4;
const EVENT_KEY_DOWN: u32 = 10;
const EVENT_KEY_UP: u32 = 11;
const EVENT_FLAGS_CHANGED: u32 = 12;
const EVENT_OTHER_MOUSE_DOWN: u32 = 25;
const EVENT_OTHER_MOUSE_UP: u32 = 26;
const EVENT_LEFT_MOUSE_DRAGGED: u32 = 6;
const EVENT_RIGHT_MOUSE_DRAGGED: u32 = 7;
const EVENT_OTHER_MOUSE_DRAGGED: u32 = 27;
const MOUSE_EVENT_BUTTON_NUMBER: u32 = 3;
#[cfg(hex_capture)]
const EVENT_MOUSE_MOVED: u32 = 5;
#[cfg(hex_capture)]
const EVENT_SCROLL_WHEEL: u32 = 22;
#[cfg(hex_capture)]
const KEYBOARD_EVENT_AUTOREPEAT: u32 = 8;
#[cfg(hex_capture)]
const SCROLL_WHEEL_DELTA_AXIS_1: u32 = 11;
#[cfg(hex_capture)]
const SCROLL_WHEEL_DELTA_AXIS_2: u32 = 12;
const EVENT_TAP_DISABLED_BY_TIMEOUT: u32 = u32::MAX - 1;
const EVENT_TAP_DISABLED_BY_USER_INPUT: u32 = u32::MAX;
const KEYBOARD_EVENT_KEYCODE: u32 = 9;
const EVENT_SOURCE_USER_DATA: u32 = 42;
const ANNOTATED_SESSION_EVENT_TAP: u32 = 2;
const HID_EVENT_TAP: u32 = 0;
const HEAD_INSERT_EVENT_TAP: u32 = 0;
const DEFAULT_EVENT_TAP: u32 = 0;
const LISTEN_ONLY_EVENT_TAP: u32 = 1;
const HID_SYSTEM_STATE: u32 = 1;
const COMBINED_SESSION_STATE: u32 = 0;
const STALE_KEY_NEUTRAL_DURATION: Duration = Duration::from_millis(100);
const TRACKED_KEY_REPEAT_TIMEOUT: Duration = Duration::from_secs(5);
const PHYSICAL_KEY_DOWN_HISTORY: usize = 8;
type EventRef = *mut c_void;
type EventTapCallback = unsafe extern "C" fn(
proxy: *mut c_void,
event_type: u32,
event: EventRef,
user_info: *mut c_void,
) -> EventRef;
#[link(name = "ApplicationServices", kind = "framework")]
unsafe extern "C" {
fn CGEventSourceFlagsState(state_id: u32) -> u64;
fn CGEventSourceKeyState(state_id: u32, key: u16) -> bool;
fn CGEventSourceButtonState(state_id: u32, button: u32) -> bool;
fn CGEventTapCreate(
tap: u32,
place: u32,
options: u32,
events_of_interest: u64,
callback: EventTapCallback,
user_info: *mut c_void,
) -> *mut c_void;
fn CGEventTapEnable(tap: *mut c_void, enable: bool);
fn CGEventTapIsEnabled(tap: *mut c_void) -> bool;
fn CGEventGetFlags(event: EventRef) -> u64;
fn CGEventGetTimestamp(event: EventRef) -> u64;
fn CGEventGetIntegerValueField(event: EventRef, field: u32) -> i64;
#[cfg(hex_capture)]
fn CGEventGetLocation(event: EventRef) -> CgPoint;
#[cfg(hex_capture)]
fn CGEventKeyboardGetUnicodeString(
event: EventRef,
max_length: usize,
actual_length: *mut usize,
buffer: *mut u16,
);
}
#[cfg(hex_capture)]
#[repr(C)]
#[derive(Clone, Copy)]
struct CgPoint {
x: f64,
y: f64,
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFMachPortCreateRunLoopSource(
allocator: *const c_void,
port: *mut c_void,
order: isize,
) -> *mut c_void;
fn CFRunLoopAddSource(run_loop: *mut c_void, source: *mut c_void, mode: *const c_void);
fn CFRunLoopRemoveSource(run_loop: *mut c_void, source: *mut c_void, mode: *const c_void);
fn CFRunLoopGetCurrent() -> *mut c_void;
fn CFRunLoopRun();
fn CFRunLoopStop(run_loop: *mut c_void);
fn CFRunLoopTimerCreate(
allocator: *const c_void,
fire_date: f64,
interval: f64,
flags: usize,
order: isize,
callout: unsafe extern "C" fn(timer: *mut c_void, info: *mut c_void),
context: *mut RunLoopTimerContext,
) -> *mut c_void;
fn CFRunLoopAddTimer(run_loop: *mut c_void, timer: *mut c_void, mode: *const c_void);
fn CFRunLoopTimerInvalidate(timer: *mut c_void);
fn CFAbsoluteTimeGetCurrent() -> f64;
fn CFMachPortIsValid(port: *mut c_void) -> u8;
fn CFRelease(value: *const c_void);
static kCFRunLoopCommonModes: *const c_void;
}
#[repr(C)]
struct RunLoopTimerContext {
version: isize,
info: *mut c_void,
retain: *const c_void,
release: *const c_void,
copy_description: *const c_void,
}
#[derive(Clone, Copy, Debug)]
pub enum InputEvent {
Flags(u64),
Key {
code: u16,
down: bool,
flags: u64,
},
PhysicalKeyDown {
code: u16,
timing: HidTiming,
},
MouseDown,
TapDisabled,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HidTiming {
pub raw: u64,
pub clock: HidClock,
pub received_at: CaptureInstant,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HidClock {
MachTicks,
Nanos,
Arrival,
}
impl HidClock {
fn as_str(self) -> &'static str {
match self {
Self::MachTicks => "mach",
Self::Nanos => "nanos",
Self::Arrival => "arrival",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DiscardCause {
Key,
HidKey,
Mouse,
ExtraModifier,
}
impl DiscardCause {
pub fn as_str(self) -> &'static str {
match self {
Self::Key => "key",
Self::HidKey => "hid-key",
Self::Mouse => "mouse",
Self::ExtraModifier => "extra-modifier",
}
}
}
impl InputEvent {
pub fn is_escape_down(self) -> bool {
matches!(
self,
Self::Key {
code: ESCAPE_KEY_CODE,
down: true,
..
}
)
}
}
#[derive(Clone, Copy, Debug)]
pub struct ObservedInputEvent {
sequence: u64,
pub event: InputEvent,
pub capture_at: CaptureInstant,
}
#[derive(Clone, Default)]
pub struct PendingInputEvents(Arc<Mutex<VecDeque<(u64, CaptureInstant)>>>);
impl PendingInputEvents {
#[cfg(test)]
pub fn with_pending_for_test(at: CaptureInstant) -> (Self, impl FnOnce()) {
let pending = Self::default();
pending.push(0, at);
let acknowledge = pending.clone();
(pending, move || acknowledge.acknowledge(0))
}
pub fn oldest(&self) -> Option<CaptureInstant> {
self.0
.lock()
.unwrap_or_else(|error| error.into_inner())
.front()
.map(|(_, at)| *at)
}
fn push(&self, sequence: u64, at: CaptureInstant) {
self.0
.lock()
.unwrap_or_else(|error| error.into_inner())
.push_back((sequence, at));
}
fn acknowledge(&self, sequence: u64) {
let mut pending = self.0.lock().unwrap_or_else(|error| error.into_inner());
if pending.front().is_some_and(|(next, _)| *next == sequence) {
pending.pop_front();
}
}
}
pub struct InputMonitor {
pub events: Receiver<ObservedInputEvent>,
pub activity: InputActivity,
pending: PendingInputEvents,
takes_escape: Arc<AtomicBool>,
takes_cancel: Arc<AtomicBool>,
caps_lock_cancel: Arc<AtomicBool>,
run_loop: Arc<AtomicPtr<c_void>>,
worker: Option<JoinHandle<()>>,
_physical_keys: Option<physical_keys::PhysicalKeyMonitor>,
}
pub struct PendingInputAcknowledgement<'a> {
pending: &'a PendingInputEvents,
sequence: u64,
}
impl Drop for PendingInputAcknowledgement<'_> {
fn drop(&mut self) {
self.pending.acknowledge(self.sequence);
}
}
#[derive(Clone, Default)]
pub struct InputActivity(Arc<AtomicU64>);
impl InputActivity {
pub fn revision(&self) -> u64 {
self.0.load(Ordering::Acquire)
}
pub fn invalidate(&self) {
self.0.fetch_add(1, Ordering::AcqRel);
}
fn observe(&self, input: InputEvent, suppressed: bool) {
if !suppressed
&& matches!(
input,
InputEvent::Key { down: true, .. } | InputEvent::MouseDown
)
{
self.invalidate();
}
}
}
impl InputMonitor {
pub fn start() -> Result<Self> {
let (sender, events) = mpsc::channel::<ObservedInputEvent>();
let (ready_sender, ready_receiver) = mpsc::sync_channel(1);
let activity = InputActivity::default();
let tap_activity = activity.clone();
let run_loop = Arc::new(AtomicPtr::new(ptr::null_mut()));
let tap_run_loop = run_loop.clone();
let takes_escape = Arc::new(AtomicBool::new(false));
let tap_takes_escape = takes_escape.clone();
let takes_cancel = Arc::new(AtomicBool::new(false));
let tap_takes_cancel = takes_cancel.clone();
let caps_lock_cancel = Arc::new(AtomicBool::new(false));
let tap_caps_lock_cancel = caps_lock_cancel.clone();
let pending = PendingInputEvents::default();
let tap_pending = pending.clone();
let worker = thread::spawn(move || {
run_event_tap(
sender,
tap_activity,
TapFlags {
takes_escape: tap_takes_escape,
takes_cancel: tap_takes_cancel,
caps_lock_cancel: tap_caps_lock_cancel,
},
tap_pending,
tap_run_loop,
ready_sender,
)
});
ready_receiver
.recv_timeout(Duration::from_secs(2))
.map_err(|_| eyre!("timed out starting the keyboard event tap"))??;
let physical_keys = physical_keys::PhysicalKeyMonitor::start();
Ok(Self {
events,
activity,
pending,
takes_escape,
takes_cancel,
caps_lock_cancel,
run_loop,
worker: Some(worker),
_physical_keys: physical_keys,
})
}
pub fn set_takes_escape(&self, enabled: bool) {
self.takes_escape.store(enabled, Ordering::Release);
}
pub fn set_takes_cancel(&self, enabled: bool) {
self.takes_cancel.store(enabled, Ordering::Release);
}
pub fn set_caps_lock_cancel(&self, enabled: bool) {
self.caps_lock_cancel.store(enabled, Ordering::Release);
}
pub fn pending_events(&self) -> PendingInputEvents {
self.pending.clone()
}
pub fn acknowledge_after(&self, event: ObservedInputEvent) -> PendingInputAcknowledgement<'_> {
PendingInputAcknowledgement {
pending: &self.pending,
sequence: event.sequence,
}
}
}
impl Drop for InputMonitor {
fn drop(&mut self) {
let run_loop = self.run_loop.load(Ordering::Acquire);
if !run_loop.is_null() {
unsafe { CFRunLoopStop(run_loop) };
}
if let Some(worker) = self.worker.take() {
let _ = worker.join();
}
}
}
pub(crate) fn alpha_shift_now() -> bool {
(unsafe { CGEventSourceFlagsState(COMBINED_SESSION_STATE) }
& crate::caps_lock_dictation::ALPHA_SHIFT_MASK)
!= 0
}
struct EventTapContext {
sender: Sender<ObservedInputEvent>,
activity: InputActivity,
takes_escape: Arc<AtomicBool>,
takes_cancel: Arc<AtomicBool>,
caps_lock_cancel: Arc<AtomicBool>,
key_tap: AtomicPtr<c_void>,
observation_tap: AtomicPtr<c_void>,
physical_tap: AtomicPtr<c_void>,
shortcut_suppression: Mutex<ShortcutSuppression>,
pending: PendingInputEvents,
next_sequence: AtomicU64,
}
unsafe extern "C" fn physical_event_callback(
_proxy: *mut c_void,
event_type: u32,
event: EventRef,
user_info: *mut c_void,
) -> EventRef {
if user_info.is_null() {
return event;
}
let context = unsafe { &*(user_info.cast::<EventTapContext>()) };
if matches!(
event_type,
EVENT_TAP_DISABLED_BY_TIMEOUT | EVENT_TAP_DISABLED_BY_USER_INPUT
) {
reenable_disabled_taps(context, event_type, TapKind::Physical.as_str());
return event;
}
if event.is_null()
|| event_type != EVENT_KEY_DOWN
|| crate::app_settings::hotkey_capture_active()
|| unsafe { CGEventGetIntegerValueField(event, EVENT_SOURCE_USER_DATA) }
== crate::keyboard::SYNTHETIC_EVENT_MARKER
{
return event;
}
let code = unsafe { CGEventGetIntegerValueField(event, KEYBOARD_EVENT_KEYCODE) as u16 };
let flags = unsafe { CGEventGetFlags(event) };
let flags = crate::app_settings::normalized_modifier_flags(flags, physical_keys::snapshot());
if !is_chord_evidence(code, flags, crate::app_settings::runtime_hotkeys()) {
return event;
}
let raw = unsafe { CGEventGetTimestamp(event) };
let received_at = CaptureInstant::now();
let (at, clock) = physical_event_instant(raw, received_at);
send_input(
context,
InputEvent::PhysicalKeyDown {
code,
timing: HidTiming {
raw,
clock,
received_at,
},
},
at,
);
event
}
fn is_chord_evidence(code: u16, flags: u64, hotkeys: RuntimeHotkeys) -> bool {
code != ESCAPE_KEY_CODE
&& !is_own_globe_key(code, hotkeys.dictation)
&& !hotkeys
.second_dictation
.is_some_and(|second| is_own_globe_key(code, second))
&& !hotkeys
.all()
.iter()
.flatten()
.any(|hotkey| hotkey.matches_key_press(code, flags))
}
fn is_own_globe_key(code: u16, binding: RuntimeHotkey) -> bool {
code == GLOBE_KEY_CODE && binding.modifiers.function && binding.key_code != Some(code)
}
fn physical_event_instant(raw: u64, now: CaptureInstant) -> (CaptureInstant, HidClock) {
const RECENT: Duration = Duration::from_secs(5);
[
(CaptureInstant::from_mach_ticks(raw), HidClock::MachTicks),
(CaptureInstant::from_nanos(raw), HidClock::Nanos),
]
.into_iter()
.find(|(at, _)| {
now.checked_duration_since(*at)
.is_some_and(|age| age <= RECENT)
})
.unwrap_or((now, HidClock::Arrival))
}
fn signed_ms(to: CaptureInstant, from: CaptureInstant) -> f64 {
(i128::from(to.as_nanos()) - i128::from(from.as_nanos())) as f64 / 1e6
}
unsafe extern "C" fn event_callback(
_proxy: *mut c_void,
event_type: u32,
event: EventRef,
user_info: *mut c_void,
) -> EventRef {
if user_info.is_null() {
return event;
}
let context = unsafe { &*(user_info.cast::<EventTapContext>()) };
if matches!(
event_type,
EVENT_TAP_DISABLED_BY_TIMEOUT | EVENT_TAP_DISABLED_BY_USER_INPUT
) {
reenable_disabled_taps(context, event_type, "session");
send_input(context, InputEvent::TapDisabled, CaptureInstant::ZERO);
return event;
}
if event.is_null() {
return event;
}
if unsafe { CGEventGetIntegerValueField(event, EVENT_SOURCE_USER_DATA) }
== crate::keyboard::SYNTHETIC_EVENT_MARKER
{
let chord_breaker = matches!(event_type, EVENT_KEY_DOWN | EVENT_KEY_UP)
&& unsafe { CGEventGetIntegerValueField(event, KEYBOARD_EVENT_KEYCODE) }
== i64::from(crate::keyboard::CHORD_BREAKER_KEY_CODE);
return if chord_breaker {
ptr::null_mut()
} else {
event
};
}
if event_type == EVENT_FLAGS_CHANGED {
let flags = unsafe { CGEventGetFlags(event) };
let shift = flags & crate::caps_lock_dictation::ALPHA_SHIFT_MASK != 0;
if crate::caps_lock_dictation::alpha_shift_changed(shift) {
let at = CaptureInstant::from_nanos(unsafe { CGEventGetTimestamp(event) });
crate::caps_lock_dictation::alpha_shift(shift, at);
}
}
#[cfg(hex_capture)]
let screenshot_swallows = match screenshot_tap(event_type, event) {
crate::screenshots::Take::No => false,
crate::screenshots::Take::Only => return ptr::null_mut(),
crate::screenshots::Take::AfterListener => true,
};
#[cfg(hex_capture)]
let list_swallows = screenshot_swallows
|| match list_tap(event_type, event) {
ListSwallow::No => false,
ListSwallow::Only => return ptr::null_mut(),
ListSwallow::AfterListener => true,
};
#[cfg(not(hex_capture))]
let list_swallows = false;
#[cfg(hex_capture)]
let capture_key = if crate::capture_host::wants_input() {
match report_capture_input(event_type, event) {
CaptureTap::Only => return event,
CaptureTap::Shared => None,
CaptureTap::Key(key) => Some(key),
}
} else {
None
};
#[cfg(hex_capture)]
if matches!(
event_type,
EVENT_LEFT_MOUSE_DOWN | EVENT_RIGHT_MOUSE_DOWN | EVENT_OTHER_MOUSE_DOWN
) && (crate::capture_host::list_open() || crate::capture_host::suggesting())
{
crate::capture_host::capsule_key(crate::capture_host::CapsuleKey::Press);
}
if matches!(
event_type,
EVENT_LEFT_MOUSE_DRAGGED | EVENT_RIGHT_MOUSE_DRAGGED | EVENT_OTHER_MOUSE_DRAGGED
) {
let button = unsafe { CGEventGetIntegerValueField(event, MOUSE_EVENT_BUTTON_NUMBER) };
let suppress = !crate::app_settings::hotkey_capture_active()
&& u16::try_from(button).ok().is_some_and(|button| {
context
.shortcut_suppression
.lock()
.unwrap_or_else(|error| error.into_inner())
.suppresses_mouse_drag(button)
});
return if suppress { ptr::null_mut() } else { event };
}
let input = match event_type {
EVENT_FLAGS_CHANGED => {
let flags = unsafe { CGEventGetFlags(event) };
InputEvent::Flags(crate::app_settings::normalized_modifier_flags(
flags,
physical_keys::snapshot(),
))
}
EVENT_KEY_DOWN | EVENT_KEY_UP => InputEvent::Key {
code: unsafe { CGEventGetIntegerValueField(event, KEYBOARD_EVENT_KEYCODE) as u16 },
down: event_type == EVENT_KEY_DOWN,
flags: crate::app_settings::normalized_modifier_flags(
unsafe { CGEventGetFlags(event) },
physical_keys::snapshot(),
),
},
EVENT_LEFT_MOUSE_DOWN
| EVENT_RIGHT_MOUSE_DOWN
| EVENT_OTHER_MOUSE_DOWN
| EVENT_LEFT_MOUSE_UP
| EVENT_RIGHT_MOUSE_UP
| EVENT_OTHER_MOUSE_UP => {
let button = unsafe { CGEventGetIntegerValueField(event, MOUSE_EVENT_BUTTON_NUMBER) };
let Ok(button) = u16::try_from(button) else {
return event;
};
let down = matches!(
event_type,
EVENT_LEFT_MOUSE_DOWN | EVENT_RIGHT_MOUSE_DOWN | EVENT_OTHER_MOUSE_DOWN
);
let flags = unsafe { CGEventGetFlags(event) };
let input = mouse_input(button, down, flags, crate::app_settings::runtime_hotkeys())
.or_else(|| {
(!down)
.then(|| {
context
.shortcut_suppression
.lock()
.unwrap_or_else(|error| error.into_inner())
.tracked_mouse_release(button, flags)
})
.flatten()
});
let Some(input) = input else {
return event;
};
input
}
_ => return event,
};
let capture_at = CaptureInstant::from_nanos(unsafe { CGEventGetTimestamp(event) });
if crate::app_settings::hotkey_capture_active() {
context.activity.observe(input, false);
context
.shortcut_suppression
.lock()
.unwrap_or_else(|error| error.into_inner())
.reset();
return event;
}
let delivered = send_input(context, input, capture_at);
let mut suppression = context
.shortcut_suppression
.lock()
.unwrap_or_else(|error| error.into_inner());
let mut hotkeys = crate::app_settings::runtime_hotkeys();
hotkeys.cancel = tap_cancel_binding(
hotkeys.cancel,
context.takes_cancel.load(Ordering::Acquire),
context.caps_lock_cancel.load(Ordering::Acquire),
);
let suppress = suppression.process_all(
input,
hotkeys,
delivered,
context.takes_escape.load(Ordering::Acquire),
);
context.activity.observe(input, suppress);
#[cfg(hex_capture)]
if let Some(key) = capture_key
&& !suppress
&& !list_swallows
{
crate::capture_host::report_input(capture_at, key);
}
if suppress || list_swallows {
ptr::null_mut()
} else {
event
}
}
#[cfg(hex_capture)]
enum ListSwallow {
No,
Only,
AfterListener,
}
#[cfg(hex_capture)]
fn list_takes_keys(capture_on: bool, open: bool, recording_shortcut: bool) -> bool {
capture_on && open && !recording_shortcut
}
#[cfg(hex_capture)]
fn screenshot_tap(event_type: u32, event: EventRef) -> crate::screenshots::Take {
if !matches!(event_type, EVENT_KEY_DOWN | EVENT_KEY_UP) {
return crate::screenshots::Take::No;
}
let (code, repeat, flags) = unsafe {
(
CGEventGetIntegerValueField(event, KEYBOARD_EVENT_KEYCODE) as u16,
CGEventGetIntegerValueField(event, KEYBOARD_EVENT_AUTOREPEAT) != 0,
CGEventGetFlags(event),
)
};
crate::screenshots::key(crate::screenshots::Key {
code,
down: event_type == EVENT_KEY_DOWN,
repeat,
flags,
})
}
#[cfg(hex_capture)]
fn list_tap(event_type: u32, event: EventRef) -> ListSwallow {
use crate::capture_host::{CapsuleKey, ListTap, capsule_key, list_key, list_open, suggesting};
static TAKEN: Mutex<u128> = Mutex::new(0);
static SHIFT: AtomicBool = AtomicBool::new(false);
if event_type == EVENT_FLAGS_CHANGED {
const SHIFT_MASK: u64 = 1 << 17;
let down = unsafe { CGEventGetFlags(event) } & SHIFT_MASK != 0;
if SHIFT.swap(down, Ordering::Relaxed) != down
&& list_takes_keys(
crate::app_settings::capture_on(),
list_open() || suggesting(),
crate::app_settings::hotkey_capture_active(),
)
{
capsule_key(CapsuleKey::Shift(down));
}
return ListSwallow::No;
}
if !matches!(event_type, EVENT_KEY_DOWN | EVENT_KEY_UP) {
return ListSwallow::No;
}
let code = unsafe { CGEventGetIntegerValueField(event, KEYBOARD_EVENT_KEYCODE) } as u16;
let Some(bit) = 1u128.checked_shl(u32::from(code)) else {
return ListSwallow::No;
};
let mut taken = TAKEN.lock().unwrap_or_else(|error| error.into_inner());
if event_type == EVENT_KEY_UP {
let ours = *taken & bit != 0;
*taken &= !bit;
return if ours {
ListSwallow::AfterListener
} else {
ListSwallow::No
};
}
let suggesting = suggesting();
if !list_takes_keys(
crate::app_settings::capture_on(),
list_open() || suggesting,
crate::app_settings::hotkey_capture_active(),
) {
return ListSwallow::No;
}
let flags = unsafe { CGEventGetFlags(event) };
if crate::app_settings::runtime_hotkeys()
.all()
.iter()
.flatten()
.any(|hotkey| hotkey.matches_key_press(code, flags))
{
return ListSwallow::No;
}
let mut typed = [0u16; 8];
let mut length = 0usize;
unsafe { CGEventKeyboardGetUnicodeString(event, typed.len(), &mut length, typed.as_mut_ptr()) };
let typed = String::from_utf16_lossy(&typed[..length.min(typed.len())]);
match list_key(code, flags, &typed) {
ListTap::Key(key) if suggesting => {
*taken |= bit;
drop(taken);
if capsule_key(key) {
ListSwallow::AfterListener
} else {
*TAKEN.lock().unwrap_or_else(|error| error.into_inner()) &= !bit;
ListSwallow::No
}
}
_ if suggesting => ListSwallow::No,
ListTap::Pass => ListSwallow::No,
ListTap::Swallow => {
*taken |= bit;
ListSwallow::AfterListener
}
ListTap::Key(key) => {
*taken |= bit;
drop(taken);
let escape = key == CapsuleKey::Escape;
capsule_key(key);
if escape {
ListSwallow::Only
} else {
ListSwallow::AfterListener
}
}
}
}
#[cfg(hex_capture)]
enum CaptureTap {
Only,
Shared,
Key(crate::capture_host::Tapped),
}
#[cfg(hex_capture)]
fn report_capture_input(event_type: u32, event: EventRef) -> CaptureTap {
use crate::capture_host::{Tapped, modifiers, report_input};
use scrawl_capture::hosted_input::HostInput;
let at = CaptureInstant::from_nanos(unsafe { CGEventGetTimestamp(event) });
let flags = unsafe { CGEventGetFlags(event) };
let CgPoint { x, y } = unsafe { CGEventGetLocation(event) };
let held = modifiers(flags);
let (input, capture_only) = match event_type {
EVENT_LEFT_MOUSE_DOWN
| EVENT_RIGHT_MOUSE_DOWN
| EVENT_LEFT_MOUSE_UP
| EVENT_RIGHT_MOUSE_UP => (
Tapped::Pointer(HostInput::Button {
down: matches!(event_type, EVENT_LEFT_MOUSE_DOWN | EVENT_RIGHT_MOUSE_DOWN),
right: matches!(event_type, EVENT_RIGHT_MOUSE_DOWN | EVENT_RIGHT_MOUSE_UP),
x,
y,
modifiers: held,
}),
!matches!(event_type, EVENT_LEFT_MOUSE_DOWN | EVENT_RIGHT_MOUSE_DOWN),
),
EVENT_MOUSE_MOVED
| EVENT_LEFT_MOUSE_DRAGGED
| EVENT_RIGHT_MOUSE_DRAGGED
| EVENT_OTHER_MOUSE_DRAGGED => (
Tapped::Pointer(HostInput::Moved {
x,
y,
modifiers: held,
}),
true,
),
EVENT_SCROLL_WHEEL => (
Tapped::Pointer(HostInput::Scroll {
x,
y,
dx: unsafe { CGEventGetIntegerValueField(event, SCROLL_WHEEL_DELTA_AXIS_2) } as f64,
dy: unsafe { CGEventGetIntegerValueField(event, SCROLL_WHEEL_DELTA_AXIS_1) } as f64,
}),
true,
),
EVENT_FLAGS_CHANGED => (
Tapped::Pointer(HostInput::ModifiersChanged {
x,
y,
modifiers: held,
}),
false,
),
EVENT_KEY_DOWN
if unsafe { CGEventGetIntegerValueField(event, KEYBOARD_EVENT_AUTOREPEAT) } == 0 =>
{
return CaptureTap::Key(Tapped::Key {
code: unsafe { CGEventGetIntegerValueField(event, KEYBOARD_EVENT_KEYCODE) as u16 },
flags,
});
}
EVENT_OTHER_MOUSE_DOWN | EVENT_KEY_DOWN | EVENT_KEY_UP => return CaptureTap::Shared,
_ => return CaptureTap::Only,
};
report_input(at, input);
if capture_only {
CaptureTap::Only
} else {
CaptureTap::Shared
}
}
fn send_input(context: &EventTapContext, event: InputEvent, capture_at: CaptureInstant) -> bool {
let sequence = context.next_sequence.fetch_add(1, Ordering::Relaxed);
context.pending.push(sequence, capture_at);
match context.sender.send(ObservedInputEvent {
sequence,
event,
capture_at,
}) {
Ok(()) => true,
Err(_) => {
context.pending.acknowledge(sequence);
unsafe { CFRunLoopStop(CFRunLoopGetCurrent()) };
false
}
}
}
#[derive(Default)]
struct ShortcutSuppression {
key_presses: HashMap<u16, bool>,
}
fn tap_cancel_binding(
armed: Option<RuntimeHotkey>,
takes_cancel: bool,
caps_lock_cancel: bool,
) -> Option<RuntimeHotkey> {
if !takes_cancel {
None
} else if caps_lock_cancel {
Some(HotkeyBinding::cancel_default().runtime())
} else {
armed
}
}
impl ShortcutSuppression {
fn tracked_mouse_release(&self, button: u16, flags: u64) -> Option<InputEvent> {
let code = crate::app_settings::HotkeyKey::mouse_code(button)?;
self.key_presses
.contains_key(&code)
.then_some(InputEvent::Key {
code,
down: false,
flags,
})
}
fn suppresses_mouse_drag(&self, button: u16) -> bool {
crate::app_settings::HotkeyKey::mouse_code(button)
.and_then(|code| self.key_presses.get(&code))
.copied()
.unwrap_or(false)
}
fn reset(&mut self) {
self.key_presses.clear();
}
fn process_all(
&mut self,
input: InputEvent,
hotkeys: RuntimeHotkeys,
delivered: bool,
takes_escape: bool,
) -> bool {
let bindings = hotkeys.all();
match input {
InputEvent::Key {
code,
down: true,
flags,
} => *self.key_presses.entry(code).or_insert_with(|| {
delivered
&& ((code == ESCAPE_KEY_CODE && takes_escape)
|| bindings
.iter()
.flatten()
.chain(hotkeys.cancel.iter())
.any(|hotkey| hotkey.matches_key_press(code, flags)))
}),
InputEvent::Key {
code, down: false, ..
} => self.key_presses.remove(&code).unwrap_or(false),
_ => false,
}
}
#[cfg(test)]
fn process(
&mut self,
input: InputEvent,
paste_key_code: u16,
hotkey: RuntimeHotkey,
delivered: bool,
) -> bool {
let mut paste_last = HotkeyBinding::paste_last_default().runtime();
paste_last.key_code = Some(paste_key_code);
let mut paste_meeting = HotkeyBinding::paste_meeting_default().runtime();
paste_meeting.key_code = Some(paste_key_code);
self.process_all(
input,
RuntimeHotkeys {
dictation: hotkey,
paste_last: Some(paste_last),
paste_meeting: Some(paste_meeting),
..RuntimeHotkeys::default()
},
delivered,
false,
)
}
}
fn paste_action(input: InputEvent, hotkeys: RuntimeHotkeys) -> Option<HotkeyAction> {
let InputEvent::Key {
code,
down: true,
flags,
} = input
else {
return None;
};
hotkeys
.paste_last
.filter(|binding| binding.matches_key_press(code, flags))
.map(|_| HotkeyAction::PasteLast)
.or_else(|| {
hotkeys
.paste_meeting
.filter(|binding| binding.matches_key_press(code, flags))
.map(|_| HotkeyAction::PasteMeeting)
})
}
#[cfg(test)]
fn paste_action_with_meetings(
input: InputEvent,
paste_key_code: u16,
meetings_enabled: bool,
) -> Option<HotkeyAction> {
let mut hotkeys = RuntimeHotkeys::default();
if let Some(binding) = &mut hotkeys.paste_last {
binding.key_code = Some(paste_key_code);
}
hotkeys.paste_meeting = meetings_enabled.then(|| {
let mut binding = HotkeyBinding::paste_meeting_default().runtime();
binding.key_code = Some(paste_key_code);
binding
});
paste_action(input, hotkeys)
}