@@ -35,6 +35,21 @@ impl App {
3535 let app_filter = AppFilter :: new ( self . config . exclusions . apps . clone ( ) ) ;
3636 let app_filter = Arc :: new ( Mutex :: new ( app_filter) ) ;
3737
38+ // On macOS, check for Accessibility permissions early and warn the user
39+ #[ cfg( target_os = "macos" ) ]
40+ {
41+ if !macos_accessibility_check ( ) {
42+ log:: error!(
43+ "Accessibility permissions NOT granted! Keycen requires Accessibility access \
44+ to intercept and simulate keystrokes. Please grant access in: \
45+ System Settings > Privacy & Security > Accessibility. \
46+ Add this application's binary to the list and restart."
47+ ) ;
48+ } else {
49+ log:: info!( "Accessibility permissions verified ✓" ) ;
50+ }
51+ }
52+
3853 // Determine input mode
3954 let requested_mode = match self . config . general . mode . as_str ( ) {
4055 "grab" => InputMode :: Grab ,
@@ -145,7 +160,7 @@ impl App {
145160 }
146161 } ) ;
147162
148- // Main thread: pump Windows messages + handle tray menu events
163+ // Main thread: pump native messages + handle tray menu events
149164 let mut current_enabled = enabled;
150165 loop {
151166 // Pump Windows messages so the tray context menu works
@@ -163,6 +178,17 @@ impl App {
163178 }
164179 }
165180
181+ // On macOS, pump the main run loop so that:
182+ // 1. The system tray icon and context menu events are processed correctly
183+ // 2. Any main-thread dispatch queues can execute (needed by some frameworks)
184+ #[ cfg( target_os = "macos" ) ]
185+ {
186+ unsafe {
187+ use core_foundation:: runloop:: { kCFRunLoopDefaultMode, CFRunLoopRunInMode } ;
188+ CFRunLoopRunInMode ( kCFRunLoopDefaultMode, 0.05 , 0 ) ;
189+ }
190+ }
191+
166192 if let Ok ( event) = MenuEvent :: receiver ( ) . try_recv ( ) {
167193 if event. id == tray_menu. toggle_item . id ( ) {
168194 current_enabled = !current_enabled;
@@ -202,6 +228,8 @@ impl App {
202228 }
203229 }
204230 // Small sleep to avoid busy-waiting
231+ // On macOS, CFRunLoopRunInMode already provides the delay
232+ #[ cfg( not( target_os = "macos" ) ) ]
205233 thread:: sleep ( Duration :: from_millis ( 50 ) ) ;
206234 }
207235 }
@@ -244,3 +272,79 @@ fn config_watch_loop(
244272 thread:: sleep ( Duration :: from_secs ( 1 ) ) ;
245273 }
246274}
275+
276+ /// Check if the process has Accessibility permissions on macOS.
277+ /// This is required for both listening to and simulating keyboard events.
278+ #[ cfg( target_os = "macos" ) ]
279+ fn macos_accessibility_check ( ) -> bool {
280+ use std:: ffi:: c_void;
281+
282+ #[ link( name = "ApplicationServices" , kind = "framework" ) ]
283+ extern "C" {
284+ fn AXIsProcessTrusted ( ) -> bool ;
285+ }
286+
287+ // Also try to prompt the user with the system dialog
288+ #[ link( name = "CoreFoundation" , kind = "framework" ) ]
289+ extern "C" {
290+ fn CFStringCreateWithCString (
291+ alloc : * const c_void ,
292+ c_str : * const u8 ,
293+ encoding : u32 ,
294+ ) -> * const c_void ;
295+ fn CFDictionaryCreate (
296+ allocator : * const c_void ,
297+ keys : * const * const c_void ,
298+ values : * const * const c_void ,
299+ num_values : i64 ,
300+ key_callbacks : * const c_void ,
301+ value_callbacks : * const c_void ,
302+ ) -> * const c_void ;
303+ fn CFRelease ( cf : * const c_void ) ;
304+ static kCFTypeDictionaryKeyCallBacks: c_void ;
305+ static kCFTypeDictionaryValueCallBacks: c_void ;
306+ static kCFBooleanTrue: * const c_void ;
307+ }
308+
309+ #[ link( name = "ApplicationServices" , kind = "framework" ) ]
310+ extern "C" {
311+ fn AXIsProcessTrustedWithOptions ( options : * const c_void ) -> bool ;
312+ }
313+
314+ unsafe {
315+ // First do a simple check
316+ let trusted = AXIsProcessTrusted ( ) ;
317+ if trusted {
318+ return true ;
319+ }
320+
321+ // If not trusted, try prompting the system dialog
322+ let key_cstr = b"AXTrustedCheckOptionPrompt\0 " ;
323+ let key = CFStringCreateWithCString (
324+ std:: ptr:: null ( ) ,
325+ key_cstr. as_ptr ( ) ,
326+ 0x08000100 , // kCFStringEncodingUTF8
327+ ) ;
328+ if !key. is_null ( ) {
329+ let keys = [ key] ;
330+ let values = [ kCFBooleanTrue] ;
331+ let options = CFDictionaryCreate (
332+ std:: ptr:: null ( ) ,
333+ keys. as_ptr ( ) ,
334+ values. as_ptr ( ) ,
335+ 1 ,
336+ & kCFTypeDictionaryKeyCallBacks as * const _ ,
337+ & kCFTypeDictionaryValueCallBacks as * const _ ,
338+ ) ;
339+ if !options. is_null ( ) {
340+ let result = AXIsProcessTrustedWithOptions ( options) ;
341+ CFRelease ( options) ;
342+ CFRelease ( key) ;
343+ return result;
344+ }
345+ CFRelease ( key) ;
346+ }
347+
348+ false
349+ }
350+ }
0 commit comments