WIP: Add tauri-specta for generating TS bindings (#322)

* initial specta commands

* refactor to use the bindings in the ui

* merge fixes

* remove unused commands

* add clamshell to lib

* Remove Invoke from everything.

* fix settings not being loaded properly

* fix settings being overwritten due to deserialization failure + remove
provider kind

* fix model loading text bug
This commit is contained in:
CJ Pais 2025-11-27 17:07:22 +07:00 committed by GitHub
parent 5085485e67
commit 9521ab9332
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 1402 additions and 584 deletions

88
src-tauri/Cargo.lock generated
View file

@ -2,6 +2,12 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]] [[package]]
name = "adler2" name = "adler2"
version = "2.0.1" version = "2.0.1"
@ -2524,6 +2530,8 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"signal-hook", "signal-hook",
"specta",
"specta-typescript",
"strsim", "strsim",
"tar", "tar",
"tauri", "tauri",
@ -2542,6 +2550,7 @@ dependencies = [
"tauri-plugin-sql", "tauri-plugin-sql",
"tauri-plugin-store", "tauri-plugin-store",
"tauri-plugin-updater", "tauri-plugin-updater",
"tauri-specta",
"tokio", "tokio",
"transcribe-rs", "transcribe-rs",
"vad-rs", "vad-rs",
@ -4306,6 +4315,12 @@ dependencies = [
"windows-link 0.2.1", "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]] [[package]]
name = "pastey" name = "pastey"
version = "0.1.1" version = "0.1.1"
@ -5907,6 +5922,50 @@ dependencies = [
"system-deps", "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]] [[package]]
name = "spin" name = "spin"
version = "0.9.8" version = "0.9.8"
@ -6557,6 +6616,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_repr", "serde_repr",
"serialize-to-javascript", "serialize-to-javascript",
"specta",
"swift-rs", "swift-rs",
"tauri-build", "tauri-build",
"tauri-macros", "tauri-macros",
@ -6952,6 +7012,34 @@ dependencies = [
"wry", "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]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "2.8.0" version = "2.8.0"

View file

@ -68,6 +68,9 @@ tar = "0.4.44"
flate2 = "1.0" flate2 = "1.0"
transcribe-rs = "0.1.4" transcribe-rs = "0.1.4"
ferrous-opencc = "0.2.3" 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] [target.'cfg(unix)'.dependencies]
signal-hook = "0.3" signal-hook = "0.3"

View file

@ -40,12 +40,11 @@ fn send_paste_ctrl_v() -> Result<(), String> {
/// Sends a Shift+Insert paste command (Windows and Linux only). /// Sends a Shift+Insert paste command (Windows and Linux only).
/// This is more universal for terminal applications and legacy software. /// This is more universal for terminal applications and legacy software.
#[cfg(not(target_os = "macos"))]
fn send_paste_shift_insert() -> Result<(), String> { fn send_paste_shift_insert() -> Result<(), String> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let insert_key_code = Key::Other(0x2D); // VK_INSERT let insert_key_code = Key::Other(0x2D); // VK_INSERT
#[cfg(target_os = "linux")] #[cfg(not(target_os = "windows"))]
let insert_key_code = Key::Other(0x76); // XK_Insert (keycode 118 / 0x76) let insert_key_code = Key::Other(0x76); // XK_Insert (keycode 118 / 0x76, also used as fallback)
let mut enigo = Enigo::new(&Settings::default()) let mut enigo = Enigo::new(&Settings::default())
.map_err(|e| format!("Failed to initialize Enigo: {}", e))?; .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). /// 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. /// 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> { fn paste_via_clipboard_shift_insert(text: &str, app_handle: &AppHandle) -> Result<(), String> {
let clipboard = app_handle.clipboard(); let clipboard = app_handle.clipboard();
@ -145,7 +143,6 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
match paste_method { match paste_method {
PasteMethod::CtrlV => paste_via_clipboard_ctrl_v(&text, &app_handle)?, PasteMethod::CtrlV => paste_via_clipboard_ctrl_v(&text, &app_handle)?,
PasteMethod::Direct => paste_via_direct_input(&text)?, PasteMethod::Direct => paste_via_direct_input(&text)?,
#[cfg(not(target_os = "macos"))]
PasteMethod::ShiftInsert => paste_via_clipboard_shift_insert(&text, &app_handle)?, PasteMethod::ShiftInsert => paste_via_clipboard_shift_insert(&text, &app_handle)?,
} }

View file

@ -4,10 +4,11 @@ use crate::managers::audio::{AudioRecordingManager, MicrophoneMode};
use crate::settings::{get_settings, write_settings}; use crate::settings::{get_settings, write_settings};
use log::warn; use log::warn;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type;
use std::sync::Arc; use std::sync::Arc;
use tauri::{AppHandle, Manager}; use tauri::{AppHandle, Manager};
#[derive(Serialize)] #[derive(Serialize, Type)]
pub struct CustomSounds { pub struct CustomSounds {
start: bool, start: bool,
stop: bool, stop: bool,
@ -23,6 +24,7 @@ fn custom_sound_exists(app: &AppHandle, sound_type: &str) -> bool {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn check_custom_sounds(app: AppHandle) -> CustomSounds { pub fn check_custom_sounds(app: AppHandle) -> CustomSounds {
CustomSounds { CustomSounds {
start: custom_sound_exists(&app, "start"), 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 struct AudioDevice {
pub index: String, pub index: String,
pub name: String, pub name: String,
@ -38,6 +40,7 @@ pub struct AudioDevice {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn update_microphone_mode(app: AppHandle, always_on: bool) -> Result<(), String> { pub fn update_microphone_mode(app: AppHandle, always_on: bool) -> Result<(), String> {
// Update settings // Update settings
let mut settings = get_settings(&app); let mut settings = get_settings(&app);
@ -57,12 +60,14 @@ pub fn update_microphone_mode(app: AppHandle, always_on: bool) -> Result<(), Str
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn get_microphone_mode(app: AppHandle) -> Result<bool, String> { pub fn get_microphone_mode(app: AppHandle) -> Result<bool, String> {
let settings = get_settings(&app); let settings = get_settings(&app);
Ok(settings.always_on_microphone) Ok(settings.always_on_microphone)
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn get_available_microphones() -> Result<Vec<AudioDevice>, String> { pub fn get_available_microphones() -> Result<Vec<AudioDevice>, String> {
let devices = let devices =
list_input_devices().map_err(|e| format!("Failed to list audio devices: {}", e))?; list_input_devices().map_err(|e| format!("Failed to list audio devices: {}", e))?;
@ -83,6 +88,7 @@ pub fn get_available_microphones() -> Result<Vec<AudioDevice>, String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn set_selected_microphone(app: AppHandle, device_name: String) -> Result<(), String> { pub fn set_selected_microphone(app: AppHandle, device_name: String) -> Result<(), String> {
let mut settings = get_settings(&app); let mut settings = get_settings(&app);
settings.selected_microphone = if device_name == "default" { settings.selected_microphone = if device_name == "default" {
@ -101,6 +107,7 @@ pub fn set_selected_microphone(app: AppHandle, device_name: String) -> Result<()
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn get_selected_microphone(app: AppHandle) -> Result<String, String> { pub fn get_selected_microphone(app: AppHandle) -> Result<String, String> {
let settings = get_settings(&app); let settings = get_settings(&app);
Ok(settings Ok(settings
@ -109,6 +116,7 @@ pub fn get_selected_microphone(app: AppHandle) -> Result<String, String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn get_available_output_devices() -> Result<Vec<AudioDevice>, String> { pub fn get_available_output_devices() -> Result<Vec<AudioDevice>, String> {
let devices = let devices =
list_output_devices().map_err(|e| format!("Failed to list output devices: {}", e))?; list_output_devices().map_err(|e| format!("Failed to list output devices: {}", e))?;
@ -129,6 +137,7 @@ pub fn get_available_output_devices() -> Result<Vec<AudioDevice>, String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn set_selected_output_device(app: AppHandle, device_name: String) -> Result<(), String> { pub fn set_selected_output_device(app: AppHandle, device_name: String) -> Result<(), String> {
let mut settings = get_settings(&app); let mut settings = get_settings(&app);
settings.selected_output_device = if device_name == "default" { 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] #[tauri::command]
#[specta::specta]
pub fn get_selected_output_device(app: AppHandle) -> Result<String, String> { pub fn get_selected_output_device(app: AppHandle) -> Result<String, String> {
let settings = get_settings(&app); let settings = get_settings(&app);
Ok(settings Ok(settings
@ -149,6 +159,7 @@ pub fn get_selected_output_device(app: AppHandle) -> Result<String, String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn play_test_sound(app: AppHandle, sound_type: String) { pub fn play_test_sound(app: AppHandle, sound_type: String) {
let sound = match sound_type.as_str() { let sound = match sound_type.as_str() {
"start" => audio_feedback::SoundType::Start, "start" => audio_feedback::SoundType::Start,
@ -162,6 +173,7 @@ pub fn play_test_sound(app: AppHandle, sound_type: String) {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn set_clamshell_microphone(app: AppHandle, device_name: String) -> Result<(), String> { pub fn set_clamshell_microphone(app: AppHandle, device_name: String) -> Result<(), String> {
let mut settings = get_settings(&app); let mut settings = get_settings(&app);
settings.clamshell_microphone = if device_name == "default" { settings.clamshell_microphone = if device_name == "default" {
@ -174,6 +186,7 @@ pub fn set_clamshell_microphone(app: AppHandle, device_name: String) -> Result<(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn get_clamshell_microphone(app: AppHandle) -> Result<String, String> { pub fn get_clamshell_microphone(app: AppHandle) -> Result<String, String> {
let settings = get_settings(&app); let settings = get_settings(&app);
Ok(settings Ok(settings

View file

@ -3,6 +3,7 @@ use std::sync::Arc;
use tauri::{AppHandle, State}; use tauri::{AppHandle, State};
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn get_history_entries( pub async fn get_history_entries(
_app: AppHandle, _app: AppHandle,
history_manager: State<'_, Arc<HistoryManager>>, history_manager: State<'_, Arc<HistoryManager>>,
@ -14,6 +15,7 @@ pub async fn get_history_entries(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn toggle_history_entry_saved( pub async fn toggle_history_entry_saved(
_app: AppHandle, _app: AppHandle,
history_manager: State<'_, Arc<HistoryManager>>, history_manager: State<'_, Arc<HistoryManager>>,
@ -26,6 +28,7 @@ pub async fn toggle_history_entry_saved(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn get_audio_file_path( pub async fn get_audio_file_path(
_app: AppHandle, _app: AppHandle,
history_manager: State<'_, Arc<HistoryManager>>, history_manager: State<'_, Arc<HistoryManager>>,
@ -38,6 +41,7 @@ pub async fn get_audio_file_path(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn delete_history_entry( pub async fn delete_history_entry(
_app: AppHandle, _app: AppHandle,
history_manager: State<'_, Arc<HistoryManager>>, history_manager: State<'_, Arc<HistoryManager>>,
@ -50,6 +54,7 @@ pub async fn delete_history_entry(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn update_history_limit( pub async fn update_history_limit(
app: AppHandle, app: AppHandle,
history_manager: State<'_, Arc<HistoryManager>>, history_manager: State<'_, Arc<HistoryManager>>,
@ -67,6 +72,7 @@ pub async fn update_history_limit(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn update_recording_retention_period( pub async fn update_recording_retention_period(
app: AppHandle, app: AppHandle,
history_manager: State<'_, Arc<HistoryManager>>, history_manager: State<'_, Arc<HistoryManager>>,

View file

@ -3,17 +3,19 @@ pub mod history;
pub mod models; pub mod models;
pub mod transcription; 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::{AppHandle, Manager};
use tauri_plugin_log::LogLevel;
use tauri_plugin_opener::OpenerExt; use tauri_plugin_opener::OpenerExt;
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn cancel_operation(app: AppHandle) { pub fn cancel_operation(app: AppHandle) {
cancel_current_operation(&app); cancel_current_operation(&app);
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn get_app_dir_path(app: AppHandle) -> Result<String, String> { pub fn get_app_dir_path(app: AppHandle) -> Result<String, String> {
let app_data_dir = app let app_data_dir = app
.path() .path()
@ -24,6 +26,19 @@ pub fn get_app_dir_path(app: AppHandle) -> Result<String, String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn get_app_settings(app: AppHandle) -> Result<AppSettings, String> {
Ok(get_settings(&app))
}
#[tauri::command]
#[specta::specta]
pub fn get_default_settings() -> Result<AppSettings, String> {
Ok(crate::settings::get_default_settings())
}
#[tauri::command]
#[specta::specta]
pub fn get_log_dir_path(app: AppHandle) -> Result<String, String> { pub fn get_log_dir_path(app: AppHandle) -> Result<String, String> {
let log_dir = app let log_dir = app
.path() .path()
@ -33,22 +48,25 @@ pub fn get_log_dir_path(app: AppHandle) -> Result<String, String> {
Ok(log_dir.to_string_lossy().to_string()) Ok(log_dir.to_string_lossy().to_string())
} }
#[specta::specta]
#[tauri::command] #[tauri::command]
pub fn set_log_level(app: AppHandle, level: LogLevel) -> Result<(), String> { 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 // Update the file log level atomic so the filter picks up the new level
crate::FILE_LOG_LEVEL.store( crate::FILE_LOG_LEVEL.store(
log_level.to_level_filter() as u8, log_level.to_level_filter() as u8,
std::sync::atomic::Ordering::Relaxed, std::sync::atomic::Ordering::Relaxed,
); );
let mut settings = settings::get_settings(&app); let mut settings = get_settings(&app);
settings.log_level = level; settings.log_level = level;
settings::write_settings(&app, settings); write_settings(&app, settings);
Ok(()) Ok(())
} }
#[specta::specta]
#[tauri::command] #[tauri::command]
pub fn open_recordings_folder(app: AppHandle) -> Result<(), String> { pub fn open_recordings_folder(app: AppHandle) -> Result<(), String> {
let app_data_dir = app let app_data_dir = app
@ -66,6 +84,7 @@ pub fn open_recordings_folder(app: AppHandle) -> Result<(), String> {
Ok(()) Ok(())
} }
#[specta::specta]
#[tauri::command] #[tauri::command]
pub fn open_log_dir(app: AppHandle) -> Result<(), String> { pub fn open_log_dir(app: AppHandle) -> Result<(), String> {
let log_dir = app let log_dir = app
@ -81,6 +100,7 @@ pub fn open_log_dir(app: AppHandle) -> Result<(), String> {
Ok(()) Ok(())
} }
#[specta::specta]
#[tauri::command] #[tauri::command]
pub fn open_app_data_dir(app: AppHandle) -> Result<(), String> { pub fn open_app_data_dir(app: AppHandle) -> Result<(), String> {
let app_data_dir = app let app_data_dir = app

View file

@ -5,6 +5,7 @@ use std::sync::Arc;
use tauri::{AppHandle, State}; use tauri::{AppHandle, State};
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn get_available_models( pub async fn get_available_models(
model_manager: State<'_, Arc<ModelManager>>, model_manager: State<'_, Arc<ModelManager>>,
) -> Result<Vec<ModelInfo>, String> { ) -> Result<Vec<ModelInfo>, String> {
@ -12,6 +13,7 @@ pub async fn get_available_models(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn get_model_info( pub async fn get_model_info(
model_manager: State<'_, Arc<ModelManager>>, model_manager: State<'_, Arc<ModelManager>>,
model_id: String, model_id: String,
@ -20,6 +22,7 @@ pub async fn get_model_info(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn download_model( pub async fn download_model(
model_manager: State<'_, Arc<ModelManager>>, model_manager: State<'_, Arc<ModelManager>>,
model_id: String, model_id: String,
@ -31,6 +34,7 @@ pub async fn download_model(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn delete_model( pub async fn delete_model(
model_manager: State<'_, Arc<ModelManager>>, model_manager: State<'_, Arc<ModelManager>>,
model_id: String, model_id: String,
@ -41,6 +45,7 @@ pub async fn delete_model(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn set_active_model( pub async fn set_active_model(
app_handle: AppHandle, app_handle: AppHandle,
model_manager: State<'_, Arc<ModelManager>>, model_manager: State<'_, Arc<ModelManager>>,
@ -70,12 +75,14 @@ pub async fn set_active_model(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn get_current_model(app_handle: AppHandle) -> Result<String, String> { pub async fn get_current_model(app_handle: AppHandle) -> Result<String, String> {
let settings = get_settings(&app_handle); let settings = get_settings(&app_handle);
Ok(settings.selected_model) Ok(settings.selected_model)
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn get_transcription_model_status( pub async fn get_transcription_model_status(
transcription_manager: State<'_, Arc<TranscriptionManager>>, transcription_manager: State<'_, Arc<TranscriptionManager>>,
) -> Result<Option<String>, String> { ) -> Result<Option<String>, String> {
@ -83,6 +90,7 @@ pub async fn get_transcription_model_status(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn is_model_loading( pub async fn is_model_loading(
transcription_manager: State<'_, Arc<TranscriptionManager>>, transcription_manager: State<'_, Arc<TranscriptionManager>>,
) -> Result<bool, String> { ) -> Result<bool, String> {
@ -92,6 +100,7 @@ pub async fn is_model_loading(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn has_any_models_available( pub async fn has_any_models_available(
model_manager: State<'_, Arc<ModelManager>>, model_manager: State<'_, Arc<ModelManager>>,
) -> Result<bool, String> { ) -> Result<bool, String> {
@ -100,6 +109,7 @@ pub async fn has_any_models_available(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn has_any_models_or_downloads( pub async fn has_any_models_or_downloads(
model_manager: State<'_, Arc<ModelManager>>, model_manager: State<'_, Arc<ModelManager>>,
) -> Result<bool, String> { ) -> Result<bool, String> {
@ -109,6 +119,7 @@ pub async fn has_any_models_or_downloads(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn cancel_download( pub async fn cancel_download(
model_manager: State<'_, Arc<ModelManager>>, model_manager: State<'_, Arc<ModelManager>>,
model_id: String, model_id: String,
@ -119,6 +130,7 @@ pub async fn cancel_download(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub async fn get_recommended_first_model() -> Result<String, String> { pub async fn get_recommended_first_model() -> Result<String, String> {
// Recommend Parakeet V3 model for first-time users - fastest and most accurate // Recommend Parakeet V3 model for first-time users - fastest and most accurate
Ok("parakeet-tdt-0.6b-v3".to_string()) Ok("parakeet-tdt-0.6b-v3".to_string())

View file

@ -1,8 +1,17 @@
use crate::managers::transcription::TranscriptionManager; use crate::managers::transcription::TranscriptionManager;
use crate::settings::{get_settings, write_settings, ModelUnloadTimeout}; use crate::settings::{get_settings, write_settings, ModelUnloadTimeout};
use serde::Serialize;
use specta::Type;
use tauri::{AppHandle, State}; use tauri::{AppHandle, State};
#[derive(Serialize, Type)]
pub struct ModelLoadStatus {
is_loaded: bool,
current_model: Option<String>,
}
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn set_model_unload_timeout(app: AppHandle, timeout: ModelUnloadTimeout) { pub fn set_model_unload_timeout(app: AppHandle, timeout: ModelUnloadTimeout) {
let mut settings = get_settings(&app); let mut settings = get_settings(&app);
settings.model_unload_timeout = timeout; settings.model_unload_timeout = timeout;
@ -10,19 +19,18 @@ pub fn set_model_unload_timeout(app: AppHandle, timeout: ModelUnloadTimeout) {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn get_model_load_status( pub fn get_model_load_status(
transcription_manager: State<TranscriptionManager>, transcription_manager: State<TranscriptionManager>,
) -> Result<serde_json::Value, String> { ) -> Result<ModelLoadStatus, String> {
let is_loaded = transcription_manager.is_model_loaded(); Ok(ModelLoadStatus {
let current_model = transcription_manager.get_current_model(); is_loaded: transcription_manager.is_model_loaded(),
current_model: transcription_manager.get_current_model(),
Ok(serde_json::json!({ })
"is_loaded": is_loaded,
"current_model": current_model
}))
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn unload_model_manually( pub fn unload_model_manually(
transcription_manager: State<TranscriptionManager>, transcription_manager: State<TranscriptionManager>,
) -> Result<(), String> { ) -> Result<(), String> {

View file

@ -6,7 +6,6 @@ use std::process::Command;
/// This queries the macOS IORegistry for the AppleClamshellState key. /// This queries the macOS IORegistry for the AppleClamshellState key.
/// Returns true if the lid is closed, false if open. /// Returns true if the lid is closed, false if open.
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[tauri::command]
pub fn is_clamshell() -> Result<bool, String> { pub fn is_clamshell() -> Result<bool, String> {
let output = Command::new("ioreg") let output = Command::new("ioreg")
.args(["-r", "-k", "AppleClamshellState", "-d", "4"]) .args(["-r", "-k", "AppleClamshellState", "-d", "4"])
@ -32,6 +31,7 @@ pub fn is_clamshell() -> Result<bool, String> {
/// Returns true if a battery is detected (laptop), false otherwise (desktop) /// Returns true if a battery is detected (laptop), false otherwise (desktop)
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn is_laptop() -> Result<bool, String> { pub fn is_laptop() -> Result<bool, String> {
let output = Command::new("pmset") let output = Command::new("pmset")
.arg("-g") .arg("-g")
@ -48,7 +48,6 @@ pub fn is_laptop() -> Result<bool, String> {
/// Stub implementation for non-macOS platforms /// Stub implementation for non-macOS platforms
/// Always returns false since clamshell mode is macOS-specific /// Always returns false since clamshell mode is macOS-specific
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
#[tauri::command]
pub fn is_clamshell() -> Result<bool, String> { pub fn is_clamshell() -> Result<bool, String> {
Ok(false) Ok(false)
} }
@ -57,6 +56,7 @@ pub fn is_clamshell() -> Result<bool, String> {
/// Always returns false since laptop detection is macOS-specific /// Always returns false since laptop detection is macOS-specific
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn is_laptop() -> Result<bool, String> { pub fn is_laptop() -> Result<bool, String> {
Ok(false) Ok(false)
} }

View file

@ -12,6 +12,8 @@ mod shortcut;
mod signal_handle; mod signal_handle;
mod tray; mod tray;
mod utils; mod utils;
use specta_typescript::{BigIntExportBehavior, Typescript};
use tauri_specta::{collect_commands, Builder};
use env_filter::Builder as EnvFilterBuilder; use env_filter::Builder as EnvFilterBuilder;
use managers::audio::AudioRecordingManager; use managers::audio::AudioRecordingManager;
@ -33,6 +35,8 @@ use tauri::{AppHandle, Manager};
use tauri_plugin_autostart::{MacosLauncher, ManagerExt}; use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
use tauri_plugin_log::{Builder as LogBuilder, RotationStrategy, Target, TargetKind}; use tauri_plugin_log::{Builder as LogBuilder, RotationStrategy, Target, TargetKind};
use crate::settings::get_settings;
// Global atomic to store the file log level filter // Global atomic to store the file log level filter
// We use u8 to store the log::LevelFilter as a number // We use u8 to store the log::LevelFilter as a number
pub static FILE_LOG_LEVEL: AtomicU8 = AtomicU8::new(log::LevelFilter::Debug as u8); 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] #[tauri::command]
#[specta::specta]
fn trigger_update_check(app: AppHandle) -> Result<(), String> { fn trigger_update_check(app: AppHandle) -> Result<(), String> {
app.emit("check-for-updates", ()) app.emit("check-for-updates", ())
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
@ -211,6 +216,90 @@ pub fn run() {
// when the variable is unset // when the variable is unset
let console_filter = build_console_filter(); let console_filter = build_console_filter();
let specta_builder = Builder::<tauri::Wry>::new().commands(collect_commands![
shortcut::change_binding,
shortcut::reset_binding,
shortcut::change_ptt_setting,
shortcut::change_audio_feedback_setting,
shortcut::change_audio_feedback_volume_setting,
shortcut::change_sound_theme_setting,
shortcut::change_start_hidden_setting,
shortcut::change_autostart_setting,
shortcut::change_translate_to_english_setting,
shortcut::change_selected_language_setting,
shortcut::change_overlay_position_setting,
shortcut::change_debug_mode_setting,
shortcut::change_word_correction_threshold_setting,
shortcut::change_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() let mut builder = tauri::Builder::default()
.plugin( .plugin(
LogBuilder::new() LogBuilder::new()
@ -268,8 +357,9 @@ pub fn run() {
)) ))
.manage(Mutex::new(ShortcutToggleStates::default())) .manage(Mutex::new(ShortcutToggleStates::default()))
.setup(move |app| { .setup(move |app| {
let settings = settings::get_settings(&app.handle()); let settings = get_settings(&app.handle());
let file_log_level: log::Level = settings.log_level.clone().into(); 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 // 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); FILE_LOG_LEVEL.store(file_log_level.to_level_filter() as u8, Ordering::Relaxed);
let app_handle = app.handle().clone(); let app_handle = app.handle().clone();
@ -307,80 +397,7 @@ pub fn run() {
} }
_ => {} _ => {}
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(specta_builder.invoke_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
])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View file

@ -3,6 +3,7 @@ use chrono::{DateTime, Local, Utc};
use log::{debug, error}; use log::{debug, error};
use rusqlite::{params, Connection, OptionalExtension}; use rusqlite::{params, Connection, OptionalExtension};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use tauri::{AppHandle, Emitter, Manager}; use tauri::{AppHandle, Emitter, Manager};
@ -10,7 +11,7 @@ use tauri_plugin_sql::{Migration, MigrationKind};
use crate::audio_toolkit::save_wav_file; use crate::audio_toolkit::save_wav_file;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize, Type)]
pub struct HistoryEntry { pub struct HistoryEntry {
pub id: i64, pub id: i64,
pub file_name: String, pub file_name: String,

View file

@ -4,6 +4,7 @@ use flate2::read::GzDecoder;
use futures_util::StreamExt; use futures_util::StreamExt;
use log::{debug, info, warn}; use log::{debug, info, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
use std::fs::File; use std::fs::File;
@ -13,13 +14,13 @@ use std::sync::Mutex;
use tar::Archive; use tar::Archive;
use tauri::{AppHandle, Emitter, Manager}; use tauri::{AppHandle, Emitter, Manager};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub enum EngineType { pub enum EngineType {
Whisper, Whisper,
Parakeet, Parakeet,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct ModelInfo { pub struct ModelInfo {
pub id: String, pub id: String,
pub name: String, pub name: String,
@ -36,7 +37,7 @@ pub struct ModelInfo {
pub speed_score: f32, // 0.0 to 1.0, higher is faster 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 struct DownloadProgress {
pub model_id: String, pub model_id: String,
pub downloaded: u64, pub downloaded: u64,

View file

@ -1,11 +1,79 @@
use log::{debug, warn}; 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 std::collections::HashMap;
use tauri::AppHandle; use tauri::AppHandle;
use tauri_plugin_log::LogLevel;
use tauri_plugin_store::StoreExt; 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<D>(deserializer: D) -> Result<Self, D::Error>
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<E: de::Error>(self, value: &str) -> Result<LogLevel, E> {
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<E: de::Error>(self, value: u64) -> Result<LogLevel, E> {
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<LogLevel> 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 struct ShortcutBinding {
pub id: String, pub id: String,
pub name: String, pub name: String,
@ -14,14 +82,14 @@ pub struct ShortcutBinding {
pub current_binding: String, pub current_binding: String,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone, Type)]
pub struct LLMPrompt { pub struct LLMPrompt {
pub id: String, pub id: String,
pub name: String, pub name: String,
pub prompt: String, pub prompt: String,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone, Type)]
pub struct PostProcessProvider { pub struct PostProcessProvider {
pub id: String, pub id: String,
pub label: String, pub label: String,
@ -32,7 +100,7 @@ pub struct PostProcessProvider {
pub models_endpoint: Option<String>, pub models_endpoint: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum OverlayPosition { pub enum OverlayPosition {
None, None,
@ -40,7 +108,7 @@ pub enum OverlayPosition {
Bottom, Bottom,
} }
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum ModelUnloadTimeout { pub enum ModelUnloadTimeout {
Never, Never,
@ -53,23 +121,22 @@ pub enum ModelUnloadTimeout {
Sec5, // Debug mode only 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")] #[serde(rename_all = "snake_case")]
pub enum PasteMethod { pub enum PasteMethod {
CtrlV, CtrlV,
Direct, Direct,
#[cfg(not(target_os = "macos"))]
ShiftInsert, ShiftInsert,
} }
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum ClipboardHandling { pub enum ClipboardHandling {
DontModify, DontModify,
CopyToClipboard, CopyToClipboard,
} }
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum RecordingRetentionPeriod { pub enum RecordingRetentionPeriod {
Never, 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")] #[serde(rename_all = "snake_case")]
pub enum SoundTheme { pub enum SoundTheme {
Marimba, Marimba,
@ -152,7 +219,7 @@ impl SoundTheme {
} }
/* still handy for composing the initial JSON in the store ------------- */ /* 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 struct AppSettings {
pub bindings: HashMap<String, ShortcutBinding>, pub bindings: HashMap<String, ShortcutBinding>,
pub push_to_talk: bool, pub push_to_talk: bool,

View file

@ -1,5 +1,6 @@
use log::{error, warn}; use log::{error, warn};
use serde::Serialize; use serde::Serialize;
use specta::Type;
use tauri::{AppHandle, Emitter, Manager}; use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_autostart::ManagerExt; use tauri_plugin_autostart::ManagerExt;
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState}; 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 { pub struct BindingResponse {
success: bool, success: bool,
binding: Option<ShortcutBinding>, binding: Option<ShortcutBinding>,
@ -30,6 +31,7 @@ pub struct BindingResponse {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_binding( pub fn change_binding(
app: AppHandle, app: AppHandle,
id: String, id: String,
@ -93,6 +95,7 @@ pub fn change_binding(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn reset_binding(app: AppHandle, id: String) -> Result<BindingResponse, String> { pub fn reset_binding(app: AppHandle, id: String) -> Result<BindingResponse, String> {
let binding = settings::get_stored_binding(&app, &id); let binding = settings::get_stored_binding(&app, &id);
@ -100,6 +103,7 @@ pub fn reset_binding(app: AppHandle, id: String) -> Result<BindingResponse, Stri
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_ptt_setting(app: AppHandle, enabled: bool) -> Result<(), String> { pub fn change_ptt_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
@ -113,6 +117,7 @@ pub fn change_ptt_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_audio_feedback_setting(app: AppHandle, enabled: bool) -> Result<(), String> { pub fn change_audio_feedback_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
settings.audio_feedback = enabled; settings.audio_feedback = enabled;
@ -121,6 +126,7 @@ pub fn change_audio_feedback_setting(app: AppHandle, enabled: bool) -> Result<()
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_audio_feedback_volume_setting(app: AppHandle, volume: f32) -> Result<(), String> { pub fn change_audio_feedback_volume_setting(app: AppHandle, volume: f32) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
settings.audio_feedback_volume = volume; settings.audio_feedback_volume = volume;
@ -129,6 +135,7 @@ pub fn change_audio_feedback_volume_setting(app: AppHandle, volume: f32) -> Resu
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_sound_theme_setting(app: AppHandle, theme: String) -> Result<(), String> { pub fn change_sound_theme_setting(app: AppHandle, theme: String) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
let parsed = match theme.as_str() { let parsed = match theme.as_str() {
@ -146,6 +153,7 @@ pub fn change_sound_theme_setting(app: AppHandle, theme: String) -> Result<(), S
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_translate_to_english_setting(app: AppHandle, enabled: bool) -> Result<(), String> { pub fn change_translate_to_english_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
settings.translate_to_english = enabled; settings.translate_to_english = enabled;
@ -154,6 +162,7 @@ pub fn change_translate_to_english_setting(app: AppHandle, enabled: bool) -> Res
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_selected_language_setting(app: AppHandle, language: String) -> Result<(), String> { pub fn change_selected_language_setting(app: AppHandle, language: String) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
settings.selected_language = language; settings.selected_language = language;
@ -162,6 +171,7 @@ pub fn change_selected_language_setting(app: AppHandle, language: String) -> Res
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_overlay_position_setting(app: AppHandle, position: String) -> Result<(), String> { pub fn change_overlay_position_setting(app: AppHandle, position: String) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
let parsed = match position.as_str() { let parsed = match position.as_str() {
@ -183,6 +193,7 @@ pub fn change_overlay_position_setting(app: AppHandle, position: String) -> Resu
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), String> { pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
settings.debug_mode = enabled; settings.debug_mode = enabled;
@ -201,6 +212,7 @@ pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), St
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_start_hidden_setting(app: AppHandle, enabled: bool) -> Result<(), String> { pub fn change_start_hidden_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
settings.start_hidden = enabled; settings.start_hidden = enabled;
@ -219,6 +231,7 @@ pub fn change_start_hidden_setting(app: AppHandle, enabled: bool) -> Result<(),
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_autostart_setting(app: AppHandle, enabled: bool) -> Result<(), String> { pub fn change_autostart_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
settings.autostart_enabled = enabled; settings.autostart_enabled = enabled;
@ -245,6 +258,7 @@ pub fn change_autostart_setting(app: AppHandle, enabled: bool) -> Result<(), Str
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn update_custom_words(app: AppHandle, words: Vec<String>) -> Result<(), String> { pub fn update_custom_words(app: AppHandle, words: Vec<String>) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
settings.custom_words = words; settings.custom_words = words;
@ -253,6 +267,7 @@ pub fn update_custom_words(app: AppHandle, words: Vec<String>) -> Result<(), Str
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_word_correction_threshold_setting( pub fn change_word_correction_threshold_setting(
app: AppHandle, app: AppHandle,
threshold: f64, threshold: f64,
@ -264,12 +279,12 @@ pub fn change_word_correction_threshold_setting(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_paste_method_setting(app: AppHandle, method: String) -> Result<(), String> { pub fn change_paste_method_setting(app: AppHandle, method: String) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
let parsed = match method.as_str() { let parsed = match method.as_str() {
"ctrl_v" => PasteMethod::CtrlV, "ctrl_v" => PasteMethod::CtrlV,
"direct" => PasteMethod::Direct, "direct" => PasteMethod::Direct,
#[cfg(not(target_os = "macos"))]
"shift_insert" => PasteMethod::ShiftInsert, "shift_insert" => PasteMethod::ShiftInsert,
other => { other => {
warn!("Invalid paste method '{}', defaulting to ctrl_v", 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] #[tauri::command]
#[specta::specta]
pub fn change_clipboard_handling_setting(app: AppHandle, handling: String) -> Result<(), String> { pub fn change_clipboard_handling_setting(app: AppHandle, handling: String) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
let parsed = match handling.as_str() { let parsed = match handling.as_str() {
@ -301,6 +317,7 @@ pub fn change_clipboard_handling_setting(app: AppHandle, handling: String) -> Re
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_post_process_enabled_setting(app: AppHandle, enabled: bool) -> Result<(), String> { pub fn change_post_process_enabled_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
settings.post_process_enabled = enabled; settings.post_process_enabled = enabled;
@ -309,6 +326,7 @@ pub fn change_post_process_enabled_setting(app: AppHandle, enabled: bool) -> Res
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_post_process_base_url_setting( pub fn change_post_process_base_url_setting(
app: AppHandle, app: AppHandle,
provider_id: String, provider_id: String,
@ -352,6 +370,7 @@ fn validate_provider_exists(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_post_process_api_key_setting( pub fn change_post_process_api_key_setting(
app: AppHandle, app: AppHandle,
provider_id: String, provider_id: String,
@ -365,6 +384,7 @@ pub fn change_post_process_api_key_setting(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn change_post_process_model_setting( pub fn change_post_process_model_setting(
app: AppHandle, app: AppHandle,
provider_id: String, provider_id: String,
@ -378,6 +398,7 @@ pub fn change_post_process_model_setting(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn set_post_process_provider(app: AppHandle, provider_id: String) -> Result<(), String> { pub fn set_post_process_provider(app: AppHandle, provider_id: String) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
validate_provider_exists(&settings, &provider_id)?; validate_provider_exists(&settings, &provider_id)?;
@ -387,6 +408,7 @@ pub fn set_post_process_provider(app: AppHandle, provider_id: String) -> Result<
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn add_post_process_prompt( pub fn add_post_process_prompt(
app: AppHandle, app: AppHandle,
name: String, name: String,
@ -410,6 +432,7 @@ pub fn add_post_process_prompt(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn update_post_process_prompt( pub fn update_post_process_prompt(
app: AppHandle, app: AppHandle,
id: String, id: String,
@ -433,6 +456,7 @@ pub fn update_post_process_prompt(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn delete_post_process_prompt(app: AppHandle, id: String) -> Result<(), String> { pub fn delete_post_process_prompt(app: AppHandle, id: String) -> Result<(), String> {
let mut settings = settings::get_settings(&app); 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] #[tauri::command]
#[specta::specta]
pub async fn fetch_post_process_models( pub async fn fetch_post_process_models(
app: AppHandle, app: AppHandle,
provider_id: String, provider_id: String,
@ -599,6 +624,7 @@ async fn fetch_models_manual(
} }
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn set_post_process_selected_prompt(app: AppHandle, id: String) -> Result<(), String> { pub fn set_post_process_selected_prompt(app: AppHandle, id: String) -> Result<(), String> {
let mut settings = settings::get_settings(&app); 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] #[tauri::command]
#[specta::specta]
pub fn change_mute_while_recording_setting(app: AppHandle, enabled: bool) -> Result<(), String> { pub fn change_mute_while_recording_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
let mut settings = settings::get_settings(&app); let mut settings = settings::get_settings(&app);
settings.mute_while_recording = enabled; 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. /// Temporarily unregister a binding while the user is editing it in the UI.
/// This avoids firing the action while keys are being recorded. /// This avoids firing the action while keys are being recorded.
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn suspend_binding(app: AppHandle, id: String) -> Result<(), String> { pub fn suspend_binding(app: AppHandle, id: String) -> Result<(), String> {
if let Some(b) = settings::get_bindings(&app).get(&id).cloned() { if let Some(b) = settings::get_bindings(&app).get(&id).cloned() {
if let Err(e) = _unregister_shortcut(&app, b) { 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. /// Re-register the binding after the user has finished editing.
#[tauri::command] #[tauri::command]
#[specta::specta]
pub fn resume_binding(app: AppHandle, id: String) -> Result<(), String> { pub fn resume_binding(app: AppHandle, id: String) -> Result<(), String> {
if let Some(b) = settings::get_bindings(&app).get(&id).cloned() { if let Some(b) = settings::get_bindings(&app).get(&id).cloned() {
if let Err(e) = _register_shortcut(&app, b) { if let Err(e) = _register_shortcut(&app, b) {

View file

@ -1,4 +1,3 @@
import { invoke } from "@tauri-apps/api/core";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import "./App.css"; import "./App.css";
@ -7,6 +6,7 @@ import Footer from "./components/footer";
import Onboarding from "./components/onboarding"; import Onboarding from "./components/onboarding";
import { Sidebar, SidebarSection, SECTIONS_CONFIG } from "./components/Sidebar"; import { Sidebar, SidebarSection, SECTIONS_CONFIG } from "./components/Sidebar";
import { useSettings } from "./hooks/useSettings"; import { useSettings } from "./hooks/useSettings";
import { commands } from "@/bindings";
const renderSettingsContent = (section: SidebarSection) => { const renderSettingsContent = (section: SidebarSection) => {
const ActiveComponent = const ActiveComponent =
@ -52,8 +52,12 @@ function App() {
const checkOnboardingStatus = async () => { const checkOnboardingStatus = async () => {
try { try {
// Always check if they have any models available // Always check if they have any models available
const modelsAvailable: boolean = await invoke("has_any_models_available"); const result = await commands.hasAnyModelsAvailable();
setShowOnboarding(!modelsAvailable); if (result.status === "ok") {
setShowOnboarding(!result.data);
} else {
setShowOnboarding(true);
}
} catch (error) { } catch (error) {
console.error("Failed to check onboarding status:", error); console.error("Failed to check onboarding status:", error);
setShowOnboarding(true); setShowOnboarding(true);

674
src/bindings.ts Normal file
View file

@ -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<Result<BindingResponse, string>> {
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<Result<BindingResponse, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<string[], string>> {
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<Result<LLMPrompt, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<void> {
await TAURI_INVOKE("cancel_operation");
},
async getAppDirPath() : Promise<Result<string, string>> {
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<Result<AppSettings, string>> {
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<Result<AppSettings, string>> {
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<Result<string, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<ModelInfo[], string>> {
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<Result<ModelInfo | null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<string, string>> {
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<Result<string | null, string>> {
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<Result<boolean, string>> {
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<Result<boolean, string>> {
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<Result<boolean, string>> {
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<Result<string, string>> {
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<Result<null, string>> {
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<Result<boolean, string>> {
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<Result<AudioDevice[], string>> {
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<Result<null, string>> {
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<Result<string, string>> {
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<Result<AudioDevice[], string>> {
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<Result<null, string>> {
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<Result<string, string>> {
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<void> {
await TAURI_INVOKE("play_test_sound", { soundType });
},
async checkCustomSounds() : Promise<CustomSounds> {
return await TAURI_INVOKE("check_custom_sounds");
},
async setClamshellMicrophone(deviceName: string) : Promise<Result<null, string>> {
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<Result<string, string>> {
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<void> {
await TAURI_INVOKE("set_model_unload_timeout", { timeout });
},
async getModelLoadStatus() : Promise<Result<ModelLoadStatus, string>> {
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<Result<null, string>> {
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<Result<HistoryEntry[], string>> {
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<Result<null, string>> {
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<Result<string, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<boolean, string>> {
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__<T> = {
listen: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
once: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
emit: null extends T
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
};
export type Result<T, E> =
| { status: "ok"; data: T }
| { status: "error"; error: E };
function __makeEvents__<T extends Record<string, any>>(
mappings: Record<keyof T, string>,
) {
return new Proxy(
{} as unknown as {
[K in keyof T]: __EventObj__<T[K]> & {
(handle: __WebviewWindow__): __EventObj__<T[K]>;
};
},
{
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__<any>) => {
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);
}
},
});
},
},
);
}

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { ModelInfo } from "../../lib/types"; import type { ModelInfo } from "@/bindings";
import { formatModelSize } from "../../lib/utils/format"; import { formatModelSize } from "../../lib/utils/format";
import { ProgressBar } from "../shared"; import { ProgressBar } from "../shared";
@ -180,7 +180,7 @@ const ModelDropdown: React.FC<ModelDropdownProps> = ({
{model.description} {model.description}
</div> </div>
<div className="mt-1 text-xs text-text/50 tabular-nums"> <div className="mt-1 text-xs text-text/50 tabular-nums">
Download size · {formatModelSize(model.size_mb)} Download size · {formatModelSize(Number(model.size_mb))}
</div> </div>
</div> </div>
<div className="text-xs text-logo-primary tabular-nums"> <div className="text-xs text-logo-primary tabular-nums">

View file

@ -1,7 +1,6 @@
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { ModelInfo } from "../../lib/types"; import { commands, type ModelInfo } from "@/bindings";
import ModelStatusButton from "./ModelStatusButton"; import ModelStatusButton from "./ModelStatusButton";
import ModelDropdown from "./ModelDropdown"; import ModelDropdown from "./ModelDropdown";
import DownloadProgressDisplay from "./DownloadProgressDisplay"; import DownloadProgressDisplay from "./DownloadProgressDisplay";
@ -238,8 +237,10 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
const loadModels = async () => { const loadModels = async () => {
try { try {
const modelList = await invoke<ModelInfo[]>("get_available_models"); const result = await commands.getAvailableModels();
setModels(modelList); if (result.status === "ok") {
setModels(result.data);
}
} catch (err) { } catch (err) {
console.error("Failed to load models:", err); console.error("Failed to load models:", err);
} }
@ -247,21 +248,25 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
const loadCurrentModel = async () => { const loadCurrentModel = async () => {
try { try {
const current = await invoke<string>("get_current_model"); const result = await commands.getCurrentModel();
setCurrentModelId(current); if (result.status === "ok") {
const current = result.data;
setCurrentModelId(current);
if (current) { if (current) {
// Check if model is actually loaded // Check if model is actually loaded
const transcriptionStatus = await invoke<string | null>( const statusResult = await commands.getTranscriptionModelStatus();
"get_transcription_model_status", if (statusResult.status === "ok") {
); const transcriptionStatus = statusResult.data;
if (transcriptionStatus === current) { if (transcriptionStatus === current) {
setModelStatus("ready"); setModelStatus("ready");
} else {
setModelStatus("unloaded");
}
}
} else { } else {
setModelStatus("unloaded"); setModelStatus("none");
} }
} else {
setModelStatus("none");
} }
} catch (err) { } catch (err) {
console.error("Failed to load current model:", err); console.error("Failed to load current model:", err);
@ -272,10 +277,16 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
const handleModelSelect = async (modelId: string) => { const handleModelSelect = async (modelId: string) => {
try { try {
setCurrentModelId(modelId); // Set optimistically so loading text shows correct model
setModelError(null); setModelError(null);
setShowModelDropdown(false); setShowModelDropdown(false);
await invoke("set_active_model", { modelId }); const result = await commands.setActiveModel(modelId);
setCurrentModelId(modelId); if (result.status === "error") {
const errorMsg = result.error;
setModelError(errorMsg);
setModelStatus("error");
onError?.(errorMsg);
}
} catch (err) { } catch (err) {
const errorMsg = `${err}`; const errorMsg = `${err}`;
setModelError(errorMsg); setModelError(errorMsg);
@ -287,7 +298,13 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
const handleModelDownload = async (modelId: string) => { const handleModelDownload = async (modelId: string) => {
try { try {
setModelError(null); 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) { } catch (err) {
const errorMsg = `${err}`; const errorMsg = `${err}`;
setModelError(errorMsg); setModelError(errorMsg);
@ -347,9 +364,11 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ onError }) => {
}; };
const handleModelDelete = async (modelId: string) => { const handleModelDelete = async (modelId: string) => {
await invoke("delete_model", { modelId }); const result = await commands.deleteModel(modelId);
await loadModels(); if (result.status === "ok") {
setModelError(null); await loadModels();
setModelError(null);
}
}; };
return ( return (

View file

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { Download } from "lucide-react"; import { Download } from "lucide-react";
import { ModelInfo } from "../../lib/types"; import type { ModelInfo } from "@/bindings";
import { formatModelSize } from "../../lib/utils/format"; import { formatModelSize } from "../../lib/utils/format";
import Badge from "../ui/Badge"; import Badge from "../ui/Badge";
@ -42,7 +42,7 @@ const ModelCard: React.FC<ModelCardProps> = ({
<h3 className="text-lg font-semibold text-text group-hover:text-logo-primary transition-colors"> <h3 className="text-lg font-semibold text-text group-hover:text-logo-primary transition-colors">
{model.name} {model.name}
</h3> </h3>
<DownloadSize sizeMb={model.size_mb} /> <DownloadSize sizeMb={Number(model.size_mb)} />
{isFeatured && <Badge variant="primary">Recommended</Badge>} {isFeatured && <Badge variant="primary">Recommended</Badge>}
</div> </div>
<p className="text-text/60 text-sm leading-relaxed"> <p className="text-text/60 text-sm leading-relaxed">

View file

@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core"; import { commands, type ModelInfo } from "@/bindings";
import { ModelInfo } from "../../lib/types";
import ModelCard from "./ModelCard"; import ModelCard from "./ModelCard";
import HandyTextLogo from "../icons/HandyTextLogo"; import HandyTextLogo from "../icons/HandyTextLogo";
@ -19,9 +18,13 @@ const Onboarding: React.FC<OnboardingProps> = ({ onModelSelected }) => {
const loadModels = async () => { const loadModels = async () => {
try { try {
const models: ModelInfo[] = await invoke("get_available_models"); const result = await commands.getAvailableModels();
// Only show downloadable models for onboarding if (result.status === "ok") {
setAvailableModels(models.filter((m) => !m.is_downloaded)); // Only show downloadable models for onboarding
setAvailableModels(result.data.filter((m) => !m.is_downloaded));
} else {
setError("Failed to load available models");
}
} catch (err) { } catch (err) {
console.error("Failed to load models:", err); console.error("Failed to load models:", err);
setError("Failed to load available models"); setError("Failed to load available models");
@ -36,7 +39,12 @@ const Onboarding: React.FC<OnboardingProps> = ({ onModelSelected }) => {
onModelSelected(); onModelSelected();
try { 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) { } catch (err) {
console.error("Download failed:", err); console.error("Download failed:", err);
setError(`Failed to download model: ${err}`); setError(`Failed to download model: ${err}`);
@ -80,7 +88,7 @@ const Onboarding: React.FC<OnboardingProps> = ({ onModelSelected }) => {
{availableModels {availableModels
.filter((model) => !getRecommendedBadge(model.id)) .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) => ( .map((model) => (
<ModelCard <ModelCard
key={model.id} key={model.id}

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core"; import { commands } from "@/bindings";
import { SettingContainer } from "../ui/SettingContainer"; import { SettingContainer } from "../ui/SettingContainer";
import { Button } from "../ui/Button"; import { Button } from "../ui/Button";
@ -19,8 +19,12 @@ export const AppDataDirectory: React.FC<AppDataDirectoryProps> = ({
useEffect(() => { useEffect(() => {
const loadAppDirectory = async () => { const loadAppDirectory = async () => {
try { try {
const result = await invoke<string>("get_app_dir_path"); const result = await commands.getAppDirPath();
setAppDirPath(result); if (result.status === "ok") {
setAppDirPath(result.data);
} else {
setError(result.error);
}
} catch (err) { } catch (err) {
setError( setError(
err instanceof Error ? err.message : "Failed to load app directory", err instanceof Error ? err.message : "Failed to load app directory",
@ -36,7 +40,7 @@ export const AppDataDirectory: React.FC<AppDataDirectoryProps> = ({
const handleOpen = async () => { const handleOpen = async () => {
if (!appDirPath) return; if (!appDirPath) return;
try { try {
await invoke("open_app_data_dir"); await commands.openAppDataDir();
} catch (openError) { } catch (openError) {
console.error("Failed to open app data directory:", openError); console.error("Failed to open app data directory:", openError);
} }

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core"; import { commands } from "@/bindings";
import { Dropdown } from "../ui/Dropdown"; import { Dropdown } from "../ui/Dropdown";
import { SettingContainer } from "../ui/SettingContainer"; import { SettingContainer } from "../ui/SettingContainer";
import { ResetButton } from "../ui/ResetButton"; import { ResetButton } from "../ui/ResetButton";
@ -27,8 +27,12 @@ export const ClamshellMicrophoneSelector: React.FC<ClamshellMicrophoneSelectorPr
useEffect(() => { useEffect(() => {
const checkIsLaptop = async () => { const checkIsLaptop = async () => {
try { try {
const result = await invoke<boolean>("is_laptop"); const result = await commands.isLaptop();
setIsLaptop(result); if (result.status === "ok") {
setIsLaptop(result.data);
} else {
setIsLaptop(false);
}
} catch (error) { } catch (error) {
console.error("Failed to check if device is laptop:", error); console.error("Failed to check if device is laptop:", error);
setIsLaptop(false); setIsLaptop(false);

View file

@ -2,7 +2,7 @@ import React from "react";
import { Dropdown } from "../ui/Dropdown"; import { Dropdown } from "../ui/Dropdown";
import { SettingContainer } from "../ui/SettingContainer"; import { SettingContainer } from "../ui/SettingContainer";
import { useSettings } from "../../hooks/useSettings"; import { useSettings } from "../../hooks/useSettings";
import type { ClipboardHandling } from "../../lib/types"; import type { ClipboardHandling } from "@/bindings";
interface ClipboardHandlingProps { interface ClipboardHandlingProps {
descriptionMode?: "inline" | "tooltip"; descriptionMode?: "inline" | "tooltip";

View file

@ -9,7 +9,7 @@ import {
import { ResetButton } from "../ui/ResetButton"; import { ResetButton } from "../ui/ResetButton";
import { SettingContainer } from "../ui/SettingContainer"; import { SettingContainer } from "../ui/SettingContainer";
import { useSettings } from "../../hooks/useSettings"; import { useSettings } from "../../hooks/useSettings";
import { invoke } from "@tauri-apps/api/core"; import { commands } from "@/bindings";
import { toast } from "sonner"; import { toast } from "sonner";
interface HandyShortcutProps { interface HandyShortcutProps {
@ -80,7 +80,7 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
if (editingShortcutId && originalBinding) { if (editingShortcutId && originalBinding) {
try { try {
await updateBinding(editingShortcutId, originalBinding); await updateBinding(editingShortcutId, originalBinding);
await invoke("resume_binding", { id: editingShortcutId }).catch( await commands.resumeBinding(editingShortcutId).catch(
console.error, console.error,
); );
} catch (error) { } catch (error) {
@ -88,7 +88,7 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
toast.error("Failed to restore original shortcut"); toast.error("Failed to restore original shortcut");
} }
} else if (editingShortcutId) { } else if (editingShortcutId) {
await invoke("resume_binding", { id: editingShortcutId }).catch( await commands.resumeBinding(editingShortcutId).catch(
console.error, console.error,
); );
} }
@ -134,7 +134,7 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
try { try {
await updateBinding(editingShortcutId, newShortcut); await updateBinding(editingShortcutId, newShortcut);
// Re-register the shortcut now that recording is finished // Re-register the shortcut now that recording is finished
await invoke("resume_binding", { id: editingShortcutId }).catch( await commands.resumeBinding(editingShortcutId).catch(
console.error, console.error,
); );
} catch (error) { } catch (error) {
@ -145,7 +145,7 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
if (originalBinding) { if (originalBinding) {
try { try {
await updateBinding(editingShortcutId, originalBinding); await updateBinding(editingShortcutId, originalBinding);
await invoke("resume_binding", { id: editingShortcutId }).catch( await commands.resumeBinding(editingShortcutId).catch(
console.error, console.error,
); );
} catch (resetError) { } catch (resetError) {
@ -173,7 +173,7 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
if (editingShortcutId && originalBinding) { if (editingShortcutId && originalBinding) {
try { try {
await updateBinding(editingShortcutId, originalBinding); await updateBinding(editingShortcutId, originalBinding);
await invoke("resume_binding", { id: editingShortcutId }).catch( await commands.resumeBinding(editingShortcutId).catch(
console.error, console.error,
); );
} catch (error) { } catch (error) {
@ -181,7 +181,7 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
toast.error("Failed to restore original shortcut"); toast.error("Failed to restore original shortcut");
} }
} else if (editingShortcutId) { } else if (editingShortcutId) {
invoke("resume_binding", { id: editingShortcutId }).catch( commands.resumeBinding(editingShortcutId).catch(
console.error, console.error,
); );
} }
@ -217,7 +217,7 @@ export const HandyShortcut: React.FC<HandyShortcutProps> = ({
if (editingShortcutId === id) return; // Already editing this shortcut if (editingShortcutId === id) return; // Already editing this shortcut
// Suspend current binding to avoid firing while recording // 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 // Store the original binding to restore if canceled
setOriginalBinding(bindings[id]?.current_binding || ""); setOriginalBinding(bindings[id]?.current_binding || "");

View file

@ -14,12 +14,12 @@ export const HistoryLimit: React.FC<HistoryLimitProps> = ({
}) => { }) => {
const { getSetting, updateSetting, isUpdating } = useSettings(); const { getSetting, updateSetting, isUpdating } = useSettings();
const historyLimit = getSetting("history_limit") ?? 5; const historyLimit = Number(getSetting("history_limit") ?? "5");
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value, 10); const value = parseInt(event.target.value, 10);
if (!isNaN(value) && value >= 0) { if (!isNaN(value) && value >= 0) {
updateSetting("history_limit", value); updateSetting("history_limit", value.toString());
} }
}; };

View file

@ -1,7 +1,6 @@
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { invoke } from "@tauri-apps/api/core";
import { useSettings } from "../../hooks/useSettings"; import { useSettings } from "../../hooks/useSettings";
import { ModelUnloadTimeout } from "../../lib/types"; import { commands, type ModelUnloadTimeout } from "@/bindings";
import { Dropdown } from "../ui/Dropdown"; import { Dropdown } from "../ui/Dropdown";
import { SettingContainer } from "../ui/SettingContainer"; import { SettingContainer } from "../ui/SettingContainer";
@ -35,7 +34,7 @@ export const ModelUnloadTimeoutSetting: React.FC<ModelUnloadTimeoutProps> = ({
const newTimeout = event.target.value as ModelUnloadTimeout; const newTimeout = event.target.value as ModelUnloadTimeout;
try { try {
await invoke("set_model_unload_timeout", { timeout: newTimeout }); await commands.setModelUnloadTimeout(newTimeout);
updateSetting("model_unload_timeout", newTimeout); updateSetting("model_unload_timeout", newTimeout);
} catch (error) { } catch (error) {
console.error("Failed to update model unload timeout:", error); console.error("Failed to update model unload timeout:", error);

View file

@ -3,7 +3,7 @@ import { Dropdown } from "../ui/Dropdown";
import { SettingContainer } from "../ui/SettingContainer"; import { SettingContainer } from "../ui/SettingContainer";
import { ResetButton } from "../ui/ResetButton"; import { ResetButton } from "../ui/ResetButton";
import { useSettings } from "../../hooks/useSettings"; import { useSettings } from "../../hooks/useSettings";
import { AudioDevice } from "../../lib/types"; import type { AudioDevice } from "@/bindings";
interface OutputDeviceSelectorProps { interface OutputDeviceSelectorProps {
descriptionMode?: "inline" | "tooltip"; descriptionMode?: "inline" | "tooltip";

View file

@ -3,7 +3,7 @@ import { type as getOsType } from "@tauri-apps/plugin-os";
import { Dropdown } from "../ui/Dropdown"; import { Dropdown } from "../ui/Dropdown";
import { SettingContainer } from "../ui/SettingContainer"; import { SettingContainer } from "../ui/SettingContainer";
import { useSettings } from "../../hooks/useSettings"; import { useSettings } from "../../hooks/useSettings";
import type { PasteMethod } from "../../lib/types"; import type { PasteMethod } from "@/bindings";
interface PasteMethodProps { interface PasteMethodProps {
descriptionMode?: "inline" | "tooltip"; descriptionMode?: "inline" | "tooltip";

View file

@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo } from "react";
import { useSettings } from "../../../hooks/useSettings"; import { useSettings } from "../../../hooks/useSettings";
import { useSettingsStore } from "../../../stores/settingsStore"; import { useSettingsStore } from "../../../stores/settingsStore";
import type { PostProcessProvider } from "../../../lib/types"; import type { PostProcessProvider } from "@/bindings";
import type { ModelOption } from "./types"; import type { ModelOption } from "./types";
import type { DropdownOption } from "../../ui/Dropdown"; import type { DropdownOption } from "../../ui/Dropdown";

View file

@ -2,7 +2,7 @@ import React from "react";
import { Dropdown } from "../ui/Dropdown"; import { Dropdown } from "../ui/Dropdown";
import { SettingContainer } from "../ui/SettingContainer"; import { SettingContainer } from "../ui/SettingContainer";
import { useSettings } from "../../hooks/useSettings"; import { useSettings } from "../../hooks/useSettings";
import { RecordingRetentionPeriod } from "../../lib/types"; import { RecordingRetentionPeriod } from "@/bindings";
interface RecordingRetentionPeriodProps { interface RecordingRetentionPeriodProps {
descriptionMode?: "inline" | "tooltip"; descriptionMode?: "inline" | "tooltip";

View file

@ -2,7 +2,7 @@ import React from "react";
import { Dropdown } from "../ui/Dropdown"; import { Dropdown } from "../ui/Dropdown";
import { SettingContainer } from "../ui/SettingContainer"; import { SettingContainer } from "../ui/SettingContainer";
import { useSettings } from "../../hooks/useSettings"; import { useSettings } from "../../hooks/useSettings";
import type { OverlayPosition } from "../../lib/types"; import type { OverlayPosition } from "@/bindings";
interface ShowOverlayProps { interface ShowOverlayProps {
descriptionMode?: "inline" | "tooltip"; descriptionMode?: "inline" | "tooltip";

View file

@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core"; import { commands } from "@/bindings";
import { SettingContainer } from "../../ui/SettingContainer"; import { SettingContainer } from "../../ui/SettingContainer";
import { Button } from "../../ui/Button"; import { Button } from "../../ui/Button";
@ -19,8 +19,12 @@ export const LogDirectory: React.FC<LogDirectoryProps> = ({
useEffect(() => { useEffect(() => {
const loadLogDirectory = async () => { const loadLogDirectory = async () => {
try { try {
const result = await invoke<string>("get_log_dir_path"); const result = await commands.getLogDirPath();
setLogDir(result); if (result.status === "ok") {
setLogDir(result.data);
} else {
setError(result.error);
}
} catch (err) { } catch (err) {
const errorMessage = const errorMessage =
err && typeof err === "object" && "message" in err err && typeof err === "object" && "message" in err
@ -38,7 +42,7 @@ export const LogDirectory: React.FC<LogDirectoryProps> = ({
const handleOpen = async () => { const handleOpen = async () => {
if (!logDir) return; if (!logDir) return;
try { try {
await invoke("open_log_dir"); await commands.openLogDir();
} catch (openError) { } catch (openError) {
console.error("Failed to open log directory:", openError); console.error("Failed to open log directory:", openError);
} }

View file

@ -2,13 +2,14 @@ import React from "react";
import { SettingContainer } from "../../ui/SettingContainer"; import { SettingContainer } from "../../ui/SettingContainer";
import { Dropdown, type DropdownOption } from "../../ui/Dropdown"; import { Dropdown, type DropdownOption } from "../../ui/Dropdown";
import { useSettings } from "../../../hooks/useSettings"; import { useSettings } from "../../../hooks/useSettings";
import type { LogLevel } from "../../../bindings";
const LOG_LEVEL_OPTIONS: DropdownOption[] = [ const LOG_LEVEL_OPTIONS: DropdownOption[] = [
{ value: "5", label: "Error" }, { value: "error", label: "Error" },
{ value: "4", label: "Warn" }, { value: "warn", label: "Warn" },
{ value: "3", label: "Info" }, { value: "info", label: "Info" },
{ value: "2", label: "Debug" }, { value: "debug", label: "Debug" },
{ value: "1", label: "Trace" }, { value: "trace", label: "Trace" },
]; ];
interface LogLevelSelectorProps { interface LogLevelSelectorProps {
@ -21,19 +22,13 @@ export const LogLevelSelector: React.FC<LogLevelSelectorProps> = ({
grouped = false, grouped = false,
}) => { }) => {
const { settings, updateSetting, isUpdating } = useSettings(); const { settings, updateSetting, isUpdating } = useSettings();
const currentLevel = settings?.log_level ?? 2; const currentLevel = settings?.log_level ?? "debug";
const isLevelUpdating = isUpdating("log_level");
const selectedValue = currentLevel.toString();
const handleSelect = async (value: string) => { const handleSelect = async (value: string) => {
const parsed = Number.parseInt(value, 10); if (value === currentLevel) return;
if (Number.isNaN(parsed) || parsed === currentLevel) {
return;
}
try { try {
await updateSetting("log_level", parsed as typeof currentLevel); await updateSetting("log_level", value as LogLevel);
} catch (error) { } catch (error) {
console.error("Failed to update log level:", error); console.error("Failed to update log level:", error);
} }
@ -47,14 +42,12 @@ export const LogLevelSelector: React.FC<LogLevelSelectorProps> = ({
grouped={grouped} grouped={grouped}
layout="horizontal" layout="horizontal"
> >
<div className="space-y-1"> <Dropdown
<Dropdown options={LOG_LEVEL_OPTIONS}
options={LOG_LEVEL_OPTIONS} selectedValue={currentLevel}
selectedValue={selectedValue} onSelect={handleSelect}
onSelect={handleSelect} disabled={!settings || isUpdating("log_level")}
disabled={!settings || isLevelUpdating} />
/>
</div>
</SettingContainer> </SettingContainer>
); );
}; };

View file

@ -2,17 +2,9 @@ import React, { useState, useEffect, useCallback } from "react";
import { AudioPlayer } from "../../ui/AudioPlayer"; import { AudioPlayer } from "../../ui/AudioPlayer";
import { Button } from "../../ui/Button"; import { Button } from "../../ui/Button";
import { Copy, Star, Check, Trash2, FolderOpen } from "lucide-react"; 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"; import { listen } from "@tauri-apps/api/event";
import { commands, type HistoryEntry } from "@/bindings";
interface HistoryEntry {
id: number;
file_name: string;
timestamp: number;
saved: boolean;
title: string;
transcription_text: string;
}
interface OpenRecordingsButtonProps { interface OpenRecordingsButtonProps {
onClick: () => void; onClick: () => void;
@ -39,8 +31,10 @@ export const HistorySettings: React.FC = () => {
const loadHistoryEntries = useCallback(async () => { const loadHistoryEntries = useCallback(async () => {
try { try {
const entries = await invoke<HistoryEntry[]>("get_history_entries"); const result = await commands.getHistoryEntries();
setHistoryEntries(entries); if (result.status === "ok") {
setHistoryEntries(result.data);
}
} catch (error) { } catch (error) {
console.error("Failed to load history entries:", error); console.error("Failed to load history entries:", error);
} finally { } finally {
@ -73,9 +67,9 @@ export const HistorySettings: React.FC = () => {
}; };
}, [loadHistoryEntries]); }, [loadHistoryEntries]);
const toggleSaved = async (id: number) => { const toggleSaved = async (id: string) => {
try { try {
await invoke("toggle_history_entry_saved", { id }); await commands.toggleHistoryEntrySaved(id);
// No need to reload here - the event listener will handle it // No need to reload here - the event listener will handle it
} catch (error) { } catch (error) {
console.error("Failed to toggle saved status:", error); console.error("Failed to toggle saved status:", error);
@ -92,20 +86,20 @@ export const HistorySettings: React.FC = () => {
const getAudioUrl = async (fileName: string) => { const getAudioUrl = async (fileName: string) => {
try { try {
const filePath = await invoke<string>("get_audio_file_path", { const result = await commands.getAudioFilePath(fileName);
fileName, if (result.status === "ok") {
}); return convertFileSrc(`${result.data}`, "asset");
}
return convertFileSrc(`${filePath}`, "asset"); return null;
} catch (error) { } catch (error) {
console.error("Failed to get audio file path:", error); console.error("Failed to get audio file path:", error);
return null; return null;
} }
}; };
const deleteAudioEntry = async (id: number) => { const deleteAudioEntry = async (id: string) => {
try { try {
await invoke("delete_history_entry", { id }); await commands.deleteHistoryEntry(id);
} catch (error) { } catch (error) {
console.error("Failed to delete audio entry:", error); console.error("Failed to delete audio entry:", error);
throw error; throw error;
@ -114,7 +108,7 @@ export const HistorySettings: React.FC = () => {
const openRecordingsFolder = async () => { const openRecordingsFolder = async () => {
try { try {
await invoke("open_recordings_folder"); await commands.openRecordingsFolder();
} catch (error) { } catch (error) {
console.error("Failed to open recordings folder:", error); console.error("Failed to open recordings folder:", error);
} }
@ -199,7 +193,7 @@ interface HistoryEntryProps {
onToggleSaved: () => void; onToggleSaved: () => void;
onCopyText: () => void; onCopyText: () => void;
getAudioUrl: (fileName: string) => Promise<string | null>; getAudioUrl: (fileName: string) => Promise<string | null>;
deleteAudio: (id: number) => Promise<void>; deleteAudio: (id: string) => Promise<void>;
} }
const HistoryEntryComponent: React.FC<HistoryEntryProps> = ({ const HistoryEntryComponent: React.FC<HistoryEntryProps> = ({

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { RefreshCcw } from "lucide-react"; import { RefreshCcw } from "lucide-react";
import { commands } from "@/bindings";
import { SettingsGroup } from "../../ui/SettingsGroup"; import { SettingsGroup } from "../../ui/SettingsGroup";
import { SettingContainer } from "../../ui/SettingContainer"; import { SettingContainer } from "../../ui/SettingContainer";
@ -16,7 +16,7 @@ import { ApiKeyField } from "../PostProcessingSettingsApi/ApiKeyField";
import { ModelSelect } from "../PostProcessingSettingsApi/ModelSelect"; import { ModelSelect } from "../PostProcessingSettingsApi/ModelSelect";
import { usePostProcessProviderState } from "../PostProcessingSettingsApi/usePostProcessProviderState"; import { usePostProcessProviderState } from "../PostProcessingSettingsApi/usePostProcessProviderState";
import { useSettings } from "../../../hooks/useSettings"; import { useSettings } from "../../../hooks/useSettings";
import type { LLMPrompt } from "../../../lib/types"; import type { LLMPrompt } from "@/bindings";
const DisabledNotice: React.FC<{ children: React.ReactNode }> = ({ const DisabledNotice: React.FC<{ children: React.ReactNode }> = ({
children, children,
@ -178,13 +178,15 @@ const PostProcessingSettingsPromptsComponent: React.FC = () => {
if (!draftName.trim() || !draftText.trim()) return; if (!draftName.trim() || !draftText.trim()) return;
try { try {
const newPrompt = await invoke<LLMPrompt>("add_post_process_prompt", { const result = await commands.addPostProcessPrompt(
name: draftName.trim(), draftName.trim(),
prompt: draftText.trim(), draftText.trim()
}); );
await refreshSettings(); if (result.status === "ok") {
updateSetting("post_process_selected_prompt_id", newPrompt.id); await refreshSettings();
setIsCreating(false); updateSetting("post_process_selected_prompt_id", result.data.id);
setIsCreating(false);
}
} catch (error) { } catch (error) {
console.error("Failed to create prompt:", error); console.error("Failed to create prompt:", error);
} }
@ -194,11 +196,11 @@ const PostProcessingSettingsPromptsComponent: React.FC = () => {
if (!selectedPromptId || !draftName.trim() || !draftText.trim()) return; if (!selectedPromptId || !draftName.trim() || !draftText.trim()) return;
try { try {
await invoke("update_post_process_prompt", { await commands.updatePostProcessPrompt(
id: selectedPromptId, selectedPromptId,
name: draftName.trim(), draftName.trim(),
prompt: draftText.trim(), draftText.trim()
}); );
await refreshSettings(); await refreshSettings();
} catch (error) { } catch (error) {
console.error("Failed to update prompt:", error); console.error("Failed to update prompt:", error);
@ -209,7 +211,7 @@ const PostProcessingSettingsPromptsComponent: React.FC = () => {
if (!promptId) return; if (!promptId) return;
try { try {
await invoke("delete_post_process_prompt", { id: promptId }); await commands.deletePostProcessPrompt(promptId);
await refreshSettings(); await refreshSettings();
setIsCreating(false); setIsCreating(false);
} catch (error) { } catch (error) {

View file

@ -1,21 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { commands, type ModelInfo } from "@/bindings";
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;
}
interface DownloadProgress { interface DownloadProgress {
model_id: string; model_id: string;
@ -43,9 +28,13 @@ export const useModels = () => {
const loadModels = async () => { const loadModels = async () => {
try { try {
const modelList = await invoke<ModelInfo[]>("get_available_models"); const result = await commands.getAvailableModels();
setModels(modelList); if (result.status === "ok") {
setError(null); setModels(result.data);
setError(null);
} else {
setError(`Failed to load models: ${result.error}`);
}
} catch (err) { } catch (err) {
setError(`Failed to load models: ${err}`); setError(`Failed to load models: ${err}`);
} finally { } finally {
@ -55,8 +44,10 @@ export const useModels = () => {
const loadCurrentModel = async () => { const loadCurrentModel = async () => {
try { try {
const current = await invoke<string>("get_current_model"); const result = await commands.getCurrentModel();
setCurrentModel(current); if (result.status === "ok") {
setCurrentModel(result.data);
}
} catch (err) { } catch (err) {
console.error("Failed to load current model:", err); console.error("Failed to load current model:", err);
} }
@ -64,10 +55,14 @@ export const useModels = () => {
const checkFirstRun = async () => { const checkFirstRun = async () => {
try { try {
const hasModels = await invoke<boolean>("has_any_models_available"); const result = await commands.hasAnyModelsAvailable();
setHasAnyModels(hasModels); if (result.status === "ok") {
setIsFirstRun(!hasModels); const hasModels = result.data;
return !hasModels; setHasAnyModels(hasModels);
setIsFirstRun(!hasModels);
return !hasModels;
}
return false;
} catch (err) { } catch (err) {
console.error("Failed to check model availability:", err); console.error("Failed to check model availability:", err);
return false; return false;
@ -77,11 +72,16 @@ export const useModels = () => {
const selectModel = async (modelId: string) => { const selectModel = async (modelId: string) => {
try { try {
setError(null); setError(null);
await invoke("set_active_model", { modelId }); const result = await commands.setActiveModel(modelId);
setCurrentModel(modelId); if (result.status === "ok") {
setIsFirstRun(false); setCurrentModel(modelId);
setHasAnyModels(true); setIsFirstRun(false);
return true; setHasAnyModels(true);
return true;
} else {
setError(`Failed to switch to model: ${result.error}`);
return false;
}
} catch (err) { } catch (err) {
setError(`Failed to switch to model: ${err}`); setError(`Failed to switch to model: ${err}`);
return false; return false;
@ -92,8 +92,18 @@ export const useModels = () => {
try { try {
setError(null); setError(null);
setDownloadingModels((prev) => new Set(prev.add(modelId))); setDownloadingModels((prev) => new Set(prev.add(modelId)));
await invoke("download_model", { modelId }); const result = await commands.downloadModel(modelId);
return true; 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) { } catch (err) {
setError(`Failed to download model: ${err}`); setError(`Failed to download model: ${err}`);
setDownloadingModels((prev) => { setDownloadingModels((prev) => {
@ -108,9 +118,14 @@ export const useModels = () => {
const deleteModel = async (modelId: string) => { const deleteModel = async (modelId: string) => {
try { try {
setError(null); setError(null);
await invoke("delete_model", { modelId }); const result = await commands.deleteModel(modelId);
await loadModels(); // Refresh the list if (result.status === "ok") {
return true; await loadModels(); // Refresh the list
return true;
} else {
setError(`Failed to delete model: ${result.error}`);
return false;
}
} catch (err) { } catch (err) {
setError(`Failed to delete model: ${err}`); setError(`Failed to delete model: ${err}`);
return false; return false;

View file

@ -1,6 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useSettingsStore } from "../stores/settingsStore"; import { useSettingsStore } from "../stores/settingsStore";
import { Settings, AudioDevice } from "../lib/types"; import type { AppSettings as Settings, AudioDevice } from "@/bindings";
interface UseSettingsReturn { interface UseSettingsReturn {
// State // State

View file

@ -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<typeof OverlayPositionSchema>;
export const ModelUnloadTimeoutSchema = z.enum([
"never",
"immediately",
"min2",
"min5",
"min10",
"min15",
"hour1",
"sec5",
]);
export type ModelUnloadTimeout = z.infer<typeof ModelUnloadTimeoutSchema>;
export const PasteMethodSchema = z.enum(["ctrl_v", "direct", "shift_insert"]);
export type PasteMethod = z.infer<typeof PasteMethodSchema>;
export const ClipboardHandlingSchema = z.enum([
"dont_modify",
"copy_to_clipboard",
]);
export type ClipboardHandling = z.infer<typeof ClipboardHandlingSchema>;
export const LogLevelSchema = z.number().int().min(1).max(5).default(2);
export type LogLevelValue = z.infer<typeof LogLevelSchema>;
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<typeof LLMPromptSchema>;
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<typeof PostProcessProviderSchema>;
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<typeof AudioDeviceSchema>;
export type BindingResponse = z.infer<typeof BindingResponseSchema>;
export type ShortcutBinding = z.infer<typeof ShortcutBindingSchema>;
export type ShortcutBindingsMap = z.infer<typeof ShortcutBindingsMapSchema>;
export type Settings = z.infer<typeof SettingsSchema>;
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<typeof ModelInfoSchema>;

View file

@ -1,4 +1,3 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { import {
@ -7,6 +6,7 @@ import {
CancelIcon, CancelIcon,
} from "../components/icons"; } from "../components/icons";
import "./RecordingOverlay.css"; import "./RecordingOverlay.css";
import { commands } from "@/bindings";
type OverlayState = "recording" | "transcribing"; type OverlayState = "recording" | "transcribing";
@ -93,7 +93,7 @@ const RecordingOverlay: React.FC = () => {
<div <div
className="cancel-button" className="cancel-button"
onClick={() => { onClick={() => {
invoke("cancel_operation"); commands.cancelOperation();
}} }}
> >
<CancelIcon /> <CancelIcon />

View file

@ -1,10 +1,11 @@
import { create } from "zustand"; import { create } from "zustand";
import { subscribeWithSelector } from "zustand/middleware"; import { subscribeWithSelector } from "zustand/middleware";
import { invoke } from "@tauri-apps/api/core"; import type { AppSettings as Settings, AudioDevice } from "@/bindings";
import { Settings, AudioDevice } from "../lib/types"; import { commands } from "@/bindings";
interface SettingsStore { interface SettingsStore {
settings: Settings | null; settings: Settings | null;
defaultSettings: Settings | null;
isLoading: boolean; isLoading: boolean;
isUpdating: Record<string, boolean>; isUpdating: Record<string, boolean>;
audioDevices: AudioDevice[]; audioDevices: AudioDevice[];
@ -14,6 +15,7 @@ interface SettingsStore {
// Actions // Actions
initialize: () => Promise<void>; initialize: () => Promise<void>;
loadDefaultSettings: () => Promise<void>;
updateSetting: <K extends keyof Settings>( updateSetting: <K extends keyof Settings>(
key: K, key: K,
value: Settings[K], value: Settings[K],
@ -48,6 +50,7 @@ interface SettingsStore {
// Internal state setters // Internal state setters
setSettings: (settings: Settings | null) => void; setSettings: (settings: Settings | null) => void;
setDefaultSettings: (defaultSettings: Settings | null) => void;
setLoading: (loading: boolean) => void; setLoading: (loading: boolean) => void;
setUpdating: (key: string, updating: boolean) => void; setUpdating: (key: string, updating: boolean) => void;
setAudioDevices: (devices: AudioDevice[]) => void; setAudioDevices: (devices: AudioDevice[]) => void;
@ -55,29 +58,8 @@ interface SettingsStore {
setCustomSounds: (sounds: { start: boolean; stop: boolean }) => void; setCustomSounds: (sounds: { start: boolean; stop: boolean }) => void;
} }
// Note: Default post-processing settings are now managed in Rust // Note: Default settings are now fetched from Rust via commands.getDefaultSettings()
// Settings will always have providers after migration // This ensures platform-specific defaults (like overlay_position, shortcuts, paste_method) work correctly
const DEFAULT_SETTINGS: Partial<Settings> = {
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,
};
const DEFAULT_AUDIO_DEVICE: AudioDevice = { const DEFAULT_AUDIO_DEVICE: AudioDevice = {
index: "default", index: "default",
@ -89,60 +71,61 @@ const settingUpdaters: {
[K in keyof Settings]?: (value: Settings[K]) => Promise<unknown>; [K in keyof Settings]?: (value: Settings[K]) => Promise<unknown>;
} = { } = {
always_on_microphone: (value) => always_on_microphone: (value) =>
invoke("update_microphone_mode", { alwaysOn: value }), commands.updateMicrophoneMode(value as boolean),
audio_feedback: (value) => audio_feedback: (value) =>
invoke("change_audio_feedback_setting", { enabled: value }), commands.changeAudioFeedbackSetting(value as boolean),
audio_feedback_volume: (value) => audio_feedback_volume: (value) =>
invoke("change_audio_feedback_volume_setting", { volume: value }), commands.changeAudioFeedbackVolumeSetting(value as number),
sound_theme: (value) => sound_theme: (value) =>
invoke("change_sound_theme_setting", { theme: value }), commands.changeSoundThemeSetting(value as string),
start_hidden: (value) => start_hidden: (value) =>
invoke("change_start_hidden_setting", { enabled: value }), commands.changeStartHiddenSetting(value as boolean),
autostart_enabled: (value) => autostart_enabled: (value) =>
invoke("change_autostart_setting", { enabled: value }), commands.changeAutostartSetting(value as boolean),
push_to_talk: (value) => invoke("change_ptt_setting", { enabled: value }), push_to_talk: (value) => commands.changePttSetting(value as boolean),
selected_microphone: (value) => selected_microphone: (value) =>
invoke("set_selected_microphone", { commands.setSelectedMicrophone(
deviceName: value === "Default" ? "default" : value, (value as string) === "Default" || value === null ? "default" : (value as string)
}), ),
clamshell_microphone: (value) => clamshell_microphone: (value) =>
invoke("set_clamshell_microphone", { commands.setClamshellMicrophone(
deviceName: value === "Default" ? "default" : value, (value as string) === "Default" ? "default" : (value as string)
}), ),
selected_output_device: (value) => selected_output_device: (value) =>
invoke("set_selected_output_device", { commands.setSelectedOutputDevice(
deviceName: value === "Default" ? "default" : value, (value as string) === "Default" || value === null ? "default" : (value as string)
}), ),
recording_retention_period: (value) => recording_retention_period: (value) =>
invoke("update_recording_retention_period", { period: value }), commands.updateRecordingRetentionPeriod(value as string),
translate_to_english: (value) => translate_to_english: (value) =>
invoke("change_translate_to_english_setting", { enabled: value }), commands.changeTranslateToEnglishSetting(value as boolean),
selected_language: (value) => selected_language: (value) =>
invoke("change_selected_language_setting", { language: value }), commands.changeSelectedLanguageSetting(value as string),
overlay_position: (value) => overlay_position: (value) =>
invoke("change_overlay_position_setting", { position: value }), commands.changeOverlayPositionSetting(value as string),
debug_mode: (value) => debug_mode: (value) =>
invoke("change_debug_mode_setting", { enabled: value }), commands.changeDebugModeSetting(value as boolean),
custom_words: (value) => invoke("update_custom_words", { words: value }), custom_words: (value) => commands.updateCustomWords(value as string[]),
word_correction_threshold: (value) => word_correction_threshold: (value) =>
invoke("change_word_correction_threshold_setting", { threshold: value }), commands.changeWordCorrectionThresholdSetting(value as number),
paste_method: (value) => paste_method: (value) =>
invoke("change_paste_method_setting", { method: value }), commands.changePasteMethodSetting(value as string),
clipboard_handling: (value) => clipboard_handling: (value) =>
invoke("change_clipboard_handling_setting", { handling: value }), commands.changeClipboardHandlingSetting(value as string),
history_limit: (value) => invoke("update_history_limit", { limit: value }), history_limit: (value) => commands.updateHistoryLimit(value as string),
post_process_enabled: (value) => post_process_enabled: (value) =>
invoke("change_post_process_enabled_setting", { enabled: value }), commands.changePostProcessEnabledSetting(value as boolean),
post_process_selected_prompt_id: (value) => post_process_selected_prompt_id: (value) =>
invoke("set_post_process_selected_prompt", { id: value }), commands.setPostProcessSelectedPrompt(value as string),
mute_while_recording: (value) => mute_while_recording: (value) =>
invoke("change_mute_while_recording_setting", { enabled: value }), commands.changeMuteWhileRecordingSetting(value as boolean),
log_level: (value) => invoke("set_log_level", { level: value }), log_level: (value) => commands.setLogLevel(value as any),
}; };
export const useSettingsStore = create<SettingsStore>()( export const useSettingsStore = create<SettingsStore>()(
subscribeWithSelector((set, get) => ({ subscribeWithSelector((set, get) => ({
settings: null, settings: null,
defaultSettings: null,
isLoading: true, isLoading: true,
isUpdating: {}, isUpdating: {},
audioDevices: [], audioDevices: [],
@ -152,6 +135,7 @@ export const useSettingsStore = create<SettingsStore>()(
// Internal setters // Internal setters
setSettings: (settings) => set({ settings }), setSettings: (settings) => set({ settings }),
setDefaultSettings: (defaultSettings) => set({ defaultSettings }),
setLoading: (isLoading) => set({ isLoading }), setLoading: (isLoading) => set({ isLoading }),
setUpdating: (key, updating) => setUpdating: (key, updating) =>
set((state) => ({ set((state) => ({
@ -168,48 +152,24 @@ export const useSettingsStore = create<SettingsStore>()(
// Load settings from store // Load settings from store
refreshSettings: async () => { refreshSettings: async () => {
try { try {
const { load } = await import("@tauri-apps/plugin-store"); const result = await commands.getAppSettings();
const store = await load("settings_store.json", { if (result.status === "ok") {
defaults: DEFAULT_SETTINGS, const settings = result.data;
autoSave: false, const normalizedSettings: Settings = {
}); ...settings,
const settings = (await store.get("settings")) as Settings; always_on_microphone: settings.always_on_microphone ?? false,
selected_microphone:
// Load additional settings that come from invoke calls settings.selected_microphone ?? "Default",
const [ clamshell_microphone:
microphoneMode, settings.clamshell_microphone ?? "Default",
selectedMicrophone, selected_output_device:
clamshellMicrophone, settings.selected_output_device ?? "Default",
selectedOutputDevice, };
] = await Promise.allSettled([ set({ settings: normalizedSettings, isLoading: false });
invoke("get_microphone_mode"), } else {
invoke("get_selected_microphone"), console.error("Failed to load settings:", result.error);
invoke("get_clamshell_microphone"), set({ isLoading: false });
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 });
} catch (error) { } catch (error) {
console.error("Failed to load settings:", error); console.error("Failed to load settings:", error);
set({ isLoading: false }); set({ isLoading: false });
@ -219,16 +179,18 @@ export const useSettingsStore = create<SettingsStore>()(
// Load audio devices // Load audio devices
refreshAudioDevices: async () => { refreshAudioDevices: async () => {
try { try {
const devices: AudioDevice[] = await invoke( const result = await commands.getAvailableMicrophones();
"get_available_microphones", if (result.status === "ok") {
); const devicesWithDefault = [
const devicesWithDefault = [ DEFAULT_AUDIO_DEVICE,
DEFAULT_AUDIO_DEVICE, ...result.data.filter(
...devices.filter( (d) => d.name !== "Default" && d.name !== "default",
(d) => d.name !== "Default" && d.name !== "default", ),
), ];
]; set({ audioDevices: devicesWithDefault });
set({ audioDevices: devicesWithDefault }); } else {
set({ audioDevices: [DEFAULT_AUDIO_DEVICE] });
}
} catch (error) { } catch (error) {
console.error("Failed to load audio devices:", error); console.error("Failed to load audio devices:", error);
set({ audioDevices: [DEFAULT_AUDIO_DEVICE] }); set({ audioDevices: [DEFAULT_AUDIO_DEVICE] });
@ -238,16 +200,18 @@ export const useSettingsStore = create<SettingsStore>()(
// Load output devices // Load output devices
refreshOutputDevices: async () => { refreshOutputDevices: async () => {
try { try {
const devices: AudioDevice[] = await invoke( const result = await commands.getAvailableOutputDevices();
"get_available_output_devices", if (result.status === "ok") {
); const devicesWithDefault = [
const devicesWithDefault = [ DEFAULT_AUDIO_DEVICE,
DEFAULT_AUDIO_DEVICE, ...result.data.filter(
...devices.filter( (d) => d.name !== "Default" && d.name !== "default",
(d) => d.name !== "Default" && d.name !== "default", ),
), ];
]; set({ outputDevices: devicesWithDefault });
set({ outputDevices: devicesWithDefault }); } else {
set({ outputDevices: [DEFAULT_AUDIO_DEVICE] });
}
} catch (error) { } catch (error) {
console.error("Failed to load output devices:", error); console.error("Failed to load output devices:", error);
set({ outputDevices: [DEFAULT_AUDIO_DEVICE] }); set({ outputDevices: [DEFAULT_AUDIO_DEVICE] });
@ -257,7 +221,7 @@ export const useSettingsStore = create<SettingsStore>()(
// Play a test sound // Play a test sound
playTestSound: async (soundType: "start" | "stop") => { playTestSound: async (soundType: "start" | "stop") => {
try { try {
await invoke("play_test_sound", { soundType }); await commands.playTestSound(soundType);
} catch (error) { } catch (error) {
console.error(`Failed to play test sound (${soundType}):`, error); console.error(`Failed to play test sound (${soundType}):`, error);
} }
@ -265,8 +229,8 @@ export const useSettingsStore = create<SettingsStore>()(
checkCustomSounds: async () => { checkCustomSounds: async () => {
try { try {
const sounds = await invoke("check_custom_sounds"); const sounds = await commands.checkCustomSounds();
get().setCustomSounds(sounds as { start: boolean; stop: boolean }); get().setCustomSounds(sounds);
} catch (error) { } catch (error) {
console.error("Failed to check custom sounds:", error); console.error("Failed to check custom sounds:", error);
} }
@ -306,9 +270,12 @@ export const useSettingsStore = create<SettingsStore>()(
// Reset a setting to its default value // Reset a setting to its default value
resetSetting: async (key) => { resetSetting: async (key) => {
const defaultValue = DEFAULT_SETTINGS[key]; const { defaultSettings } = get();
if (defaultValue !== undefined) { if (defaultSettings) {
await get().updateSetting(key, defaultValue as any); const defaultValue = defaultSettings[key];
if (defaultValue !== undefined) {
await get().updateSetting(key, defaultValue as any);
}
} }
}, },
@ -329,7 +296,7 @@ export const useSettingsStore = create<SettingsStore>()(
bindings: { bindings: {
...state.settings.bindings, ...state.settings.bindings,
[id]: { [id]: {
...state.settings.bindings[id], ...state.settings.bindings[id]!,
current_binding: binding, current_binding: binding,
}, },
}, },
@ -337,7 +304,7 @@ export const useSettingsStore = create<SettingsStore>()(
: null, : null,
})); }));
await invoke("change_binding", { id, binding }); await commands.changeBinding(id, binding);
} catch (error) { } catch (error) {
console.error(`Failed to update binding ${id}:`, error); console.error(`Failed to update binding ${id}:`, error);
@ -350,7 +317,7 @@ export const useSettingsStore = create<SettingsStore>()(
bindings: { bindings: {
...state.settings.bindings, ...state.settings.bindings,
[id]: { [id]: {
...state.settings.bindings[id], ...state.settings.bindings[id]!,
current_binding: originalBinding, current_binding: originalBinding,
}, },
}, },
@ -371,7 +338,7 @@ export const useSettingsStore = create<SettingsStore>()(
setUpdating(updateKey, true); setUpdating(updateKey, true);
try { try {
await invoke("reset_binding", { id }); await commands.resetBinding(id);
await refreshSettings(); await refreshSettings();
} catch (error) { } catch (error) {
console.error(`Failed to reset binding ${id}:`, error); console.error(`Failed to reset binding ${id}:`, error);
@ -396,7 +363,7 @@ export const useSettingsStore = create<SettingsStore>()(
} }
try { try {
await invoke("set_post_process_provider", { providerId }); await commands.setPostProcessProvider(providerId);
await refreshSettings(); await refreshSettings();
} catch (error) { } catch (error) {
console.error("Failed to set post-process provider:", error); console.error("Failed to set post-process provider:", error);
@ -421,27 +388,16 @@ export const useSettingsStore = create<SettingsStore>()(
const { setUpdating, refreshSettings } = get(); const { setUpdating, refreshSettings } = get();
const updateKey = `post_process_${settingType}:${providerId}`; 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); setUpdating(updateKey, true);
try { try {
await invoke(commandMap[settingType], { if (settingType === "base_url") {
providerId, await commands.changePostProcessBaseUrlSetting(providerId, value);
[paramMap[settingType]]: value, } else if (settingType === "api_key") {
}); await commands.changePostProcessApiKeySetting(providerId, value);
} else if (settingType === "model") {
await commands.changePostProcessModelSetting(providerId, value);
}
await refreshSettings(); await refreshSettings();
} catch (error) { } catch (error) {
console.error( console.error(
@ -480,12 +436,14 @@ export const useSettingsStore = create<SettingsStore>()(
try { try {
// Call Tauri backend command instead of fetch // Call Tauri backend command instead of fetch
const models: string[] = await invoke("fetch_post_process_models", { const result = await commands.fetchPostProcessModels(providerId);
providerId, if (result.status === "ok") {
}); setPostProcessModelOptions(providerId, result.data);
return result.data;
setPostProcessModelOptions(providerId, models); } else {
return models; console.error("Failed to fetch models:", result.error);
return [];
}
} catch (error) { } catch (error) {
console.error("Failed to fetch models:", error); console.error("Failed to fetch models:", error);
// Don't cache empty array on error - let user retry // Don't cache empty array on error - let user retry
@ -503,6 +461,20 @@ export const useSettingsStore = create<SettingsStore>()(
}, },
})), })),
// 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 everything
initialize: async () => { initialize: async () => {
const { const {
@ -510,8 +482,10 @@ export const useSettingsStore = create<SettingsStore>()(
refreshAudioDevices, refreshAudioDevices,
refreshOutputDevices, refreshOutputDevices,
checkCustomSounds, checkCustomSounds,
loadDefaultSettings,
} = get(); } = get();
await Promise.all([ await Promise.all([
loadDefaultSettings(),
refreshSettings(), refreshSettings(),
refreshAudioDevices(), refreshAudioDevices(),
refreshOutputDevices(), refreshOutputDevices(),

View file

@ -15,6 +15,12 @@
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
/* Path Aliases */
"baseUrl": ".",
"paths": {
"@/bindings": ["./src/bindings.ts"]
},
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": false, "noUnusedLocals": false,

View file

@ -9,6 +9,13 @@ const host = process.env.TAURI_DEV_HOST;
export default defineConfig(async () => ({ export default defineConfig(async () => ({
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss()],
// Path aliases
resolve: {
alias: {
"@/bindings": resolve(__dirname, "./src/bindings.ts"),
},
},
// Multiple entry points for main app and overlay // Multiple entry points for main app and overlay
build: { build: {
rollupOptions: { rollupOptions: {