use std::os::raw::c_void;
use std::ptr;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Arc, OnceLock, RwLock};
use std::thread;
use std::time::{Duration, Instant};
use hyprcorrect_core::{Chord, Key};
use super::chord_capture::ChordCaptureSlot;
use super::ffi::*;
use super::keymap;
#[derive(Debug, Clone, Copy)]
pub struct ResetKeyConfig {
pub enter: bool,
pub tab: bool,
pub escape: bool,
pub up: bool,
pub down: bool,
pub page_up: bool,
pub page_down: bool,
pub delete: bool,
pub insert: bool,
}
impl Default for ResetKeyConfig {
fn default() -> Self {
Self {
enter: true,
tab: false,
escape: false,
up: true,
down: true,
page_up: true,
page_down: true,
delete: true,
insert: true,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum CaptureError {
#[error(
"capture permission is not granted — enable hyprcorrect under System \
Settings → Privacy & Security → Accessibility (it covers capture on \
macOS 13+; on 11–12 use Input Monitoring). It starts automatically \
once you do — no restart needed."
)]
Permission,
#[error("could not create the CGEventTap (Input Monitoring may be denied)")]
TapCreation,
#[error("could not spawn the capture run-loop thread: {0}")]
Thread(String),
}
static RESET_KEY_CONFIG: OnceLock<RwLock<ResetKeyConfig>> = OnceLock::new();
static CARET_SUSPECT: OnceLock<Arc<AtomicBool>> = OnceLock::new();
static MODS_STATE: AtomicU64 = AtomicU64::new(0);
static CAPTURE_ACTIVE: AtomicBool = AtomicBool::new(false);
fn reset_key_config() -> &'static RwLock<ResetKeyConfig> {
RESET_KEY_CONFIG.get_or_init(|| RwLock::new(ResetKeyConfig::default()))
}
pub fn set_reset_keys(cfg: ResetKeyConfig) {
*reset_key_config().write().expect("reset-key lock") = cfg;
}
pub fn caret_suspect_flag() -> Arc<AtomicBool> {
CARET_SUSPECT
.get_or_init(|| Arc::new(AtomicBool::new(false)))
.clone()
}
pub fn wait_mods_clear(timeout: Duration) -> bool {
if !CAPTURE_ACTIVE.load(Ordering::Relaxed) {
return true;
}
const CHORD_MODS: u64 = kCGEventFlagMaskCommand
| kCGEventFlagMaskControl
| kCGEventFlagMaskAlternate
| kCGEventFlagMaskShift;
let deadline = Instant::now() + timeout;
loop {
if MODS_STATE.load(Ordering::Relaxed) & CHORD_MODS == 0 {
return true;
}
if Instant::now() >= deadline {
return false;
}
thread::sleep(Duration::from_millis(5));
}
}
#[derive(Clone, Copy)]
struct ChordKey {
vkey: u16,
ctrl: bool,
shift: bool,
alt: bool,
super_: bool,
}
struct TapContext {
tx: Sender<Key>,
suppression: Vec<ChordKey>,
chord_capture: Arc<ChordCaptureSlot>,
caret_suspect: Arc<AtomicBool>,
port: AtomicUsize,
}
pub fn listen_access_granted() -> bool {
unsafe { CGPreflightListenEventAccess() }
}
pub fn accessibility_granted() -> bool {
unsafe { AXIsProcessTrusted() }
}
pub fn fire_accessibility_prompt() {
unsafe {
let key = kAXTrustedCheckOptionPrompt;
let value = kCFBooleanTrue;
let options = CFDictionaryCreate(
std::ptr::null(),
&key as *const _,
&value as *const _,
1,
&raw const kCFTypeDictionaryKeyCallBacks,
&raw const kCFTypeDictionaryValueCallBacks,
);
AXIsProcessTrustedWithOptions(options);
if !options.is_null() {
CFRelease(options);
}
}
}
pub fn start(
chords: &[Chord],
chord_capture: Arc<ChordCaptureSlot>,
) -> Result<Receiver<Key>, CaptureError> {
if !unsafe { CGPreflightListenEventAccess() } && !accessibility_granted() {
return Err(CaptureError::Permission);
}
let suppression: Vec<ChordKey> = chords
.iter()
.filter_map(|c| {
keymap::key_token_to_vkey(&c.key).map(|vkey| ChordKey {
vkey,
ctrl: c.ctrl,
shift: c.shift,
alt: c.alt,
super_: c.super_,
})
})
.collect();
let (tx, rx) = mpsc::channel::<Key>();
let caret_suspect = caret_suspect_flag();
let (ready_tx, ready_rx) = mpsc::channel::<Result<(), CaptureError>>();
let spawn = thread::Builder::new()
.name("hyprcorrect-capture".into())
.spawn(move || {
let ctx = Box::new(TapContext {
tx,
suppression,
chord_capture,
caret_suspect,
port: AtomicUsize::new(0),
});
let ctx_ptr = Box::into_raw(ctx);
let mask = event_mask_bit(kCGEventKeyDown)
| event_mask_bit(kCGEventFlagsChanged)
| event_mask_bit(kCGEventLeftMouseDown);
let port = unsafe {
CGEventTapCreate(
kCGSessionEventTap,
kCGHeadInsertEventTap,
kCGEventTapOptionListenOnly,
mask,
tap_callback,
ctx_ptr as *mut c_void,
)
};
if port.is_null() {
let _ = ready_tx.send(Err(CaptureError::TapCreation));
drop(unsafe { Box::from_raw(ctx_ptr) });
return;
}
unsafe { (*ctx_ptr).port.store(port as usize, Ordering::Relaxed) };
let source = unsafe { CFMachPortCreateRunLoopSource(ptr::null(), port, 0) };
if source.is_null() {
let _ = ready_tx.send(Err(CaptureError::TapCreation));
unsafe { CFRelease(port as *const c_void) };
drop(unsafe { Box::from_raw(ctx_ptr) });
return;
}
unsafe {
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
CGEventTapEnable(port, true);
CFRelease(source);
}
CAPTURE_ACTIVE.store(true, Ordering::Relaxed);
let _ = ready_tx.send(Ok(()));
unsafe { CFRunLoopRun() };
});
if let Err(e) = spawn {
return Err(CaptureError::Thread(e.to_string()));
}
match ready_rx.recv() {
Ok(Ok(())) => Ok(rx),
Ok(Err(e)) => Err(e),
Err(_) => Err(CaptureError::TapCreation),
}
}
#[allow(non_upper_case_globals)]
unsafe extern "C" fn tap_callback(
_proxy: CGEventTapProxy,
etype: u32,
event: CGEventRef,
user_info: *mut c_void,
) -> CGEventRef {
let ctx = unsafe { &*(user_info as *const TapContext) };
match etype {
kCGEventTapDisabledByTimeout => {
let port = ctx.port.load(Ordering::Relaxed) as *mut c_void;
if !port.is_null() {
unsafe { CGEventTapEnable(port, true) };
}
return event;
}
kCGEventTapDisabledByUserInput => {
log::warn!("macos capture: tap disabled (secure input or permission change)");
return event;
}
kCGEventLeftMouseDown => {
ctx.caret_suspect.store(true, Ordering::Relaxed);
return event;
}
kCGEventFlagsChanged => {
MODS_STATE.store(unsafe { CGEventGetFlags(event) }, Ordering::Relaxed);
return event;
}
kCGEventKeyDown => { }
_ => return event,
}
if unsafe { CGEventGetIntegerValueField(event, kCGEventSourceUserData) } == SYNTHETIC_MARK {
return event;
}
let keycode = unsafe { CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode) } as u16;
let flags = unsafe { CGEventGetFlags(event) };
MODS_STATE.store(flags, Ordering::Relaxed);
let m = Mods::from_flags(flags);
if ctx.chord_capture.is_armed()
&& let Some(token) = keymap::vkey_to_token(keycode)
{
let chord_string = build_chord_string(&m, &token);
if ctx.chord_capture.try_emit(chord_string) {
return event;
}
}
if ctx.suppression.iter().any(|c| {
c.vkey == keycode
&& c.ctrl == m.ctrl
&& c.shift == m.shift
&& c.alt == m.alt
&& c.super_ == m.command
}) {
return event;
}
if let Some(key) = classify(keycode, &m) {
let _ = ctx.tx.send(key);
return event;
}
if m.command || m.ctrl {
let _ = ctx.tx.send(Key::Reset);
return event;
}
if let Some(c) = typed_char(event) {
let _ = ctx.tx.send(Key::Char(c));
}
event
}
struct Mods {
ctrl: bool,
shift: bool,
alt: bool,
command: bool,
}
impl Mods {
fn from_flags(flags: u64) -> Self {
Self {
ctrl: flags & kCGEventFlagMaskControl != 0,
shift: flags & kCGEventFlagMaskShift != 0,
alt: flags & kCGEventFlagMaskAlternate != 0,
command: flags & kCGEventFlagMaskCommand != 0,
}
}
}
fn classify(keycode: u16, m: &Mods) -> Option<Key> {
let cfg = *reset_key_config().read().expect("reset-key lock");
if m.ctrl && !m.command && !m.alt {
match keycode {
0x00 => return Some(Key::LineStart), 0x0E => return Some(Key::LineEnd), 0x03 => return Some(Key::MoveRight), 0x0B => return Some(Key::MoveLeft), _ => {}
}
}
Some(match keycode {
0x33 if m.alt || m.command => Key::Reset,
0x33 => Key::Backspace, 0x7B => {
if m.alt {
Key::WordLeft
} else if m.command {
Key::LineStart
} else {
Key::MoveLeft
}
}
0x7C => {
if m.alt {
Key::WordRight
} else if m.command {
Key::LineEnd
} else {
Key::MoveRight
}
}
0x73 => Key::LineStart, 0x77 => Key::LineEnd, 0x7E if cfg.up => Key::Reset,
0x7D if cfg.down => Key::Reset,
0x74 if cfg.page_up => Key::Reset,
0x79 if cfg.page_down => Key::Reset,
0x24 | 0x4C if cfg.enter => Key::Reset, 0x30 if cfg.tab => Key::Reset,
0x35 if cfg.escape => Key::Reset,
0x75 if cfg.delete => Key::Reset, 0x72 if cfg.insert => Key::Reset, 0x7E | 0x7D | 0x74 | 0x79 | 0x24 | 0x4C | 0x30 | 0x35 | 0x75 | 0x72 => return None,
_ => return None,
})
}
fn typed_char(event: CGEventRef) -> Option<char> {
let mut buf = [0u16; 8];
let mut actual: usize = 0;
unsafe {
CGEventKeyboardGetUnicodeString(event, buf.len(), &mut actual, buf.as_mut_ptr());
}
if actual == 0 {
return None;
}
let s = String::from_utf16_lossy(&buf[..actual]);
let mut chars = s.chars();
let c = chars.next()?;
if chars.next().is_some() {
return None; }
if c.is_control() {
return None;
}
Some(c)
}
fn build_chord_string(m: &Mods, token: &str) -> String {
let mut s = String::new();
if m.ctrl {
s.push_str("CTRL+");
}
if m.shift {
s.push_str("SHIFT+");
}
if m.alt {
s.push_str("ALT+");
}
if m.command {
s.push_str("SUPER+");
}
s.push_str(token);
s
}