diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 332bf6e..966b700 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -71,3 +71,9 @@ tauri-plugin-autostart = "2" tauri-plugin-global-shortcut = "2" tauri-plugin-single-instance = "2.3.2" tauri-plugin-updater = "2" + +[profile.release] +lto = true +codegen-units = 1 +strip = true +panic = "abort" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 634a4cc..3ff518b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -49,10 +49,101 @@ fn show_main_window(app: &AppHandle) { } } } else { - eprintln!("Main window not found"); + eprintln!("Main window not found."); } } +fn initialize_core_logic(app_handle: &AppHandle) { + // First, initialize the managers + let recording_manager = Arc::new( + AudioRecordingManager::new(app_handle).expect("Failed to initialize recording manager"), + ); + let model_manager = + Arc::new(ModelManager::new(app_handle).expect("Failed to initialize model manager")); + let transcription_manager = Arc::new( + TranscriptionManager::new(app_handle, model_manager.clone()) + .expect("Failed to initialize transcription manager"), + ); + let history_manager = + Arc::new(HistoryManager::new(app_handle).expect("Failed to initialize history manager")); + + // Add managers to Tauri's managed state + app_handle.manage(recording_manager.clone()); + app_handle.manage(model_manager.clone()); + app_handle.manage(transcription_manager.clone()); + app_handle.manage(history_manager.clone()); + + // Initialize the shortcuts + shortcut::init_shortcuts(app_handle); + + // Apply macOS Accessory policy if starting hidden + #[cfg(target_os = "macos")] + { + let settings = settings::get_settings(app_handle); + if settings.start_hidden { + let _ = app_handle.set_activation_policy(tauri::ActivationPolicy::Accessory); + } + } + // Get the current theme to set the appropriate initial icon + let initial_theme = tray::get_current_theme(app_handle); + + // Choose the appropriate initial icon based on theme + let initial_icon_path = tray::get_icon_path(initial_theme, tray::TrayIconState::Idle); + + let tray = TrayIconBuilder::new() + .icon( + Image::from_path( + app_handle + .path() + .resolve(initial_icon_path, tauri::path::BaseDirectory::Resource) + .unwrap(), + ) + .unwrap(), + ) + .show_menu_on_left_click(true) + .icon_as_template(true) + .on_menu_event(|app, event| match event.id.as_ref() { + "settings" => { + show_main_window(app); + } + "check_updates" => { + show_main_window(app); + let _ = app.emit("check-for-updates", ()); + } + "cancel" => { + use crate::utils::cancel_current_operation; + + // Use centralized cancellation that handles all operations + cancel_current_operation(app); + } + "quit" => { + app.exit(0); + } + _ => {} + }) + .build(app_handle) + .unwrap(); + app_handle.manage(tray); + + // Initialize tray menu with idle state + utils::update_tray_menu(app_handle, &utils::TrayIconState::Idle); + + // Get the autostart manager and configure based on user setting + let autostart_manager = app_handle.autolaunch(); + let settings = settings::get_settings(&app_handle); + + if settings.autostart_enabled { + // Enable autostart if user has opted in + let _ = autostart_manager.enable(); + } else { + // Disable autostart if user has opted out + let _ = autostart_manager.disable(); + } + + // Create the recording overlay window (hidden by default) + utils::create_recording_overlay(app_handle); +} + #[tauri::command] fn trigger_update_check(app: AppHandle) -> Result<(), String> { app.emit("check-for-updates", ()) @@ -91,97 +182,18 @@ pub fn run() { )) .manage(Mutex::new(ShortcutToggleStates::default())) .setup(move |app| { - // Apply macOS Accessory policy early if starting hidden - #[cfg(target_os = "macos")] - { - let settings = settings::get_settings(&app.handle()); - if settings.start_hidden { - let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory); + let settings = settings::get_settings(&app.handle()); + let app_handle = app.handle().clone(); + + initialize_core_logic(&app_handle); + + // Show main window only if not starting hidden + if !settings.start_hidden { + if let Some(main_window) = app_handle.get_webview_window("main") { + main_window.show().unwrap(); + main_window.set_focus().unwrap(); } } - // Get the current theme to set the appropriate initial icon - let initial_theme = tray::get_current_theme(&app.handle()); - - // Choose the appropriate initial icon based on theme - let initial_icon_path = tray::get_icon_path(initial_theme, tray::TrayIconState::Idle); - - let tray = TrayIconBuilder::new() - .icon(Image::from_path(app.path().resolve( - initial_icon_path, - tauri::path::BaseDirectory::Resource, - )?)?) - .show_menu_on_left_click(true) - .icon_as_template(true) - .on_menu_event(|app, event| match event.id.as_ref() { - "settings" => { - show_main_window(app); - } - "check_updates" => { - show_main_window(app); - let _ = app.emit("check-for-updates", ()); - } - "cancel" => { - use crate::utils::cancel_current_operation; - - // Use centralized cancellation that handles all operations - cancel_current_operation(app); - } - "quit" => { - app.exit(0); - } - _ => {} - }) - .build(app)?; - app.manage(tray); - - // Initialize tray menu with idle state - utils::update_tray_menu(&app.handle(), &utils::TrayIconState::Idle); - - // Get the autostart manager and configure based on user setting - let autostart_manager = app.autolaunch(); - let settings = settings::get_settings(&app.handle()); - - if settings.autostart_enabled { - // Enable autostart if user has opted in - let _ = autostart_manager.enable(); - } else { - // Disable autostart if user has opted out - let _ = autostart_manager.disable(); - } - - // Window is configured to start hidden to avoid flicker. - // If user didn't choose Start Hidden, show it now. - let settings = settings::get_settings(&app.handle()); - if settings.start_hidden { - if let Some(main_window) = app.get_webview_window("main") { - let _ = main_window.hide(); - } - } else { - show_main_window(&app.handle()); - } - - let recording_manager = Arc::new( - AudioRecordingManager::new(app).expect("Failed to initialize recording manager"), - ); - let model_manager = - Arc::new(ModelManager::new(&app).expect("Failed to initialize model manager")); - let transcription_manager = Arc::new( - TranscriptionManager::new(&app, model_manager.clone()) - .expect("Failed to initialize transcription manager"), - ); - let history_manager = - Arc::new(HistoryManager::new(&app).expect("Failed to initialize history manager")); - - // Add managers to Tauri's managed state - app.manage(recording_manager.clone()); - app.manage(model_manager.clone()); - app.manage(transcription_manager.clone()); - app.manage(history_manager.clone()); - - // Create the recording overlay window (hidden by default) - utils::create_recording_overlay(&app.handle()); - - shortcut::init_shortcuts(app); Ok(()) }) diff --git a/src-tauri/src/managers/audio.rs b/src-tauri/src/managers/audio.rs index 19df778..4301798 100644 --- a/src-tauri/src/managers/audio.rs +++ b/src-tauri/src/managers/audio.rs @@ -4,7 +4,7 @@ use crate::utils; use log::{debug, info}; use std::sync::{Arc, Mutex}; use std::time::Instant; -use tauri::{App, Manager}; +use tauri::Manager; const WHISPER_SAMPLE_RATE: usize = 16000; @@ -63,8 +63,8 @@ pub struct AudioRecordingManager { impl AudioRecordingManager { /* ---------- construction ------------------------------------------------ */ - pub fn new(app: &App) -> Result { - let settings = get_settings(&app.handle()); + pub fn new(app: &tauri::AppHandle) -> Result { + let settings = get_settings(app); let mode = if settings.always_on_microphone { MicrophoneMode::AlwaysOn } else { @@ -74,7 +74,7 @@ impl AudioRecordingManager { let manager = Self { state: Arc::new(Mutex::new(RecordingState::Idle)), mode: Arc::new(Mutex::new(mode.clone())), - app_handle: app.handle().clone(), + app_handle: app.clone(), recorder: Arc::new(Mutex::new(None)), is_open: Arc::new(Mutex::new(false)), diff --git a/src-tauri/src/managers/history.rs b/src-tauri/src/managers/history.rs index 402da90..55e62fd 100644 --- a/src-tauri/src/managers/history.rs +++ b/src-tauri/src/managers/history.rs @@ -5,7 +5,7 @@ use rusqlite::{params, Connection, OptionalExtension}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; -use tauri::{App, AppHandle, Emitter, Manager}; +use tauri::{AppHandle, Emitter, Manager}; use tauri_plugin_sql::{Migration, MigrationKind}; use crate::audio_toolkit::save_wav_file; @@ -27,11 +27,9 @@ pub struct HistoryManager { } impl HistoryManager { - pub fn new(app: &App) -> Result { - let app_handle = app.app_handle().clone(); - + pub fn new(app_handle: &AppHandle) -> Result { // Create recordings directory in app data dir - let app_data_dir = app.path().app_data_dir()?; + let app_data_dir = app_handle.path().app_data_dir()?; let recordings_dir = app_data_dir.join("recordings"); let db_path = app_data_dir.join("history.db"); @@ -42,7 +40,7 @@ impl HistoryManager { } let manager = Self { - app_handle, + app_handle: app_handle.clone(), recordings_dir, db_path, }; diff --git a/src-tauri/src/managers/model.rs b/src-tauri/src/managers/model.rs index ffdfa80..ba0a188 100644 --- a/src-tauri/src/managers/model.rs +++ b/src-tauri/src/managers/model.rs @@ -10,7 +10,7 @@ use std::io::Write; use std::path::PathBuf; use std::sync::Mutex; use tar::Archive; -use tauri::{App, AppHandle, Emitter, Manager}; +use tauri::{AppHandle, Emitter, Manager}; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum EngineType { @@ -48,11 +48,9 @@ pub struct ModelManager { } impl ModelManager { - pub fn new(app: &App) -> Result { - let app_handle = app.app_handle().clone(); - + pub fn new(app_handle: &AppHandle) -> Result { // Create models directory in app data - let models_dir = app + let models_dir = app_handle .path() .app_data_dir() .map_err(|e| anyhow::anyhow!("Failed to get app data dir: {}", e))? @@ -152,7 +150,7 @@ impl ModelManager { ); let manager = Self { - app_handle, + app_handle: app_handle.clone(), models_dir, available_models: Mutex::new(available_models), }; diff --git a/src-tauri/src/managers/transcription.rs b/src-tauri/src/managers/transcription.rs index 397bde4..ca345f0 100644 --- a/src-tauri/src/managers/transcription.rs +++ b/src-tauri/src/managers/transcription.rs @@ -8,7 +8,7 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Condvar, Mutex}; use std::thread; use std::time::{Duration, SystemTime}; -use tauri::{App, AppHandle, Emitter, Manager}; +use tauri::{AppHandle, Emitter}; use transcribe_rs::{ engines::{ parakeet::{ @@ -46,9 +46,7 @@ pub struct TranscriptionManager { } impl TranscriptionManager { - pub fn new(app: &App, model_manager: Arc) -> Result { - let app_handle = app.app_handle().clone(); - + pub fn new(app_handle: &AppHandle, model_manager: Arc) -> Result { let manager = Self { engine: Arc::new(Mutex::new(None)), model_manager, @@ -105,7 +103,7 @@ impl TranscriptionManager { let _ = app_handle_cloned.emit( "model-state-changed", ModelStateEvent { - event_type: "unloaded_due_to_idle".to_string(), + event_type: "unloaded".to_string(), model_id: None, model_name: None, error: None, @@ -126,10 +124,6 @@ impl TranscriptionManager { *manager.watcher_handle.lock().unwrap() = Some(handle); } - // Try to load the default model from settings, but don't fail if no models are available - let settings = get_settings(&app_handle); - let _ = manager.load_model(&settings.selected_model); - Ok(manager) } @@ -161,7 +155,7 @@ impl TranscriptionManager { let _ = self.app_handle.emit( "model-state-changed", ModelStateEvent { - event_type: "unloaded_manually".to_string(), + event_type: "unloaded".to_string(), model_id: None, model_name: None, error: None, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index b7a4c88..934aad3 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use tauri::{App, AppHandle}; +use tauri::AppHandle; use tauri_plugin_store::StoreExt; #[derive(Serialize, Deserialize, Debug, Clone)] @@ -247,7 +247,7 @@ pub fn get_default_settings() -> AppSettings { } } -pub fn load_or_create_app_settings(app: &App) -> AppSettings { +pub fn load_or_create_app_settings(app: &AppHandle) -> AppSettings { // Initialize store let store = app .store(SETTINGS_STORE_PATH) diff --git a/src-tauri/src/shortcut.rs b/src-tauri/src/shortcut.rs index a5ce7d3..a0576a1 100644 --- a/src-tauri/src/shortcut.rs +++ b/src-tauri/src/shortcut.rs @@ -1,21 +1,19 @@ use serde::Serialize; -use tauri::{App, AppHandle, Emitter, Manager}; +use tauri::{AppHandle, Emitter, Manager}; use tauri_plugin_autostart::ManagerExt; -use tauri_plugin_global_shortcut::GlobalShortcutExt; -use tauri_plugin_global_shortcut::{Shortcut, ShortcutState}; +use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState}; use crate::actions::ACTION_MAP; use crate::settings::ShortcutBinding; use crate::settings::{self, get_settings, OverlayPosition, PasteMethod, SoundTheme}; use crate::ManagedToggleState; -pub fn init_shortcuts(app: &App) { +pub fn init_shortcuts(app: &AppHandle) { let settings = settings::load_or_create_app_settings(app); // Register shortcuts with the bindings from settings for (_id, binding) in settings.bindings { - // Pass app.handle() which is &AppHandle - if let Err(e) = _register_shortcut(app.handle(), binding) { + if let Err(e) = _register_shortcut(app, binding) { eprintln!("Failed to register shortcut {} during init: {}", _id, e); } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 4bcc12b..30a45f8 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -13,13 +13,15 @@ "macOSPrivateApi": true, "windows": [ { + "label": "main", "title": "Handy", "width": 680, "height": 510, "minWidth": 680, "minHeight": 510, "resizable": true, - "maximizable": false + "maximizable": false, + "visible": false } ], "security": { diff --git a/src/components/model-selector/ModelSelector.tsx b/src/components/model-selector/ModelSelector.tsx index 88a7b71..472aa98 100644 --- a/src/components/model-selector/ModelSelector.tsx +++ b/src/components/model-selector/ModelSelector.tsx @@ -20,7 +20,14 @@ interface DownloadProgress { percentage: number; } -type ModelStatus = "ready" | "loading" | "downloading" | "extracting" | "error" | "none"; +type ModelStatus = + | "ready" + | "loading" + | "downloading" + | "extracting" + | "error" + | "unloaded" + | "none"; interface DownloadStats { startTime: number; @@ -36,7 +43,7 @@ interface ModelSelectorProps { const ModelSelector: React.FC = ({ onError }) => { const [models, setModels] = useState([]); const [currentModelId, setCurrentModelId] = useState(""); - const [modelStatus, setModelStatus] = useState("loading"); + const [modelStatus, setModelStatus] = useState("unloaded"); const [modelError, setModelError] = useState(null); const [modelDownloadProgress, setModelDownloadProgress] = useState< Map @@ -75,6 +82,10 @@ const ModelSelector: React.FC = ({ onError }) => { setModelStatus("error"); setModelError(error || "Failed to load model"); break; + case "unloaded": + setModelStatus("unloaded"); + setModelError(null); + break; } }, ); @@ -247,7 +258,7 @@ const ModelSelector: React.FC = ({ onError }) => { if (transcriptionStatus === current) { setModelStatus("ready"); } else { - setModelStatus("loading"); + setModelStatus("unloaded"); } } else { setModelStatus("none"); @@ -324,10 +335,12 @@ const ModelSelector: React.FC = ({ onError }) => { return currentModel ? `Extracting ${currentModel.name}...` : "Extracting..."; case "error": return modelError || "Model Error"; + case "unloaded": + return currentModel?.name || "Model Unloaded"; case "none": return "No Model - Download Required"; default: - return currentModel?.name || "Select Model"; + return currentModel?.name || "Model Unloaded"; } }; diff --git a/src/components/model-selector/ModelStatusButton.tsx b/src/components/model-selector/ModelStatusButton.tsx index 0f40f7e..28e2edf 100644 --- a/src/components/model-selector/ModelStatusButton.tsx +++ b/src/components/model-selector/ModelStatusButton.tsx @@ -6,6 +6,7 @@ type ModelStatus = | "downloading" | "extracting" | "error" + | "unloaded" | "none"; interface ModelStatusButtonProps { @@ -35,6 +36,8 @@ const ModelStatusButton: React.FC = ({ return "bg-orange-400 animate-pulse"; case "error": return "bg-red-400"; + case "unloaded": + return "bg-mid-gray/60"; case "none": return "bg-red-400"; default: