diff --git a/src/app.rs b/src/app.rs index 43de72e..c41c474 100644 --- a/src/app.rs +++ b/src/app.rs @@ -177,6 +177,7 @@ pub enum SetConfigFields { ToDefault, ToggleHotkey(String), ClipboardHotkey(String), + HyperkeyHotkey(String), SetPosition(Position), PlaceHolder(String), SearchUrl(String), diff --git a/src/app/menubar.rs b/src/app/menubar.rs index cc86b32..99d59f9 100644 --- a/src/app/menubar.rs +++ b/src/app/menubar.rs @@ -89,10 +89,10 @@ fn get_image() -> DynamicImage { fn init_event_handler(sender: ExtSender, shortcut: Shortcut) { let runtime = Runtime::new().unwrap(); - let shortcut = shortcut.clone(); + let shortcut = shortcut; MenuEvent::set_event_handler(Some(move |x: MenuEvent| { - let shortcut = shortcut.clone(); + let shortcut = shortcut; let sender = sender.clone(); let sender = sender.0.clone(); info!("Menubar event called: {}", x.id.0); @@ -113,7 +113,7 @@ fn init_event_handler(sender: ExtSender, shortcut: Shortcut) { runtime.spawn(async move { sender .clone() - .try_send(Message::KeyPressed(shortcut.clone())) + .try_send(Message::KeyPressed(shortcut)) .unwrap(); }); } diff --git a/src/app/pages/settings.rs b/src/app/pages/settings.rs index 2fb8036..de65fb7 100644 --- a/src/app/pages/settings.rs +++ b/src/app/pages/settings.rs @@ -209,6 +209,29 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat theme.clone(), ); + let theme_clone = theme.clone(); + let hyperkey = settings_row_with_reset( + settings_item_row([ + settings_hint_text( + theme.clone(), + "Hyperkey hotkey", + Some("Simulate CMD+SHIFT+CTRL+ALT with another hotkey"), + ), + Space::new().width(Length::Fill).into(), + text_input( + "Hyperkey Hotkey", + &config.hyperkey_hotkey.clone().unwrap_or("".to_string()), + ) + .on_input(|input| Message::SetConfig(SetConfigFields::HyperkeyHotkey(input.clone()))) + .on_submit(Message::WriteConfig) + .width(Length::Fixed(SETTINGS_INPUT_WIDTH)) + .style(move |_, _| settings_text_input_item_style(&theme_clone)) + .into(), + ]), + ResetField::ToggleHotkey, + theme.clone(), + ); + let theme_clone = theme.clone(); let placeholder_setting = settings_row_with_reset( settings_item_row([ @@ -412,6 +435,7 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat Column::from_iter([ hotkey, cb_hotkey, + hyperkey, placeholder_setting, search, debounce, diff --git a/src/app/tile.rs b/src/app/tile.rs index d70928f..95e717c 100644 --- a/src/app/tile.rs +++ b/src/app/tile.rs @@ -214,12 +214,16 @@ pub struct Hotkeys { pub handle: Option, pub toggle: Shortcut, pub clipboard_hotkey: Shortcut, + pub hyperkey: Option, pub shells: HashMap, } impl Hotkeys { pub fn all_hotkeys(&self) -> Vec { - let mut a = vec![self.toggle.clone(), self.clipboard_hotkey.clone()]; + let mut a = vec![self.toggle, self.clipboard_hotkey]; + if let Some(hyperkey_shortcut) = self.hyperkey { + a.push(hyperkey_shortcut); + } a.extend( self.shells .keys() diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index b2b5c03..d4ce32b 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -47,6 +47,7 @@ use crate::debounce::DebouncePolicy; use crate::platform::macos::events::Event; use crate::platform::macos::launching::Shortcut; use crate::platform::macos::launching::global_handler; +use crate::platform::macos::send_hyperkey_event; use crate::platform::macos::screen_with_mouse; use crate::platform::macos::{start_at_login, stop_at_login}; use crate::quit::get_open_apps; @@ -380,6 +381,16 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { tile.hotkeys.clipboard_hotkey = hotkey } + if let Some(hyperkey) = &new_config + .hyperkey_hotkey + .as_ref() + .and_then(|x| Shortcut::parse(x).ok()) + { + tile.hotkeys.hyperkey = Some(*hyperkey) + } else { + tile.hotkeys.hyperkey = None + } + if let Ok(hotkey) = Shortcut::parse(&new_config.toggle_hotkey) { tile.hotkeys.toggle = hotkey } @@ -433,9 +444,15 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { ))); } + let is_hyperkey_hotkey = Some(shortcut) == tile.hotkeys.hyperkey; let is_clipboard_hotkey = shortcut == tile.hotkeys.clipboard_hotkey; let is_open_hotkey = shortcut == tile.hotkeys.toggle; + if is_hyperkey_hotkey { + send_hyperkey_event(); + return Task::none(); + } + let clipboard_page_task = if is_clipboard_hotkey { info!("Switching to clipboard page"); Task::done(Message::SwitchToPage(Page::ClipboardHistory)) @@ -832,6 +849,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { match config.clone() { SetConfigFields::ToggleHotkey(hk) => final_config.toggle_hotkey = hk, SetConfigFields::ClipboardHotkey(hk) => final_config.clipboard_hotkey = hk, + SetConfigFields::HyperkeyHotkey(key) => { + if !key.trim().is_empty() { + final_config.hyperkey_hotkey = Some(key); + } + } SetConfigFields::ClipboardHistory(cbhist) => final_config.cbhist = cbhist, SetConfigFields::Modes(Editable::Create((key, value))) => { final_config.modes.insert(key, value); @@ -1438,6 +1460,7 @@ mod tests { hotkeys: Hotkeys { toggle: Shortcut::parse("alt+space").unwrap(), clipboard_hotkey: Shortcut::parse("cmd+shift+c").unwrap(), + hyperkey: None, shells: HashMap::new(), handle: None, }, diff --git a/src/config.rs b/src/config.rs index bd92447..d495733 100644 --- a/src/config.rs +++ b/src/config.rs @@ -95,6 +95,7 @@ impl std::fmt::Display for Position { pub struct Config { pub toggle_hotkey: String, pub clipboard_hotkey: String, + pub hyperkey_hotkey: Option, pub buffer_rules: Buffer, pub event_duration: u32, pub main_page: MainPage, @@ -122,6 +123,7 @@ impl Default for Config { Self { toggle_hotkey: "ALT+SPACE".to_string(), clipboard_hotkey: "SUPER+SHIFT+C".to_string(), + hyperkey_hotkey: None, buffer_rules: Buffer::default(), theme: Theme::default(), start_at_login: true, diff --git a/src/main.rs b/src/main.rs index 1ec7c28..da6ec2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,6 +84,10 @@ fn main() -> iced::Result { toggle: show_hide, clipboard_hotkey: cbhist, shells: shell_map, + hyperkey: config + .hyperkey_hotkey + .as_ref() + .and_then(|x| Shortcut::parse(x).ok()), handle: None, }; diff --git a/src/platform/macos/launching.rs b/src/platform/macos/launching.rs index 68e1a6e..60ffe87 100644 --- a/src/platform/macos/launching.rs +++ b/src/platform/macos/launching.rs @@ -176,7 +176,7 @@ pub fn global_handler(sender: ExtSender, targets: Vec) -> Result, pub mods: Option, diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index c851b4a..15bccb3 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -149,6 +149,35 @@ pub fn simulate_paste(pid: libc::pid_t) { } } +pub fn send_hyperkey_event() { + use objc2_core_graphics::{ + CGEvent, CGEventFlags, CGEventSource, CGEventSourceStateID, CGEventTapLocation, + }; + + let source = CGEventSource::new(CGEventSourceStateID::HIDSystemState); + let source_ref = source.as_deref(); + + // Use a keycode that won't interfere - 0xFF or a null keycode + // Alternatively use a specific key like F18 (0x4F) if you want a real key + let keycode: u16 = 0; // kVK_ANSI_A as placeholder, or use your target key + + let hyper_flags = CGEventFlags::MaskCommand + | CGEventFlags::MaskAlternate // OPT + | CGEventFlags::MaskShift + | CGEventFlags::MaskControl; + + // Key down + if let Some(keydown) = CGEvent::new_keyboard_event(source_ref, keycode, true) { + CGEvent::set_flags(Some(&keydown), hyper_flags); + CGEvent::post(CGEventTapLocation::HIDEventTap, Some(&keydown)); + } + + // Key up + if let Some(keyup) = CGEvent::new_keyboard_event(source_ref, keycode, false) { + CGEvent::set_flags(Some(&keyup), hyper_flags); + CGEvent::post(CGEventTapLocation::HIDEventTap, Some(&keyup)); + } +} /// This is the function that transforms the process to a UI element, and hides the dock icon /// /// see mostly