@@ -11,10 +11,15 @@ use super::*;
1111use crate :: kanata:: CalculatedMouseMove ;
1212use crate :: oskbd:: KeyEvent ;
1313use anyhow:: anyhow;
14+ use core_foundation:: runloop:: { CFRunLoop , kCFRunLoopCommonModes} ;
1415use core_graphics:: base:: CGFloat ;
1516use core_graphics:: display:: { CGDisplay , CGPoint } ;
16- use core_graphics:: event:: { CGEvent , CGEventTapLocation , CGEventType , CGMouseButton , EventField } ;
17+ use core_graphics:: event:: {
18+ CGEvent , CGEventTap , CGEventTapLocation , CGEventTapOptions , CGEventTapPlacement , CGEventType ,
19+ CGMouseButton , EventField ,
20+ } ;
1721use core_graphics:: event_source:: { CGEventSource , CGEventSourceStateID } ;
22+ use kanata_parser:: cfg:: MappedKeys ;
1823use kanata_parser:: custom_action:: * ;
1924use kanata_parser:: keys:: * ;
2025use karabiner_driverkit:: * ;
@@ -25,6 +30,7 @@ use std::convert::TryFrom;
2530use std:: fmt;
2631use std:: io;
2732use std:: io:: Error ;
33+ use std:: sync:: mpsc:: SyncSender as Sender ;
2834use std:: time:: { Duration , Instant } ;
2935
3036#[ derive( Debug , Clone , Copy ) ]
@@ -416,47 +422,79 @@ impl KbdOut {
416422 event. post ( CGEventTapLocation :: HID ) ;
417423 Ok ( ( ) )
418424 }
425+ /// Synthesize a mouse button press or release via CGEvent.
426+ ///
427+ /// Side buttons (Backward/Forward) use OtherMouseDown/Up with
428+ /// CGMouseButton::Center as a placeholder, then override the
429+ /// MOUSE_EVENT_BUTTON_NUMBER field to the real index (3=Back, 4=Forward).
430+ /// The Rust CGMouseButton enum only has 3 variants but the underlying
431+ /// Apple API supports up to 32 buttons via this field.
432+ ///
433+ /// Ref: [init(mouseEventSource:mouseType:mouseCursorPosition:mouseButton:)][1], [setIntegerValueField][2]
434+ ///
435+ /// [1]: https://developer.apple.com/documentation/coregraphics/cgevent/init(mouseeventsource:mousetype:mousecursorposition:mousebutton:)
436+ /// [2]: https://developer.apple.com/documentation/coregraphics/cgevent/setintegervaluefield(_:value:)
419437 fn button_action ( & mut self , _btn : Btn , is_click : bool ) -> Result < ( ) , io:: Error > {
420- let ( event_type, button) = match _btn {
438+ // (event_type, placeholder_button, real_button_number_override)
439+ let ( event_type, button, button_number) = match _btn {
421440 Btn :: Left => (
422441 if is_click {
423442 CGEventType :: LeftMouseDown
424443 } else {
425444 CGEventType :: LeftMouseUp
426445 } ,
427- Some ( CGMouseButton :: Left ) ,
446+ CGMouseButton :: Left ,
447+ None ,
428448 ) ,
429449 Btn :: Right => (
430450 if is_click {
431451 CGEventType :: RightMouseDown
432452 } else {
433453 CGEventType :: RightMouseUp
434454 } ,
435- Some ( CGMouseButton :: Right ) ,
455+ CGMouseButton :: Right ,
456+ None ,
436457 ) ,
437458 Btn :: Mid => (
438459 if is_click {
439460 CGEventType :: OtherMouseDown
440461 } else {
441462 CGEventType :: OtherMouseUp
442463 } ,
443- Some ( CGMouseButton :: Center ) ,
464+ CGMouseButton :: Center ,
465+ None ,
466+ ) ,
467+ // Side buttons use OtherMouseDown/Up (same event type as middle click)
468+ // with the button number overridden after event creation.
469+ Btn :: Backward => (
470+ if is_click {
471+ CGEventType :: OtherMouseDown
472+ } else {
473+ CGEventType :: OtherMouseUp
474+ } ,
475+ CGMouseButton :: Center ,
476+ Some ( 3 ) , // USB HID button 4 -> CGEvent button 3 (0-indexed)
477+ ) ,
478+ Btn :: Forward => (
479+ if is_click {
480+ CGEventType :: OtherMouseDown
481+ } else {
482+ CGEventType :: OtherMouseUp
483+ } ,
484+ CGMouseButton :: Center ,
485+ Some ( 4 ) , // USB HID button 5 -> CGEvent button 4 (0-indexed)
444486 ) ,
445- // It's unclear to me which event type to use here, hence unsupported for now
446- Btn :: Forward => ( CGEventType :: Null , None ) ,
447- Btn :: Backward => ( CGEventType :: Null , None ) ,
448487 } ;
449- // CGEventType doesn't implement Eq, therefore the casting to u8
450- if event_type as u8 == CGEventType :: Null as u8 {
451- panic ! ( "mouse buttons other than left, right, and middle aren't currently supported" )
452- }
453488
454489 let event_source = Self :: make_event_source ( ) ?;
455490 let event = Self :: make_event ( ) ?;
456491 let mouse_position = event. location ( ) ;
457- let event =
458- CGEvent :: new_mouse_event ( event_source, event_type, mouse_position, button. unwrap ( ) )
459- . map_err ( |_| std:: io:: Error :: other ( "Failed to create mouse event" ) ) ?;
492+ let event = CGEvent :: new_mouse_event ( event_source, event_type, mouse_position, button)
493+ . map_err ( |_| std:: io:: Error :: other ( "Failed to create mouse event" ) ) ?;
494+
495+ if let Some ( num) = button_number {
496+ event. set_integer_value_field ( EventField :: MOUSE_EVENT_BUTTON_NUMBER , num) ;
497+ }
460498
461499 // Mouse control only seems to work with CGEventTapLocation::HID.
462500 event. post ( CGEventTapLocation :: HID ) ;
@@ -568,3 +606,148 @@ impl KbdOut {
568606 }
569607 }
570608}
609+
610+ /// Convert a `(CGEventType, button_number)` pair from a CGEventTap into a
611+ /// kanata `KeyEvent`. The button number field is only meaningful for
612+ /// `OtherMouseDown`/`OtherMouseUp` (2=Middle, 3=Back, 4=Forward); Left/Right
613+ /// are determined entirely by the event type.
614+ impl TryFrom < ( CGEventType , i64 ) > for KeyEvent {
615+ type Error = ( ) ;
616+ fn try_from ( ( event_type, button_number) : ( CGEventType , i64 ) ) -> Result < Self , ( ) > {
617+ use OsCode :: * ;
618+ let ( code, value) = match event_type {
619+ CGEventType :: LeftMouseDown => ( BTN_LEFT , KeyValue :: Press ) ,
620+ CGEventType :: LeftMouseUp => ( BTN_LEFT , KeyValue :: Release ) ,
621+ CGEventType :: RightMouseDown => ( BTN_RIGHT , KeyValue :: Press ) ,
622+ CGEventType :: RightMouseUp => ( BTN_RIGHT , KeyValue :: Release ) ,
623+ CGEventType :: OtherMouseDown | CGEventType :: OtherMouseUp => {
624+ let code = match button_number {
625+ 2 => BTN_MIDDLE ,
626+ 3 => BTN_SIDE ,
627+ 4 => BTN_EXTRA ,
628+ _ => return Err ( ( ) ) ,
629+ } ;
630+ let value = if matches ! ( event_type, CGEventType :: OtherMouseDown ) {
631+ KeyValue :: Press
632+ } else {
633+ KeyValue :: Release
634+ } ;
635+ ( code, value)
636+ }
637+ _ => return Err ( ( ) ) ,
638+ } ;
639+ Ok ( KeyEvent { code, value } )
640+ }
641+ }
642+
643+ /// Start a CGEventTap on a background thread to intercept mouse button events.
644+ /// macOS equivalent of the Windows mouse hook in `windows/llhook.rs`.
645+ ///
646+ /// Mapped buttons are suppressed and forwarded to the processing channel;
647+ /// unmapped buttons pass through. Only installed if the config has mouse
648+ /// buttons in defsrc.
649+ ///
650+ /// Requires Accessibility or Input Monitoring permission.
651+ pub fn start_mouse_listener (
652+ tx : Sender < KeyEvent > ,
653+ mapped_keys : & MappedKeys ,
654+ ) -> Option < std:: thread:: JoinHandle < ( ) > > {
655+ use OsCode :: * ;
656+ let mouse_oscodes = [ BTN_LEFT , BTN_RIGHT , BTN_MIDDLE , BTN_SIDE , BTN_EXTRA ] ;
657+ // Copy only the mouse-relevant mapped keys for the callback closure.
658+ let mapped: MappedKeys = mapped_keys
659+ . iter ( )
660+ . copied ( )
661+ . filter ( |k| mouse_oscodes. contains ( k) )
662+ . collect ( ) ;
663+ if mapped. is_empty ( ) {
664+ log:: info!( "No mouse buttons in defsrc. Not installing mouse event tap." ) ;
665+ return None ;
666+ }
667+
668+ let handle = std:: thread:: Builder :: new ( )
669+ . name ( "mouse-event-tap" . into ( ) )
670+ . spawn ( move || {
671+ let events_of_interest = vec ! [
672+ CGEventType :: LeftMouseDown ,
673+ CGEventType :: LeftMouseUp ,
674+ CGEventType :: RightMouseDown ,
675+ CGEventType :: RightMouseUp ,
676+ CGEventType :: OtherMouseDown ,
677+ CGEventType :: OtherMouseUp ,
678+ ] ;
679+
680+ let tap = match CGEventTap :: new (
681+ CGEventTapLocation :: HID ,
682+ CGEventTapPlacement :: HeadInsertEventTap ,
683+ CGEventTapOptions :: Default ,
684+ events_of_interest,
685+ // Callback receives &CGEvent; return Some(clone) to pass through,
686+ // None to suppress the event.
687+ move |_proxy, event_type, event| {
688+ let button_number =
689+ event. get_integer_value_field ( EventField :: MOUSE_EVENT_BUTTON_NUMBER ) ;
690+ let mut key_event = match KeyEvent :: try_from ( ( event_type, button_number) ) {
691+ Ok ( ev) => ev,
692+ Err ( ( ) ) => return Some ( event. clone ( ) ) ,
693+ } ;
694+
695+ if !mapped. contains ( & key_event. code ) {
696+ return Some ( event. clone ( ) ) ;
697+ }
698+
699+ // Track pressed state to convert duplicate presses into repeats,
700+ // matching the keyboard event loop behavior.
701+ match key_event. value {
702+ KeyValue :: Release => {
703+ crate :: kanata:: PRESSED_KEYS . lock ( ) . remove ( & key_event. code ) ;
704+ }
705+ KeyValue :: Press => {
706+ let mut pressed_keys = crate :: kanata:: PRESSED_KEYS . lock ( ) ;
707+ if pressed_keys. contains ( & key_event. code ) {
708+ key_event. value = KeyValue :: Repeat ;
709+ } else {
710+ pressed_keys. insert ( key_event. code ) ;
711+ }
712+ }
713+ _ => { }
714+ }
715+
716+ log:: debug!( "mouse tap: {key_event:?}" ) ;
717+
718+ if let Err ( e) = tx. try_send ( key_event) {
719+ log:: warn!( "mouse tap: failed to send event: {e}" ) ;
720+ return Some ( event. clone ( ) ) ;
721+ }
722+
723+ // Suppress the original event so it doesn't reach the system.
724+ None
725+ } ,
726+ ) {
727+ Ok ( tap) => tap,
728+ Err ( ( ) ) => {
729+ log:: error!(
730+ "Failed to create mouse event tap. \
731+ Ensure kanata has Accessibility or Input Monitoring permission \
732+ in System Settings > Privacy & Security."
733+ ) ;
734+ return ;
735+ }
736+ } ;
737+
738+ let loop_source = tap
739+ . mach_port
740+ . create_runloop_source ( 0 )
741+ . expect ( "failed to create CFRunLoop source for mouse event tap" ) ;
742+ // Safety: kCFRunLoopCommonModes is an extern static from CoreFoundation.
743+ // Accessing it requires unsafe but is always valid in a running process.
744+ let mode = unsafe { kCFRunLoopCommonModes } ;
745+ CFRunLoop :: get_current ( ) . add_source ( & loop_source, mode) ;
746+ tap. enable ( ) ;
747+ log:: info!( "Mouse event tap installed and active." ) ;
748+ CFRunLoop :: run_current ( ) ;
749+ } )
750+ . expect ( "failed to spawn mouse event tap thread" ) ;
751+
752+ Some ( handle)
753+ }
0 commit comments