use serde::{Deserialize, Serialize}; use std::collections::HashMap; use tauri::AppHandle; use tauri_plugin_store::StoreExt; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ShortcutBinding { pub id: String, pub name: String, pub description: String, pub default_binding: String, pub current_binding: String, } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum OverlayPosition { None, Top, Bottom, } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ModelUnloadTimeout { Never, Immediately, Min2, Min5, Min10, Min15, Hour1, Sec5, // Debug mode only } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum PasteMethod { CtrlV, Direct, } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ClipboardHandling { DontModify, CopyToClipboard, } impl Default for ModelUnloadTimeout { fn default() -> Self { ModelUnloadTimeout::Never } } impl Default for PasteMethod { fn default() -> Self { // Default to CtrlV for macOS and Windows, Direct for Linux #[cfg(target_os = "linux")] return PasteMethod::Direct; #[cfg(not(target_os = "linux"))] return PasteMethod::CtrlV; } } impl Default for ClipboardHandling { fn default() -> Self { ClipboardHandling::DontModify } } impl ModelUnloadTimeout { pub fn to_minutes(self) -> Option { match self { ModelUnloadTimeout::Never => None, ModelUnloadTimeout::Immediately => Some(0), // Special case for immediate unloading ModelUnloadTimeout::Min2 => Some(2), ModelUnloadTimeout::Min5 => Some(5), ModelUnloadTimeout::Min10 => Some(10), ModelUnloadTimeout::Min15 => Some(15), ModelUnloadTimeout::Hour1 => Some(60), ModelUnloadTimeout::Sec5 => Some(0), // Special case for debug - handled separately } } pub fn to_seconds(self) -> Option { match self { ModelUnloadTimeout::Never => None, ModelUnloadTimeout::Immediately => Some(0), // Special case for immediate unloading ModelUnloadTimeout::Sec5 => Some(5), _ => self.to_minutes().map(|m| m * 60), } } } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum SoundTheme { Marimba, Pop, Custom, } impl SoundTheme { fn as_str(&self) -> &'static str { match self { SoundTheme::Marimba => "marimba", SoundTheme::Pop => "pop", SoundTheme::Custom => "custom", } } pub fn to_start_path(&self) -> String { format!("resources/{}_start.wav", self.as_str()) } pub fn to_stop_path(&self) -> String { format!("resources/{}_stop.wav", self.as_str()) } } /* still handy for composing the initial JSON in the store ------------- */ #[derive(Serialize, Deserialize, Debug, Clone)] pub struct AppSettings { pub bindings: HashMap, pub push_to_talk: bool, pub audio_feedback: bool, #[serde(default = "default_audio_feedback_volume")] pub audio_feedback_volume: f32, #[serde(default = "default_sound_theme")] pub sound_theme: SoundTheme, #[serde(default = "default_start_hidden")] pub start_hidden: bool, #[serde(default = "default_autostart_enabled")] pub autostart_enabled: bool, #[serde(default = "default_model")] pub selected_model: String, #[serde(default = "default_always_on_microphone")] pub always_on_microphone: bool, #[serde(default)] pub selected_microphone: Option, #[serde(default)] pub selected_output_device: Option, #[serde(default = "default_translate_to_english")] pub translate_to_english: bool, #[serde(default = "default_selected_language")] pub selected_language: String, #[serde(default = "default_overlay_position")] pub overlay_position: OverlayPosition, #[serde(default = "default_debug_mode")] pub debug_mode: bool, #[serde(default)] pub custom_words: Vec, #[serde(default)] pub model_unload_timeout: ModelUnloadTimeout, #[serde(default = "default_word_correction_threshold")] pub word_correction_threshold: f64, #[serde(default = "default_history_limit")] pub history_limit: usize, #[serde(default)] pub paste_method: PasteMethod, #[serde(default)] pub clipboard_handling: ClipboardHandling, #[serde(default)] pub mute_while_recording: bool, } fn default_model() -> String { "".to_string() } fn default_always_on_microphone() -> bool { false } fn default_translate_to_english() -> bool { false } fn default_start_hidden() -> bool { false } fn default_autostart_enabled() -> bool { false } fn default_selected_language() -> String { "auto".to_string() } fn default_overlay_position() -> OverlayPosition { #[cfg(target_os = "linux")] return OverlayPosition::None; #[cfg(not(target_os = "linux"))] return OverlayPosition::Bottom; } fn default_debug_mode() -> bool { false } fn default_word_correction_threshold() -> f64 { 0.18 } fn default_history_limit() -> usize { 5 } fn default_audio_feedback_volume() -> f32 { 1.0 } fn default_sound_theme() -> SoundTheme { SoundTheme::Marimba } pub const SETTINGS_STORE_PATH: &str = "settings_store.json"; pub fn get_default_settings() -> AppSettings { #[cfg(target_os = "windows")] let default_shortcut = "ctrl+space"; #[cfg(target_os = "macos")] let default_shortcut = "option+space"; #[cfg(target_os = "linux")] let default_shortcut = "ctrl+space"; #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] let default_shortcut = "alt+space"; let mut bindings = HashMap::new(); bindings.insert( "transcribe".to_string(), ShortcutBinding { id: "transcribe".to_string(), name: "Transcribe".to_string(), description: "Converts your speech into text.".to_string(), default_binding: default_shortcut.to_string(), current_binding: default_shortcut.to_string(), }, ); AppSettings { bindings, push_to_talk: true, audio_feedback: false, audio_feedback_volume: default_audio_feedback_volume(), sound_theme: default_sound_theme(), start_hidden: default_start_hidden(), autostart_enabled: default_autostart_enabled(), selected_model: "".to_string(), always_on_microphone: false, selected_microphone: None, selected_output_device: None, translate_to_english: false, selected_language: "auto".to_string(), overlay_position: OverlayPosition::Bottom, debug_mode: false, custom_words: Vec::new(), model_unload_timeout: ModelUnloadTimeout::Never, word_correction_threshold: default_word_correction_threshold(), history_limit: default_history_limit(), paste_method: PasteMethod::default(), clipboard_handling: ClipboardHandling::default(), mute_while_recording: false, } } pub fn load_or_create_app_settings(app: &AppHandle) -> AppSettings { // Initialize store let store = app .store(SETTINGS_STORE_PATH) .expect("Failed to initialize store"); let settings = if let Some(settings_value) = store.get("settings") { // Parse the entire settings object match serde_json::from_value::(settings_value) { Ok(settings) => { println!("Found existing settings: {:?}", settings); settings } Err(e) => { println!("Failed to parse settings: {}", e); // Fall back to default settings if parsing fails let default_settings = get_default_settings(); // Store the default settings store.set("settings", serde_json::to_value(&default_settings).unwrap()); default_settings } } } else { // Create default settings let default_settings = get_default_settings(); // Store the settings store.set("settings", serde_json::to_value(&default_settings).unwrap()); default_settings }; settings } pub fn get_settings(app: &AppHandle) -> AppSettings { let store = app .store(SETTINGS_STORE_PATH) .expect("Failed to initialize store"); if let Some(settings_value) = store.get("settings") { serde_json::from_value::(settings_value) .unwrap_or_else(|_| get_default_settings()) } else { get_default_settings() } } pub fn write_settings(app: &AppHandle, settings: AppSettings) { let store = app .store(SETTINGS_STORE_PATH) .expect("Failed to initialize store"); store.set("settings", serde_json::to_value(&settings).unwrap()); } pub fn get_bindings(app: &AppHandle) -> HashMap { let settings = get_settings(app); settings.bindings } pub fn get_stored_binding(app: &AppHandle, id: &str) -> ShortcutBinding { let bindings = get_bindings(app); let binding = bindings.get(id).unwrap().clone(); binding } pub fn get_history_limit(app: &AppHandle) -> usize { let settings = get_settings(app); settings.history_limit }