From fec24ef16c9df875217cef6eee53c07624c0a8b6 Mon Sep 17 00:00:00 2001 From: msk1039 Date: Sat, 11 Apr 2026 02:28:27 +0530 Subject: [PATCH 1/2] fixed the app for macos --- Cargo.lock | 1 + Cargo.toml | 1 + src/app.rs | 108 ++++++++++++++++++++++++++++++++++++++- src/buffer/classifier.rs | 10 ++-- src/buffer/mod.rs | 12 ++++- src/correction/mod.rs | 6 ++- 6 files changed, 131 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c289af0..fa12683 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -922,6 +922,7 @@ name = "keycen" version = "0.1.0" dependencies = [ "clap", + "core-foundation 0.9.4", "dirs", "env_logger", "log", diff --git a/Cargo.toml b/Cargo.toml index 319d12a..f3dac9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ x11rb = { version = "0.13", features = ["allow-unsafe-code"] } [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.5" objc2-app-kit = { version = "0.2", features = ["NSWorkspace", "NSRunningApplication"] } +core-foundation = "0.9" [profile.release] opt-level = 3 diff --git a/src/app.rs b/src/app.rs index 7821178..5645592 100644 --- a/src/app.rs +++ b/src/app.rs @@ -35,6 +35,21 @@ impl App { let app_filter = AppFilter::new(self.config.exclusions.apps.clone()); let app_filter = Arc::new(Mutex::new(app_filter)); + // On macOS, check for Accessibility permissions early and warn the user + #[cfg(target_os = "macos")] + { + if !macos_accessibility_check() { + log::error!( + "Accessibility permissions NOT granted! Keycen requires Accessibility access \ + to intercept and simulate keystrokes. Please grant access in: \ + System Settings > Privacy & Security > Accessibility. \ + Add this application's binary to the list and restart." + ); + } else { + log::info!("Accessibility permissions verified ✓"); + } + } + // Determine input mode let requested_mode = match self.config.general.mode.as_str() { "grab" => InputMode::Grab, @@ -145,7 +160,7 @@ impl App { } }); - // Main thread: pump Windows messages + handle tray menu events + // Main thread: pump native messages + handle tray menu events let mut current_enabled = enabled; loop { // Pump Windows messages so the tray context menu works @@ -163,6 +178,19 @@ impl App { } } + // On macOS, pump the main run loop so that: + // 1. The system tray icon and context menu events are processed correctly + // 2. Any main-thread dispatch queues can execute (needed by some frameworks) + #[cfg(target_os = "macos")] + { + unsafe { + use core_foundation::runloop::{ + CFRunLoopRunInMode, kCFRunLoopDefaultMode, + }; + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.05, 0); + } + } + if let Ok(event) = MenuEvent::receiver().try_recv() { if event.id == tray_menu.toggle_item.id() { current_enabled = !current_enabled; @@ -202,6 +230,8 @@ impl App { } } // Small sleep to avoid busy-waiting + // On macOS, CFRunLoopRunInMode already provides the delay + #[cfg(not(target_os = "macos"))] thread::sleep(Duration::from_millis(50)); } } @@ -244,3 +274,79 @@ fn config_watch_loop( thread::sleep(Duration::from_secs(1)); } } + +/// Check if the process has Accessibility permissions on macOS. +/// This is required for both listening to and simulating keyboard events. +#[cfg(target_os = "macos")] +fn macos_accessibility_check() -> bool { + use std::ffi::c_void; + + #[link(name = "ApplicationServices", kind = "framework")] + extern "C" { + fn AXIsProcessTrusted() -> bool; + } + + // Also try to prompt the user with the system dialog + #[link(name = "CoreFoundation", kind = "framework")] + extern "C" { + fn CFStringCreateWithCString( + alloc: *const c_void, + c_str: *const u8, + encoding: u32, + ) -> *const c_void; + fn CFDictionaryCreate( + allocator: *const c_void, + keys: *const *const c_void, + values: *const *const c_void, + num_values: i64, + key_callbacks: *const c_void, + value_callbacks: *const c_void, + ) -> *const c_void; + fn CFRelease(cf: *const c_void); + static kCFTypeDictionaryKeyCallBacks: c_void; + static kCFTypeDictionaryValueCallBacks: c_void; + static kCFBooleanTrue: *const c_void; + } + + #[link(name = "ApplicationServices", kind = "framework")] + extern "C" { + fn AXIsProcessTrustedWithOptions(options: *const c_void) -> bool; + } + + unsafe { + // First do a simple check + let trusted = AXIsProcessTrusted(); + if trusted { + return true; + } + + // If not trusted, try prompting the system dialog + let key_cstr = b"AXTrustedCheckOptionPrompt\0"; + let key = CFStringCreateWithCString( + std::ptr::null(), + key_cstr.as_ptr(), + 0x08000100, // kCFStringEncodingUTF8 + ); + if !key.is_null() { + let keys = [key]; + let values = [kCFBooleanTrue]; + let options = CFDictionaryCreate( + std::ptr::null(), + keys.as_ptr(), + values.as_ptr(), + 1, + &kCFTypeDictionaryKeyCallBacks as *const _ as *const c_void, + &kCFTypeDictionaryValueCallBacks as *const _ as *const c_void, + ); + if !options.is_null() { + let result = AXIsProcessTrustedWithOptions(options); + CFRelease(options); + CFRelease(key); + return result; + } + CFRelease(key); + } + + false + } +} diff --git a/src/buffer/classifier.rs b/src/buffer/classifier.rs index 378deeb..4aac376 100644 --- a/src/buffer/classifier.rs +++ b/src/buffer/classifier.rs @@ -20,13 +20,15 @@ pub enum KeyClass { } /// Classify a key event into a buffer action -pub fn classify_key(key: Key, name: Option<&str>, ctrl_held: bool) -> KeyClass { - // Check for paste operation (Ctrl+V) - if ctrl_held { +pub fn classify_key(key: Key, name: Option<&str>, ctrl_held: bool, meta_held: bool) -> KeyClass { + // Check for paste/undo/cut operations + // On macOS, Cmd (Meta) is used instead of Ctrl for these shortcuts + let shortcut_held = ctrl_held || meta_held; + if shortcut_held { match key { Key::KeyV => return KeyClass::Paste, Key::KeyZ | Key::KeyX => return KeyClass::BufferReset, - _ => return KeyClass::Ignore, // Other ctrl combos don't produce text + _ => return KeyClass::Ignore, // Other shortcut combos don't produce text } } diff --git a/src/buffer/mod.rs b/src/buffer/mod.rs index bbb707b..e49967c 100644 --- a/src/buffer/mod.rs +++ b/src/buffer/mod.rs @@ -21,6 +21,8 @@ pub enum BufferAction { pub struct WordBuffer { buffer: String, ctrl_held: bool, + /// On macOS, Cmd (Meta) is used instead of Ctrl for shortcuts like paste + meta_held: bool, } impl WordBuffer { @@ -28,6 +30,7 @@ impl WordBuffer { WordBuffer { buffer: String::with_capacity(64), ctrl_held: false, + meta_held: false, } } @@ -39,10 +42,14 @@ impl WordBuffer { self.ctrl_held = true; return BufferAction::Ignored; } + Key::MetaLeft | Key::MetaRight => { + self.meta_held = true; + return BufferAction::Ignored; + } _ => {} } - let class = classify_key(key, name, self.ctrl_held); + let class = classify_key(key, name, self.ctrl_held, self.meta_held); match class { KeyClass::WordChar(ch) => { @@ -77,6 +84,9 @@ impl WordBuffer { Key::ControlLeft | Key::ControlRight => { self.ctrl_held = false; } + Key::MetaLeft | Key::MetaRight => { + self.meta_held = false; + } _ => {} } } diff --git a/src/correction/mod.rs b/src/correction/mod.rs index d1c60b2..9c46540 100644 --- a/src/correction/mod.rs +++ b/src/correction/mod.rs @@ -16,7 +16,11 @@ pub static IS_SIMULATING: AtomicBool = AtomicBool::new(false); pub static CORRECTION_IN_PROGRESS: AtomicBool = AtomicBool::new(false); /// Delay between simulated keystrokes (ms) -/// Keep this minimal to avoid interleaving with user typing +/// macOS requires longer delays for the OS to process simulated events. +/// The rdev docs recommend at least 20ms on macOS. +#[cfg(target_os = "macos")] +const SIMULATE_DELAY_MS: u64 = 20; +#[cfg(not(target_os = "macos"))] const SIMULATE_DELAY_MS: u64 = 1; /// Maximum word length we'll attempt to correct (safety limit) From efbbe90faf04d0b4fd747d4eeb6a01d3752494b5 Mon Sep 17 00:00:00 2001 From: msk1039 Date: Sat, 11 Apr 2026 14:21:32 +0530 Subject: [PATCH 2/2] ci fix --- src/app.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5645592..884e62e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -184,9 +184,7 @@ impl App { #[cfg(target_os = "macos")] { unsafe { - use core_foundation::runloop::{ - CFRunLoopRunInMode, kCFRunLoopDefaultMode, - }; + use core_foundation::runloop::{kCFRunLoopDefaultMode, CFRunLoopRunInMode}; CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.05, 0); } } @@ -335,8 +333,8 @@ fn macos_accessibility_check() -> bool { keys.as_ptr(), values.as_ptr(), 1, - &kCFTypeDictionaryKeyCallBacks as *const _ as *const c_void, - &kCFTypeDictionaryValueCallBacks as *const _ as *const c_void, + &kCFTypeDictionaryKeyCallBacks as *const _, + &kCFTypeDictionaryValueCallBacks as *const _, ); if !options.is_null() { let result = AXIsProcessTrustedWithOptions(options);