Add a paste delay setting (#694)

* Add a paste delay setting.

* format
This commit is contained in:
CJ Pais 2026-02-01 20:58:02 +08:00 committed by GitHub
parent 90bfa73377
commit e9a6108559
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 65 additions and 4 deletions

View file

@ -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,
)?
}
}

View file

@ -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(),
}
}

View file

@ -703,7 +703,7 @@ async isLaptop() : Promise<Result<boolean, string>> {
/** 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"

View file

@ -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")}
/>
<WordCorrectionThreshold descriptionMode="tooltip" grouped={true} />
<PasteDelay descriptionMode="tooltip" grouped={true} />
<AlwaysOnMicrophone descriptionMode="tooltip" grouped={true} />
<ClamshellMicrophoneSelector descriptionMode="tooltip" grouped={true} />
{/* Cancel shortcut is disabled on Linux due to instability with dynamic shortcut registration */}

View file

@ -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<PasteDelayProps> = ({
descriptionMode = "tooltip",
grouped = false,
}) => {
const { t } = useTranslation();
const { settings, updateSetting } = useSettings();
const handleDelayChange = (value: number) => {
updateSetting("paste_delay_ms", value);
};
return (
<Slider
value={settings?.paste_delay_ms ?? 60}
onChange={handleDelayChange}
min={10}
max={200}
step={10}
label={t("settings.debug.pasteDelay.title")}
description={t("settings.debug.pasteDelay.description")}
descriptionMode={descriptionMode}
grouped={grouped}
formatValue={(v) => `${v}ms`}
/>
);
};

View file

@ -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:",