diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6897a89..5e6a5a6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -122,6 +122,25 @@ 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); + } "quit" => { app.exit(0); } diff --git a/src-tauri/src/managers/audio.rs b/src-tauri/src/managers/audio.rs index e5c8994..0988aff 100644 --- a/src-tauri/src/managers/audio.rs +++ b/src-tauri/src/managers/audio.rs @@ -258,4 +258,25 @@ impl AudioRecordingManager { _ => None, } } + + /// Cancel any ongoing recording without returning audio samples + pub fn cancel_recording(&self) { + let mut state = self.state.lock().unwrap(); + + if let RecordingState::Recording { .. } = *state { + *state = RecordingState::Idle; + drop(state); + + if let Some(rec) = self.recorder.lock().unwrap().as_ref() { + let _ = rec.stop(); // Discard the result + } + + *self.is_recording.lock().unwrap() = false; + + // In on-demand mode turn the mic off again + if matches!(*self.mode.lock().unwrap(), MicrophoneMode::OnDemand) { + self.stop_microphone_stream(); + } + } + } } diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index be67035..89b6822 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -14,6 +14,7 @@ use tauri::image::Image; 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> { @@ -70,6 +71,7 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> { Ok(()) } +#[derive(Clone, Debug, PartialEq)] pub enum TrayIconState { Idle, Recording, @@ -93,6 +95,72 @@ pub fn change_tray_icon(app: &AppHandle, icon: TrayIconState) { ) .expect("failed to set icon"), )); + + // Update menu based on state + 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); + + // 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 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 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, &[ + &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::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") + } + }; + + let tray = app.state::(); + let _ = tray.set_menu(Some(menu)); } /// Plays an audio resource from the resources directory.