diff --git a/src-tauri/src/clipboard.rs b/src-tauri/src/clipboard.rs index 6fd8f10..634dd90 100644 --- a/src-tauri/src/clipboard.rs +++ b/src-tauri/src/clipboard.rs @@ -2,6 +2,7 @@ use crate::input::{self, EnigoState}; use crate::settings::{get_settings, ClipboardHandling, PasteMethod}; use enigo::Enigo; use log::info; +use std::time::Duration; use tauri::{AppHandle, Manager}; use tauri_plugin_clipboard_manager::ClipboardExt; @@ -16,6 +17,7 @@ fn paste_via_clipboard( text: &str, app_handle: &AppHandle, paste_method: &PasteMethod, + paste_delay_ms: u64, ) -> Result<(), String> { let clipboard = app_handle.clipboard(); let clipboard_content = clipboard.read_text().unwrap_or_default(); @@ -25,7 +27,7 @@ fn paste_via_clipboard( .write_text(text) .map_err(|e| format!("Failed to write to clipboard: {}", e))?; - std::thread::sleep(std::time::Duration::from_millis(50)); + std::thread::sleep(Duration::from_millis(paste_delay_ms)); // Send paste key combo #[cfg(target_os = "linux")] @@ -364,6 +366,7 @@ fn paste_direct(enigo: &mut Enigo, text: &str) -> Result<(), String> { pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> { let settings = get_settings(&app_handle); let paste_method = settings.paste_method; + let paste_delay_ms = settings.paste_delay_ms; // Append trailing space if setting is enabled let text = if settings.append_trailing_space { @@ -372,7 +375,10 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> { text }; - info!("Using paste method: {:?}", paste_method); + info!( + "Using paste method: {:?}, delay: {}ms", + paste_method, paste_delay_ms + ); // Get the managed Enigo instance let enigo_state = app_handle @@ -392,7 +398,13 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> { paste_direct(&mut enigo, &text)?; } PasteMethod::CtrlV | PasteMethod::CtrlShiftV | PasteMethod::ShiftInsert => { - paste_via_clipboard(&mut enigo, &text, &app_handle, &paste_method)? + paste_via_clipboard( + &mut enigo, + &text, + &app_handle, + &paste_method, + paste_delay_ms, + )? } } diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 8feb07f..2bbb559 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -315,6 +315,8 @@ pub struct AppSettings { pub experimental_enabled: bool, #[serde(default)] pub keyboard_implementation: KeyboardImplementation, + #[serde(default = "default_paste_delay_ms")] + pub paste_delay_ms: u64, } fn default_model() -> String { @@ -364,6 +366,10 @@ fn default_word_correction_threshold() -> f64 { 0.18 } +fn default_paste_delay_ms() -> u64 { + 60 +} + fn default_history_limit() -> usize { 5 } @@ -605,6 +611,7 @@ pub fn get_default_settings() -> AppSettings { app_language: default_app_language(), experimental_enabled: false, keyboard_implementation: KeyboardImplementation::default(), + paste_delay_ms: default_paste_delay_ms(), } } diff --git a/src/bindings.ts b/src/bindings.ts index e382486..325dc7f 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -703,7 +703,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?: number; 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; app_language?: string; experimental_enabled?: boolean; keyboard_implementation?: KeyboardImplementation } +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?: number; 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; app_language?: string; experimental_enabled?: boolean; keyboard_implementation?: KeyboardImplementation; paste_delay_ms?: number } 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/debug/DebugSettings.tsx b/src/components/settings/debug/DebugSettings.tsx index 60102b0..02ad527 100644 --- a/src/components/settings/debug/DebugSettings.tsx +++ b/src/components/settings/debug/DebugSettings.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { type } from "@tauri-apps/plugin-os"; import { WordCorrectionThreshold } from "./WordCorrectionThreshold"; import { LogLevelSelector } from "./LogLevelSelector"; +import { PasteDelay } from "./PasteDelay"; import { SettingsGroup } from "../../ui/SettingsGroup"; import { AlwaysOnMicrophone } from "../AlwaysOnMicrophone"; import { SoundPicker } from "../SoundPicker"; @@ -27,6 +28,7 @@ export const DebugSettings: React.FC = () => { description={t("settings.debug.soundTheme.description")} /> + {/* Cancel shortcut is disabled on Linux due to instability with dynamic shortcut registration */} diff --git a/src/components/settings/debug/PasteDelay.tsx b/src/components/settings/debug/PasteDelay.tsx new file mode 100644 index 0000000..a2f22e7 --- /dev/null +++ b/src/components/settings/debug/PasteDelay.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Slider } from "../../ui/Slider"; +import { useSettings } from "../../../hooks/useSettings"; + +interface PasteDelayProps { + descriptionMode?: "tooltip" | "inline"; + grouped?: boolean; +} + +export const PasteDelay: React.FC = ({ + descriptionMode = "tooltip", + grouped = false, +}) => { + const { t } = useTranslation(); + const { settings, updateSetting } = useSettings(); + + const handleDelayChange = (value: number) => { + updateSetting("paste_delay_ms", value); + }; + + return ( + `${v}ms`} + /> + ); +}; diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 059bec6..b2a16a3 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -370,6 +370,10 @@ "description": "Choose the keyboard shortcut backend.", "bindingsReset": "Keyboard shortcuts were incompatible and reset to defaults" }, + "pasteDelay": { + "title": "Paste Delay", + "description": "Delay before sending paste keystroke (in milliseconds). Increase if wrong text is being pasted." + }, "paths": { "appData": "App Data:", "models": "Models:",