From 246feda45dc808ad83f3dc0a378ded401bf87abe Mon Sep 17 00:00:00 2001 From: Danny Smith Date: Fri, 5 Dec 2025 10:37:33 +0000 Subject: [PATCH] Add option to append trailing space after pasted text (#405) --- src-tauri/src/clipboard.rs | 7 +++++ src-tauri/src/lib.rs | 1 + src-tauri/src/settings.rs | 3 +++ src-tauri/src/shortcut.rs | 10 +++++++ src/bindings.ts | 10 ++++++- .../settings/AppendTrailingSpace.tsx | 27 +++++++++++++++++++ .../settings/debug/DebugSettings.tsx | 2 ++ src/stores/settingsStore.ts | 2 ++ 8 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 src/components/settings/AppendTrailingSpace.tsx diff --git a/src-tauri/src/clipboard.rs b/src-tauri/src/clipboard.rs index 2eb3a11..602b379 100644 --- a/src-tauri/src/clipboard.rs +++ b/src-tauri/src/clipboard.rs @@ -234,6 +234,13 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> { let settings = get_settings(&app_handle); let paste_method = settings.paste_method; + // Append trailing space if setting is enabled + let text = if settings.append_trailing_space { + format!("{} ", text) + } else { + text + }; + info!("Using paste method: {:?}", paste_method); // Perform the paste operation diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7958fd9..4ccc4bf 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -253,6 +253,7 @@ pub fn run() { shortcut::suspend_binding, shortcut::resume_binding, shortcut::change_mute_while_recording_setting, + shortcut::change_append_trailing_space_setting, shortcut::change_update_checks_setting, trigger_update_check, commands::cancel_operation, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 1981e4c..26563c2 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -285,6 +285,8 @@ pub struct AppSettings { pub post_process_selected_prompt_id: Option, #[serde(default)] pub mute_while_recording: bool, + #[serde(default)] + pub append_trailing_space: bool, } fn default_model() -> String { @@ -483,6 +485,7 @@ pub fn get_default_settings() -> AppSettings { post_process_prompts: default_post_process_prompts(), post_process_selected_prompt_id: None, mute_while_recording: false, + append_trailing_space: false, } } diff --git a/src-tauri/src/shortcut.rs b/src-tauri/src/shortcut.rs index fdd5d75..04d8e0b 100644 --- a/src-tauri/src/shortcut.rs +++ b/src-tauri/src/shortcut.rs @@ -693,6 +693,16 @@ pub fn change_mute_while_recording_setting(app: AppHandle, enabled: bool) -> Res Ok(()) } +#[tauri::command] +#[specta::specta] +pub fn change_append_trailing_space_setting(app: AppHandle, enabled: bool) -> Result<(), String> { + let mut settings = settings::get_settings(&app); + settings.append_trailing_space = enabled; + settings::write_settings(&app, settings); + + Ok(()) +} + /// Determine whether a shortcut string contains at least one non-modifier key. /// We allow single non-modifier keys (e.g. "f5" or "space") but disallow /// modifier-only combos (e.g. "ctrl" or "ctrl+shift"). diff --git a/src/bindings.ts b/src/bindings.ts index ebde299..ec51ac5 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -244,6 +244,14 @@ async changeMuteWhileRecordingSetting(enabled: boolean) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_append_trailing_space_setting", { enabled }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async changeUpdateChecksSetting(enabled: boolean) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("change_update_checks_setting", { enabled }) }; @@ -602,7 +610,7 @@ async isLaptop() : Promise> { /** 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; update_checks_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 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; update_checks_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; append_trailing_space?: 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" diff --git a/src/components/settings/AppendTrailingSpace.tsx b/src/components/settings/AppendTrailingSpace.tsx new file mode 100644 index 0000000..9374c45 --- /dev/null +++ b/src/components/settings/AppendTrailingSpace.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { ToggleSwitch } from "../ui/ToggleSwitch"; +import { useSettings } from "../../hooks/useSettings"; + +interface AppendTrailingSpaceProps { + descriptionMode?: "inline" | "tooltip"; + grouped?: boolean; +} + +export const AppendTrailingSpace: React.FC = + React.memo(({ descriptionMode = "tooltip", grouped = false }) => { + const { getSetting, updateSetting, isUpdating } = useSettings(); + + const enabled = getSetting("append_trailing_space") ?? false; + + return ( + updateSetting("append_trailing_space", enabled)} + isUpdating={isUpdating("append_trailing_space")} + label="Append Trailing Space" + description="Automatically add a space at the end of transcribed text, making it easier to dictate multiple sentences in a row." + descriptionMode={descriptionMode} + grouped={grouped} + /> + ); + }); diff --git a/src/components/settings/debug/DebugSettings.tsx b/src/components/settings/debug/DebugSettings.tsx index 01e6c5b..e81a2bb 100644 --- a/src/components/settings/debug/DebugSettings.tsx +++ b/src/components/settings/debug/DebugSettings.tsx @@ -9,6 +9,7 @@ import { AlwaysOnMicrophone } from "../AlwaysOnMicrophone"; import { SoundPicker } from "../SoundPicker"; import { PostProcessingToggle } from "../PostProcessingToggle"; import { MuteWhileRecording } from "../MuteWhileRecording"; +import { AppendTrailingSpace } from "../AppendTrailingSpace"; import { RecordingRetentionPeriodSelector } from "../RecordingRetentionPeriod"; import { ClamshellMicrophoneSelector } from "../ClamshellMicrophoneSelector"; import { HandyShortcut } from "../HandyShortcut"; @@ -40,6 +41,7 @@ export const DebugSettings: React.FC = () => { + {/* Cancel shortcut is disabled on Linux due to instability with dynamic shortcut registration */} {!isLinux && ( commands.changeMuteWhileRecordingSetting(value as boolean), + append_trailing_space: (value) => + commands.changeAppendTrailingSpaceSetting(value as boolean), log_level: (value) => commands.setLogLevel(value as any), };