Fixes #1184. The tray icon was created and refreshed without setting a tooltip, which caused Windows to show an empty hover tooltip. This change sets the tooltip when building the tray icon and refreshes it during tray menu updates using the existing version label.
604 lines
24 KiB
Rust
604 lines
24 KiB
Rust
mod actions;
|
|
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
|
mod apple_intelligence;
|
|
mod audio_feedback;
|
|
pub mod audio_toolkit;
|
|
pub mod cli;
|
|
mod clipboard;
|
|
mod commands;
|
|
mod helpers;
|
|
mod input;
|
|
mod llm_client;
|
|
mod managers;
|
|
mod overlay;
|
|
pub mod portable;
|
|
mod settings;
|
|
mod shortcut;
|
|
mod signal_handle;
|
|
mod transcription_coordinator;
|
|
mod tray;
|
|
mod tray_i18n;
|
|
mod utils;
|
|
|
|
pub use cli::CliArgs;
|
|
#[cfg(debug_assertions)]
|
|
use specta_typescript::{BigIntExportBehavior, Typescript};
|
|
use tauri_specta::{collect_commands, collect_events, Builder};
|
|
|
|
use env_filter::Builder as EnvFilterBuilder;
|
|
use managers::audio::AudioRecordingManager;
|
|
use managers::history::HistoryManager;
|
|
use managers::model::ModelManager;
|
|
use managers::transcription::TranscriptionManager;
|
|
#[cfg(unix)]
|
|
use signal_hook::consts::{SIGUSR1, SIGUSR2};
|
|
#[cfg(unix)]
|
|
use signal_hook::iterator::Signals;
|
|
use std::sync::atomic::{AtomicU8, Ordering};
|
|
use std::sync::Arc;
|
|
use tauri::image::Image;
|
|
pub use transcription_coordinator::TranscriptionCoordinator;
|
|
|
|
use tauri::tray::TrayIconBuilder;
|
|
use tauri::{AppHandle, Emitter, Listener, Manager};
|
|
use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
|
|
use tauri_plugin_log::{Builder as LogBuilder, RotationStrategy, Target, TargetKind};
|
|
|
|
use crate::settings::get_settings;
|
|
|
|
// Global atomic to store the file log level filter
|
|
// We use u8 to store the log::LevelFilter as a number
|
|
pub static FILE_LOG_LEVEL: AtomicU8 = AtomicU8::new(log::LevelFilter::Debug as u8);
|
|
|
|
fn level_filter_from_u8(value: u8) -> log::LevelFilter {
|
|
match value {
|
|
0 => log::LevelFilter::Off,
|
|
1 => log::LevelFilter::Error,
|
|
2 => log::LevelFilter::Warn,
|
|
3 => log::LevelFilter::Info,
|
|
4 => log::LevelFilter::Debug,
|
|
5 => log::LevelFilter::Trace,
|
|
_ => log::LevelFilter::Trace,
|
|
}
|
|
}
|
|
|
|
fn build_console_filter() -> env_filter::Filter {
|
|
let mut builder = EnvFilterBuilder::new();
|
|
|
|
match std::env::var("RUST_LOG") {
|
|
Ok(spec) if !spec.trim().is_empty() => {
|
|
if let Err(err) = builder.try_parse(&spec) {
|
|
log::warn!(
|
|
"Ignoring invalid RUST_LOG value '{}': {}. Falling back to info-level console logging",
|
|
spec,
|
|
err
|
|
);
|
|
builder.filter_level(log::LevelFilter::Info);
|
|
}
|
|
}
|
|
_ => {
|
|
builder.filter_level(log::LevelFilter::Info);
|
|
}
|
|
}
|
|
|
|
builder.build()
|
|
}
|
|
|
|
fn show_main_window(app: &AppHandle) {
|
|
if let Some(main_window) = app.get_webview_window("main") {
|
|
if let Err(e) = main_window.unminimize() {
|
|
log::error!("Failed to unminimize webview window: {}", e);
|
|
}
|
|
if let Err(e) = main_window.show() {
|
|
log::error!("Failed to show webview window: {}", e);
|
|
}
|
|
if let Err(e) = main_window.set_focus() {
|
|
log::error!("Failed to focus webview window: {}", e);
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
if let Err(e) = app.set_activation_policy(tauri::ActivationPolicy::Regular) {
|
|
log::error!("Failed to set activation policy to Regular: {}", e);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
let webview_labels = app.webview_windows().keys().cloned().collect::<Vec<_>>();
|
|
log::error!(
|
|
"Main window not found. Webview labels: {:?}",
|
|
webview_labels
|
|
);
|
|
}
|
|
|
|
#[allow(unused_variables)]
|
|
fn should_force_show_permissions_window(app: &AppHandle) -> bool {
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
let model_manager = app.state::<Arc<ModelManager>>();
|
|
let has_downloaded_models = model_manager
|
|
.get_available_models()
|
|
.iter()
|
|
.any(|model| model.is_downloaded);
|
|
|
|
if !has_downloaded_models {
|
|
return false;
|
|
}
|
|
|
|
let status = commands::audio::get_windows_microphone_permission_status();
|
|
if status.supported && status.overall_access == commands::audio::PermissionAccess::Denied {
|
|
log::info!(
|
|
"Windows microphone permissions are denied; forcing main window visible for onboarding"
|
|
);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
fn initialize_core_logic(app_handle: &AppHandle) {
|
|
// Note: Enigo (keyboard/mouse simulation) is NOT initialized here.
|
|
// The frontend is responsible for calling the `initialize_enigo` command
|
|
// after onboarding completes. This avoids triggering permission dialogs
|
|
// on macOS before the user is ready.
|
|
|
|
// 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"));
|
|
|
|
// Apply accelerator preferences before any model loads
|
|
managers::transcription::apply_accelerator_settings(app_handle);
|
|
|
|
// 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());
|
|
|
|
// Note: Shortcuts are NOT initialized here.
|
|
// The frontend is responsible for calling the `initialize_shortcuts` command
|
|
// after permissions are confirmed (on macOS) or after onboarding completes.
|
|
// This matches the pattern used for Enigo initialization.
|
|
|
|
#[cfg(unix)]
|
|
let signals = Signals::new(&[SIGUSR1, SIGUSR2]).unwrap();
|
|
// Set up signal handlers for toggling transcription
|
|
#[cfg(unix)]
|
|
signal_handle::setup_signal_handler(app_handle.clone(), signals);
|
|
|
|
// Apply macOS Accessory policy if starting hidden and tray is available.
|
|
// If the tray icon is disabled, keep the dock icon so the user can reopen.
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let settings = settings::get_settings(app_handle);
|
|
if settings.start_hidden && settings.show_tray_icon {
|
|
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(),
|
|
)
|
|
.tooltip(tray::tray_tooltip())
|
|
.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" => {
|
|
let settings = settings::get_settings(app);
|
|
if settings.update_checks_enabled {
|
|
show_main_window(app);
|
|
let _ = app.emit("check-for-updates", ());
|
|
}
|
|
}
|
|
"copy_last_transcript" => {
|
|
tray::copy_last_transcript(app);
|
|
}
|
|
"unload_model" => {
|
|
let transcription_manager = app.state::<Arc<TranscriptionManager>>();
|
|
if !transcription_manager.is_model_loaded() {
|
|
log::warn!("No model is currently loaded.");
|
|
return;
|
|
}
|
|
match transcription_manager.unload_model() {
|
|
Ok(()) => log::info!("Model unloaded via tray."),
|
|
Err(e) => log::error!("Failed to unload model via tray: {}", e),
|
|
}
|
|
}
|
|
"cancel" => {
|
|
use crate::utils::cancel_current_operation;
|
|
|
|
// Use centralized cancellation that handles all operations
|
|
cancel_current_operation(app);
|
|
}
|
|
"quit" => {
|
|
app.exit(0);
|
|
}
|
|
id if id.starts_with("model_select:") => {
|
|
let model_id = id.strip_prefix("model_select:").unwrap().to_string();
|
|
let current_model = settings::get_settings(app).selected_model;
|
|
if model_id == current_model {
|
|
return;
|
|
}
|
|
let app_clone = app.clone();
|
|
std::thread::spawn(move || {
|
|
match commands::models::switch_active_model(&app_clone, &model_id) {
|
|
Ok(()) => {
|
|
log::info!("Model switched to {} via tray.", model_id);
|
|
}
|
|
Err(e) => {
|
|
log::error!("Failed to switch model via tray: {}", e);
|
|
}
|
|
}
|
|
tray::update_tray_menu(&app_clone, &tray::TrayIconState::Idle, None);
|
|
});
|
|
}
|
|
_ => {}
|
|
})
|
|
.build(app_handle)
|
|
.unwrap();
|
|
app_handle.manage(tray);
|
|
|
|
// Initialize tray menu with idle state
|
|
utils::update_tray_menu(app_handle, &utils::TrayIconState::Idle, None);
|
|
|
|
// Apply show_tray_icon setting
|
|
let settings = settings::get_settings(app_handle);
|
|
if !settings.show_tray_icon {
|
|
tray::set_tray_visibility(app_handle, false);
|
|
}
|
|
|
|
// Refresh tray menu when model state changes
|
|
let app_handle_for_listener = app_handle.clone();
|
|
app_handle.listen("model-state-changed", move |_| {
|
|
tray::update_tray_menu(&app_handle_for_listener, &tray::TrayIconState::Idle, None);
|
|
});
|
|
|
|
// 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]
|
|
#[specta::specta]
|
|
fn trigger_update_check(app: AppHandle) -> Result<(), String> {
|
|
let settings = settings::get_settings(&app);
|
|
if !settings.update_checks_enabled {
|
|
return Ok(());
|
|
}
|
|
app.emit("check-for-updates", ())
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
fn show_main_window_command(app: AppHandle) -> Result<(), String> {
|
|
show_main_window(&app);
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run(cli_args: CliArgs) {
|
|
// Detect portable mode before anything else
|
|
portable::init();
|
|
|
|
// Parse console logging directives from RUST_LOG, falling back to info-level logging
|
|
// when the variable is unset
|
|
let console_filter = build_console_filter();
|
|
|
|
let specta_builder = Builder::<tauri::Wry>::new()
|
|
.commands(collect_commands![
|
|
shortcut::change_binding,
|
|
shortcut::reset_binding,
|
|
shortcut::change_ptt_setting,
|
|
shortcut::change_audio_feedback_setting,
|
|
shortcut::change_audio_feedback_volume_setting,
|
|
shortcut::change_sound_theme_setting,
|
|
shortcut::change_start_hidden_setting,
|
|
shortcut::change_autostart_setting,
|
|
shortcut::change_translate_to_english_setting,
|
|
shortcut::change_selected_language_setting,
|
|
shortcut::change_overlay_position_setting,
|
|
shortcut::change_debug_mode_setting,
|
|
shortcut::change_word_correction_threshold_setting,
|
|
shortcut::change_extra_recording_buffer_setting,
|
|
shortcut::change_paste_delay_ms_setting,
|
|
shortcut::change_paste_method_setting,
|
|
shortcut::get_available_typing_tools,
|
|
shortcut::change_typing_tool_setting,
|
|
shortcut::change_external_script_path_setting,
|
|
shortcut::change_clipboard_handling_setting,
|
|
shortcut::change_auto_submit_setting,
|
|
shortcut::change_auto_submit_key_setting,
|
|
shortcut::change_post_process_enabled_setting,
|
|
shortcut::change_experimental_enabled_setting,
|
|
shortcut::change_post_process_base_url_setting,
|
|
shortcut::change_post_process_api_key_setting,
|
|
shortcut::change_post_process_model_setting,
|
|
shortcut::set_post_process_provider,
|
|
shortcut::fetch_post_process_models,
|
|
shortcut::add_post_process_prompt,
|
|
shortcut::update_post_process_prompt,
|
|
shortcut::delete_post_process_prompt,
|
|
shortcut::set_post_process_selected_prompt,
|
|
shortcut::update_custom_words,
|
|
shortcut::suspend_binding,
|
|
shortcut::resume_binding,
|
|
shortcut::change_mute_while_recording_setting,
|
|
shortcut::change_append_trailing_space_setting,
|
|
shortcut::change_lazy_stream_close_setting,
|
|
shortcut::change_app_language_setting,
|
|
shortcut::change_update_checks_setting,
|
|
shortcut::change_keyboard_implementation_setting,
|
|
shortcut::get_keyboard_implementation,
|
|
shortcut::change_show_tray_icon_setting,
|
|
shortcut::change_whisper_accelerator_setting,
|
|
shortcut::change_ort_accelerator_setting,
|
|
shortcut::change_whisper_gpu_device,
|
|
shortcut::get_available_accelerators,
|
|
shortcut::handy_keys::start_handy_keys_recording,
|
|
shortcut::handy_keys::stop_handy_keys_recording,
|
|
trigger_update_check,
|
|
show_main_window_command,
|
|
commands::cancel_operation,
|
|
commands::is_portable,
|
|
commands::get_app_dir_path,
|
|
commands::get_app_settings,
|
|
commands::get_default_settings,
|
|
commands::get_log_dir_path,
|
|
commands::set_log_level,
|
|
commands::open_recordings_folder,
|
|
commands::open_log_dir,
|
|
commands::open_app_data_dir,
|
|
commands::check_apple_intelligence_available,
|
|
commands::initialize_enigo,
|
|
commands::initialize_shortcuts,
|
|
commands::models::get_available_models,
|
|
commands::models::get_model_info,
|
|
commands::models::download_model,
|
|
commands::models::delete_model,
|
|
commands::models::cancel_download,
|
|
commands::models::set_active_model,
|
|
commands::models::get_current_model,
|
|
commands::models::get_transcription_model_status,
|
|
commands::models::is_model_loading,
|
|
commands::models::has_any_models_available,
|
|
commands::models::has_any_models_or_downloads,
|
|
commands::audio::update_microphone_mode,
|
|
commands::audio::get_microphone_mode,
|
|
commands::audio::get_windows_microphone_permission_status,
|
|
commands::audio::open_microphone_privacy_settings,
|
|
commands::audio::get_available_microphones,
|
|
commands::audio::set_selected_microphone,
|
|
commands::audio::get_selected_microphone,
|
|
commands::audio::get_available_output_devices,
|
|
commands::audio::set_selected_output_device,
|
|
commands::audio::get_selected_output_device,
|
|
commands::audio::play_test_sound,
|
|
commands::audio::check_custom_sounds,
|
|
commands::audio::set_clamshell_microphone,
|
|
commands::audio::get_clamshell_microphone,
|
|
commands::audio::is_recording,
|
|
commands::transcription::set_model_unload_timeout,
|
|
commands::transcription::get_model_load_status,
|
|
commands::transcription::unload_model_manually,
|
|
commands::history::get_history_entries,
|
|
commands::history::toggle_history_entry_saved,
|
|
commands::history::get_audio_file_path,
|
|
commands::history::delete_history_entry,
|
|
commands::history::retry_history_entry_transcription,
|
|
commands::history::update_history_limit,
|
|
commands::history::update_recording_retention_period,
|
|
helpers::clamshell::is_laptop,
|
|
])
|
|
.events(collect_events![managers::history::HistoryUpdatePayload,]);
|
|
|
|
#[cfg(debug_assertions)] // <- Only export on non-release builds
|
|
specta_builder
|
|
.export(
|
|
Typescript::default().bigint(BigIntExportBehavior::Number),
|
|
"../src/bindings.ts",
|
|
)
|
|
.expect("Failed to export typescript bindings");
|
|
|
|
let invoke_handler = specta_builder.invoke_handler();
|
|
|
|
#[allow(unused_mut)]
|
|
let mut builder = tauri::Builder::default()
|
|
.device_event_filter(tauri::DeviceEventFilter::Always)
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.plugin(
|
|
LogBuilder::new()
|
|
.level(log::LevelFilter::Trace) // Set to most verbose level globally
|
|
.max_file_size(500_000)
|
|
.rotation_strategy(RotationStrategy::KeepOne)
|
|
.clear_targets()
|
|
.targets([
|
|
// Console output respects RUST_LOG environment variable
|
|
Target::new(TargetKind::Stdout).filter({
|
|
let console_filter = console_filter.clone();
|
|
move |metadata| console_filter.enabled(metadata)
|
|
}),
|
|
// File logs respect the user's settings (stored in FILE_LOG_LEVEL atomic)
|
|
Target::new(if let Some(data_dir) = portable::data_dir() {
|
|
TargetKind::Folder {
|
|
path: data_dir.join("logs"),
|
|
file_name: Some("handy".into()),
|
|
}
|
|
} else {
|
|
TargetKind::LogDir {
|
|
file_name: Some("handy".into()),
|
|
}
|
|
})
|
|
.filter(|metadata| {
|
|
let file_level = FILE_LOG_LEVEL.load(Ordering::Relaxed);
|
|
metadata.level() <= level_filter_from_u8(file_level)
|
|
}),
|
|
])
|
|
.build(),
|
|
);
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
builder = builder.plugin(tauri_nspanel::init());
|
|
}
|
|
|
|
builder
|
|
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
|
if args.iter().any(|a| a == "--toggle-transcription") {
|
|
signal_handle::send_transcription_input(app, "transcribe", "CLI");
|
|
} else if args.iter().any(|a| a == "--toggle-post-process") {
|
|
signal_handle::send_transcription_input(app, "transcribe_with_post_process", "CLI");
|
|
} else if args.iter().any(|a| a == "--cancel") {
|
|
crate::utils::cancel_current_operation(app);
|
|
} else {
|
|
show_main_window(app);
|
|
}
|
|
}))
|
|
.plugin(tauri_plugin_fs::init())
|
|
.plugin(tauri_plugin_process::init())
|
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
|
.plugin(tauri_plugin_os::init())
|
|
.plugin(tauri_plugin_clipboard_manager::init())
|
|
.plugin(tauri_plugin_macos_permissions::init())
|
|
.plugin(tauri_plugin_opener::init())
|
|
.plugin(tauri_plugin_store::Builder::default().build())
|
|
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
|
.plugin(tauri_plugin_autostart::init(
|
|
MacosLauncher::LaunchAgent,
|
|
Some(vec![]),
|
|
))
|
|
.manage(cli_args.clone())
|
|
.setup(move |app| {
|
|
specta_builder.mount_events(app);
|
|
|
|
// Create main window programmatically so we can set data_directory
|
|
// for portable mode (redirects WebView2 cache to portable Data dir)
|
|
let mut win_builder =
|
|
tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("/".into()))
|
|
.title("Handy")
|
|
.inner_size(680.0, 570.0)
|
|
.min_inner_size(680.0, 570.0)
|
|
.resizable(true)
|
|
.maximizable(false)
|
|
.visible(false);
|
|
|
|
if let Some(data_dir) = portable::data_dir() {
|
|
win_builder = win_builder.data_directory(data_dir.join("webview"));
|
|
}
|
|
|
|
win_builder.build()?;
|
|
|
|
let mut settings = get_settings(&app.handle());
|
|
|
|
// CLI --debug flag overrides debug_mode and log level (runtime-only, not persisted)
|
|
if cli_args.debug {
|
|
settings.debug_mode = true;
|
|
settings.log_level = settings::LogLevel::Trace;
|
|
}
|
|
|
|
let tauri_log_level: tauri_plugin_log::LogLevel = settings.log_level.into();
|
|
let file_log_level: log::Level = tauri_log_level.into();
|
|
// Store the file log level in the atomic for the filter to use
|
|
FILE_LOG_LEVEL.store(file_log_level.to_level_filter() as u8, Ordering::Relaxed);
|
|
let app_handle = app.handle().clone();
|
|
app.manage(TranscriptionCoordinator::new(app_handle.clone()));
|
|
|
|
initialize_core_logic(&app_handle);
|
|
|
|
// Hide tray icon if --no-tray was passed
|
|
if cli_args.no_tray {
|
|
tray::set_tray_visibility(&app_handle, false);
|
|
}
|
|
|
|
// Show main window only if not starting hidden.
|
|
// CLI --start-hidden flag overrides the setting.
|
|
// But if permission onboarding is required, always show the window.
|
|
let should_hide = settings.start_hidden || cli_args.start_hidden;
|
|
let should_force_show = should_force_show_permissions_window(&app_handle);
|
|
|
|
// If start_hidden but tray is disabled, we must show the window
|
|
// anyway. Without a tray icon, the dock is the only way back in.
|
|
let tray_available = settings.show_tray_icon && !cli_args.no_tray;
|
|
if should_force_show || !should_hide || !tray_available {
|
|
show_main_window(&app_handle);
|
|
}
|
|
|
|
Ok(())
|
|
})
|
|
.on_window_event(|window, event| match event {
|
|
tauri::WindowEvent::CloseRequested { api, .. } => {
|
|
api.prevent_close();
|
|
let _res = window.hide();
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let settings = get_settings(&window.app_handle());
|
|
let tray_visible =
|
|
settings.show_tray_icon && !window.app_handle().state::<CliArgs>().no_tray;
|
|
if tray_visible {
|
|
// Tray is available: hide the dock icon, app lives in the tray
|
|
let res = window
|
|
.app_handle()
|
|
.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
|
if let Err(e) = res {
|
|
log::error!("Failed to set activation policy: {}", e);
|
|
}
|
|
}
|
|
// No tray: keep the dock icon visible so the user can reopen
|
|
}
|
|
}
|
|
tauri::WindowEvent::ThemeChanged(theme) => {
|
|
log::info!("Theme changed to: {:?}", theme);
|
|
// Update tray icon to match new theme, maintaining idle state
|
|
utils::change_tray_icon(&window.app_handle(), utils::TrayIconState::Idle);
|
|
}
|
|
_ => {}
|
|
})
|
|
.invoke_handler(invoke_handler)
|
|
.build(tauri::generate_context!())
|
|
.expect("error while building tauri application")
|
|
.run(|app, event| {
|
|
#[cfg(target_os = "macos")]
|
|
if let tauri::RunEvent::Reopen { .. } = &event {
|
|
show_main_window(app);
|
|
}
|
|
let _ = (app, event); // suppress unused warnings on non-macOS
|
|
});
|
|
}
|