A few optimizations on startup (#182)

* Use an optimal release profile... load managers & hotkeys first, add a
splash, simplify app handler

* removed splashscreen, hopefully app boots fast enough anyhow

* No need to load transcription model on start now that its loaded on
demand & cleanup

* Make an unloaded model status

* tiny modification to unloading

* update comment

---------

Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
Jackson 2025-10-18 08:05:43 +09:00 committed by GitHub
parent a286fe5143
commit 93215dc097
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 153 additions and 129 deletions

View file

@ -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"

View file

@ -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(())
})

View file

@ -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<Self, anyhow::Error> {
let settings = get_settings(&app.handle());
pub fn new(app: &tauri::AppHandle) -> Result<Self, anyhow::Error> {
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)),

View file

@ -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<Self> {
let app_handle = app.app_handle().clone();
pub fn new(app_handle: &AppHandle) -> Result<Self> {
// 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,
};

View file

@ -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<Self> {
let app_handle = app.app_handle().clone();
pub fn new(app_handle: &AppHandle) -> Result<Self> {
// 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),
};

View file

@ -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<ModelManager>) -> Result<Self> {
let app_handle = app.app_handle().clone();
pub fn new(app_handle: &AppHandle, model_manager: Arc<ModelManager>) -> Result<Self> {
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,

View file

@ -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)

View file

@ -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);
}
}

View file

@ -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": {

View file

@ -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<ModelSelectorProps> = ({ onError }) => {
const [models, setModels] = useState<ModelInfo[]>([]);
const [currentModelId, setCurrentModelId] = useState<string>("");
const [modelStatus, setModelStatus] = useState<ModelStatus>("loading");
const [modelStatus, setModelStatus] = useState<ModelStatus>("unloaded");
const [modelError, setModelError] = useState<string | null>(null);
const [modelDownloadProgress, setModelDownloadProgress] = useState<
Map<string, DownloadProgress>
@ -75,6 +82,10 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ 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<ModelSelectorProps> = ({ onError }) => {
if (transcriptionStatus === current) {
setModelStatus("ready");
} else {
setModelStatus("loading");
setModelStatus("unloaded");
}
} else {
setModelStatus("none");
@ -324,10 +335,12 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ 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";
}
};

View file

@ -6,6 +6,7 @@ type ModelStatus =
| "downloading"
| "extracting"
| "error"
| "unloaded"
| "none";
interface ModelStatusButtonProps {
@ -35,6 +36,8 @@ const ModelStatusButton: React.FC<ModelStatusButtonProps> = ({
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: