From 7afc9f86bddcba0937df811fa5e8ea6ca546b1c7 Mon Sep 17 00:00:00 2001 From: hl9020 <114366310+hl9020@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:24:03 +0200 Subject: [PATCH] fix(settings): persist activation mode across restarts Activation mode lived only in React state: there was no config field and no save/get command, so every launch fell back to the hardcoded push-to-talk default. Store it in config.json alongside the other settings. --- src-tauri/src/config.rs | 30 ++++++++++++++++++++++++++++++ src-tauri/src/main.rs | 2 ++ src/App.tsx | 14 ++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 0e3623b..66eac98 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -14,6 +14,9 @@ fn config_mutex() -> &'static Mutex<()> { /// Default endpointing (seconds) used when the field is absent. pub const DEFAULT_ENDPOINTING: f64 = 0.1; +/// Default activation mode used when the field is absent. +pub const DEFAULT_ACTIVATION_MODE: &str = "push-to-talk"; + fn default_hotkey() -> String { if cfg!(target_os = "macos") { "Fn".to_string() @@ -33,6 +36,9 @@ struct Config { /// Whether to leave the final transcription on the clipboard after a /// dictation session (instead of restoring the user's original clipboard). copy_to_clipboard: Option, + /// How the trigger key starts a dictation: "push-to-talk" (hold) or + /// "toggle" (press once to start, once to stop). + activation_mode: Option, custom_vocabulary: Option>, /// Endpointing duration (seconds of silence before an utterance is /// considered final). Missing (older config files) defaults to 0.1 — @@ -61,6 +67,7 @@ impl Default for Config { languages: Some(vec!["en".to_string()]), code_switching: Some(false), copy_to_clipboard: Some(false), + activation_mode: Some(DEFAULT_ACTIVATION_MODE.to_string()), custom_vocabulary: Some(default_custom_vocabulary()), endpointing: Some(DEFAULT_ENDPOINTING), audio_device_selection: Some(AudioDeviceSelection::Automatic), @@ -88,6 +95,9 @@ impl Config { if self.copy_to_clipboard.is_none() { self.copy_to_clipboard = defaults.copy_to_clipboard; } + if self.activation_mode.is_none() { + self.activation_mode = defaults.activation_mode; + } if self.custom_vocabulary.is_none() { self.custom_vocabulary = defaults.custom_vocabulary; } @@ -378,6 +388,26 @@ pub async fn get_copy_to_clipboard() -> Result { Ok(copy_to_clipboard()) } +#[tauri::command] +pub async fn save_activation_mode(mode: String) -> Result<(), String> { + if mode != "toggle" && mode != DEFAULT_ACTIVATION_MODE { + return Err(format!("Unknown activation mode: {mode}")); + } + with_config(|config| { + config.activation_mode = Some(mode); + }) +} + +#[tauri::command] +pub async fn get_activation_mode() -> Result { + Ok(read_config(|config| { + config + .activation_mode + .clone() + .unwrap_or_else(|| DEFAULT_ACTIVATION_MODE.to_string()) + })) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 728ef28..6170ec3 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1264,6 +1264,8 @@ fn main() { config::get_endpointing, config::save_copy_to_clipboard, config::get_copy_to_clipboard, + config::save_activation_mode, + config::get_activation_mode, config::save_audio_device_selection, config::get_audio_device_selection, vocabulary::save_custom_vocabulary, diff --git a/src/App.tsx b/src/App.tsx index fc79e69..82d3493 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import type { TranscriptionHistoryPage, AudioDeviceInfo, AudioDeviceSelection, + ActivationMode, } from "./types"; import { invoke } from "@tauri-apps/api/core"; import { getVersion } from "@tauri-apps/api/app"; @@ -190,6 +191,7 @@ export default function App() { const vocabularySettingsLoaded = useRef(false); const endpointingLoaded = useRef(false); const copyToClipboardLoaded = useRef(false); + const activationModeLoaded = useRef(false); const audioDeviceSelectionLoaded = useRef(false); const isRecordingRef = useRef(false); @@ -288,6 +290,12 @@ export default function App() { enabled: settings.copyToClipboard, }).catch(console.error); }, [settings.copyToClipboard]); + useEffect(() => { + if (!activationModeLoaded.current) return; + invoke("save_activation_mode", { mode: settings.activationMode }).catch( + console.error, + ); + }, [settings.activationMode]); useEffect(() => { if (!audioDeviceSelectionLoaded.current) return; invoke("save_audio_device_selection", { @@ -463,6 +471,10 @@ export default function App() { "get_copy_to_clipboard", ).catch(() => false); + const savedActivationMode = await invoke( + "get_activation_mode", + ).catch(() => "push-to-talk" as ActivationMode); + const savedAudioDeviceSelection = await invoke( "get_audio_device_selection", ).catch(() => ({ mode: "automatic" }) as AudioDeviceSelection); @@ -478,6 +490,7 @@ export default function App() { : {}), endpointing: savedEndpointing, copyToClipboard: savedCopyToClipboard, + activationMode: savedActivationMode, audioDeviceSelection: savedAudioDeviceSelection, customVocabulary: savedVocabulary, })); @@ -485,6 +498,7 @@ export default function App() { vocabularySettingsLoaded.current = true; endpointingLoaded.current = true; copyToClipboardLoaded.current = true; + activationModeLoaded.current = true; audioDeviceSelectionLoaded.current = true; setSettingsReady(true); };