diff --git a/app/src-tauri/src/dictation_hotkeys.rs b/app/src-tauri/src/dictation_hotkeys.rs new file mode 100644 index 000000000..e4c6fb3bf --- /dev/null +++ b/app/src-tauri/src/dictation_hotkeys.rs @@ -0,0 +1,86 @@ +use std::sync::Mutex; + +/// Tracks the currently registered dictation hotkey string so we can unregister it later. +pub(crate) struct DictationHotkeyState(pub(crate) Mutex>); + +pub(crate) fn expand_dictation_shortcuts(shortcut: &str) -> Vec { + let trimmed = shortcut.trim(); + if trimmed.is_empty() { + return vec![]; + } + + #[cfg(target_os = "macos")] + { + if trimmed.contains("CmdOrCtrl") { + let cmd_variant = trimmed.replace("CmdOrCtrl", "Cmd"); + let ctrl_variant = trimmed.replace("CmdOrCtrl", "Ctrl"); + if cmd_variant == ctrl_variant { + return vec![cmd_variant]; + } + return vec![cmd_variant, ctrl_variant]; + } + } + + #[cfg(not(target_os = "macos"))] + { + if trimmed.contains("CmdOrCtrl") { + return vec![trimmed.replace("CmdOrCtrl", "Ctrl")]; + } + } + + vec![trimmed.to_string()] +} + +#[cfg(test)] +mod tests { + use super::expand_dictation_shortcuts; + + #[test] + fn expand_dictation_shortcuts_cmd_or_ctrl_expansion() { + #[cfg(target_os = "macos")] + { + let result = expand_dictation_shortcuts("CmdOrCtrl+Shift+D"); + assert_eq!(result.len(), 2); + assert!(result.contains(&"Cmd+Shift+D".to_string())); + assert!(result.contains(&"Ctrl+Shift+D".to_string())); + } + + #[cfg(not(target_os = "macos"))] + { + let result = expand_dictation_shortcuts("CmdOrCtrl+Shift+D"); + assert_eq!(result.len(), 1); + assert_eq!(result[0], "Ctrl+Shift+D"); + } + } + + #[test] + fn expand_dictation_shortcuts_plain_shortcut() { + let result = expand_dictation_shortcuts("Ctrl+Alt+T"); + assert_eq!(result.len(), 1); + assert_eq!(result[0], "Ctrl+Alt+T"); + } + + #[test] + fn expand_dictation_shortcuts_empty_input() { + let result = expand_dictation_shortcuts(""); + assert!(result.is_empty()); + + let result = expand_dictation_shortcuts(" "); + assert!(result.is_empty()); + } + + #[test] + #[cfg(target_os = "macos")] + fn expand_dictation_shortcuts_macos_cmd_only() { + let result = expand_dictation_shortcuts("CmdOrCtrl+Space"); + assert!(result.contains(&"Cmd+Space".to_string())); + } + + #[test] + #[cfg(not(target_os = "macos"))] + fn expand_dictation_shortcuts_non_macos_ctrl_only() { + let result = expand_dictation_shortcuts("CmdOrCtrl+Space"); + assert_eq!(result.len(), 1); + assert_eq!(result[0], "Ctrl+Space"); + } +} diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index 8a3df7096..166a51ca1 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -7,6 +7,7 @@ mod cef_preflight; mod cef_profile; mod core_process; mod core_rpc; +mod dictation_hotkeys; mod discord_scanner; mod gmessages_scanner; mod imessage_scanner; @@ -21,8 +22,6 @@ mod webview_apis; mod whatsapp_scanner; mod window_state; -use std::sync::Mutex; - #[cfg(target_os = "macos")] use tauri::WindowEvent; #[cfg(not(target_os = "linux"))] @@ -46,37 +45,6 @@ use objc2_app_kit::{NSPanel, NSWindowCollectionBehavior, NSWindowStyleMask}; // CEF is the only runtime; alias kept so command handlers thread the runtime generic uniformly. pub(crate) type AppRuntime = tauri::Cef; -/// Tracks the currently registered dictation hotkey string so we can unregister it later. -struct DictationHotkeyState(Mutex>); - -fn expand_dictation_shortcuts(shortcut: &str) -> Vec { - let trimmed = shortcut.trim(); - if trimmed.is_empty() { - return vec![]; - } - - #[cfg(target_os = "macos")] - { - if trimmed.contains("CmdOrCtrl") { - let cmd_variant = trimmed.replace("CmdOrCtrl", "Cmd"); - let ctrl_variant = trimmed.replace("CmdOrCtrl", "Ctrl"); - if cmd_variant == ctrl_variant { - return vec![cmd_variant]; - } - return vec![cmd_variant, ctrl_variant]; - } - } - - #[cfg(not(target_os = "macos"))] - { - if trimmed.contains("CmdOrCtrl") { - return vec![trimmed.replace("CmdOrCtrl", "Ctrl")]; - } - } - - vec![trimmed.to_string()] -} - #[tauri::command] fn core_rpc_url() -> String { crate::core_rpc::core_rpc_url_value() @@ -636,12 +604,12 @@ async fn register_dictation_hotkey( log::info!("[dictation] register_dictation_hotkey: shortcut={shortcut}"); let old_shortcuts = { - let state = app.state::(); + let state = app.state::(); let guard = state.0.lock().unwrap(); guard.clone() }; - let expanded_shortcuts = expand_dictation_shortcuts(&shortcut); + let expanded_shortcuts = dictation_hotkeys::expand_dictation_shortcuts(&shortcut); if expanded_shortcuts.is_empty() { return Err("Shortcut cannot be empty".to_string()); } @@ -707,7 +675,7 @@ async fn register_dictation_hotkey( // Persist all newly registered shortcuts. { - let state = app.state::(); + let state = app.state::(); let mut guard = state.0.lock().unwrap(); *guard = expanded_shortcuts.clone(); } @@ -723,7 +691,7 @@ async fn register_dictation_hotkey( #[tauri::command] async fn unregister_dictation_hotkey(app: AppHandle) -> Result<(), String> { log::info!("[dictation] unregister_dictation_hotkey: called"); - let state = app.state::(); + let state = app.state::(); let mut guard = state.0.lock().unwrap(); if guard.is_empty() { log::debug!("[dictation] no shortcut registered — nothing to unregister"); @@ -1368,7 +1336,9 @@ pub fn run() { // build time with `TAURI_SIGNING_PRIVATE_KEY` (+ `_PASSWORD`); see // docs/AUTO_UPDATE.md for the full pipeline. .plugin(tauri_plugin_updater::Builder::new().build()) - .manage(DictationHotkeyState(Mutex::new(Vec::new()))) + .manage(dictation_hotkeys::DictationHotkeyState( + std::sync::Mutex::new(Vec::new()), + )) .manage(webview_accounts::WebviewAccountsState::default()) .manage(notification_settings::NotificationSettingsState::new()) .manage(PendingAppUpdateState::default()); @@ -1947,43 +1917,6 @@ mod tests { let _result = is_daemon_mode(); } - /// Test expand_dictation_shortcuts for CmdOrCtrl expansion - #[test] - fn expand_dictation_shortcuts_cmd_or_ctrl_expansion() { - #[cfg(target_os = "macos")] - { - let result = expand_dictation_shortcuts("CmdOrCtrl+Shift+D"); - assert_eq!(result.len(), 2); - assert!(result.contains(&"Cmd+Shift+D".to_string())); - assert!(result.contains(&"Ctrl+Shift+D".to_string())); - } - - #[cfg(not(target_os = "macos"))] - { - let result = expand_dictation_shortcuts("CmdOrCtrl+Shift+D"); - assert_eq!(result.len(), 1); - assert_eq!(result[0], "Ctrl+Shift+D"); - } - } - - /// Test expand_dictation_shortcuts with plain shortcut (no CmdOrCtrl) - #[test] - fn expand_dictation_shortcuts_plain_shortcut() { - let result = expand_dictation_shortcuts("Ctrl+Alt+T"); - assert_eq!(result.len(), 1); - assert_eq!(result[0], "Ctrl+Alt+T"); - } - - /// Test expand_dictation_shortcuts with empty/whitespace input - #[test] - fn expand_dictation_shortcuts_empty_input() { - let result = expand_dictation_shortcuts(""); - assert!(result.is_empty()); - - let result = expand_dictation_shortcuts(" "); - assert!(result.is_empty()); - } - /// Test core_rpc_url returns expected format #[test] fn core_rpc_url_returns_expected_format() { @@ -2078,23 +2011,4 @@ mod tests { // // This test passes if the code compiles with these log messages. } - - /// Test expand_dictation_shortcuts with Cmd-only variant on macOS - #[test] - #[cfg(target_os = "macos")] - fn expand_dictation_shortcuts_macos_cmd_only() { - // When CmdOrCtrl is replaced with just Cmd - let result = expand_dictation_shortcuts("CmdOrCtrl+Space"); - assert!(result.contains(&"Cmd+Space".to_string())); - } - - /// Test expand_dictation_shortcuts with Ctrl-only variant on non-macOS - #[test] - #[cfg(not(target_os = "macos"))] - fn expand_dictation_shortcuts_non_macos_ctrl_only() { - // When CmdOrCtrl is replaced with just Ctrl - let result = expand_dictation_shortcuts("CmdOrCtrl+Space"); - assert_eq!(result.len(), 1); - assert_eq!(result[0], "Ctrl+Space"); - } }