diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 396bc18..354dd0d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "adler2" version = "2.0.1" @@ -2524,6 +2530,8 @@ dependencies = [ "serde", "serde_json", "signal-hook", + "specta", + "specta-typescript", "strsim", "tar", "tauri", @@ -2542,6 +2550,7 @@ dependencies = [ "tauri-plugin-sql", "tauri-plugin-store", "tauri-plugin-updater", + "tauri-specta", "tokio", "transcribe-rs", "vad-rs", @@ -4306,6 +4315,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pastey" version = "0.1.1" @@ -5907,6 +5922,50 @@ dependencies = [ "system-deps", ] +[[package]] +name = "specta" +version = "2.0.0-rc.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab7f01e9310a820edd31c80fde3cae445295adde21a3f9416517d7d65015b971" +dependencies = [ + "paste", + "specta-macros", + "thiserror 1.0.69", +] + +[[package]] +name = "specta-macros" +version = "2.0.0-rc.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0074b9e30ed84c6924eb63ad8d2fe71cdc82628525d84b1fcb1f2fd40676517" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "specta-serde" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77216504061374659e7245eac53d30c7b3e5fe64b88da97c753e7184b0781e63" +dependencies = [ + "specta", + "thiserror 1.0.69", +] + +[[package]] +name = "specta-typescript" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3220a0c365e51e248ac98eab5a6a32f544ff6f961906f09d3ee10903a4f52b2d" +dependencies = [ + "specta", + "specta-serde", + "thiserror 1.0.69", +] + [[package]] name = "spin" version = "0.9.8" @@ -6557,6 +6616,7 @@ dependencies = [ "serde_json", "serde_repr", "serialize-to-javascript", + "specta", "swift-rs", "tauri-build", "tauri-macros", @@ -6952,6 +7012,34 @@ dependencies = [ "wry", ] +[[package]] +name = "tauri-specta" +version = "2.0.0-rc.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23c0132dd3cf6064e5cd919b82b3f47780e9280e7b5910babfe139829b76655" +dependencies = [ + "heck 0.5.0", + "serde", + "serde_json", + "specta", + "specta-typescript", + "tauri", + "tauri-specta-macros", + "thiserror 2.0.17", +] + +[[package]] +name = "tauri-specta-macros" +version = "2.0.0-rc.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a4aa93823e07859546aa796b8a5d608190cd8037a3a5dce3eb63d491c34bda8" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "tauri-utils" version = "2.8.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fc0918b..130615e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -68,6 +68,9 @@ tar = "0.4.44" flate2 = "1.0" transcribe-rs = "0.1.4" ferrous-opencc = "0.2.3" +specta = "=2.0.0-rc.22" +specta-typescript = "0.0.9" +tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] } [target.'cfg(unix)'.dependencies] signal-hook = "0.3" diff --git a/src-tauri/src/clipboard.rs b/src-tauri/src/clipboard.rs index 7618a9e..8b26c7e 100644 --- a/src-tauri/src/clipboard.rs +++ b/src-tauri/src/clipboard.rs @@ -40,12 +40,11 @@ fn send_paste_ctrl_v() -> Result<(), String> { /// Sends a Shift+Insert paste command (Windows and Linux only). /// This is more universal for terminal applications and legacy software. -#[cfg(not(target_os = "macos"))] fn send_paste_shift_insert() -> Result<(), String> { #[cfg(target_os = "windows")] let insert_key_code = Key::Other(0x2D); // VK_INSERT - #[cfg(target_os = "linux")] - let insert_key_code = Key::Other(0x76); // XK_Insert (keycode 118 / 0x76) + #[cfg(not(target_os = "windows"))] + let insert_key_code = Key::Other(0x76); // XK_Insert (keycode 118 / 0x76, also used as fallback) let mut enigo = Enigo::new(&Settings::default()) .map_err(|e| format!("Failed to initialize Enigo: {}", e))?; @@ -109,7 +108,6 @@ fn paste_via_clipboard_ctrl_v(text: &str, app_handle: &AppHandle) -> Result<(), /// Pastes text using the clipboard method with Shift+Insert (Windows/Linux only). /// Saves the current clipboard, writes the text, sends paste command, then restores the clipboard. -#[cfg(not(target_os = "macos"))] fn paste_via_clipboard_shift_insert(text: &str, app_handle: &AppHandle) -> Result<(), String> { let clipboard = app_handle.clipboard(); @@ -145,7 +143,6 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> { match paste_method { PasteMethod::CtrlV => paste_via_clipboard_ctrl_v(&text, &app_handle)?, PasteMethod::Direct => paste_via_direct_input(&text)?, - #[cfg(not(target_os = "macos"))] PasteMethod::ShiftInsert => paste_via_clipboard_shift_insert(&text, &app_handle)?, } diff --git a/src-tauri/src/commands/audio.rs b/src-tauri/src/commands/audio.rs index da17d07..22a3d46 100644 --- a/src-tauri/src/commands/audio.rs +++ b/src-tauri/src/commands/audio.rs @@ -4,10 +4,11 @@ use crate::managers::audio::{AudioRecordingManager, MicrophoneMode}; use crate::settings::{get_settings, write_settings}; use log::warn; use serde::{Deserialize, Serialize}; +use specta::Type; use std::sync::Arc; use tauri::{AppHandle, Manager}; -#[derive(Serialize)] +#[derive(Serialize, Type)] pub struct CustomSounds { start: bool, stop: bool, @@ -23,6 +24,7 @@ fn custom_sound_exists(app: &AppHandle, sound_type: &str) -> bool { } #[tauri::command] +#[specta::specta] pub fn check_custom_sounds(app: AppHandle) -> CustomSounds { CustomSounds { start: custom_sound_exists(&app, "start"), @@ -30,7 +32,7 @@ pub fn check_custom_sounds(app: AppHandle) -> CustomSounds { } } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Type)] pub struct AudioDevice { pub index: String, pub name: String, @@ -38,6 +40,7 @@ pub struct AudioDevice { } #[tauri::command] +#[specta::specta] pub fn update_microphone_mode(app: AppHandle, always_on: bool) -> Result<(), String> { // Update settings let mut settings = get_settings(&app); @@ -57,12 +60,14 @@ pub fn update_microphone_mode(app: AppHandle, always_on: bool) -> Result<(), Str } #[tauri::command] +#[specta::specta] pub fn get_microphone_mode(app: AppHandle) -> Result { let settings = get_settings(&app); Ok(settings.always_on_microphone) } #[tauri::command] +#[specta::specta] pub fn get_available_microphones() -> Result, String> { let devices = list_input_devices().map_err(|e| format!("Failed to list audio devices: {}", e))?; @@ -83,6 +88,7 @@ pub fn get_available_microphones() -> Result, String> { } #[tauri::command] +#[specta::specta] pub fn set_selected_microphone(app: AppHandle, device_name: String) -> Result<(), String> { let mut settings = get_settings(&app); settings.selected_microphone = if device_name == "default" { @@ -101,6 +107,7 @@ pub fn set_selected_microphone(app: AppHandle, device_name: String) -> Result<() } #[tauri::command] +#[specta::specta] pub fn get_selected_microphone(app: AppHandle) -> Result { let settings = get_settings(&app); Ok(settings @@ -109,6 +116,7 @@ pub fn get_selected_microphone(app: AppHandle) -> Result { } #[tauri::command] +#[specta::specta] pub fn get_available_output_devices() -> Result, String> { let devices = list_output_devices().map_err(|e| format!("Failed to list output devices: {}", e))?; @@ -129,6 +137,7 @@ pub fn get_available_output_devices() -> Result, String> { } #[tauri::command] +#[specta::specta] pub fn set_selected_output_device(app: AppHandle, device_name: String) -> Result<(), String> { let mut settings = get_settings(&app); settings.selected_output_device = if device_name == "default" { @@ -141,6 +150,7 @@ pub fn set_selected_output_device(app: AppHandle, device_name: String) -> Result } #[tauri::command] +#[specta::specta] pub fn get_selected_output_device(app: AppHandle) -> Result { let settings = get_settings(&app); Ok(settings @@ -149,6 +159,7 @@ pub fn get_selected_output_device(app: AppHandle) -> Result { } #[tauri::command] +#[specta::specta] pub fn play_test_sound(app: AppHandle, sound_type: String) { let sound = match sound_type.as_str() { "start" => audio_feedback::SoundType::Start, @@ -162,6 +173,7 @@ pub fn play_test_sound(app: AppHandle, sound_type: String) { } #[tauri::command] +#[specta::specta] pub fn set_clamshell_microphone(app: AppHandle, device_name: String) -> Result<(), String> { let mut settings = get_settings(&app); settings.clamshell_microphone = if device_name == "default" { @@ -174,6 +186,7 @@ pub fn set_clamshell_microphone(app: AppHandle, device_name: String) -> Result<( } #[tauri::command] +#[specta::specta] pub fn get_clamshell_microphone(app: AppHandle) -> Result { let settings = get_settings(&app); Ok(settings diff --git a/src-tauri/src/commands/history.rs b/src-tauri/src/commands/history.rs index 6b9f4d3..95f909a 100644 --- a/src-tauri/src/commands/history.rs +++ b/src-tauri/src/commands/history.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use tauri::{AppHandle, State}; #[tauri::command] +#[specta::specta] pub async fn get_history_entries( _app: AppHandle, history_manager: State<'_, Arc>, @@ -14,6 +15,7 @@ pub async fn get_history_entries( } #[tauri::command] +#[specta::specta] pub async fn toggle_history_entry_saved( _app: AppHandle, history_manager: State<'_, Arc>, @@ -26,6 +28,7 @@ pub async fn toggle_history_entry_saved( } #[tauri::command] +#[specta::specta] pub async fn get_audio_file_path( _app: AppHandle, history_manager: State<'_, Arc>, @@ -38,6 +41,7 @@ pub async fn get_audio_file_path( } #[tauri::command] +#[specta::specta] pub async fn delete_history_entry( _app: AppHandle, history_manager: State<'_, Arc>, @@ -50,6 +54,7 @@ pub async fn delete_history_entry( } #[tauri::command] +#[specta::specta] pub async fn update_history_limit( app: AppHandle, history_manager: State<'_, Arc>, @@ -67,6 +72,7 @@ pub async fn update_history_limit( } #[tauri::command] +#[specta::specta] pub async fn update_recording_retention_period( app: AppHandle, history_manager: State<'_, Arc>, diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index aedb2b5..35feee0 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -3,17 +3,19 @@ pub mod history; pub mod models; pub mod transcription; -use crate::{settings, utils::cancel_current_operation}; +use crate::settings::{get_settings, write_settings, AppSettings, LogLevel}; +use crate::utils::cancel_current_operation; use tauri::{AppHandle, Manager}; -use tauri_plugin_log::LogLevel; use tauri_plugin_opener::OpenerExt; #[tauri::command] +#[specta::specta] pub fn cancel_operation(app: AppHandle) { cancel_current_operation(&app); } #[tauri::command] +#[specta::specta] pub fn get_app_dir_path(app: AppHandle) -> Result { let app_data_dir = app .path() @@ -24,6 +26,19 @@ pub fn get_app_dir_path(app: AppHandle) -> Result { } #[tauri::command] +#[specta::specta] +pub fn get_app_settings(app: AppHandle) -> Result { + Ok(get_settings(&app)) +} + +#[tauri::command] +#[specta::specta] +pub fn get_default_settings() -> Result { + Ok(crate::settings::get_default_settings()) +} + +#[tauri::command] +#[specta::specta] pub fn get_log_dir_path(app: AppHandle) -> Result { let log_dir = app .path() @@ -33,22 +48,25 @@ pub fn get_log_dir_path(app: AppHandle) -> Result { Ok(log_dir.to_string_lossy().to_string()) } +#[specta::specta] #[tauri::command] pub fn set_log_level(app: AppHandle, level: LogLevel) -> Result<(), String> { - let log_level: log::Level = level.clone().into(); + let tauri_log_level: tauri_plugin_log::LogLevel = level.into(); + let log_level: log::Level = tauri_log_level.into(); // Update the file log level atomic so the filter picks up the new level crate::FILE_LOG_LEVEL.store( log_level.to_level_filter() as u8, std::sync::atomic::Ordering::Relaxed, ); - let mut settings = settings::get_settings(&app); + let mut settings = get_settings(&app); settings.log_level = level; - settings::write_settings(&app, settings); + write_settings(&app, settings); Ok(()) } +#[specta::specta] #[tauri::command] pub fn open_recordings_folder(app: AppHandle) -> Result<(), String> { let app_data_dir = app @@ -66,6 +84,7 @@ pub fn open_recordings_folder(app: AppHandle) -> Result<(), String> { Ok(()) } +#[specta::specta] #[tauri::command] pub fn open_log_dir(app: AppHandle) -> Result<(), String> { let log_dir = app @@ -81,6 +100,7 @@ pub fn open_log_dir(app: AppHandle) -> Result<(), String> { Ok(()) } +#[specta::specta] #[tauri::command] pub fn open_app_data_dir(app: AppHandle) -> Result<(), String> { let app_data_dir = app diff --git a/src-tauri/src/commands/models.rs b/src-tauri/src/commands/models.rs index 5afe46d..b2e30b6 100644 --- a/src-tauri/src/commands/models.rs +++ b/src-tauri/src/commands/models.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use tauri::{AppHandle, State}; #[tauri::command] +#[specta::specta] pub async fn get_available_models( model_manager: State<'_, Arc>, ) -> Result, String> { @@ -12,6 +13,7 @@ pub async fn get_available_models( } #[tauri::command] +#[specta::specta] pub async fn get_model_info( model_manager: State<'_, Arc>, model_id: String, @@ -20,6 +22,7 @@ pub async fn get_model_info( } #[tauri::command] +#[specta::specta] pub async fn download_model( model_manager: State<'_, Arc>, model_id: String, @@ -31,6 +34,7 @@ pub async fn download_model( } #[tauri::command] +#[specta::specta] pub async fn delete_model( model_manager: State<'_, Arc>, model_id: String, @@ -41,6 +45,7 @@ pub async fn delete_model( } #[tauri::command] +#[specta::specta] pub async fn set_active_model( app_handle: AppHandle, model_manager: State<'_, Arc>, @@ -70,12 +75,14 @@ pub async fn set_active_model( } #[tauri::command] +#[specta::specta] pub async fn get_current_model(app_handle: AppHandle) -> Result { let settings = get_settings(&app_handle); Ok(settings.selected_model) } #[tauri::command] +#[specta::specta] pub async fn get_transcription_model_status( transcription_manager: State<'_, Arc>, ) -> Result, String> { @@ -83,6 +90,7 @@ pub async fn get_transcription_model_status( } #[tauri::command] +#[specta::specta] pub async fn is_model_loading( transcription_manager: State<'_, Arc>, ) -> Result { @@ -92,6 +100,7 @@ pub async fn is_model_loading( } #[tauri::command] +#[specta::specta] pub async fn has_any_models_available( model_manager: State<'_, Arc>, ) -> Result { @@ -100,6 +109,7 @@ pub async fn has_any_models_available( } #[tauri::command] +#[specta::specta] pub async fn has_any_models_or_downloads( model_manager: State<'_, Arc>, ) -> Result { @@ -109,6 +119,7 @@ pub async fn has_any_models_or_downloads( } #[tauri::command] +#[specta::specta] pub async fn cancel_download( model_manager: State<'_, Arc>, model_id: String, @@ -119,6 +130,7 @@ pub async fn cancel_download( } #[tauri::command] +#[specta::specta] pub async fn get_recommended_first_model() -> Result { // Recommend Parakeet V3 model for first-time users - fastest and most accurate Ok("parakeet-tdt-0.6b-v3".to_string()) diff --git a/src-tauri/src/commands/transcription.rs b/src-tauri/src/commands/transcription.rs index d494b6b..6cfa2e6 100644 --- a/src-tauri/src/commands/transcription.rs +++ b/src-tauri/src/commands/transcription.rs @@ -1,8 +1,17 @@ use crate::managers::transcription::TranscriptionManager; use crate::settings::{get_settings, write_settings, ModelUnloadTimeout}; +use serde::Serialize; +use specta::Type; use tauri::{AppHandle, State}; +#[derive(Serialize, Type)] +pub struct ModelLoadStatus { + is_loaded: bool, + current_model: Option, +} + #[tauri::command] +#[specta::specta] pub fn set_model_unload_timeout(app: AppHandle, timeout: ModelUnloadTimeout) { let mut settings = get_settings(&app); settings.model_unload_timeout = timeout; @@ -10,19 +19,18 @@ pub fn set_model_unload_timeout(app: AppHandle, timeout: ModelUnloadTimeout) { } #[tauri::command] +#[specta::specta] pub fn get_model_load_status( transcription_manager: State, -) -> Result { - let is_loaded = transcription_manager.is_model_loaded(); - let current_model = transcription_manager.get_current_model(); - - Ok(serde_json::json!({ - "is_loaded": is_loaded, - "current_model": current_model - })) +) -> Result { + Ok(ModelLoadStatus { + is_loaded: transcription_manager.is_model_loaded(), + current_model: transcription_manager.get_current_model(), + }) } #[tauri::command] +#[specta::specta] pub fn unload_model_manually( transcription_manager: State, ) -> Result<(), String> { diff --git a/src-tauri/src/helpers/clamshell.rs b/src-tauri/src/helpers/clamshell.rs index 74847e0..4f06405 100644 --- a/src-tauri/src/helpers/clamshell.rs +++ b/src-tauri/src/helpers/clamshell.rs @@ -6,7 +6,6 @@ use std::process::Command; /// This queries the macOS IORegistry for the AppleClamshellState key. /// Returns true if the lid is closed, false if open. #[cfg(target_os = "macos")] -#[tauri::command] pub fn is_clamshell() -> Result { let output = Command::new("ioreg") .args(["-r", "-k", "AppleClamshellState", "-d", "4"]) @@ -32,6 +31,7 @@ pub fn is_clamshell() -> Result { /// Returns true if a battery is detected (laptop), false otherwise (desktop) #[cfg(target_os = "macos")] #[tauri::command] +#[specta::specta] pub fn is_laptop() -> Result { let output = Command::new("pmset") .arg("-g") @@ -48,7 +48,6 @@ pub fn is_laptop() -> Result { /// Stub implementation for non-macOS platforms /// Always returns false since clamshell mode is macOS-specific #[cfg(not(target_os = "macos"))] -#[tauri::command] pub fn is_clamshell() -> Result { Ok(false) } @@ -57,6 +56,7 @@ pub fn is_clamshell() -> Result { /// Always returns false since laptop detection is macOS-specific #[cfg(not(target_os = "macos"))] #[tauri::command] +#[specta::specta] pub fn is_laptop() -> Result { Ok(false) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ff1bf96..5e05be5 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,6 +12,8 @@ mod shortcut; mod signal_handle; mod tray; mod utils; +use specta_typescript::{BigIntExportBehavior, Typescript}; +use tauri_specta::{collect_commands, Builder}; use env_filter::Builder as EnvFilterBuilder; use managers::audio::AudioRecordingManager; @@ -33,6 +35,8 @@ use tauri::{AppHandle, 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); @@ -199,6 +203,7 @@ fn initialize_core_logic(app_handle: &AppHandle) { } #[tauri::command] +#[specta::specta] fn trigger_update_check(app: AppHandle) -> Result<(), String> { app.emit("check-for-updates", ()) .map_err(|e| e.to_string())?; @@ -211,6 +216,90 @@ pub fn run() { // when the variable is unset let console_filter = build_console_filter(); + let specta_builder = Builder::::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_paste_method_setting, + shortcut::change_clipboard_handling_setting, + shortcut::change_post_process_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, + trigger_update_check, + commands::cancel_operation, + 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::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::models::get_recommended_first_model, + commands::audio::update_microphone_mode, + commands::audio::get_microphone_mode, + 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::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::update_history_limit, + commands::history::update_recording_retention_period, + helpers::clamshell::is_laptop, + ]); + + #[cfg(debug_assertions)] // <- Only export on non-release builds + specta_builder + .export( + Typescript::default().bigint(BigIntExportBehavior::String), + "../src/bindings.ts", + ) + .expect("Failed to export typescript bindings"); + let mut builder = tauri::Builder::default() .plugin( LogBuilder::new() @@ -268,8 +357,9 @@ pub fn run() { )) .manage(Mutex::new(ShortcutToggleStates::default())) .setup(move |app| { - let settings = settings::get_settings(&app.handle()); - let file_log_level: log::Level = settings.log_level.clone().into(); + let settings = get_settings(&app.handle()); + 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(); @@ -307,80 +397,7 @@ pub fn run() { } _ => {} }) - .invoke_handler(tauri::generate_handler![ - 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_paste_method_setting, - shortcut::change_clipboard_handling_setting, - shortcut::change_post_process_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, - trigger_update_check, - commands::cancel_operation, - commands::get_app_dir_path, - commands::get_log_dir_path, - commands::set_log_level, - commands::open_recordings_folder, - commands::open_log_dir, - commands::open_app_data_dir, - 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::models::get_recommended_first_model, - commands::audio::update_microphone_mode, - commands::audio::get_microphone_mode, - 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, - helpers::clamshell::is_clamshell, - helpers::clamshell::is_laptop, - 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::update_history_limit, - commands::history::update_recording_retention_period - ]) + .invoke_handler(specta_builder.invoke_handler()) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/managers/history.rs b/src-tauri/src/managers/history.rs index 481d9a9..a2dcaf8 100644 --- a/src-tauri/src/managers/history.rs +++ b/src-tauri/src/managers/history.rs @@ -3,6 +3,7 @@ use chrono::{DateTime, Local, Utc}; use log::{debug, error}; use rusqlite::{params, Connection, OptionalExtension}; use serde::{Deserialize, Serialize}; +use specta::Type; use std::fs; use std::path::PathBuf; use tauri::{AppHandle, Emitter, Manager}; @@ -10,7 +11,7 @@ use tauri_plugin_sql::{Migration, MigrationKind}; use crate::audio_toolkit::save_wav_file; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, Type)] pub struct HistoryEntry { pub id: i64, pub file_name: String, diff --git a/src-tauri/src/managers/model.rs b/src-tauri/src/managers/model.rs index 4ee4465..f20b706 100644 --- a/src-tauri/src/managers/model.rs +++ b/src-tauri/src/managers/model.rs @@ -4,6 +4,7 @@ use flate2::read::GzDecoder; use futures_util::StreamExt; use log::{debug, info, warn}; use serde::{Deserialize, Serialize}; +use specta::Type; use std::collections::HashMap; use std::fs; use std::fs::File; @@ -13,13 +14,13 @@ use std::sync::Mutex; use tar::Archive; use tauri::{AppHandle, Emitter, Manager}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Type)] pub enum EngineType { Whisper, Parakeet, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Type)] pub struct ModelInfo { pub id: String, pub name: String, @@ -36,7 +37,7 @@ pub struct ModelInfo { pub speed_score: f32, // 0.0 to 1.0, higher is faster } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Type)] pub struct DownloadProgress { pub model_id: String, pub downloaded: u64, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index d2ba1bd..8054d38 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -1,11 +1,79 @@ use log::{debug, warn}; -use serde::{Deserialize, Serialize}; +use serde::de::{self, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; +use specta::Type; use std::collections::HashMap; use tauri::AppHandle; -use tauri_plugin_log::LogLevel; use tauri_plugin_store::StoreExt; -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq, Type)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, +} + +// Custom deserializer to handle both old numeric format (1-5) and new string format ("trace", "debug", etc.) +impl<'de> Deserialize<'de> for LogLevel { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct LogLevelVisitor; + + impl<'de> Visitor<'de> for LogLevelVisitor { + type Value = LogLevel; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string or integer representing log level") + } + + fn visit_str(self, value: &str) -> Result { + match value.to_lowercase().as_str() { + "trace" => Ok(LogLevel::Trace), + "debug" => Ok(LogLevel::Debug), + "info" => Ok(LogLevel::Info), + "warn" => Ok(LogLevel::Warn), + "error" => Ok(LogLevel::Error), + _ => Err(E::unknown_variant( + value, + &["trace", "debug", "info", "warn", "error"], + )), + } + } + + fn visit_u64(self, value: u64) -> Result { + match value { + 1 => Ok(LogLevel::Trace), + 2 => Ok(LogLevel::Debug), + 3 => Ok(LogLevel::Info), + 4 => Ok(LogLevel::Warn), + 5 => Ok(LogLevel::Error), + _ => Err(E::invalid_value(de::Unexpected::Unsigned(value), &"1-5")), + } + } + } + + deserializer.deserialize_any(LogLevelVisitor) + } +} + +impl From for tauri_plugin_log::LogLevel { + fn from(level: LogLevel) -> Self { + match level { + LogLevel::Trace => tauri_plugin_log::LogLevel::Trace, + LogLevel::Debug => tauri_plugin_log::LogLevel::Debug, + LogLevel::Info => tauri_plugin_log::LogLevel::Info, + LogLevel::Warn => tauri_plugin_log::LogLevel::Warn, + LogLevel::Error => tauri_plugin_log::LogLevel::Error, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Type)] pub struct ShortcutBinding { pub id: String, pub name: String, @@ -14,14 +82,14 @@ pub struct ShortcutBinding { pub current_binding: String, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Type)] pub struct LLMPrompt { pub id: String, pub name: String, pub prompt: String, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Type)] pub struct PostProcessProvider { pub id: String, pub label: String, @@ -32,7 +100,7 @@ pub struct PostProcessProvider { pub models_endpoint: Option, } -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)] #[serde(rename_all = "lowercase")] pub enum OverlayPosition { None, @@ -40,7 +108,7 @@ pub enum OverlayPosition { Bottom, } -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)] #[serde(rename_all = "snake_case")] pub enum ModelUnloadTimeout { Never, @@ -53,23 +121,22 @@ pub enum ModelUnloadTimeout { Sec5, // Debug mode only } -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)] #[serde(rename_all = "snake_case")] pub enum PasteMethod { CtrlV, Direct, - #[cfg(not(target_os = "macos"))] ShiftInsert, } -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)] #[serde(rename_all = "snake_case")] pub enum ClipboardHandling { DontModify, CopyToClipboard, } -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)] #[serde(rename_all = "snake_case")] pub enum RecordingRetentionPeriod { Never, @@ -125,7 +192,7 @@ impl ModelUnloadTimeout { } } -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)] #[serde(rename_all = "snake_case")] pub enum SoundTheme { Marimba, @@ -152,7 +219,7 @@ impl SoundTheme { } /* still handy for composing the initial JSON in the store ------------- */ -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Type)] pub struct AppSettings { pub bindings: HashMap, pub push_to_talk: bool, diff --git a/src-tauri/src/shortcut.rs b/src-tauri/src/shortcut.rs index 1296ec6..fb03031 100644 --- a/src-tauri/src/shortcut.rs +++ b/src-tauri/src/shortcut.rs @@ -1,5 +1,6 @@ use log::{error, warn}; use serde::Serialize; +use specta::Type; use tauri::{AppHandle, Emitter, Manager}; use tauri_plugin_autostart::ManagerExt; use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState}; @@ -22,7 +23,7 @@ pub fn init_shortcuts(app: &AppHandle) { } } -#[derive(Serialize)] +#[derive(Serialize, Type)] pub struct BindingResponse { success: bool, binding: Option, @@ -30,6 +31,7 @@ pub struct BindingResponse { } #[tauri::command] +#[specta::specta] pub fn change_binding( app: AppHandle, id: String, @@ -93,6 +95,7 @@ pub fn change_binding( } #[tauri::command] +#[specta::specta] pub fn reset_binding(app: AppHandle, id: String) -> Result { let binding = settings::get_stored_binding(&app, &id); @@ -100,6 +103,7 @@ pub fn reset_binding(app: AppHandle, id: String) -> Result Result<(), String> { let mut settings = settings::get_settings(&app); @@ -113,6 +117,7 @@ pub fn change_ptt_setting(app: AppHandle, enabled: bool) -> Result<(), String> { } #[tauri::command] +#[specta::specta] pub fn change_audio_feedback_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.audio_feedback = enabled; @@ -121,6 +126,7 @@ pub fn change_audio_feedback_setting(app: AppHandle, enabled: bool) -> Result<() } #[tauri::command] +#[specta::specta] pub fn change_audio_feedback_volume_setting(app: AppHandle, volume: f32) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.audio_feedback_volume = volume; @@ -129,6 +135,7 @@ pub fn change_audio_feedback_volume_setting(app: AppHandle, volume: f32) -> Resu } #[tauri::command] +#[specta::specta] pub fn change_sound_theme_setting(app: AppHandle, theme: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); let parsed = match theme.as_str() { @@ -146,6 +153,7 @@ pub fn change_sound_theme_setting(app: AppHandle, theme: String) -> Result<(), S } #[tauri::command] +#[specta::specta] pub fn change_translate_to_english_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.translate_to_english = enabled; @@ -154,6 +162,7 @@ pub fn change_translate_to_english_setting(app: AppHandle, enabled: bool) -> Res } #[tauri::command] +#[specta::specta] pub fn change_selected_language_setting(app: AppHandle, language: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.selected_language = language; @@ -162,6 +171,7 @@ pub fn change_selected_language_setting(app: AppHandle, language: String) -> Res } #[tauri::command] +#[specta::specta] pub fn change_overlay_position_setting(app: AppHandle, position: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); let parsed = match position.as_str() { @@ -183,6 +193,7 @@ pub fn change_overlay_position_setting(app: AppHandle, position: String) -> Resu } #[tauri::command] +#[specta::specta] pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.debug_mode = enabled; @@ -201,6 +212,7 @@ pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), St } #[tauri::command] +#[specta::specta] pub fn change_start_hidden_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.start_hidden = enabled; @@ -219,6 +231,7 @@ pub fn change_start_hidden_setting(app: AppHandle, enabled: bool) -> Result<(), } #[tauri::command] +#[specta::specta] pub fn change_autostart_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.autostart_enabled = enabled; @@ -245,6 +258,7 @@ pub fn change_autostart_setting(app: AppHandle, enabled: bool) -> Result<(), Str } #[tauri::command] +#[specta::specta] pub fn update_custom_words(app: AppHandle, words: Vec) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.custom_words = words; @@ -253,6 +267,7 @@ pub fn update_custom_words(app: AppHandle, words: Vec) -> Result<(), Str } #[tauri::command] +#[specta::specta] pub fn change_word_correction_threshold_setting( app: AppHandle, threshold: f64, @@ -264,12 +279,12 @@ pub fn change_word_correction_threshold_setting( } #[tauri::command] +#[specta::specta] pub fn change_paste_method_setting(app: AppHandle, method: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); let parsed = match method.as_str() { "ctrl_v" => PasteMethod::CtrlV, "direct" => PasteMethod::Direct, - #[cfg(not(target_os = "macos"))] "shift_insert" => PasteMethod::ShiftInsert, other => { warn!("Invalid paste method '{}', defaulting to ctrl_v", other); @@ -282,6 +297,7 @@ pub fn change_paste_method_setting(app: AppHandle, method: String) -> Result<(), } #[tauri::command] +#[specta::specta] pub fn change_clipboard_handling_setting(app: AppHandle, handling: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); let parsed = match handling.as_str() { @@ -301,6 +317,7 @@ pub fn change_clipboard_handling_setting(app: AppHandle, handling: String) -> Re } #[tauri::command] +#[specta::specta] pub fn change_post_process_enabled_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.post_process_enabled = enabled; @@ -309,6 +326,7 @@ pub fn change_post_process_enabled_setting(app: AppHandle, enabled: bool) -> Res } #[tauri::command] +#[specta::specta] pub fn change_post_process_base_url_setting( app: AppHandle, provider_id: String, @@ -352,6 +370,7 @@ fn validate_provider_exists( } #[tauri::command] +#[specta::specta] pub fn change_post_process_api_key_setting( app: AppHandle, provider_id: String, @@ -365,6 +384,7 @@ pub fn change_post_process_api_key_setting( } #[tauri::command] +#[specta::specta] pub fn change_post_process_model_setting( app: AppHandle, provider_id: String, @@ -378,6 +398,7 @@ pub fn change_post_process_model_setting( } #[tauri::command] +#[specta::specta] pub fn set_post_process_provider(app: AppHandle, provider_id: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); validate_provider_exists(&settings, &provider_id)?; @@ -387,6 +408,7 @@ pub fn set_post_process_provider(app: AppHandle, provider_id: String) -> Result< } #[tauri::command] +#[specta::specta] pub fn add_post_process_prompt( app: AppHandle, name: String, @@ -410,6 +432,7 @@ pub fn add_post_process_prompt( } #[tauri::command] +#[specta::specta] pub fn update_post_process_prompt( app: AppHandle, id: String, @@ -433,6 +456,7 @@ pub fn update_post_process_prompt( } #[tauri::command] +#[specta::specta] pub fn delete_post_process_prompt(app: AppHandle, id: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); @@ -460,6 +484,7 @@ pub fn delete_post_process_prompt(app: AppHandle, id: String) -> Result<(), Stri } #[tauri::command] +#[specta::specta] pub async fn fetch_post_process_models( app: AppHandle, provider_id: String, @@ -599,6 +624,7 @@ async fn fetch_models_manual( } #[tauri::command] +#[specta::specta] pub fn set_post_process_selected_prompt(app: AppHandle, id: String) -> Result<(), String> { let mut settings = settings::get_settings(&app); @@ -613,6 +639,7 @@ pub fn set_post_process_selected_prompt(app: AppHandle, id: String) -> Result<() } #[tauri::command] +#[specta::specta] pub fn change_mute_while_recording_setting(app: AppHandle, enabled: bool) -> Result<(), String> { let mut settings = settings::get_settings(&app); settings.mute_while_recording = enabled; @@ -642,6 +669,7 @@ fn validate_shortcut_string(raw: &str) -> Result<(), String> { /// Temporarily unregister a binding while the user is editing it in the UI. /// This avoids firing the action while keys are being recorded. #[tauri::command] +#[specta::specta] pub fn suspend_binding(app: AppHandle, id: String) -> Result<(), String> { if let Some(b) = settings::get_bindings(&app).get(&id).cloned() { if let Err(e) = _unregister_shortcut(&app, b) { @@ -654,6 +682,7 @@ pub fn suspend_binding(app: AppHandle, id: String) -> Result<(), String> { /// Re-register the binding after the user has finished editing. #[tauri::command] +#[specta::specta] pub fn resume_binding(app: AppHandle, id: String) -> Result<(), String> { if let Some(b) = settings::get_bindings(&app).get(&id).cloned() { if let Err(e) = _register_shortcut(&app, b) { diff --git a/src/App.tsx b/src/App.tsx index 054e327..29579b1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,3 @@ -import { invoke } from "@tauri-apps/api/core"; import { useEffect, useState } from "react"; import { Toaster } from "sonner"; import "./App.css"; @@ -7,6 +6,7 @@ import Footer from "./components/footer"; import Onboarding from "./components/onboarding"; import { Sidebar, SidebarSection, SECTIONS_CONFIG } from "./components/Sidebar"; import { useSettings } from "./hooks/useSettings"; +import { commands } from "@/bindings"; const renderSettingsContent = (section: SidebarSection) => { const ActiveComponent = @@ -52,8 +52,12 @@ function App() { const checkOnboardingStatus = async () => { try { // Always check if they have any models available - const modelsAvailable: boolean = await invoke("has_any_models_available"); - setShowOnboarding(!modelsAvailable); + const result = await commands.hasAnyModelsAvailable(); + if (result.status === "ok") { + setShowOnboarding(!result.data); + } else { + setShowOnboarding(true); + } } catch (error) { console.error("Failed to check onboarding status:", error); setShowOnboarding(true); diff --git a/src/bindings.ts b/src/bindings.ts new file mode 100644 index 0000000..1259c24 --- /dev/null +++ b/src/bindings.ts @@ -0,0 +1,674 @@ + +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +/** user-defined commands **/ + + +export const commands = { +async changeBinding(id: string, binding: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_binding", { id, binding }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async resetBinding(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("reset_binding", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changePttSetting(enabled: boolean) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_ptt_setting", { enabled }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changeAudioFeedbackSetting(enabled: boolean) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_audio_feedback_setting", { enabled }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changeAudioFeedbackVolumeSetting(volume: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_audio_feedback_volume_setting", { volume }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changeSoundThemeSetting(theme: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_sound_theme_setting", { theme }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changeStartHiddenSetting(enabled: boolean) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_start_hidden_setting", { enabled }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changeAutostartSetting(enabled: boolean) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_autostart_setting", { enabled }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changeTranslateToEnglishSetting(enabled: boolean) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_translate_to_english_setting", { enabled }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changeSelectedLanguageSetting(language: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_selected_language_setting", { language }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changeOverlayPositionSetting(position: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_overlay_position_setting", { position }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changeDebugModeSetting(enabled: boolean) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_debug_mode_setting", { enabled }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changeWordCorrectionThresholdSetting(threshold: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_word_correction_threshold_setting", { threshold }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changePasteMethodSetting(method: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_paste_method_setting", { method }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changeClipboardHandlingSetting(handling: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_clipboard_handling_setting", { handling }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changePostProcessEnabledSetting(enabled: boolean) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_post_process_enabled_setting", { enabled }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changePostProcessBaseUrlSetting(providerId: string, baseUrl: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_post_process_base_url_setting", { providerId, baseUrl }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changePostProcessApiKeySetting(providerId: string, apiKey: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_post_process_api_key_setting", { providerId, apiKey }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changePostProcessModelSetting(providerId: string, model: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_post_process_model_setting", { providerId, model }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async setPostProcessProvider(providerId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("set_post_process_provider", { providerId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async fetchPostProcessModels(providerId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("fetch_post_process_models", { providerId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async addPostProcessPrompt(name: string, prompt: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("add_post_process_prompt", { name, prompt }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async updatePostProcessPrompt(id: string, name: string, prompt: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("update_post_process_prompt", { id, name, prompt }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async deletePostProcessPrompt(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("delete_post_process_prompt", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async setPostProcessSelectedPrompt(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("set_post_process_selected_prompt", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async updateCustomWords(words: string[]) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("update_custom_words", { words }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Temporarily unregister a binding while the user is editing it in the UI. + * This avoids firing the action while keys are being recorded. + */ +async suspendBinding(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("suspend_binding", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Re-register the binding after the user has finished editing. + */ +async resumeBinding(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("resume_binding", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async changeMuteWhileRecordingSetting(enabled: boolean) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_mute_while_recording_setting", { enabled }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async triggerUpdateCheck() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("trigger_update_check") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async cancelOperation() : Promise { + await TAURI_INVOKE("cancel_operation"); +}, +async getAppDirPath() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_app_dir_path") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getAppSettings() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_app_settings") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getDefaultSettings() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_default_settings") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getLogDirPath() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_log_dir_path") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async setLogLevel(level: LogLevel) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("set_log_level", { level }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async openRecordingsFolder() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("open_recordings_folder") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async openLogDir() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("open_log_dir") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async openAppDataDir() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("open_app_data_dir") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getAvailableModels() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_available_models") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getModelInfo(modelId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_model_info", { modelId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async downloadModel(modelId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("download_model", { modelId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async deleteModel(modelId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("delete_model", { modelId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async cancelDownload(modelId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("cancel_download", { modelId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async setActiveModel(modelId: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("set_active_model", { modelId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getCurrentModel() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_current_model") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getTranscriptionModelStatus() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_transcription_model_status") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async isModelLoading() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("is_model_loading") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async hasAnyModelsAvailable() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("has_any_models_available") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async hasAnyModelsOrDownloads() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("has_any_models_or_downloads") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getRecommendedFirstModel() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_recommended_first_model") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async updateMicrophoneMode(alwaysOn: boolean) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("update_microphone_mode", { alwaysOn }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getMicrophoneMode() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_microphone_mode") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getAvailableMicrophones() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_available_microphones") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async setSelectedMicrophone(deviceName: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("set_selected_microphone", { deviceName }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getSelectedMicrophone() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_selected_microphone") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getAvailableOutputDevices() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_available_output_devices") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async setSelectedOutputDevice(deviceName: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("set_selected_output_device", { deviceName }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getSelectedOutputDevice() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_selected_output_device") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async playTestSound(soundType: string) : Promise { + await TAURI_INVOKE("play_test_sound", { soundType }); +}, +async checkCustomSounds() : Promise { + return await TAURI_INVOKE("check_custom_sounds"); +}, +async setClamshellMicrophone(deviceName: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("set_clamshell_microphone", { deviceName }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getClamshellMicrophone() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_clamshell_microphone") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async setModelUnloadTimeout(timeout: ModelUnloadTimeout) : Promise { + await TAURI_INVOKE("set_model_unload_timeout", { timeout }); +}, +async getModelLoadStatus() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_model_load_status") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async unloadModelManually() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("unload_model_manually") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getHistoryEntries() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_history_entries") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async toggleHistoryEntrySaved(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("toggle_history_entry_saved", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getAudioFilePath(fileName: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_audio_file_path", { fileName }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async deleteHistoryEntry(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("delete_history_entry", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async updateHistoryLimit(limit: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("update_history_limit", { limit }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async updateRecordingRetentionPeriod(period: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("update_recording_retention_period", { period }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +/** + * Checks if the Mac is a laptop by detecting battery presence + * + * This uses pmset to check for battery information. + * Returns true if a battery is detected (laptop), false otherwise (desktop) + */ +async isLaptop() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("is_laptop") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +} +} + +/** user-defined events **/ + + + +/** user-defined constants **/ + + + +/** user-defined types **/ + +export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: string; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean } +export type AudioDevice = { index: string; name: string; is_default: boolean } +export type BindingResponse = { success: boolean; binding: ShortcutBinding | null; error: string | null } +export type ClipboardHandling = "dont_modify" | "copy_to_clipboard" +export type CustomSounds = { start: boolean; stop: boolean } +export type EngineType = "Whisper" | "Parakeet" +export type HistoryEntry = { id: string; file_name: string; timestamp: string; saved: boolean; title: string; transcription_text: string; post_processed_text: string | null; post_process_prompt: string | null } +export type LLMPrompt = { id: string; name: string; prompt: string } +export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" +export type ModelInfo = { id: string; name: string; description: string; filename: string; url: string | null; size_mb: string; is_downloaded: boolean; is_downloading: boolean; partial_size: string; is_directory: boolean; engine_type: EngineType; accuracy_score: number; speed_score: number } +export type ModelLoadStatus = { is_loaded: boolean; current_model: string | null } +export type ModelUnloadTimeout = "never" | "immediately" | "min_2" | "min_5" | "min_10" | "min_15" | "hour_1" | "sec_5" +export type OverlayPosition = "none" | "top" | "bottom" +export type PasteMethod = "ctrl_v" | "direct" | "shift_insert" +export type PostProcessProvider = { id: string; label: string; base_url: string; allow_base_url_edit?: boolean; models_endpoint?: string | null } +export type RecordingRetentionPeriod = "never" | "preserve_limit" | "days_3" | "weeks_2" | "months_3" +export type ShortcutBinding = { id: string; name: string; description: string; default_binding: string; current_binding: string } +export type SoundTheme = "marimba" | "pop" | "custom" + +/** tauri-specta globals **/ + +import { + invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, +} from "@tauri-apps/api/core"; +import * as TAURI_API_EVENT from "@tauri-apps/api/event"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; + +type __EventObj__ = { + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; +}; + +export type Result = + | { status: "ok"; data: T } + | { status: "error"; error: E }; + +function __makeEvents__>( + mappings: Record, +) { + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); +} diff --git a/src/components/model-selector/ModelDropdown.tsx b/src/components/model-selector/ModelDropdown.tsx index 54cc633..4f7477c 100644 --- a/src/components/model-selector/ModelDropdown.tsx +++ b/src/components/model-selector/ModelDropdown.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { ModelInfo } from "../../lib/types"; +import type { ModelInfo } from "@/bindings"; import { formatModelSize } from "../../lib/utils/format"; import { ProgressBar } from "../shared"; @@ -180,7 +180,7 @@ const ModelDropdown: React.FC = ({ {model.description}
- Download size · {formatModelSize(model.size_mb)} + Download size · {formatModelSize(Number(model.size_mb))}
diff --git a/src/components/model-selector/ModelSelector.tsx b/src/components/model-selector/ModelSelector.tsx index 98b24b4..6a801bd 100644 --- a/src/components/model-selector/ModelSelector.tsx +++ b/src/components/model-selector/ModelSelector.tsx @@ -1,7 +1,6 @@ import React, { useState, useRef, useEffect } from "react"; -import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; -import { ModelInfo } from "../../lib/types"; +import { commands, type ModelInfo } from "@/bindings"; import ModelStatusButton from "./ModelStatusButton"; import ModelDropdown from "./ModelDropdown"; import DownloadProgressDisplay from "./DownloadProgressDisplay"; @@ -238,8 +237,10 @@ const ModelSelector: React.FC = ({ onError }) => { const loadModels = async () => { try { - const modelList = await invoke("get_available_models"); - setModels(modelList); + const result = await commands.getAvailableModels(); + if (result.status === "ok") { + setModels(result.data); + } } catch (err) { console.error("Failed to load models:", err); } @@ -247,21 +248,25 @@ const ModelSelector: React.FC = ({ onError }) => { const loadCurrentModel = async () => { try { - const current = await invoke("get_current_model"); - setCurrentModelId(current); + const result = await commands.getCurrentModel(); + if (result.status === "ok") { + const current = result.data; + setCurrentModelId(current); - if (current) { - // Check if model is actually loaded - const transcriptionStatus = await invoke( - "get_transcription_model_status", - ); - if (transcriptionStatus === current) { - setModelStatus("ready"); + if (current) { + // Check if model is actually loaded + const statusResult = await commands.getTranscriptionModelStatus(); + if (statusResult.status === "ok") { + const transcriptionStatus = statusResult.data; + if (transcriptionStatus === current) { + setModelStatus("ready"); + } else { + setModelStatus("unloaded"); + } + } } else { - setModelStatus("unloaded"); + setModelStatus("none"); } - } else { - setModelStatus("none"); } } catch (err) { console.error("Failed to load current model:", err); @@ -272,10 +277,16 @@ const ModelSelector: React.FC = ({ onError }) => { const handleModelSelect = async (modelId: string) => { try { + setCurrentModelId(modelId); // Set optimistically so loading text shows correct model setModelError(null); setShowModelDropdown(false); - await invoke("set_active_model", { modelId }); - setCurrentModelId(modelId); + const result = await commands.setActiveModel(modelId); + if (result.status === "error") { + const errorMsg = result.error; + setModelError(errorMsg); + setModelStatus("error"); + onError?.(errorMsg); + } } catch (err) { const errorMsg = `${err}`; setModelError(errorMsg); @@ -287,7 +298,13 @@ const ModelSelector: React.FC = ({ onError }) => { const handleModelDownload = async (modelId: string) => { try { setModelError(null); - await invoke("download_model", { modelId }); + const result = await commands.downloadModel(modelId); + if (result.status === "error") { + const errorMsg = result.error; + setModelError(errorMsg); + setModelStatus("error"); + onError?.(errorMsg); + } } catch (err) { const errorMsg = `${err}`; setModelError(errorMsg); @@ -347,9 +364,11 @@ const ModelSelector: React.FC = ({ onError }) => { }; const handleModelDelete = async (modelId: string) => { - await invoke("delete_model", { modelId }); - await loadModels(); - setModelError(null); + const result = await commands.deleteModel(modelId); + if (result.status === "ok") { + await loadModels(); + setModelError(null); + } }; return ( diff --git a/src/components/onboarding/ModelCard.tsx b/src/components/onboarding/ModelCard.tsx index ff5141b..0c7b34d 100644 --- a/src/components/onboarding/ModelCard.tsx +++ b/src/components/onboarding/ModelCard.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Download } from "lucide-react"; -import { ModelInfo } from "../../lib/types"; +import type { ModelInfo } from "@/bindings"; import { formatModelSize } from "../../lib/utils/format"; import Badge from "../ui/Badge"; @@ -42,7 +42,7 @@ const ModelCard: React.FC = ({

{model.name}

- + {isFeatured && Recommended}

diff --git a/src/components/onboarding/Onboarding.tsx b/src/components/onboarding/Onboarding.tsx index 7b503a9..edf4511 100644 --- a/src/components/onboarding/Onboarding.tsx +++ b/src/components/onboarding/Onboarding.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from "react"; -import { invoke } from "@tauri-apps/api/core"; -import { ModelInfo } from "../../lib/types"; +import { commands, type ModelInfo } from "@/bindings"; import ModelCard from "./ModelCard"; import HandyTextLogo from "../icons/HandyTextLogo"; @@ -19,9 +18,13 @@ const Onboarding: React.FC = ({ onModelSelected }) => { const loadModels = async () => { try { - const models: ModelInfo[] = await invoke("get_available_models"); - // Only show downloadable models for onboarding - setAvailableModels(models.filter((m) => !m.is_downloaded)); + const result = await commands.getAvailableModels(); + if (result.status === "ok") { + // Only show downloadable models for onboarding + setAvailableModels(result.data.filter((m) => !m.is_downloaded)); + } else { + setError("Failed to load available models"); + } } catch (err) { console.error("Failed to load models:", err); setError("Failed to load available models"); @@ -36,7 +39,12 @@ const Onboarding: React.FC = ({ onModelSelected }) => { onModelSelected(); try { - await invoke("download_model", { modelId }); + const result = await commands.downloadModel(modelId); + if (result.status === "error") { + console.error("Download failed:", result.error); + setError(`Failed to download model: ${result.error}`); + setDownloading(false); + } } catch (err) { console.error("Download failed:", err); setError(`Failed to download model: ${err}`); @@ -80,7 +88,7 @@ const Onboarding: React.FC = ({ onModelSelected }) => { {availableModels .filter((model) => !getRecommendedBadge(model.id)) - .sort((a, b) => a.size_mb - b.size_mb) + .sort((a, b) => Number(a.size_mb) - Number(b.size_mb)) .map((model) => ( = ({ useEffect(() => { const loadAppDirectory = async () => { try { - const result = await invoke("get_app_dir_path"); - setAppDirPath(result); + const result = await commands.getAppDirPath(); + if (result.status === "ok") { + setAppDirPath(result.data); + } else { + setError(result.error); + } } catch (err) { setError( err instanceof Error ? err.message : "Failed to load app directory", @@ -36,7 +40,7 @@ export const AppDataDirectory: React.FC = ({ const handleOpen = async () => { if (!appDirPath) return; try { - await invoke("open_app_data_dir"); + await commands.openAppDataDir(); } catch (openError) { console.error("Failed to open app data directory:", openError); } diff --git a/src/components/settings/ClamshellMicrophoneSelector.tsx b/src/components/settings/ClamshellMicrophoneSelector.tsx index 06117da..a702380 100644 --- a/src/components/settings/ClamshellMicrophoneSelector.tsx +++ b/src/components/settings/ClamshellMicrophoneSelector.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { invoke } from "@tauri-apps/api/core"; +import { commands } from "@/bindings"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; import { ResetButton } from "../ui/ResetButton"; @@ -27,8 +27,12 @@ export const ClamshellMicrophoneSelector: React.FC { const checkIsLaptop = async () => { try { - const result = await invoke("is_laptop"); - setIsLaptop(result); + const result = await commands.isLaptop(); + if (result.status === "ok") { + setIsLaptop(result.data); + } else { + setIsLaptop(false); + } } catch (error) { console.error("Failed to check if device is laptop:", error); setIsLaptop(false); diff --git a/src/components/settings/ClipboardHandling.tsx b/src/components/settings/ClipboardHandling.tsx index 457fb76..a47d921 100644 --- a/src/components/settings/ClipboardHandling.tsx +++ b/src/components/settings/ClipboardHandling.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; import { useSettings } from "../../hooks/useSettings"; -import type { ClipboardHandling } from "../../lib/types"; +import type { ClipboardHandling } from "@/bindings"; interface ClipboardHandlingProps { descriptionMode?: "inline" | "tooltip"; diff --git a/src/components/settings/HandyShortcut.tsx b/src/components/settings/HandyShortcut.tsx index 660914c..8b64e71 100644 --- a/src/components/settings/HandyShortcut.tsx +++ b/src/components/settings/HandyShortcut.tsx @@ -9,7 +9,7 @@ import { import { ResetButton } from "../ui/ResetButton"; import { SettingContainer } from "../ui/SettingContainer"; import { useSettings } from "../../hooks/useSettings"; -import { invoke } from "@tauri-apps/api/core"; +import { commands } from "@/bindings"; import { toast } from "sonner"; interface HandyShortcutProps { @@ -80,7 +80,7 @@ export const HandyShortcut: React.FC = ({ if (editingShortcutId && originalBinding) { try { await updateBinding(editingShortcutId, originalBinding); - await invoke("resume_binding", { id: editingShortcutId }).catch( + await commands.resumeBinding(editingShortcutId).catch( console.error, ); } catch (error) { @@ -88,7 +88,7 @@ export const HandyShortcut: React.FC = ({ toast.error("Failed to restore original shortcut"); } } else if (editingShortcutId) { - await invoke("resume_binding", { id: editingShortcutId }).catch( + await commands.resumeBinding(editingShortcutId).catch( console.error, ); } @@ -134,7 +134,7 @@ export const HandyShortcut: React.FC = ({ try { await updateBinding(editingShortcutId, newShortcut); // Re-register the shortcut now that recording is finished - await invoke("resume_binding", { id: editingShortcutId }).catch( + await commands.resumeBinding(editingShortcutId).catch( console.error, ); } catch (error) { @@ -145,7 +145,7 @@ export const HandyShortcut: React.FC = ({ if (originalBinding) { try { await updateBinding(editingShortcutId, originalBinding); - await invoke("resume_binding", { id: editingShortcutId }).catch( + await commands.resumeBinding(editingShortcutId).catch( console.error, ); } catch (resetError) { @@ -173,7 +173,7 @@ export const HandyShortcut: React.FC = ({ if (editingShortcutId && originalBinding) { try { await updateBinding(editingShortcutId, originalBinding); - await invoke("resume_binding", { id: editingShortcutId }).catch( + await commands.resumeBinding(editingShortcutId).catch( console.error, ); } catch (error) { @@ -181,7 +181,7 @@ export const HandyShortcut: React.FC = ({ toast.error("Failed to restore original shortcut"); } } else if (editingShortcutId) { - invoke("resume_binding", { id: editingShortcutId }).catch( + commands.resumeBinding(editingShortcutId).catch( console.error, ); } @@ -217,7 +217,7 @@ export const HandyShortcut: React.FC = ({ if (editingShortcutId === id) return; // Already editing this shortcut // Suspend current binding to avoid firing while recording - await invoke("suspend_binding", { id }).catch(console.error); + await commands.suspendBinding(id).catch(console.error); // Store the original binding to restore if canceled setOriginalBinding(bindings[id]?.current_binding || ""); diff --git a/src/components/settings/HistoryLimit.tsx b/src/components/settings/HistoryLimit.tsx index d685cae..2d45fc3 100644 --- a/src/components/settings/HistoryLimit.tsx +++ b/src/components/settings/HistoryLimit.tsx @@ -14,12 +14,12 @@ export const HistoryLimit: React.FC = ({ }) => { const { getSetting, updateSetting, isUpdating } = useSettings(); - const historyLimit = getSetting("history_limit") ?? 5; + const historyLimit = Number(getSetting("history_limit") ?? "5"); const handleChange = async (event: React.ChangeEvent) => { const value = parseInt(event.target.value, 10); if (!isNaN(value) && value >= 0) { - updateSetting("history_limit", value); + updateSetting("history_limit", value.toString()); } }; diff --git a/src/components/settings/ModelUnloadTimeout.tsx b/src/components/settings/ModelUnloadTimeout.tsx index 6df72f3..dd992bf 100644 --- a/src/components/settings/ModelUnloadTimeout.tsx +++ b/src/components/settings/ModelUnloadTimeout.tsx @@ -1,7 +1,6 @@ import React, { useMemo } from "react"; -import { invoke } from "@tauri-apps/api/core"; import { useSettings } from "../../hooks/useSettings"; -import { ModelUnloadTimeout } from "../../lib/types"; +import { commands, type ModelUnloadTimeout } from "@/bindings"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; @@ -35,7 +34,7 @@ export const ModelUnloadTimeoutSetting: React.FC = ({ const newTimeout = event.target.value as ModelUnloadTimeout; try { - await invoke("set_model_unload_timeout", { timeout: newTimeout }); + await commands.setModelUnloadTimeout(newTimeout); updateSetting("model_unload_timeout", newTimeout); } catch (error) { console.error("Failed to update model unload timeout:", error); diff --git a/src/components/settings/OutputDeviceSelector.tsx b/src/components/settings/OutputDeviceSelector.tsx index 9613fd5..ec45ee2 100644 --- a/src/components/settings/OutputDeviceSelector.tsx +++ b/src/components/settings/OutputDeviceSelector.tsx @@ -3,7 +3,7 @@ import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; import { ResetButton } from "../ui/ResetButton"; import { useSettings } from "../../hooks/useSettings"; -import { AudioDevice } from "../../lib/types"; +import type { AudioDevice } from "@/bindings"; interface OutputDeviceSelectorProps { descriptionMode?: "inline" | "tooltip"; diff --git a/src/components/settings/PasteMethod.tsx b/src/components/settings/PasteMethod.tsx index 5e4822c..89807fd 100644 --- a/src/components/settings/PasteMethod.tsx +++ b/src/components/settings/PasteMethod.tsx @@ -3,7 +3,7 @@ import { type as getOsType } from "@tauri-apps/plugin-os"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; import { useSettings } from "../../hooks/useSettings"; -import type { PasteMethod } from "../../lib/types"; +import type { PasteMethod } from "@/bindings"; interface PasteMethodProps { descriptionMode?: "inline" | "tooltip"; diff --git a/src/components/settings/PostProcessingSettingsApi/usePostProcessProviderState.ts b/src/components/settings/PostProcessingSettingsApi/usePostProcessProviderState.ts index dc657cc..d603335 100644 --- a/src/components/settings/PostProcessingSettingsApi/usePostProcessProviderState.ts +++ b/src/components/settings/PostProcessingSettingsApi/usePostProcessProviderState.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo } from "react"; import { useSettings } from "../../../hooks/useSettings"; import { useSettingsStore } from "../../../stores/settingsStore"; -import type { PostProcessProvider } from "../../../lib/types"; +import type { PostProcessProvider } from "@/bindings"; import type { ModelOption } from "./types"; import type { DropdownOption } from "../../ui/Dropdown"; diff --git a/src/components/settings/RecordingRetentionPeriod.tsx b/src/components/settings/RecordingRetentionPeriod.tsx index 85b7ce2..819472e 100644 --- a/src/components/settings/RecordingRetentionPeriod.tsx +++ b/src/components/settings/RecordingRetentionPeriod.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; import { useSettings } from "../../hooks/useSettings"; -import { RecordingRetentionPeriod } from "../../lib/types"; +import { RecordingRetentionPeriod } from "@/bindings"; interface RecordingRetentionPeriodProps { descriptionMode?: "inline" | "tooltip"; diff --git a/src/components/settings/ShowOverlay.tsx b/src/components/settings/ShowOverlay.tsx index e7475b3..5b317c9 100644 --- a/src/components/settings/ShowOverlay.tsx +++ b/src/components/settings/ShowOverlay.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; import { useSettings } from "../../hooks/useSettings"; -import type { OverlayPosition } from "../../lib/types"; +import type { OverlayPosition } from "@/bindings"; interface ShowOverlayProps { descriptionMode?: "inline" | "tooltip"; diff --git a/src/components/settings/debug/LogDirectory.tsx b/src/components/settings/debug/LogDirectory.tsx index 0225134..c07f8a9 100644 --- a/src/components/settings/debug/LogDirectory.tsx +++ b/src/components/settings/debug/LogDirectory.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { invoke } from "@tauri-apps/api/core"; +import { commands } from "@/bindings"; import { SettingContainer } from "../../ui/SettingContainer"; import { Button } from "../../ui/Button"; @@ -19,8 +19,12 @@ export const LogDirectory: React.FC = ({ useEffect(() => { const loadLogDirectory = async () => { try { - const result = await invoke("get_log_dir_path"); - setLogDir(result); + const result = await commands.getLogDirPath(); + if (result.status === "ok") { + setLogDir(result.data); + } else { + setError(result.error); + } } catch (err) { const errorMessage = err && typeof err === "object" && "message" in err @@ -38,7 +42,7 @@ export const LogDirectory: React.FC = ({ const handleOpen = async () => { if (!logDir) return; try { - await invoke("open_log_dir"); + await commands.openLogDir(); } catch (openError) { console.error("Failed to open log directory:", openError); } diff --git a/src/components/settings/debug/LogLevelSelector.tsx b/src/components/settings/debug/LogLevelSelector.tsx index c88fb97..b3f3e5d 100644 --- a/src/components/settings/debug/LogLevelSelector.tsx +++ b/src/components/settings/debug/LogLevelSelector.tsx @@ -2,13 +2,14 @@ import React from "react"; import { SettingContainer } from "../../ui/SettingContainer"; import { Dropdown, type DropdownOption } from "../../ui/Dropdown"; import { useSettings } from "../../../hooks/useSettings"; +import type { LogLevel } from "../../../bindings"; const LOG_LEVEL_OPTIONS: DropdownOption[] = [ - { value: "5", label: "Error" }, - { value: "4", label: "Warn" }, - { value: "3", label: "Info" }, - { value: "2", label: "Debug" }, - { value: "1", label: "Trace" }, + { value: "error", label: "Error" }, + { value: "warn", label: "Warn" }, + { value: "info", label: "Info" }, + { value: "debug", label: "Debug" }, + { value: "trace", label: "Trace" }, ]; interface LogLevelSelectorProps { @@ -21,19 +22,13 @@ export const LogLevelSelector: React.FC = ({ grouped = false, }) => { const { settings, updateSetting, isUpdating } = useSettings(); - const currentLevel = settings?.log_level ?? 2; - const isLevelUpdating = isUpdating("log_level"); - - const selectedValue = currentLevel.toString(); + const currentLevel = settings?.log_level ?? "debug"; const handleSelect = async (value: string) => { - const parsed = Number.parseInt(value, 10); - if (Number.isNaN(parsed) || parsed === currentLevel) { - return; - } + if (value === currentLevel) return; try { - await updateSetting("log_level", parsed as typeof currentLevel); + await updateSetting("log_level", value as LogLevel); } catch (error) { console.error("Failed to update log level:", error); } @@ -47,14 +42,12 @@ export const LogLevelSelector: React.FC = ({ grouped={grouped} layout="horizontal" > -

- -
+ ); }; diff --git a/src/components/settings/history/HistorySettings.tsx b/src/components/settings/history/HistorySettings.tsx index 3a462fc..3835ea6 100644 --- a/src/components/settings/history/HistorySettings.tsx +++ b/src/components/settings/history/HistorySettings.tsx @@ -2,17 +2,9 @@ import React, { useState, useEffect, useCallback } from "react"; import { AudioPlayer } from "../../ui/AudioPlayer"; import { Button } from "../../ui/Button"; import { Copy, Star, Check, Trash2, FolderOpen } from "lucide-react"; -import { convertFileSrc, invoke } from "@tauri-apps/api/core"; +import { convertFileSrc } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; - -interface HistoryEntry { - id: number; - file_name: string; - timestamp: number; - saved: boolean; - title: string; - transcription_text: string; -} +import { commands, type HistoryEntry } from "@/bindings"; interface OpenRecordingsButtonProps { onClick: () => void; @@ -39,8 +31,10 @@ export const HistorySettings: React.FC = () => { const loadHistoryEntries = useCallback(async () => { try { - const entries = await invoke("get_history_entries"); - setHistoryEntries(entries); + const result = await commands.getHistoryEntries(); + if (result.status === "ok") { + setHistoryEntries(result.data); + } } catch (error) { console.error("Failed to load history entries:", error); } finally { @@ -73,9 +67,9 @@ export const HistorySettings: React.FC = () => { }; }, [loadHistoryEntries]); - const toggleSaved = async (id: number) => { + const toggleSaved = async (id: string) => { try { - await invoke("toggle_history_entry_saved", { id }); + await commands.toggleHistoryEntrySaved(id); // No need to reload here - the event listener will handle it } catch (error) { console.error("Failed to toggle saved status:", error); @@ -92,20 +86,20 @@ export const HistorySettings: React.FC = () => { const getAudioUrl = async (fileName: string) => { try { - const filePath = await invoke("get_audio_file_path", { - fileName, - }); - - return convertFileSrc(`${filePath}`, "asset"); + const result = await commands.getAudioFilePath(fileName); + if (result.status === "ok") { + return convertFileSrc(`${result.data}`, "asset"); + } + return null; } catch (error) { console.error("Failed to get audio file path:", error); return null; } }; - const deleteAudioEntry = async (id: number) => { + const deleteAudioEntry = async (id: string) => { try { - await invoke("delete_history_entry", { id }); + await commands.deleteHistoryEntry(id); } catch (error) { console.error("Failed to delete audio entry:", error); throw error; @@ -114,7 +108,7 @@ export const HistorySettings: React.FC = () => { const openRecordingsFolder = async () => { try { - await invoke("open_recordings_folder"); + await commands.openRecordingsFolder(); } catch (error) { console.error("Failed to open recordings folder:", error); } @@ -199,7 +193,7 @@ interface HistoryEntryProps { onToggleSaved: () => void; onCopyText: () => void; getAudioUrl: (fileName: string) => Promise; - deleteAudio: (id: number) => Promise; + deleteAudio: (id: string) => Promise; } const HistoryEntryComponent: React.FC = ({ diff --git a/src/components/settings/post-processing/PostProcessingSettings.tsx b/src/components/settings/post-processing/PostProcessingSettings.tsx index bfd6971..b5affd6 100644 --- a/src/components/settings/post-processing/PostProcessingSettings.tsx +++ b/src/components/settings/post-processing/PostProcessingSettings.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; -import { invoke } from "@tauri-apps/api/core"; import { RefreshCcw } from "lucide-react"; +import { commands } from "@/bindings"; import { SettingsGroup } from "../../ui/SettingsGroup"; import { SettingContainer } from "../../ui/SettingContainer"; @@ -16,7 +16,7 @@ import { ApiKeyField } from "../PostProcessingSettingsApi/ApiKeyField"; import { ModelSelect } from "../PostProcessingSettingsApi/ModelSelect"; import { usePostProcessProviderState } from "../PostProcessingSettingsApi/usePostProcessProviderState"; import { useSettings } from "../../../hooks/useSettings"; -import type { LLMPrompt } from "../../../lib/types"; +import type { LLMPrompt } from "@/bindings"; const DisabledNotice: React.FC<{ children: React.ReactNode }> = ({ children, @@ -178,13 +178,15 @@ const PostProcessingSettingsPromptsComponent: React.FC = () => { if (!draftName.trim() || !draftText.trim()) return; try { - const newPrompt = await invoke("add_post_process_prompt", { - name: draftName.trim(), - prompt: draftText.trim(), - }); - await refreshSettings(); - updateSetting("post_process_selected_prompt_id", newPrompt.id); - setIsCreating(false); + const result = await commands.addPostProcessPrompt( + draftName.trim(), + draftText.trim() + ); + if (result.status === "ok") { + await refreshSettings(); + updateSetting("post_process_selected_prompt_id", result.data.id); + setIsCreating(false); + } } catch (error) { console.error("Failed to create prompt:", error); } @@ -194,11 +196,11 @@ const PostProcessingSettingsPromptsComponent: React.FC = () => { if (!selectedPromptId || !draftName.trim() || !draftText.trim()) return; try { - await invoke("update_post_process_prompt", { - id: selectedPromptId, - name: draftName.trim(), - prompt: draftText.trim(), - }); + await commands.updatePostProcessPrompt( + selectedPromptId, + draftName.trim(), + draftText.trim() + ); await refreshSettings(); } catch (error) { console.error("Failed to update prompt:", error); @@ -209,7 +211,7 @@ const PostProcessingSettingsPromptsComponent: React.FC = () => { if (!promptId) return; try { - await invoke("delete_post_process_prompt", { id: promptId }); + await commands.deletePostProcessPrompt(promptId); await refreshSettings(); setIsCreating(false); } catch (error) { diff --git a/src/hooks/useModels.ts b/src/hooks/useModels.ts index 9f0b629..2e804a1 100644 --- a/src/hooks/useModels.ts +++ b/src/hooks/useModels.ts @@ -1,21 +1,6 @@ import { useEffect, useState } from "react"; -import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; - -interface ModelInfo { - id: string; - name: string; - description: string; - filename: string; - url?: string; - size_mb: number; - is_downloaded: boolean; - is_downloading: boolean; - partial_size: number; - is_directory: boolean; - accuracy_score: number; - speed_score: number; -} +import { commands, type ModelInfo } from "@/bindings"; interface DownloadProgress { model_id: string; @@ -43,9 +28,13 @@ export const useModels = () => { const loadModels = async () => { try { - const modelList = await invoke("get_available_models"); - setModels(modelList); - setError(null); + const result = await commands.getAvailableModels(); + if (result.status === "ok") { + setModels(result.data); + setError(null); + } else { + setError(`Failed to load models: ${result.error}`); + } } catch (err) { setError(`Failed to load models: ${err}`); } finally { @@ -55,8 +44,10 @@ export const useModels = () => { const loadCurrentModel = async () => { try { - const current = await invoke("get_current_model"); - setCurrentModel(current); + const result = await commands.getCurrentModel(); + if (result.status === "ok") { + setCurrentModel(result.data); + } } catch (err) { console.error("Failed to load current model:", err); } @@ -64,10 +55,14 @@ export const useModels = () => { const checkFirstRun = async () => { try { - const hasModels = await invoke("has_any_models_available"); - setHasAnyModels(hasModels); - setIsFirstRun(!hasModels); - return !hasModels; + const result = await commands.hasAnyModelsAvailable(); + if (result.status === "ok") { + const hasModels = result.data; + setHasAnyModels(hasModels); + setIsFirstRun(!hasModels); + return !hasModels; + } + return false; } catch (err) { console.error("Failed to check model availability:", err); return false; @@ -77,11 +72,16 @@ export const useModels = () => { const selectModel = async (modelId: string) => { try { setError(null); - await invoke("set_active_model", { modelId }); - setCurrentModel(modelId); - setIsFirstRun(false); - setHasAnyModels(true); - return true; + const result = await commands.setActiveModel(modelId); + if (result.status === "ok") { + setCurrentModel(modelId); + setIsFirstRun(false); + setHasAnyModels(true); + return true; + } else { + setError(`Failed to switch to model: ${result.error}`); + return false; + } } catch (err) { setError(`Failed to switch to model: ${err}`); return false; @@ -92,8 +92,18 @@ export const useModels = () => { try { setError(null); setDownloadingModels((prev) => new Set(prev.add(modelId))); - await invoke("download_model", { modelId }); - return true; + const result = await commands.downloadModel(modelId); + if (result.status === "ok") { + return true; + } else { + setError(`Failed to download model: ${result.error}`); + setDownloadingModels((prev) => { + const next = new Set(prev); + next.delete(modelId); + return next; + }); + return false; + } } catch (err) { setError(`Failed to download model: ${err}`); setDownloadingModels((prev) => { @@ -108,9 +118,14 @@ export const useModels = () => { const deleteModel = async (modelId: string) => { try { setError(null); - await invoke("delete_model", { modelId }); - await loadModels(); // Refresh the list - return true; + const result = await commands.deleteModel(modelId); + if (result.status === "ok") { + await loadModels(); // Refresh the list + return true; + } else { + setError(`Failed to delete model: ${result.error}`); + return false; + } } catch (err) { setError(`Failed to delete model: ${err}`); return false; diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index f664c1b..ab6b830 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { useSettingsStore } from "../stores/settingsStore"; -import { Settings, AudioDevice } from "../lib/types"; +import type { AppSettings as Settings, AudioDevice } from "@/bindings"; interface UseSettingsReturn { // State diff --git a/src/lib/types.ts b/src/lib/types.ts deleted file mode 100644 index b56433d..0000000 --- a/src/lib/types.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { z } from "zod"; - -export const ShortcutBindingSchema = z.object({ - id: z.string(), - name: z.string(), - description: z.string(), - default_binding: z.string(), - current_binding: z.string(), -}); - -export const ShortcutBindingsMapSchema = z.record( - z.string(), - ShortcutBindingSchema, -); - -export const AudioDeviceSchema = z.object({ - index: z.string(), - name: z.string(), - is_default: z.boolean(), -}); - -export const OverlayPositionSchema = z.enum(["none", "top", "bottom"]); -export type OverlayPosition = z.infer; - -export const ModelUnloadTimeoutSchema = z.enum([ - "never", - "immediately", - "min2", - "min5", - "min10", - "min15", - "hour1", - "sec5", -]); -export type ModelUnloadTimeout = z.infer; - -export const PasteMethodSchema = z.enum(["ctrl_v", "direct", "shift_insert"]); -export type PasteMethod = z.infer; - -export const ClipboardHandlingSchema = z.enum([ - "dont_modify", - "copy_to_clipboard", -]); -export type ClipboardHandling = z.infer; - -export const LogLevelSchema = z.number().int().min(1).max(5).default(2); -export type LogLevelValue = z.infer; - -export const RecordingRetentionPeriodSchema = z.enum([ - "never", - "preserve_limit", - "days3", - "weeks2", - "months3", -]); -export type RecordingRetentionPeriod = z.infer< - typeof RecordingRetentionPeriodSchema ->; - -export const LLMPromptSchema = z.object({ - id: z.string(), - name: z.string(), - prompt: z.string(), -}); - -export type LLMPrompt = z.infer; - -export const PostProcessProviderSchema = z.object({ - id: z.string(), - label: z.string(), - base_url: z.string(), - allow_base_url_edit: z.boolean().optional().default(false), - models_endpoint: z.string().nullable().optional(), - kind: z - .enum(["openai_compatible", "anthropic"]) - .optional() - .default("openai_compatible"), -}); - -export type PostProcessProvider = z.infer; - -export const SettingsSchema = z.object({ - bindings: ShortcutBindingsMapSchema, - push_to_talk: z.boolean(), - audio_feedback: z.boolean(), - audio_feedback_volume: z.number().optional().default(1.0), - sound_theme: z - .enum(["marimba", "pop", "custom"]) - .optional() - .default("marimba"), - start_hidden: z.boolean().optional().default(false), - autostart_enabled: z.boolean().optional().default(false), - selected_model: z.string(), - always_on_microphone: z.boolean(), - selected_microphone: z.string().nullable().optional(), - clamshell_microphone: z.string().nullable().optional(), - selected_output_device: z.string().nullable().optional(), - translate_to_english: z.boolean(), - selected_language: z.string(), - overlay_position: OverlayPositionSchema, - debug_mode: z.boolean(), - log_level: LogLevelSchema.optional().default(2), - custom_words: z.array(z.string()).optional().default([]), - model_unload_timeout: ModelUnloadTimeoutSchema.optional().default("never"), - word_correction_threshold: z.number().optional().default(0.18), - history_limit: z.number().optional().default(5), - recording_retention_period: - RecordingRetentionPeriodSchema.optional().default("preserve_limit"), - paste_method: PasteMethodSchema.optional().default("ctrl_v"), - clipboard_handling: ClipboardHandlingSchema.optional().default("dont_modify"), - post_process_enabled: z.boolean().optional().default(false), - post_process_provider_id: z.string().optional().default("openai"), - post_process_providers: z - .array(PostProcessProviderSchema) - .optional() - .default([]), - post_process_api_keys: z.record(z.string()).optional().default({}), - post_process_models: z.record(z.string()).optional().default({}), - post_process_prompts: z.array(LLMPromptSchema).optional().default([]), - post_process_selected_prompt_id: z.string().nullable().optional(), - mute_while_recording: z.boolean().optional().default(false), -}); - -export const BindingResponseSchema = z.object({ - success: z.boolean(), - binding: ShortcutBindingSchema.nullable(), - error: z.string().nullable(), -}); - -export type AudioDevice = z.infer; -export type BindingResponse = z.infer; -export type ShortcutBinding = z.infer; -export type ShortcutBindingsMap = z.infer; -export type Settings = z.infer; - -export const ModelInfoSchema = z.object({ - id: z.string(), - name: z.string(), - description: z.string(), - filename: z.string(), - url: z.string().optional(), - size_mb: z.number(), - is_downloaded: z.boolean(), - is_downloading: z.boolean(), - partial_size: z.number(), - is_directory: z.boolean(), - accuracy_score: z.number(), - speed_score: z.number(), -}); - -export type ModelInfo = z.infer; diff --git a/src/overlay/RecordingOverlay.tsx b/src/overlay/RecordingOverlay.tsx index 85a54fb..1b841c1 100644 --- a/src/overlay/RecordingOverlay.tsx +++ b/src/overlay/RecordingOverlay.tsx @@ -1,4 +1,3 @@ -import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import React, { useEffect, useRef, useState } from "react"; import { @@ -7,6 +6,7 @@ import { CancelIcon, } from "../components/icons"; import "./RecordingOverlay.css"; +import { commands } from "@/bindings"; type OverlayState = "recording" | "transcribing"; @@ -93,7 +93,7 @@ const RecordingOverlay: React.FC = () => {
{ - invoke("cancel_operation"); + commands.cancelOperation(); }} > diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts index adecaa7..ce9b7ba 100644 --- a/src/stores/settingsStore.ts +++ b/src/stores/settingsStore.ts @@ -1,10 +1,11 @@ import { create } from "zustand"; import { subscribeWithSelector } from "zustand/middleware"; -import { invoke } from "@tauri-apps/api/core"; -import { Settings, AudioDevice } from "../lib/types"; +import type { AppSettings as Settings, AudioDevice } from "@/bindings"; +import { commands } from "@/bindings"; interface SettingsStore { settings: Settings | null; + defaultSettings: Settings | null; isLoading: boolean; isUpdating: Record; audioDevices: AudioDevice[]; @@ -14,6 +15,7 @@ interface SettingsStore { // Actions initialize: () => Promise; + loadDefaultSettings: () => Promise; updateSetting: ( key: K, value: Settings[K], @@ -48,6 +50,7 @@ interface SettingsStore { // Internal state setters setSettings: (settings: Settings | null) => void; + setDefaultSettings: (defaultSettings: Settings | null) => void; setLoading: (loading: boolean) => void; setUpdating: (key: string, updating: boolean) => void; setAudioDevices: (devices: AudioDevice[]) => void; @@ -55,29 +58,8 @@ interface SettingsStore { setCustomSounds: (sounds: { start: boolean; stop: boolean }) => void; } -// Note: Default post-processing settings are now managed in Rust -// Settings will always have providers after migration -const DEFAULT_SETTINGS: Partial = { - always_on_microphone: false, - audio_feedback: true, - audio_feedback_volume: 1.0, - sound_theme: "marimba", - start_hidden: false, - autostart_enabled: false, - push_to_talk: false, - selected_microphone: "Default", - clamshell_microphone: "Default", - selected_output_device: "Default", - translate_to_english: false, - selected_language: "auto", - overlay_position: "bottom", - debug_mode: false, - log_level: 2, - custom_words: [], - history_limit: 5, - recording_retention_period: "preserve_limit", - mute_while_recording: false, -}; +// Note: Default settings are now fetched from Rust via commands.getDefaultSettings() +// This ensures platform-specific defaults (like overlay_position, shortcuts, paste_method) work correctly const DEFAULT_AUDIO_DEVICE: AudioDevice = { index: "default", @@ -89,60 +71,61 @@ const settingUpdaters: { [K in keyof Settings]?: (value: Settings[K]) => Promise; } = { always_on_microphone: (value) => - invoke("update_microphone_mode", { alwaysOn: value }), + commands.updateMicrophoneMode(value as boolean), audio_feedback: (value) => - invoke("change_audio_feedback_setting", { enabled: value }), + commands.changeAudioFeedbackSetting(value as boolean), audio_feedback_volume: (value) => - invoke("change_audio_feedback_volume_setting", { volume: value }), + commands.changeAudioFeedbackVolumeSetting(value as number), sound_theme: (value) => - invoke("change_sound_theme_setting", { theme: value }), + commands.changeSoundThemeSetting(value as string), start_hidden: (value) => - invoke("change_start_hidden_setting", { enabled: value }), + commands.changeStartHiddenSetting(value as boolean), autostart_enabled: (value) => - invoke("change_autostart_setting", { enabled: value }), - push_to_talk: (value) => invoke("change_ptt_setting", { enabled: value }), + commands.changeAutostartSetting(value as boolean), + push_to_talk: (value) => commands.changePttSetting(value as boolean), selected_microphone: (value) => - invoke("set_selected_microphone", { - deviceName: value === "Default" ? "default" : value, - }), + commands.setSelectedMicrophone( + (value as string) === "Default" || value === null ? "default" : (value as string) + ), clamshell_microphone: (value) => - invoke("set_clamshell_microphone", { - deviceName: value === "Default" ? "default" : value, - }), + commands.setClamshellMicrophone( + (value as string) === "Default" ? "default" : (value as string) + ), selected_output_device: (value) => - invoke("set_selected_output_device", { - deviceName: value === "Default" ? "default" : value, - }), + commands.setSelectedOutputDevice( + (value as string) === "Default" || value === null ? "default" : (value as string) + ), recording_retention_period: (value) => - invoke("update_recording_retention_period", { period: value }), + commands.updateRecordingRetentionPeriod(value as string), translate_to_english: (value) => - invoke("change_translate_to_english_setting", { enabled: value }), + commands.changeTranslateToEnglishSetting(value as boolean), selected_language: (value) => - invoke("change_selected_language_setting", { language: value }), + commands.changeSelectedLanguageSetting(value as string), overlay_position: (value) => - invoke("change_overlay_position_setting", { position: value }), + commands.changeOverlayPositionSetting(value as string), debug_mode: (value) => - invoke("change_debug_mode_setting", { enabled: value }), - custom_words: (value) => invoke("update_custom_words", { words: value }), + commands.changeDebugModeSetting(value as boolean), + custom_words: (value) => commands.updateCustomWords(value as string[]), word_correction_threshold: (value) => - invoke("change_word_correction_threshold_setting", { threshold: value }), + commands.changeWordCorrectionThresholdSetting(value as number), paste_method: (value) => - invoke("change_paste_method_setting", { method: value }), + commands.changePasteMethodSetting(value as string), clipboard_handling: (value) => - invoke("change_clipboard_handling_setting", { handling: value }), - history_limit: (value) => invoke("update_history_limit", { limit: value }), + commands.changeClipboardHandlingSetting(value as string), + history_limit: (value) => commands.updateHistoryLimit(value as string), post_process_enabled: (value) => - invoke("change_post_process_enabled_setting", { enabled: value }), + commands.changePostProcessEnabledSetting(value as boolean), post_process_selected_prompt_id: (value) => - invoke("set_post_process_selected_prompt", { id: value }), + commands.setPostProcessSelectedPrompt(value as string), mute_while_recording: (value) => - invoke("change_mute_while_recording_setting", { enabled: value }), - log_level: (value) => invoke("set_log_level", { level: value }), + commands.changeMuteWhileRecordingSetting(value as boolean), + log_level: (value) => commands.setLogLevel(value as any), }; export const useSettingsStore = create()( subscribeWithSelector((set, get) => ({ settings: null, + defaultSettings: null, isLoading: true, isUpdating: {}, audioDevices: [], @@ -152,6 +135,7 @@ export const useSettingsStore = create()( // Internal setters setSettings: (settings) => set({ settings }), + setDefaultSettings: (defaultSettings) => set({ defaultSettings }), setLoading: (isLoading) => set({ isLoading }), setUpdating: (key, updating) => set((state) => ({ @@ -168,48 +152,24 @@ export const useSettingsStore = create()( // Load settings from store refreshSettings: async () => { try { - const { load } = await import("@tauri-apps/plugin-store"); - const store = await load("settings_store.json", { - defaults: DEFAULT_SETTINGS, - autoSave: false, - }); - const settings = (await store.get("settings")) as Settings; - - // Load additional settings that come from invoke calls - const [ - microphoneMode, - selectedMicrophone, - clamshellMicrophone, - selectedOutputDevice, - ] = await Promise.allSettled([ - invoke("get_microphone_mode"), - invoke("get_selected_microphone"), - invoke("get_clamshell_microphone"), - invoke("get_selected_output_device"), - ]); - - // Merge all settings - const mergedSettings: Settings = { - ...settings, - always_on_microphone: - microphoneMode.status === "fulfilled" - ? (microphoneMode.value as boolean) - : false, - selected_microphone: - selectedMicrophone.status === "fulfilled" - ? (selectedMicrophone.value as string) - : "Default", - clamshell_microphone: - clamshellMicrophone.status === "fulfilled" - ? (clamshellMicrophone.value as string) - : "Default", - selected_output_device: - selectedOutputDevice.status === "fulfilled" - ? (selectedOutputDevice.value as string) - : "Default", - }; - - set({ settings: mergedSettings, isLoading: false }); + const result = await commands.getAppSettings(); + if (result.status === "ok") { + const settings = result.data; + const normalizedSettings: Settings = { + ...settings, + always_on_microphone: settings.always_on_microphone ?? false, + selected_microphone: + settings.selected_microphone ?? "Default", + clamshell_microphone: + settings.clamshell_microphone ?? "Default", + selected_output_device: + settings.selected_output_device ?? "Default", + }; + set({ settings: normalizedSettings, isLoading: false }); + } else { + console.error("Failed to load settings:", result.error); + set({ isLoading: false }); + } } catch (error) { console.error("Failed to load settings:", error); set({ isLoading: false }); @@ -219,16 +179,18 @@ export const useSettingsStore = create()( // Load audio devices refreshAudioDevices: async () => { try { - const devices: AudioDevice[] = await invoke( - "get_available_microphones", - ); - const devicesWithDefault = [ - DEFAULT_AUDIO_DEVICE, - ...devices.filter( - (d) => d.name !== "Default" && d.name !== "default", - ), - ]; - set({ audioDevices: devicesWithDefault }); + const result = await commands.getAvailableMicrophones(); + if (result.status === "ok") { + const devicesWithDefault = [ + DEFAULT_AUDIO_DEVICE, + ...result.data.filter( + (d) => d.name !== "Default" && d.name !== "default", + ), + ]; + set({ audioDevices: devicesWithDefault }); + } else { + set({ audioDevices: [DEFAULT_AUDIO_DEVICE] }); + } } catch (error) { console.error("Failed to load audio devices:", error); set({ audioDevices: [DEFAULT_AUDIO_DEVICE] }); @@ -238,16 +200,18 @@ export const useSettingsStore = create()( // Load output devices refreshOutputDevices: async () => { try { - const devices: AudioDevice[] = await invoke( - "get_available_output_devices", - ); - const devicesWithDefault = [ - DEFAULT_AUDIO_DEVICE, - ...devices.filter( - (d) => d.name !== "Default" && d.name !== "default", - ), - ]; - set({ outputDevices: devicesWithDefault }); + const result = await commands.getAvailableOutputDevices(); + if (result.status === "ok") { + const devicesWithDefault = [ + DEFAULT_AUDIO_DEVICE, + ...result.data.filter( + (d) => d.name !== "Default" && d.name !== "default", + ), + ]; + set({ outputDevices: devicesWithDefault }); + } else { + set({ outputDevices: [DEFAULT_AUDIO_DEVICE] }); + } } catch (error) { console.error("Failed to load output devices:", error); set({ outputDevices: [DEFAULT_AUDIO_DEVICE] }); @@ -257,7 +221,7 @@ export const useSettingsStore = create()( // Play a test sound playTestSound: async (soundType: "start" | "stop") => { try { - await invoke("play_test_sound", { soundType }); + await commands.playTestSound(soundType); } catch (error) { console.error(`Failed to play test sound (${soundType}):`, error); } @@ -265,8 +229,8 @@ export const useSettingsStore = create()( checkCustomSounds: async () => { try { - const sounds = await invoke("check_custom_sounds"); - get().setCustomSounds(sounds as { start: boolean; stop: boolean }); + const sounds = await commands.checkCustomSounds(); + get().setCustomSounds(sounds); } catch (error) { console.error("Failed to check custom sounds:", error); } @@ -306,9 +270,12 @@ export const useSettingsStore = create()( // Reset a setting to its default value resetSetting: async (key) => { - const defaultValue = DEFAULT_SETTINGS[key]; - if (defaultValue !== undefined) { - await get().updateSetting(key, defaultValue as any); + const { defaultSettings } = get(); + if (defaultSettings) { + const defaultValue = defaultSettings[key]; + if (defaultValue !== undefined) { + await get().updateSetting(key, defaultValue as any); + } } }, @@ -329,7 +296,7 @@ export const useSettingsStore = create()( bindings: { ...state.settings.bindings, [id]: { - ...state.settings.bindings[id], + ...state.settings.bindings[id]!, current_binding: binding, }, }, @@ -337,7 +304,7 @@ export const useSettingsStore = create()( : null, })); - await invoke("change_binding", { id, binding }); + await commands.changeBinding(id, binding); } catch (error) { console.error(`Failed to update binding ${id}:`, error); @@ -350,7 +317,7 @@ export const useSettingsStore = create()( bindings: { ...state.settings.bindings, [id]: { - ...state.settings.bindings[id], + ...state.settings.bindings[id]!, current_binding: originalBinding, }, }, @@ -371,7 +338,7 @@ export const useSettingsStore = create()( setUpdating(updateKey, true); try { - await invoke("reset_binding", { id }); + await commands.resetBinding(id); await refreshSettings(); } catch (error) { console.error(`Failed to reset binding ${id}:`, error); @@ -396,7 +363,7 @@ export const useSettingsStore = create()( } try { - await invoke("set_post_process_provider", { providerId }); + await commands.setPostProcessProvider(providerId); await refreshSettings(); } catch (error) { console.error("Failed to set post-process provider:", error); @@ -421,27 +388,16 @@ export const useSettingsStore = create()( const { setUpdating, refreshSettings } = get(); const updateKey = `post_process_${settingType}:${providerId}`; - // Map setting types to command names - const commandMap = { - base_url: "change_post_process_base_url_setting", - api_key: "change_post_process_api_key_setting", - model: "change_post_process_model_setting", - }; - - // Map setting types to param names - const paramMap = { - base_url: "baseUrl", - api_key: "apiKey", - model: "model", - }; - setUpdating(updateKey, true); try { - await invoke(commandMap[settingType], { - providerId, - [paramMap[settingType]]: value, - }); + if (settingType === "base_url") { + await commands.changePostProcessBaseUrlSetting(providerId, value); + } else if (settingType === "api_key") { + await commands.changePostProcessApiKeySetting(providerId, value); + } else if (settingType === "model") { + await commands.changePostProcessModelSetting(providerId, value); + } await refreshSettings(); } catch (error) { console.error( @@ -480,12 +436,14 @@ export const useSettingsStore = create()( try { // Call Tauri backend command instead of fetch - const models: string[] = await invoke("fetch_post_process_models", { - providerId, - }); - - setPostProcessModelOptions(providerId, models); - return models; + const result = await commands.fetchPostProcessModels(providerId); + if (result.status === "ok") { + setPostProcessModelOptions(providerId, result.data); + return result.data; + } else { + console.error("Failed to fetch models:", result.error); + return []; + } } catch (error) { console.error("Failed to fetch models:", error); // Don't cache empty array on error - let user retry @@ -503,6 +461,20 @@ export const useSettingsStore = create()( }, })), + // Load default settings from Rust + loadDefaultSettings: async () => { + try { + const result = await commands.getDefaultSettings(); + if (result.status === "ok") { + set({ defaultSettings: result.data }); + } else { + console.error("Failed to load default settings:", result.error); + } + } catch (error) { + console.error("Failed to load default settings:", error); + } + }, + // Initialize everything initialize: async () => { const { @@ -510,8 +482,10 @@ export const useSettingsStore = create()( refreshAudioDevices, refreshOutputDevices, checkCustomSounds, + loadDefaultSettings, } = get(); await Promise.all([ + loadDefaultSettings(), refreshSettings(), refreshAudioDevices(), refreshOutputDevices(), diff --git a/tsconfig.json b/tsconfig.json index db83aff..928c114 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,12 @@ "noEmit": true, "jsx": "react-jsx", + /* Path Aliases */ + "baseUrl": ".", + "paths": { + "@/bindings": ["./src/bindings.ts"] + }, + /* Linting */ "strict": true, "noUnusedLocals": false, diff --git a/vite.config.ts b/vite.config.ts index 9ea7659..e474209 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,6 +9,13 @@ const host = process.env.TAURI_DEV_HOST; export default defineConfig(async () => ({ plugins: [react(), tailwindcss()], + // Path aliases + resolve: { + alias: { + "@/bindings": resolve(__dirname, "./src/bindings.ts"), + }, + }, + // Multiple entry points for main app and overlay build: { rollupOptions: {