Handy/src-tauri/src/settings.rs
Maicon Moreira 7c04ee85a7
feat: add “mute while recording” setting (#257)
* feat: mute while recording

* refactor: move MuteWhileRecording from AdvancedSettings to DebugSettings
2025-10-28 17:49:33 -07:00

345 lines
9.5 KiB
Rust

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<u64> {
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<u64> {
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<String, ShortcutBinding>,
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<String>,
#[serde(default)]
pub selected_output_device: Option<String>,
#[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<String>,
#[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::<AppSettings>(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::<AppSettings>(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<String, ShortcutBinding> {
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
}