diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 2d8ee0a..1cddaa5 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,2 +1,10 @@ pub mod audio; pub mod models; + +use crate::utils::cancel_current_operation; +use tauri::AppHandle; + +#[tauri::command] +pub fn cancel_operation(app: AppHandle) { + cancel_current_operation(&app); +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5e6a5a6..39879f7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,7 +12,7 @@ use managers::transcription::TranscriptionManager; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use tauri::image::Image; -use tauri::menu::{Menu, MenuItem, PredefinedMenuItem}; + use tauri::tray::TrayIconBuilder; use tauri::Emitter; use tauri::{AppHandle, Manager}; @@ -52,48 +52,11 @@ pub fn run() { )) .manage(Mutex::new(ShortcutToggleStates::default())) .setup(move |app| { - let version = env!("CARGO_PKG_VERSION"); - let version_label = format!("Handy v{}", version); - let version_i = MenuItem::with_id(app, "version", &version_label, false, None::<&str>)?; - - // Platform-specific accelerators - #[cfg(target_os = "macos")] - let settings_accelerator = Some("Cmd+,"); - #[cfg(not(target_os = "macos"))] - let settings_accelerator = Some("Ctrl+,"); - - #[cfg(target_os = "macos")] - let quit_accelerator = Some("Cmd+Q"); - #[cfg(not(target_os = "macos"))] - let quit_accelerator = Some("Ctrl+Q"); - - let settings_i = - MenuItem::with_id(app, "settings", "Settings...", true, settings_accelerator)?; - let check_updates_i = MenuItem::with_id( - app, - "check_updates", - "Check for Updates...", - true, - None::<&str>, - )?; - let quit_i = MenuItem::with_id(app, "quit", "Quit", true, quit_accelerator)?; - let menu = Menu::with_items( - app, - &[ - &version_i, - &PredefinedMenuItem::separator(app)?, - &settings_i, - &check_updates_i, - &PredefinedMenuItem::separator(app)?, - &quit_i, - ], - )?; let tray = TrayIconBuilder::new() .icon(Image::from_path(app.path().resolve( "resources/tray_idle.png", tauri::path::BaseDirectory::Resource, )?)?) - .menu(&menu) .show_menu_on_left_click(true) .on_menu_event(|app, event| match event.id.as_ref() { "settings" => { @@ -122,24 +85,11 @@ pub fn run() { "check_updates" => { let _ = app.emit("check-for-updates", ()); } - "cancel_listening" => { - use crate::utils::{change_tray_icon, TrayIconState}; - use crate::managers::audio::AudioRecordingManager; - use std::sync::Arc; - - // Cancel the recording without transcribing - let rm = app.state::>(); - rm.cancel_recording(); - - // Return to idle state - change_tray_icon(app, TrayIconState::Idle); - } - "cancel_transcribing" => { - use crate::utils::{change_tray_icon, TrayIconState}; - - // For now, just return to idle state - // TODO: In a future version, we could add actual request cancellation - change_tray_icon(app, TrayIconState::Idle); + "cancel" => { + use crate::utils::cancel_current_operation; + + // Use centralized cancellation that handles all operations + cancel_current_operation(app); } "quit" => { app.exit(0); @@ -149,6 +99,9 @@ pub fn run() { .build(app)?; app.manage(tray); + // Initialize tray menu with idle state + utils::update_tray_menu(&app.handle(), &utils::TrayIconState::Idle); + // Get the autostart manager let autostart_manager = app.autolaunch(); // Enable autostart @@ -196,6 +149,7 @@ pub fn run() { shortcut::change_audio_feedback_setting, shortcut::change_translate_to_english_setting, trigger_update_check, + commands::cancel_operation, commands::models::get_available_models, commands::models::get_model_info, commands::models::download_model, diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 89b6822..3e6e0a8 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -11,10 +11,10 @@ use std::fs::File; use std::io::BufReader; use std::thread; use tauri::image::Image; +use tauri::menu::{Menu, MenuItem, PredefinedMenuItem}; use tauri::tray::TrayIcon; use tauri::AppHandle; use tauri::Manager; -use tauri::menu::{Menu, MenuItem, PredefinedMenuItem}; use tauri_plugin_clipboard_manager::ClipboardExt; fn send_paste() -> Result<(), String> { @@ -100,63 +100,111 @@ pub fn change_tray_icon(app: &AppHandle, icon: TrayIconState) { update_tray_menu(app, &icon); } -pub fn update_tray_menu(app: &AppHandle, state: &TrayIconState) { - let version = env!("CARGO_PKG_VERSION"); - let version_label = format!("Handy v{}", version); +/// Centralized cancellation function that can be called from anywhere in the app. +/// Handles cancelling both recording and transcription operations and updates UI state. +pub fn cancel_current_operation(app: &AppHandle) { + use crate::actions::ACTION_MAP; + use crate::managers::audio::AudioRecordingManager; + use crate::ManagedToggleState; + use std::sync::Arc; + println!("Initiating operation cancellation..."); + + // First, reset all shortcut toggle states and call stop actions + // This is critical for non-push-to-talk mode where shortcuts toggle on/off + let toggle_state_manager = app.state::(); + if let Ok(mut states) = toggle_state_manager.lock() { + // For each currently active toggle, call its stop action and reset state + let active_bindings: Vec = states + .active_toggles + .iter() + .filter(|(_, &is_active)| is_active) + .map(|(binding_id, _)| binding_id.clone()) + .collect(); + + for binding_id in active_bindings { + println!("Stopping active action for binding: {}", binding_id); + + // Call the action's stop method to ensure proper cleanup + if let Some(action) = ACTION_MAP.get(&binding_id) { + action.stop(app, &binding_id, "cancelled"); + } + + // Reset the toggle state + if let Some(is_active) = states.active_toggles.get_mut(&binding_id) { + *is_active = false; + } + } + } else { + eprintln!("Warning: Failed to lock toggle state manager during cancellation"); + } + + // Cancel any ongoing recording + let audio_manager = app.state::>(); + audio_manager.cancel_recording(); + + // Update tray icon and menu to idle state + change_tray_icon(app, TrayIconState::Idle); + + println!("Operation cancellation completed - returned to idle state"); +} + +pub fn update_tray_menu(app: &AppHandle, state: &TrayIconState) { // Platform-specific accelerators #[cfg(target_os = "macos")] - let settings_accelerator = Some("Cmd+,"); + let (settings_accelerator, quit_accelerator) = (Some("Cmd+,"), Some("Cmd+Q")); #[cfg(not(target_os = "macos"))] - let settings_accelerator = Some("Ctrl+,"); + let (settings_accelerator, quit_accelerator) = (Some("Ctrl+,"), Some("Ctrl+Q")); - #[cfg(target_os = "macos")] - let quit_accelerator = Some("Cmd+Q"); - #[cfg(not(target_os = "macos"))] - let quit_accelerator = Some("Ctrl+Q"); - - let version_i = MenuItem::with_id(app, "version", &version_label, false, None::<&str>).expect("failed to create version item"); - let settings_i = MenuItem::with_id(app, "settings", "Settings...", true, settings_accelerator).expect("failed to create settings item"); - let check_updates_i = MenuItem::with_id(app, "check_updates", "Check for Updates...", true, None::<&str>).expect("failed to create check updates item"); - let quit_i = MenuItem::with_id(app, "quit", "Quit", true, quit_accelerator).expect("failed to create quit item"); + // Create common menu items + let version_label = format!("Handy v{}", env!("CARGO_PKG_VERSION")); + let version_i = MenuItem::with_id(app, "version", &version_label, false, None::<&str>) + .expect("failed to create version item"); + let settings_i = MenuItem::with_id(app, "settings", "Settings...", true, settings_accelerator) + .expect("failed to create settings item"); + let check_updates_i = MenuItem::with_id( + app, + "check_updates", + "Check for Updates...", + true, + None::<&str>, + ) + .expect("failed to create check updates item"); + let quit_i = MenuItem::with_id(app, "quit", "Quit", true, quit_accelerator) + .expect("failed to create quit item"); + let separator = || PredefinedMenuItem::separator(app).expect("failed to create separator"); let menu = match state { - TrayIconState::Recording => { - let cancel_i = MenuItem::with_id(app, "cancel_listening", "Cancel Listening", true, None::<&str>).expect("failed to create cancel listening item"); - Menu::with_items(app, &[ + TrayIconState::Recording | TrayIconState::Transcribing => { + let cancel_i = MenuItem::with_id(app, "cancel", "Cancel", true, None::<&str>) + .expect("failed to create cancel item"); + Menu::with_items( + app, + &[ + &version_i, + &separator(), + &cancel_i, + &separator(), + &settings_i, + &check_updates_i, + &separator(), + &quit_i, + ], + ) + .expect("failed to create menu") + } + TrayIconState::Idle => Menu::with_items( + app, + &[ &version_i, - &PredefinedMenuItem::separator(app).expect("failed to create separator"), - &cancel_i, - &PredefinedMenuItem::separator(app).expect("failed to create separator"), + &separator(), &settings_i, &check_updates_i, - &PredefinedMenuItem::separator(app).expect("failed to create separator"), + &separator(), &quit_i, - ]).expect("failed to create menu") - } - TrayIconState::Transcribing => { - let cancel_i = MenuItem::with_id(app, "cancel_transcribing", "Cancel Transcribing", true, None::<&str>).expect("failed to create cancel transcribing item"); - Menu::with_items(app, &[ - &version_i, - &PredefinedMenuItem::separator(app).expect("failed to create separator"), - &cancel_i, - &PredefinedMenuItem::separator(app).expect("failed to create separator"), - &settings_i, - &check_updates_i, - &PredefinedMenuItem::separator(app).expect("failed to create separator"), - &quit_i, - ]).expect("failed to create menu") - } - TrayIconState::Idle => { - Menu::with_items(app, &[ - &version_i, - &PredefinedMenuItem::separator(app).expect("failed to create separator"), - &settings_i, - &check_updates_i, - &PredefinedMenuItem::separator(app).expect("failed to create separator"), - &quit_i, - ]).expect("failed to create menu") - } + ], + ) + .expect("failed to create menu"), }; let tray = app.state::();