From 8c3d3f664659bf2e078c3c262bfc889523349f90 Mon Sep 17 00:00:00 2001 From: unsecretised Date: Fri, 3 Jul 2026 16:23:06 +0800 Subject: [PATCH 1/3] feat: add hyperkey to rustcast --- src/app.rs | 1 + src/app/pages/settings.rs | 24 ++++++++++++++++++++++++ src/app/tile.rs | 6 +++++- src/app/tile/update.rs | 22 ++++++++++++++++++++++ src/config.rs | 2 ++ src/main.rs | 4 ++++ src/platform/macos/launching.rs | 2 +- src/platform/macos/mod.rs | 30 ++++++++++++++++++++++++++++++ 8 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index ba74701..7035064 100644 --- a/src/app.rs +++ b/src/app.rs @@ -177,6 +177,7 @@ pub enum SetConfigFields { ToDefault, ToggleHotkey(String), ClipboardHotkey(String), + HyperkeyHotkey(String), PlaceHolder(String), SearchUrl(String), ClipboardHistory(bool), diff --git a/src/app/pages/settings.rs b/src/app/pages/settings.rs index 203d21f..909a5ab 100644 --- a/src/app/pages/settings.rs +++ b/src/app/pages/settings.rs @@ -205,6 +205,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([ @@ -391,6 +414,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 fee0de4..8706b46 100644 --- a/src/app/tile.rs +++ b/src/app/tile.rs @@ -26,7 +26,7 @@ use iced::{event, window}; use log::{info, warn}; use objc2::rc::Retained; -use objc2_app_kit::NSRunningApplication; +use objc2_app_kit::{NSPickerTouchBarItemControlRepresentation, NSRunningApplication}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::slice::ParallelSliceMut; use tokio::io::{AsyncBufReadExt, AsyncRead}; @@ -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()]; + 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 6c29cec..5a918cb 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -44,6 +44,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::{start_at_login, stop_at_login}; use crate::quit::get_open_apps; use crate::unit_conversion; @@ -363,6 +364,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 } @@ -416,9 +427,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)) @@ -815,6 +832,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); diff --git a/src/config.rs b/src/config.rs index 1f74021..50c99b6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,6 +19,7 @@ use crate::{ 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, @@ -45,6 +46,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..0d9665b 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 25dcabf..15f78a5 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 9eeca17..cf374ce 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -8,6 +8,7 @@ pub mod urlscheme; pub mod window; use iced::wgpu::rwh::WindowHandle; +use objc2_core_graphics::CGEventSource; pub(super) use self::discovery::get_installed_apps; pub(super) use self::haptics::perform_haptic; @@ -123,6 +124,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 From 7525fd8847efb45626d94f553bd388e459c817cc Mon Sep 17 00:00:00 2001 From: unsecretised Date: Fri, 3 Jul 2026 16:23:40 +0800 Subject: [PATCH 2/3] chore: clippy --- src/app/menubar.rs | 6 +++--- src/app/tile.rs | 6 +++--- src/app/tile/update.rs | 12 ++++++------ src/config.rs | 8 ++------ src/main.rs | 2 +- src/platform/macos/launching.rs | 2 +- src/platform/macos/mod.rs | 1 - 7 files changed, 16 insertions(+), 21 deletions(-) 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/tile.rs b/src/app/tile.rs index 8706b46..95e717c 100644 --- a/src/app/tile.rs +++ b/src/app/tile.rs @@ -26,7 +26,7 @@ use iced::{event, window}; use log::{info, warn}; use objc2::rc::Retained; -use objc2_app_kit::{NSPickerTouchBarItemControlRepresentation, NSRunningApplication}; +use objc2_app_kit::NSRunningApplication; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::slice::ParallelSliceMut; use tokio::io::{AsyncBufReadExt, AsyncRead}; @@ -220,7 +220,7 @@ pub struct Hotkeys { 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); } @@ -300,7 +300,7 @@ impl Tile { } else if modifiers.command() { s.parse::() .ok() - .filter(|&n| n >= 1 && n <= 9) + .filter(|&n| (1..=9).contains(&n)) .map(|n| Message::OpenResult((n - 1) as u32)) } else if s == "p" && modifiers.control() { Some(Message::ChangeFocus(ArrowKey::Up, 1)) diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 5a918cb..9f91769 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -513,12 +513,12 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { ]) } Message::RunFunction(command) => { - if let Function::TileWindow(pos) = &command { - if let Some(pid) = tile.frontmost.as_ref().map(|a| a.processIdentifier()) { - let ok = crate::platform::macos::window::tile_focused_window(pid, pos); - if !ok && tile.config.haptic_feedback { - perform_haptic(HapticPattern::Alignment); - } + if let Function::TileWindow(pos) = &command + && let Some(pid) = tile.frontmost.as_ref().map(|a| a.processIdentifier()) + { + let ok = crate::platform::macos::window::tile_focused_window(pid, pos); + if !ok && tile.config.haptic_feedback { + perform_haptic(HapticPattern::Alignment); } } command.execute(&tile.config); diff --git a/src/config.rs b/src/config.rs index 50c99b6..7df7426 100644 --- a/src/config.rs +++ b/src/config.rs @@ -93,18 +93,14 @@ impl std::fmt::Display for MainPage { /// The mode for the theme (dark, light, or follow system) #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Copy)] #[serde(rename_all = "lowercase")] +#[derive(Default)] pub enum ThemeMode { + #[default] Dark, Light, System, } -impl Default for ThemeMode { - fn default() -> Self { - ThemeMode::Dark - } -} - impl ThemeMode { /// Return preset text and background colors for this mode. pub fn presets( diff --git a/src/main.rs b/src/main.rs index 0d9665b..da6ec2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -87,7 +87,7 @@ fn main() -> iced::Result { hyperkey: config .hyperkey_hotkey .as_ref() - .and_then(|x| Shortcut::parse(&x).ok()), + .and_then(|x| Shortcut::parse(x).ok()), handle: None, }; diff --git a/src/platform/macos/launching.rs b/src/platform/macos/launching.rs index 15f78a5..60ffe87 100644 --- a/src/platform/macos/launching.rs +++ b/src/platform/macos/launching.rs @@ -121,7 +121,7 @@ extern "C-unwind" fn keyboard_event_callback( _ => return unsafe { event.as_mut() }, }; - if !data.targets.iter().any(|t| *t == shortcut) { + if !data.targets.contains(&shortcut) { return unsafe { event.as_mut() }; } diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index cf374ce..f23acdb 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -8,7 +8,6 @@ pub mod urlscheme; pub mod window; use iced::wgpu::rwh::WindowHandle; -use objc2_core_graphics::CGEventSource; pub(super) use self::discovery::get_installed_apps; pub(super) use self::haptics::perform_haptic; From 1e965c1ae64a0a81677b96d5c14fa1d1c72114ba Mon Sep 17 00:00:00 2001 From: unsecretised Date: Fri, 3 Jul 2026 16:27:48 +0800 Subject: [PATCH 3/3] bug: fix tests --- src/app/tile/update.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 9f91769..4da55f1 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -1440,6 +1440,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, },