Handy/src-tauri/src/utils.rs

312 lines
10 KiB
Rust

use crate::settings;
use enigo::Enigo;
use enigo::Key;
use enigo::Keyboard;
use enigo::Settings;
use cpal::traits::{DeviceTrait, HostTrait};
use rodio::OutputStreamBuilder;
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_plugin_clipboard_manager::ClipboardExt;
fn send_paste() -> Result<(), String> {
// Determine the modifier key based on the OS
#[cfg(target_os = "macos")]
let modifier_key = Key::Meta; // Command key on macOS
#[cfg(not(target_os = "macos"))]
let modifier_key = Key::Control; // Control key on other systems
let mut enigo = Enigo::new(&Settings::default())
.map_err(|e| format!("Failed to initialize Enigo: {}", e))?;
// Press both keys
enigo
.key(modifier_key, enigo::Direction::Press)
.map_err(|e| format!("Failed to press modifier key: {}", e))?;
enigo
.key(Key::Unicode('v'), enigo::Direction::Press)
.map_err(|e| format!("Failed to press V key: {}", e))?;
// Release both keys
enigo
.key(Key::Unicode('v'), enigo::Direction::Release)
.map_err(|e| format!("Failed to release V key: {}", e))?;
enigo
.key(modifier_key, enigo::Direction::Release)
.map_err(|e| format!("Failed to release modifier key: {}", e))?;
Ok(())
}
pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
let clipboard = app_handle.clipboard();
// get the current clipboard content
let clipboard_content = clipboard.read_text().unwrap_or_default();
clipboard
.write_text(&text)
.map_err(|e| format!("Failed to write to clipboard: {}", e))?;
// small delay to ensure the clipboard content has been written to
std::thread::sleep(std::time::Duration::from_millis(50));
send_paste()?;
std::thread::sleep(std::time::Duration::from_millis(50));
// restore the clipboard
clipboard
.write_text(&clipboard_content)
.map_err(|e| format!("Failed to restore clipboard: {}", e))?;
Ok(())
}
#[derive(Clone, Debug, PartialEq)]
pub enum TrayIconState {
Idle,
Recording,
Transcribing,
}
pub fn change_tray_icon(app: &AppHandle, icon: TrayIconState) {
let tray = app.state::<TrayIcon>();
let icon_path = match icon {
TrayIconState::Idle => "resources/tray_idle.png",
TrayIconState::Recording => "resources/tray_recording.png",
TrayIconState::Transcribing => "resources/tray_transcribing.png",
};
let _ = tray.set_icon(Some(
Image::from_path(
app.path()
.resolve(icon_path, tauri::path::BaseDirectory::Resource)
.expect("failed to resolve"),
)
.expect("failed to set icon"),
));
// Update menu based on state
update_tray_menu(app, &icon);
}
/// 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::<ManagedToggleState>();
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<String> = 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::<Arc<AudioRecordingManager>>();
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, quit_accelerator) = (Some("Cmd+,"), Some("Cmd+Q"));
#[cfg(not(target_os = "macos"))]
let (settings_accelerator, quit_accelerator) = (Some("Ctrl+,"), Some("Ctrl+Q"));
// 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 | 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,
&separator(),
&settings_i,
&check_updates_i,
&separator(),
&quit_i,
],
)
.expect("failed to create menu"),
};
let tray = app.state::<TrayIcon>();
let _ = tray.set_menu(Some(menu));
let _ = tray.set_icon_as_template(true);
}
/// Plays an audio resource from the resources directory.
/// Checks if audio feedback is enabled in settings before playing.
pub fn play_sound(app: &AppHandle, resource_path: &str) {
// Check if audio feedback is enabled
let settings = settings::get_settings(app);
if !settings.audio_feedback {
return;
}
let app_handle = app.clone();
let resource_path = resource_path.to_string();
// Spawn a new thread to play the audio without blocking the main thread
thread::spawn(move || {
// Get the path to the audio file in resources
let audio_path = match app_handle
.path()
.resolve(&resource_path, tauri::path::BaseDirectory::Resource)
{
Ok(path) => path,
Err(e) => {
eprintln!(
"Failed to resolve audio file path '{}': {}",
resource_path, e
);
return;
}
};
// Get the selected output device from settings
let settings = settings::get_settings(&app_handle);
let selected_device = settings.selected_output_device.clone();
// Try to play the audio file
if let Err(e) = play_audio_file(&audio_path, selected_device) {
eprintln!("Failed to play sound '{}': {}", resource_path, e);
}
});
}
/// Convenience function to play the recording start sound
pub fn play_recording_start_sound(app: &AppHandle) {
play_sound(app, "resources/rec_start.wav");
}
/// Convenience function to play the recording stop sound
pub fn play_recording_stop_sound(app: &AppHandle) {
play_sound(app, "resources/rec_stop.wav");
}
fn play_audio_file(
path: &std::path::Path,
selected_device: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let stream_builder = if let Some(device_name) = selected_device {
if device_name == "Default" {
println!("Using default device");
// Use default device
OutputStreamBuilder::from_default_device()?
} else {
// Try to find the device by name
let host = cpal::default_host();
let devices = host.output_devices()?;
let mut found_device = None;
for device in devices {
if device.name()? == device_name {
found_device = Some(device);
break;
}
}
match found_device {
Some(device) => OutputStreamBuilder::from_device(device)?,
None => {
eprintln!("Device '{}' not found, using default device", device_name);
OutputStreamBuilder::from_default_device()?
}
}
}
} else {
println!("Using default device");
// Use default device
OutputStreamBuilder::from_default_device()?
};
let stream_handle = stream_builder.open_stream()?;
let mixer = stream_handle.mixer();
// Load the audio file
let file = File::open(path)?;
let buf_reader = BufReader::new(file);
let sink = rodio::play(mixer, buf_reader)?;
sink.sleep_until_end();
Ok(())
}