diff --git a/Cargo.lock b/Cargo.lock index 3b9e2f5..763e458 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3157,10 +3157,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.11.0", + "block2 0.6.2", "dispatch2", + "libc", "objc2 0.6.3", "objc2-core-foundation", "objc2-io-surface", + "objc2-metal 0.3.2", ] [[package]] @@ -3172,7 +3175,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-metal 0.2.2", ] [[package]] @@ -3325,6 +3328,17 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-quartz-core" version = "0.2.2" @@ -3335,7 +3349,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-metal 0.2.2", ] [[package]] @@ -4165,6 +4179,7 @@ dependencies = [ "objc2-app-kit 0.3.2", "objc2-application-services", "objc2-core-foundation", + "objc2-core-graphics", "objc2-event-kit", "objc2-foundation 0.3.2", "objc2-service-management", diff --git a/Cargo.toml b/Cargo.toml index c5d5262..c539c91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ objc2 = "0.6.3" objc2-app-kit = { version = "0.3.2", features = ["NSImage"] } objc2-application-services = { version = "0.3.2", default-features = false, features = ["HIServices", "Processes"] } objc2-core-foundation = "0.3.2" +objc2-core-graphics = { version = "0.3.2", features = ["CGEvent"] } objc2-event-kit = "0.3.2" objc2-foundation = { version = "0.3.2", features = ["NSDateFormatter", "NSFormatter", "NSString"] } objc2-service-management = "0.3.2" diff --git a/src/app/tile.rs b/src/app/tile.rs index e2a2b64..3b442d4 100644 --- a/src/app/tile.rs +++ b/src/app/tile.rs @@ -10,7 +10,7 @@ use crate::config::{Config, Shelly}; use crate::debounce::Debouncer; use crate::platform::default_app_paths; use crate::platform::macos::events::Event; -use crate::platform::macos::launching::Shortcut; +use crate::platform::macos::launching::{EventTapHandle, Shortcut}; use arboard::Clipboard; @@ -207,11 +207,25 @@ pub struct Tile { /// Stores the toggle [`HotKey`] and the Clipboard [`HotKey`] #[derive(Clone, Debug)] pub struct Hotkeys { + pub handle: Option, pub toggle: Shortcut, pub clipboard_hotkey: Shortcut, pub shells: HashMap, } +impl Hotkeys { + pub fn all_hotkeys(&self) -> Vec { + let mut a = vec![self.toggle.clone(), self.clipboard_hotkey.clone()]; + a.extend( + self.shells + .keys() + .map(|x| x.to_owned()) + .collect::>(), + ); + a + } +} + impl Tile { /// This returns the theme of the window pub fn theme(&self, _: window::Id) -> Option { diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 0c5431e..35f19f0 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -185,7 +185,13 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Message::SetSender(sender) => { tile.sender = Some(sender.clone()); - global_handler(sender.clone()); + match global_handler(sender.clone(), tile.hotkeys.all_hotkeys()) { + Ok(a) => tile.hotkeys.handle = Some(a), + Err(e) => { + log::error!("Error when registering hotkey: {e}"); + std::process::exit(1); + } + }; if tile.config.show_trayicon { tile.tray_icon = Some(menu_icon(tile.config.clone(), sender)); } @@ -413,7 +419,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { tile.theme = new_config.theme.to_owned().into(); tile.config = new_config; - Task::batch([Task::done(Message::LoadRanking), update_apps_task]) + Task::batch([ + Task::done(Message::LoadRanking), + update_apps_task, + Task::done(Message::SetSender(tile.sender.clone().unwrap())), + ]) } Message::KeyPressed(shortcut) => { @@ -760,7 +770,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Message::SetConfig(config) => { let mut final_config = tile.config.clone(); - match config { + match config.clone() { SetConfigFields::ToggleHotkey(hk) => final_config.toggle_hotkey = hk, SetConfigFields::ClipboardHotkey(hk) => final_config.clipboard_hotkey = hk, SetConfigFields::ClipboardHistory(cbhist) => final_config.cbhist = cbhist, @@ -1120,7 +1130,7 @@ fn execute_query(tile: &mut Tile, id: Id) -> Task { _ => {} } - let deferred_action = if let Some(action) = + let _deferred_action = if let Some(action) = classify_query_action(&tile.page, &tile.query, &tile.query_lc) { match action { @@ -1300,6 +1310,7 @@ mod tests { toggle: Shortcut::parse("alt+space").unwrap(), clipboard_hotkey: Shortcut::parse("cmd+shift+c").unwrap(), shells: HashMap::new(), + handle: None, }, clipboard_content: Vec::new(), tray_icon: None, diff --git a/src/main.rs b/src/main.rs index 0c120d9..1ec7c28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ mod utils; use std::{collections::HashMap, fs::OpenOptions, path::Path}; -use rustcast::{ +use crate::{ app::tile::{self, Hotkeys, Tile}, config::Config, platform::macos::{get_autostart_status, launching::Shortcut}, @@ -24,7 +24,7 @@ use rustcast::{ use log::info; use tracing_subscriber::{EnvFilter, Layer, util::SubscriberInitExt}; -use rustcast::platform::set_activation_policy_accessory; +use crate::platform::set_activation_policy_accessory; fn main() -> iced::Result { set_activation_policy_accessory(); @@ -84,6 +84,7 @@ fn main() -> iced::Result { toggle: show_hide, clipboard_hotkey: cbhist, shells: shell_map, + handle: None, }; info!("Hotkeys loaded"); diff --git a/src/platform/macos/launching.rs b/src/platform/macos/launching.rs index e6d22a9..366c8e7 100644 --- a/src/platform/macos/launching.rs +++ b/src/platform/macos/launching.rs @@ -1,93 +1,179 @@ -use std::sync::{Arc, Mutex}; +use std::{ + ffi::c_void, + ptr::NonNull, + sync::{Arc, Mutex}, +}; -use block2::RcBlock; -use objc2_app_kit::{NSEvent, NSEventMask, NSEventModifierFlags, NSEventType}; +use objc2_app_kit::NSEventModifierFlags; +use objc2_core_foundation::{ + CFMachPort, CFRetained, CFRunLoop, CFRunLoopSource, kCFRunLoopCommonModes, +}; +use objc2_core_graphics::{ + CGEvent, CGEventField, CGEventFlags, CGEventTapLocation, CGEventTapOptions, + CGEventTapPlacement, CGEventTapProxy, CGEventType, +}; use crate::{ app::{Message, tile::ExtSender}, platform::macos::accessibility::ensure_accessibility_permission, }; -pub fn global_handler(sender: ExtSender) { - ensure_accessibility_permission(); - local_handler(sender.clone()); - let mask = NSEventMask::KeyDown | NSEventMask::FlagsChanged; - let sender = Arc::new(Mutex::new(sender.0.clone())); - - let block = RcBlock::new({ - move |event: std::ptr::NonNull| { - let event = unsafe { event.as_ref() }; - let event_type = event.r#type(); - - let key_code = event.keyCode(); - let mods = event.modifierFlags() - & (NSEventModifierFlags::Command - | NSEventModifierFlags::Option - | NSEventModifierFlags::Control - | NSEventModifierFlags::Function - | NSEventModifierFlags::CapsLock - | NSEventModifierFlags::Shift); - - let shortcut = match event_type { - NSEventType::KeyDown => Shortcut { - key_code: Some(key_code), - mods: if mods.0 != 0 { Some(mods.0) } else { None }, - }, - NSEventType::FlagsChanged => Shortcut { - key_code: None, - mods: if mods.0 != 0 { Some(mods.0) } else { None }, - }, - _ => return, - }; +#[derive(Clone, Debug)] +pub struct EventTapHandle { + tap_port: CFRetained, + loop_source: CFRetained, + callback_data: *mut c_void, +} - let mut s = sender.lock().unwrap(); - let _ = s.try_send(Message::KeyPressed(shortcut)); - } - }); +impl Drop for EventTapHandle { + fn drop(&mut self) { + CGEvent::tap_enable(&self.tap_port, false); + + let run_loop = CFRunLoop::main().expect("Failed to get main CFRunLoop"); + run_loop.remove_source(Some(&self.loop_source), unsafe { kCFRunLoopCommonModes }); - NSEvent::addGlobalMonitorForEventsMatchingMask_handler(mask, &block); + // Free the callback data + if !self.callback_data.is_null() { + unsafe { + drop(Box::from_raw(self.callback_data as *mut CallbackData)); + } + } + } } -pub fn local_handler(sender: ExtSender) { - let mask = NSEventMask::KeyDown | NSEventMask::FlagsChanged; - let sender = Arc::new(Mutex::new(sender.0.clone())); - - let block = RcBlock::new({ - move |event: std::ptr::NonNull| -> *mut NSEvent { - let event_ref = unsafe { event.as_ref() }; - let event_type = event_ref.r#type(); - - let key_code = event_ref.keyCode(); - let mods = event_ref.modifierFlags() - & (NSEventModifierFlags::Command - | NSEventModifierFlags::Option - | NSEventModifierFlags::Control - | NSEventModifierFlags::Function - | NSEventModifierFlags::CapsLock - | NSEventModifierFlags::Shift); - - let shortcut = match event_type { - NSEventType::KeyDown => Shortcut { - key_code: Some(key_code), - mods: if mods.0 != 0 { Some(mods.0) } else { None }, - }, - NSEventType::FlagsChanged => Shortcut { - key_code: None, - mods: if mods.0 != 0 { Some(mods.0) } else { None }, - }, - _ => return event.as_ptr(), // pass through unhandled events +extern "C-unwind" fn keyboard_event_callback( + _proxy: CGEventTapProxy, + event_type: CGEventType, + mut event: NonNull, + user_info: *mut c_void, +) -> *mut CGEvent { + if user_info.is_null() { + log::error!("Null user_info in keyboard_event_callback"); + return unsafe { event.as_mut() }; + } + + let data = unsafe { &*(user_info as *const CallbackData) }; + + let key_code: u16 = unsafe { + CGEvent::integer_value_field(Some(event.as_ref()), CGEventField::KeyboardEventKeycode) + } as u16; + + let flags: CGEventFlags = unsafe { CGEvent::flags(Some(event.as_ref())) }; + + let mut mods = NSEventModifierFlags::empty(); + + if flags.contains(CGEventFlags::MaskCommand) { + mods |= NSEventModifierFlags::Command; + } + if flags.contains(CGEventFlags::MaskAlternate) { + mods |= NSEventModifierFlags::Option; + } + if flags.contains(CGEventFlags::MaskControl) { + mods |= NSEventModifierFlags::Control; + } + if flags.contains(CGEventFlags::MaskShift) { + mods |= NSEventModifierFlags::Shift; + } + if flags.contains(CGEventFlags::MaskAlphaShift) { + mods |= NSEventModifierFlags::CapsLock; + } + if flags.contains(CGEventFlags::MaskSecondaryFn) { + mods |= NSEventModifierFlags::Function; + } + + let shortcut = match event_type { + CGEventType::KeyDown => Shortcut { + key_code: Some(key_code), + mods: if mods.0 != 0 { Some(mods.0) } else { None }, + }, + CGEventType::FlagsChanged => { + let is_press = match key_code { + 56 | 60 => flags.contains(CGEventFlags::MaskShift), // LSHIFT | RSHIFT + 59 | 62 => flags.contains(CGEventFlags::MaskControl), // LCTRL | RCTRL + 58 | 61 => flags.contains(CGEventFlags::MaskAlternate), // LOPT | ROPT + 55 | 54 => flags.contains(CGEventFlags::MaskCommand), // LCMD | RCMD + 63 => flags.contains(CGEventFlags::MaskSecondaryFn), // FN + 57 => flags.contains(CGEventFlags::MaskAlphaShift), // CAPSLOCK + _ => false, + }; + + if !is_press { + return unsafe { event.as_mut() }; + } + + let self_flag = match key_code { + 56 | 60 => NSEventModifierFlags::Shift, // LSHIFT | RSHIFT + 59 | 62 => NSEventModifierFlags::Control, // LCTRL | RCTRL + 58 | 61 => NSEventModifierFlags::Option, // LOPT | ROPT + 55 | 54 => NSEventModifierFlags::Command, // LCMD | RCMD + 63 => NSEventModifierFlags::Function, // FN + 57 => NSEventModifierFlags::CapsLock, // CAPSLOCK + _ => NSEventModifierFlags::empty(), }; - let mut s = sender.lock().unwrap(); - let _ = s.try_send(Message::KeyPressed(shortcut)); + mods.remove(self_flag); - event.as_ptr() + Shortcut { + key_code: Some(key_code), + mods: if mods.is_empty() { None } else { Some(mods.0) }, + } } + _ => return unsafe { event.as_mut() }, + }; + + if !data.targets.iter().any(|t| *t == shortcut) { + return unsafe { event.as_mut() }; + } + + if let Ok(mut sender) = data.sender.lock() { + sender.0.try_send(Message::KeyPressed(shortcut)).unwrap(); + } + + unsafe { event.as_mut() } +} + +pub struct CallbackData { + sender: Arc>, + targets: Vec, +} + +pub fn global_handler(sender: ExtSender, targets: Vec) -> Result { + ensure_accessibility_permission(); // make it return Result + + let callback_data = Box::new(CallbackData { + sender: Arc::new(Mutex::new(sender)), + targets, }); + let user_info = Box::into_raw(callback_data) as *mut c_void; - unsafe { - NSEvent::addLocalMonitorForEventsMatchingMask_handler(mask, &block); + let mask = + (1u64 << CGEventType::KeyDown.0 as u64) | (1u64 << CGEventType::FlagsChanged.0 as u64); + + let tap_port = unsafe { + CGEvent::tap_create( + CGEventTapLocation::SessionEventTap, + CGEventTapPlacement::HeadInsertEventTap, + CGEventTapOptions::Default, + mask, + Some(keyboard_event_callback), + user_info, + ) } + .unwrap(); + + let loop_source = CFMachPort::new_run_loop_source(None, Some(&tap_port), 0) + .ok_or_else(|| "Failed to create run loop source".to_string())?; + + let run_loop = CFRunLoop::main().ok_or_else(|| "Failed to get main run loop".to_string())?; + run_loop.add_source(Some(&loop_source), unsafe { kCFRunLoopCommonModes }); + + CGEvent::tap_enable(&tap_port, true); + + Ok(EventTapHandle { + tap_port, + loop_source, + callback_data: user_info, + }) } #[derive(Debug, Clone, PartialEq, Eq, Hash)]