//! Keyboard shortcut management module //! //! This module provides a unified interface for keyboard shortcuts with //! multiple backend implementations: //! //! - `tauri`: Uses Tauri's built-in global-shortcut plugin //! - `handy_keys`: Uses the handy-keys library for more control //! //! The active implementation is determined by the `keyboard_implementation` //! setting and can be changed at runtime. mod handler; pub mod handy_keys; mod tauri_impl; use log::{error, info, warn}; use serde::Serialize; use specta::Type; use tauri::{AppHandle, Emitter, Manager}; use tauri_plugin_autostart::ManagerExt; #[cfg(all(target_os = "macos", target_arch = "aarch64"))] use crate::settings::APPLE_INTELLIGENCE_DEFAULT_MODEL_ID; use crate::settings::{ self, get_settings, AutoSubmitKey, ClipboardHandling, KeyboardImplementation, LLMPrompt, OverlayPosition, PasteMethod, ShortcutBinding, SoundTheme, TypingTool, APPLE_INTELLIGENCE_PROVIDER_ID, }; use crate::tray; // Note: Commands are accessed via shortcut::handy_keys:: in lib.rs /// Initialize shortcuts using the configured implementation pub fn init_shortcuts(app: &AppHandle) { let user_settings = settings::load_or_create_app_settings(app); // Check which implementation to use match user_settings.keyboard_implementation { KeyboardImplementation::Tauri => { tauri_impl::init_shortcuts(app); } KeyboardImplementation::HandyKeys => { if let Err(e) = handy_keys::init_shortcuts(app) { error!("Failed to initialize handy-keys shortcuts: {}", e); // Fall back to Tauri implementation and persist this fallback warn!("Falling back to Tauri global shortcut implementation and saving fallback to settings"); // Update settings to persist the fallback so we don't retry HandyKeys on next launch let mut settings = settings::get_settings(app); settings.keyboard_implementation = KeyboardImplementation::Tauri; settings::write_settings(app, settings); tauri_impl::init_shortcuts(app); } } } } /// Register the cancel shortcut (called when recording starts) pub fn register_cancel_shortcut(app: &AppHandle) { let settings = get_settings(app); match settings.keyboard_implementation { KeyboardImplementation::Tauri => tauri_impl::register_cancel_shortcut(app), KeyboardImplementation::HandyKeys => handy_keys::register_cancel_shortcut(app), } } /// Unregister the cancel shortcut (called when recording stops) pub fn unregister_cancel_shortcut(app: &AppHandle) { let settings = get_settings(app); match settings.keyboard_implementation { KeyboardImplementation::Tauri => tauri_impl::unregister_cancel_shortcut(app), KeyboardImplementation::HandyKeys => handy_keys::unregister_cancel_shortcut(app), } } /// Register a shortcut using the appropriate implementation pub fn register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> { let settings = get_settings(app); match settings.keyboard_implementation { KeyboardImplementation::Tauri => tauri_impl::register_shortcut(app, binding), KeyboardImplementation::HandyKeys => handy_keys::register_shortcut(app, binding), } } /// Unregister a shortcut using the appropriate implementation pub fn unregister_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> { let settings = get_settings(app); match settings.keyboard_implementation { KeyboardImplementation::Tauri => tauri_impl::unregister_shortcut(app, binding), KeyboardImplementation::HandyKeys => handy_keys::unregister_shortcut(app, binding), } } // ============================================================================ // Binding Management Commands // ============================================================================ #[derive(Serialize, Type)] pub struct BindingResponse { success: bool, binding: Option, error: Option, } #[tauri::command] #[specta::specta] pub fn change_binding( app: AppHandle, id: String, binding: String, ) -> Result { // Reject empty bindings — every shortcut should have a value if binding.trim().is_empty() { return Err("Binding cannot be empty".to_string()); } let mut settings = settings::get_settings(&app); // Get the binding to modify, or create it from defaults if it doesn't exist let binding_to_modify = match settings.bindings.get(&id) { Some(binding) => binding.clone(), None => { // Try to get the default binding for this id let default_settings = settings::get_default_settings(); match default_settings.bindings.get(&id) { Some(default_binding) => { warn!( "Binding '{}' not found in settings, creating from defaults", id ); default_binding.clone() } None => { let error_msg = format!("Binding with id '{}' not found in defaults", id); warn!("change_binding error: {}", error_msg); return Ok(BindingResponse { success: false, binding: None, error: Some(error_msg), }); } } } }; // If this is the cancel binding, just update the settings and return // It's managed dynamically, so we don't register/unregister here if id == "cancel" { if let Some(mut b) = settings.bindings.get(&id).cloned() { b.current_binding = binding; settings.bindings.insert(id.clone(), b.clone()); settings::write_settings(&app, settings); return Ok(BindingResponse { success: true, binding: Some(b.clone()), error: None, }); } } // Unregister the existing binding if let Err(e) = unregister_shortcut(&app, binding_to_modify.clone()) { let error_msg = format!("Failed to unregister shortcut: {}", e); error!("change_binding error: {}", error_msg); } // Validate the new shortcut for the current keyboard implementation if let Err(e) = validate_shortcut_for_implementation(&binding, settings.keyboard_implementation) { warn!("change_binding validation error: {}", e); return Err(e); } // Create an updated binding let mut updated_binding = binding_to_modify; updated_binding.current_binding = binding; // Register the new binding if let Err(e) = register_shortcut(&app, updated_binding.clone()) { let error_msg = format!("Failed to register shortcut: {}", e); error!("change_binding error: {}", error_msg); return Ok(BindingResponse { success: false, binding: None, error: Some(error_msg), }); } // Update the binding in the settings settings.bindings.insert(id, updated_binding.clone()); // Save the settings settings::write_settings(&app, settings); // Return the updated binding Ok(BindingResponse { success: true, binding: Some(updated_binding), error: None, }) } #[tauri::command] #[specta::specta] pub fn reset_binding(app: AppHandle, id: String) -> Result { let binding = settings::get_stored_binding(&app, &id); change_binding(app, id, binding.default_binding) } /// Temporarily unregister a binding while the user is editing it in the UI. /// This avoids firing the action while keys are being recorded. #[tauri::command] #[specta::specta] pub fn suspend_binding(app: AppHandle, id: String) -> Result<(), String> { if let Some(b) = settings::get_bindings(&app).get(&id).cloned() { if let Err(e) = unregister_shortcut(&app, b) { error!("suspend_binding error for id '{}': {}", id, e); return Err(e); } } Ok(()) } /// Re-register the binding after the user has finished editing. #[tauri::command] #[specta::specta] pub fn resume_binding(app: AppHandle, id: String) -> Result<(), String> { if let Some(b) = settings::get_bindings(&app).get(&id).cloned() { if let Err(e) = register_shortcut(&app, b) { error!("resume_binding error for id '{}': {}", id, e); return Err(e); } } Ok(()) } // ============================================================================ // Keyboard Implementation Switching // ============================================================================ /// Result of changing keyboard implementation #[derive(Serialize, Type)] pub struct ImplementationChangeResult { pub success: bool, /// List of binding IDs that were reset to defaults due to incompatibility pub reset_bindings: Vec, } /// Change the keyboard implementation with runtime switching. /// This will unregister all shortcuts from the old implementation, /// validate shortcuts for the new implementation (resetting invalid ones to defaults), /// and register them with the new implementation. #[tauri::command] #[specta::specta] pub fn change_keyboard_implementation_setting( app: AppHandle, implementation: String, ) -> Result { let current_settings = settings::get_settings(&app); let current_impl = current_settings.keyboard_implementation; let new_impl = parse_keyboard_implementation(&implementation); // If same implementation, nothing to do if current_impl == new_impl { return Ok(ImplementationChangeResult { success: true, reset_bindings: vec![], }); } info!( "Switching keyboard implementation from {:?} to {:?}", current_impl, new_impl ); // Unregister all shortcuts from the current implementation unregister_all_shortcuts(&app, current_impl); // Update the setting let mut settings = settings::get_settings(&app); settings.keyboard_implementation = new_impl; settings::write_settings(&app, settings); // Initialize new implementation if needed (HandyKeys needs state) if new_impl == KeyboardImplementation::HandyKeys { if initialize_handy_keys_with_rollback(&app)? { // Shortcuts already registered during init return Ok(ImplementationChangeResult { success: true, reset_bindings: vec![], }); } } // Register all shortcuts with new implementation, resetting invalid ones let reset_bindings = register_all_shortcuts_for_implementation(&app, new_impl); // Emit event to notify frontend of the change let _ = app.emit( "settings-changed", serde_json::json!({ "setting": "keyboard_implementation", "value": implementation, "reset_bindings": reset_bindings }), ); info!("Keyboard implementation switched to {:?}", new_impl); Ok(ImplementationChangeResult { success: true, reset_bindings, }) } /// Get the current keyboard implementation #[tauri::command] #[specta::specta] pub fn get_keyboard_implementation(app: AppHandle) -> String { let settings = settings::get_settings(&app); match settings.keyboard_implementation { KeyboardImplementation::Tauri => "tauri".to_string(), KeyboardImplementation::HandyKeys => "handy_keys".to_string(), } } // ============================================================================ // Validation Helpers // ============================================================================ /// Validate a shortcut for a specific implementation fn validate_shortcut_for_implementation( raw: &str, implementation: KeyboardImplementation, ) -> Result<(), String> { match implementation { KeyboardImplementation::Tauri => tauri_impl::validate_shortcut(raw), KeyboardImplementation::HandyKeys => handy_keys::validate_shortcut(raw), } } /// Parse a keyboard implementation string into the enum fn parse_keyboard_implementation(s: &str) -> KeyboardImplementation { match s { "tauri" => KeyboardImplementation::Tauri, "handy_keys" => KeyboardImplementation::HandyKeys, other => { warn!( "Invalid keyboard implementation '{}', defaulting to tauri", other ); KeyboardImplementation::Tauri } } } /// Unregister all shortcuts for the current implementation fn unregister_all_shortcuts(app: &AppHandle, implementation: KeyboardImplementation) { let bindings = settings::get_bindings(app); for (id, binding) in bindings { // Skip cancel shortcut as it's dynamically registered if id == "cancel" { continue; } let result = match implementation { KeyboardImplementation::Tauri => tauri_impl::unregister_shortcut(app, binding), KeyboardImplementation::HandyKeys => handy_keys::unregister_shortcut(app, binding), }; if let Err(e) = result { warn!( "Failed to unregister shortcut '{}' during switch: {}", id, e ); } } } /// Register all shortcuts for a specific implementation, validating and resetting invalid ones fn register_all_shortcuts_for_implementation( app: &AppHandle, implementation: KeyboardImplementation, ) -> Vec { let mut reset_bindings = Vec::new(); let default_bindings = settings::get_default_settings().bindings; let mut current_settings = settings::get_settings(app); for (id, default_binding) in &default_bindings { // Skip cancel shortcut as it's dynamically registered if id == "cancel" { continue; } // Skip post-processing shortcut when the feature is disabled if id == "transcribe_with_post_process" && !current_settings.post_process_enabled { continue; } let mut binding = current_settings .bindings .get(id) .cloned() .unwrap_or_else(|| default_binding.clone()); // Validate the shortcut for the target implementation if let Err(e) = validate_shortcut_for_implementation(&binding.current_binding, implementation) { info!( "Shortcut '{}' ({}) is invalid for {:?}: {}. Resetting to default.", id, binding.current_binding, implementation, e ); // Reset to default binding.current_binding = default_binding.current_binding.clone(); current_settings .bindings .insert(id.clone(), binding.clone()); reset_bindings.push(id.clone()); } // Register with the appropriate implementation let result = match implementation { KeyboardImplementation::Tauri => tauri_impl::register_shortcut(app, binding), KeyboardImplementation::HandyKeys => handy_keys::register_shortcut(app, binding), }; if let Err(e) = result { error!( "Failed to register shortcut '{}' for {:?}: {}", id, implementation, e ); } } // Save settings if any bindings were reset if !reset_bindings.is_empty() { settings::write_settings(app, current_settings); } reset_bindings } /// Initialize HandyKeys if not already initialized, with rollback on failure fn initialize_handy_keys_with_rollback(app: &AppHandle) -> Result { if app.try_state::().is_some() { return Ok(false); // Already initialized, caller should continue } if let Err(e) = handy_keys::init_shortcuts(app) { error!("Failed to initialize HandyKeys: {}", e); // Rollback to Tauri let mut settings = settings::get_settings(app); settings.keyboard_implementation = KeyboardImplementation::Tauri; settings::write_settings(app, settings); tauri_impl::init_shortcuts(app); return Err(format!( "Failed to initialize HandyKeys: {}. Reverted to Tauri.", e )); } // init_shortcuts already registered shortcuts Ok(true) } // ============================================================================ // General Settings Commands // ============================================================================ #[tauri::command] #[specta::specta] pub fn change_ptt_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.push_to_talk = enabled; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_audio_feedback_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.audio_feedback = enabled; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_audio_feedback_volume_setting(app: AppHandle, volume: f32) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.audio_feedback_volume = volume; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_sound_theme_setting(app: AppHandle, theme: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); let parsed = match theme.as_str() { "marimba" => SoundTheme::Marimba, "pop" => SoundTheme::Pop, "custom" => SoundTheme::Custom, other => { warn!("Invalid sound theme '{}', defaulting to marimba", other); SoundTheme::Marimba } }; settings.sound_theme = parsed; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_translate_to_english_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.translate_to_english = enabled; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_selected_language_setting(app: AppHandle, language: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.selected_language = language; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_overlay_position_setting(app: AppHandle, position: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); let parsed = match position.as_str() { "none" => OverlayPosition::None, "top" => OverlayPosition::Top, "bottom" => OverlayPosition::Bottom, other => { warn!("Invalid overlay position '{}', defaulting to bottom", other); OverlayPosition::Bottom } }; settings.overlay_position = parsed; settings::write_settings(&app, settings); // Update overlay position without recreating window crate::utils::update_overlay_position(&app); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.debug_mode = enabled; settings::write_settings(&app, settings); // Emit event to notify frontend of debug mode change let _ = app.emit( "settings-changed", serde_json::json!({ "setting": "debug_mode", "value": enabled }), ); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_start_hidden_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.start_hidden = enabled; settings::write_settings(&app, settings); // Notify frontend let _ = app.emit( "settings-changed", serde_json::json!({ "setting": "start_hidden", "value": enabled }), ); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_autostart_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.autostart_enabled = enabled; settings::write_settings(&app, settings); // Apply the autostart setting immediately let autostart_manager = app.autolaunch(); if enabled { let _ = autostart_manager.enable(); } else { let _ = autostart_manager.disable(); } // Notify frontend let _ = app.emit( "settings-changed", serde_json::json!({ "setting": "autostart_enabled", "value": enabled }), ); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_update_checks_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.update_checks_enabled = enabled; settings::write_settings(&app, settings); let _ = app.emit( "settings-changed", serde_json::json!({ "setting": "update_checks_enabled", "value": enabled }), ); Ok(()) } #[tauri::command] #[specta::specta] pub fn update_custom_words(app: AppHandle, words: Vec) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.custom_words = words; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_word_correction_threshold_setting( app: AppHandle, threshold: f64, ) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.word_correction_threshold = threshold; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_paste_method_setting(app: AppHandle, method: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); let parsed = match method.as_str() { "ctrl_v" => PasteMethod::CtrlV, "direct" => PasteMethod::Direct, "none" => PasteMethod::None, "shift_insert" => PasteMethod::ShiftInsert, "ctrl_shift_v" => PasteMethod::CtrlShiftV, "external_script" => PasteMethod::ExternalScript, other => { warn!("Invalid paste method '{}', defaulting to ctrl_v", other); PasteMethod::CtrlV } }; settings.paste_method = parsed; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn get_available_typing_tools() -> Vec { #[cfg(target_os = "linux")] { crate::clipboard::get_available_typing_tools() } #[cfg(not(target_os = "linux"))] { vec!["auto".to_string()] } } #[tauri::command] #[specta::specta] pub fn change_typing_tool_setting(app: AppHandle, tool: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); let parsed = match tool.as_str() { "auto" => TypingTool::Auto, "wtype" => TypingTool::Wtype, "kwtype" => TypingTool::Kwtype, "dotool" => TypingTool::Dotool, "ydotool" => TypingTool::Ydotool, "xdotool" => TypingTool::Xdotool, other => { warn!("Invalid typing tool '{}', defaulting to auto", other); TypingTool::Auto } }; settings.typing_tool = parsed; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_external_script_path_setting( app: AppHandle, path: Option, ) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.external_script_path = path; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_clipboard_handling_setting(app: AppHandle, handling: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); let parsed = match handling.as_str() { "dont_modify" => ClipboardHandling::DontModify, "copy_to_clipboard" => ClipboardHandling::CopyToClipboard, other => { warn!( "Invalid clipboard handling '{}', defaulting to dont_modify", other ); ClipboardHandling::DontModify } }; settings.clipboard_handling = parsed; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_auto_submit_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.auto_submit = enabled; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_auto_submit_key_setting(app: AppHandle, key: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); let parsed = match key.as_str() { "enter" => AutoSubmitKey::Enter, "ctrl_enter" => AutoSubmitKey::CtrlEnter, "cmd_enter" => AutoSubmitKey::CmdEnter, other => { warn!("Invalid auto submit key '{}', defaulting to enter", other); AutoSubmitKey::Enter } }; settings.auto_submit_key = parsed; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_post_process_enabled_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.post_process_enabled = enabled; settings::write_settings(&app, settings.clone()); // Register or unregister the post-processing shortcut if let Some(binding) = settings .bindings .get("transcribe_with_post_process") .cloned() { if enabled { let _ = register_shortcut(&app, binding); } else { let _ = unregister_shortcut(&app, binding); } } Ok(()) } #[tauri::command] #[specta::specta] pub fn change_experimental_enabled_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.experimental_enabled = enabled; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_post_process_base_url_setting( app: AppHandle, provider_id: String, base_url: String, ) -> Result<(), String> { let mut settings = settings::get_settings(&app); let label = settings .post_process_provider(&provider_id) .map(|provider| provider.label.clone()) .ok_or_else(|| format!("Provider '{}' not found", provider_id))?; let provider = settings .post_process_provider_mut(&provider_id) .expect("Provider looked up above must exist"); if provider.id != "custom" { return Err(format!( "Provider '{}' does not allow editing the base URL", label )); } provider.base_url = base_url; settings::write_settings(&app, settings); Ok(()) } /// Generic helper to validate provider exists fn validate_provider_exists( settings: &settings::AppSettings, provider_id: &str, ) -> Result<(), String> { if !settings .post_process_providers .iter() .any(|provider| provider.id == provider_id) { return Err(format!("Provider '{}' not found", provider_id)); } Ok(()) } #[tauri::command] #[specta::specta] pub fn change_post_process_api_key_setting( app: AppHandle, provider_id: String, api_key: String, ) -> Result<(), String> { let mut settings = settings::get_settings(&app); validate_provider_exists(&settings, &provider_id)?; settings.post_process_api_keys.insert(provider_id, api_key); settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_post_process_model_setting( app: AppHandle, provider_id: String, model: String, ) -> Result<(), String> { let mut settings = settings::get_settings(&app); validate_provider_exists(&settings, &provider_id)?; settings.post_process_models.insert(provider_id, model); settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn set_post_process_provider(app: AppHandle, provider_id: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); validate_provider_exists(&settings, &provider_id)?; settings.post_process_provider_id = provider_id; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn add_post_process_prompt( app: AppHandle, name: String, prompt: String, ) -> Result { let mut settings = settings::get_settings(&app); // Generate unique ID using timestamp and random component let id = format!("prompt_{}", chrono::Utc::now().timestamp_millis()); let new_prompt = LLMPrompt { id: id.clone(), name, prompt, }; settings.post_process_prompts.push(new_prompt.clone()); settings::write_settings(&app, settings); Ok(new_prompt) } #[tauri::command] #[specta::specta] pub fn update_post_process_prompt( app: AppHandle, id: String, name: String, prompt: String, ) -> Result<(), String> { let mut settings = settings::get_settings(&app); if let Some(existing_prompt) = settings .post_process_prompts .iter_mut() .find(|p| p.id == id) { existing_prompt.name = name; existing_prompt.prompt = prompt; settings::write_settings(&app, settings); Ok(()) } else { Err(format!("Prompt with id '{}' not found", id)) } } #[tauri::command] #[specta::specta] pub fn delete_post_process_prompt(app: AppHandle, id: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); // Don't allow deleting the last prompt if settings.post_process_prompts.len() <= 1 { return Err("Cannot delete the last prompt".to_string()); } // Find and remove the prompt let original_len = settings.post_process_prompts.len(); settings.post_process_prompts.retain(|p| p.id != id); if settings.post_process_prompts.len() == original_len { return Err(format!("Prompt with id '{}' not found", id)); } // If the deleted prompt was selected, select the first one or None if settings.post_process_selected_prompt_id.as_ref() == Some(&id) { settings.post_process_selected_prompt_id = settings.post_process_prompts.first().map(|p| p.id.clone()); } settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub async fn fetch_post_process_models( app: AppHandle, provider_id: String, ) -> Result, String> { let settings = settings::get_settings(&app); // Find the provider let provider = settings .post_process_providers .iter() .find(|p| p.id == provider_id) .ok_or_else(|| format!("Provider '{}' not found", provider_id))?; if provider.id == APPLE_INTELLIGENCE_PROVIDER_ID { #[cfg(all(target_os = "macos", target_arch = "aarch64"))] { return Ok(vec![APPLE_INTELLIGENCE_DEFAULT_MODEL_ID.to_string()]); } #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))] { return Err("Apple Intelligence is only available on Apple silicon Macs running macOS 15 or later.".to_string()); } } // Get API key let api_key = settings .post_process_api_keys .get(&provider_id) .cloned() .unwrap_or_default(); // Skip fetching if no API key for providers that typically need one if api_key.trim().is_empty() && provider.id != "custom" { return Err(format!( "API key is required for {}. Please add an API key to list available models.", provider.label )); } crate::llm_client::fetch_models(provider, api_key).await } #[tauri::command] #[specta::specta] pub fn set_post_process_selected_prompt(app: AppHandle, id: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); // Verify the prompt exists if !settings.post_process_prompts.iter().any(|p| p.id == id) { return Err(format!("Prompt with id '{}' not found", id)); } settings.post_process_selected_prompt_id = Some(id); settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_mute_while_recording_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.mute_while_recording = enabled; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_append_trailing_space_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.append_trailing_space = enabled; settings::write_settings(&app, settings); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_app_language_setting(app: AppHandle, language: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.app_language = language.clone(); settings::write_settings(&app, settings); // Refresh the tray menu with the new language tray::update_tray_menu(&app, &tray::TrayIconState::Idle, Some(&language)); Ok(()) } #[tauri::command] #[specta::specta] pub fn change_show_tray_icon_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.show_tray_icon = enabled; settings::write_settings(&app, settings); // Apply change immediately tray::set_tray_visibility(&app, enabled); Ok(()) }