From aaade62f9d5fff4ea59ab3b9dab1ea1a38bcbfa4 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Wed, 10 Dec 2025 02:49:43 -0800 Subject: [PATCH] Add ctrl+shift+v as option on windows and linux (#430) * ctrl+shift+v * clean up some clipboard code * format --- src-tauri/src/clipboard.rs | 100 ++++++++++++++---------- src-tauri/src/settings.rs | 1 + src-tauri/src/shortcut.rs | 1 + src/bindings.ts | 2 +- src/components/settings/PasteMethod.tsx | 20 ++--- 5 files changed, 73 insertions(+), 51 deletions(-) diff --git a/src-tauri/src/clipboard.rs b/src-tauri/src/clipboard.rs index 602b379..fe5aaad 100644 --- a/src-tauri/src/clipboard.rs +++ b/src-tauri/src/clipboard.rs @@ -48,6 +48,48 @@ fn send_paste_ctrl_v() -> Result<(), String> { Ok(()) } +/// Sends a Ctrl+Shift+V paste command. +/// This is commonly used in terminal applications on Linux to paste without formatting. +fn send_paste_ctrl_shift_v() -> Result<(), String> { + #[cfg(target_os = "linux")] + if try_wayland_send_paste(&PasteMethod::CtrlShiftV)? { + return Ok(()); + } + + // Platform-specific key definitions + #[cfg(target_os = "macos")] + let (modifier_key, v_key_code) = (Key::Meta, Key::Other(9)); // Cmd+Shift+V on macOS + #[cfg(target_os = "windows")] + let (modifier_key, v_key_code) = (Key::Control, Key::Other(0x56)); // VK_V + #[cfg(target_os = "linux")] + let (modifier_key, v_key_code) = (Key::Control, Key::Unicode('v')); + + let mut enigo = Enigo::new(&Settings::default()) + .map_err(|e| format!("Failed to initialize Enigo: {}", e))?; + + // Press Ctrl/Cmd + Shift + V + enigo + .key(modifier_key, enigo::Direction::Press) + .map_err(|e| format!("Failed to press modifier key: {}", e))?; + enigo + .key(Key::Shift, enigo::Direction::Press) + .map_err(|e| format!("Failed to press Shift key: {}", e))?; + enigo + .key(v_key_code, enigo::Direction::Click) + .map_err(|e| format!("Failed to click V key: {}", e))?; + + std::thread::sleep(std::time::Duration::from_millis(100)); + + enigo + .key(Key::Shift, enigo::Direction::Release) + .map_err(|e| format!("Failed to release Shift key: {}", e))?; + enigo + .key(modifier_key, enigo::Direction::Release) + .map_err(|e| format!("Failed to release modifier key: {}", e))?; + + Ok(()) +} + /// Sends a Shift+Insert paste command (Windows and Linux only). /// This is more universal for terminal applications and legacy software. fn send_paste_shift_insert() -> Result<(), String> { @@ -94,53 +136,23 @@ fn paste_via_direct_input(text: &str) -> Result<(), String> { Ok(()) } -/// Pastes text using the clipboard method with Ctrl+V/Cmd+V. -/// Saves the current clipboard, writes the text, sends paste command, then restores the clipboard. -fn paste_via_clipboard_ctrl_v(text: &str, app_handle: &AppHandle) -> Result<(), String> { +/// Pastes text using the clipboard: saves current content, writes text, sends paste keystroke, restores clipboard. +fn paste_via_clipboard( + text: &str, + app_handle: &AppHandle, + send_paste: fn() -> Result<(), String>, +) -> Result<(), String> { let clipboard = app_handle.clipboard(); - - // get the current clipboard content let clipboard_content = clipboard.read_text().unwrap_or_default(); clipboard .write_text(text) .map_err(|e| format!("Failed to write to clipboard: {}", e))?; - // small delay to ensure the clipboard content has been written to + std::thread::sleep(std::time::Duration::from_millis(50)); + send_paste()?; std::thread::sleep(std::time::Duration::from_millis(50)); - send_paste_ctrl_v()?; - - std::thread::sleep(std::time::Duration::from_millis(50)); - - // restore the clipboard - clipboard - .write_text(&clipboard_content) - .map_err(|e| format!("Failed to restore clipboard: {}", e))?; - - Ok(()) -} - -/// Pastes text using the clipboard method with Shift+Insert (Windows/Linux only). -/// Saves the current clipboard, writes the text, sends paste command, then restores the clipboard. -fn paste_via_clipboard_shift_insert(text: &str, app_handle: &AppHandle) -> Result<(), String> { - let clipboard = app_handle.clipboard(); - - // get the current clipboard content - let clipboard_content = clipboard.read_text().unwrap_or_default(); - - clipboard - .write_text(text) - .map_err(|e| format!("Failed to write to clipboard: {}", e))?; - - // small delay to ensure the clipboard content has been written to - std::thread::sleep(std::time::Duration::from_millis(50)); - - send_paste_shift_insert()?; - - std::thread::sleep(std::time::Duration::from_millis(50)); - - // restore the clipboard clipboard .write_text(&clipboard_content) .map_err(|e| format!("Failed to restore clipboard: {}", e))?; @@ -192,6 +204,7 @@ fn send_paste_via_wtype(paste_method: &PasteMethod) -> Result<(), String> { let args: Vec<&str> = match paste_method { PasteMethod::CtrlV => vec!["-M", "ctrl", "-k", "v"], PasteMethod::ShiftInsert => vec!["-M", "shift", "-k", "Insert"], + PasteMethod::CtrlShiftV => vec!["-M", "ctrl", "-M", "shift", "-k", "v"], _ => return Err("Unsupported paste method".into()), }; @@ -215,6 +228,7 @@ fn send_paste_via_dotool(paste_method: &PasteMethod) -> Result<(), String> { match paste_method { PasteMethod::CtrlV => command = "echo key ctrl+v | dotool", PasteMethod::ShiftInsert => command = "echo key shift+insert | dotool", + PasteMethod::CtrlShiftV => command = "echo key ctrl+shift+v | dotool", _ => return Err("Unsupported paste method".into()), } let output = Command::new("sh") @@ -246,12 +260,16 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> { // Perform the paste operation match paste_method { PasteMethod::None => { - // Intentionally do not perform any paste action; history/clipboard update info!("PasteMethod::None selected - skipping paste action"); } - PasteMethod::CtrlV => paste_via_clipboard_ctrl_v(&text, &app_handle)?, PasteMethod::Direct => paste_via_direct_input(&text)?, - PasteMethod::ShiftInsert => paste_via_clipboard_shift_insert(&text, &app_handle)?, + PasteMethod::CtrlV => paste_via_clipboard(&text, &app_handle, send_paste_ctrl_v)?, + PasteMethod::CtrlShiftV => { + paste_via_clipboard(&text, &app_handle, send_paste_ctrl_shift_v)? + } + PasteMethod::ShiftInsert => { + paste_via_clipboard(&text, &app_handle, send_paste_shift_insert)? + } } // After pasting, optionally copy to clipboard based on settings diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 0c58fa6..c2b98bd 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -131,6 +131,7 @@ pub enum PasteMethod { Direct, None, ShiftInsert, + CtrlShiftV, } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)] diff --git a/src-tauri/src/shortcut.rs b/src-tauri/src/shortcut.rs index 34c8588..288815c 100644 --- a/src-tauri/src/shortcut.rs +++ b/src-tauri/src/shortcut.rs @@ -332,6 +332,7 @@ pub fn change_paste_method_setting(app: AppHandle, method: String) -> Result<(), "direct" => PasteMethod::Direct, "none" => PasteMethod::None, "shift_insert" => PasteMethod::ShiftInsert, + "ctrl_shift_v" => PasteMethod::CtrlShiftV, other => { warn!("Invalid paste method '{}', defaulting to ctrl_v", other); PasteMethod::CtrlV diff --git a/src/bindings.ts b/src/bindings.ts index ec51ac5..702dd2d 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -623,7 +623,7 @@ export type ModelInfo = { id: string; name: string; description: string; filenam export type ModelLoadStatus = { is_loaded: boolean; current_model: string | null } export type ModelUnloadTimeout = "never" | "immediately" | "min_2" | "min_5" | "min_10" | "min_15" | "hour_1" | "sec_5" export type OverlayPosition = "none" | "top" | "bottom" -export type PasteMethod = "ctrl_v" | "direct" | "none" | "shift_insert" +export type PasteMethod = "ctrl_v" | "direct" | "none" | "shift_insert" | "ctrl_shift_v" export type PostProcessProvider = { id: string; label: string; base_url: string; allow_base_url_edit?: boolean; models_endpoint?: string | null } export type RecordingRetentionPeriod = "never" | "preserve_limit" | "days_3" | "weeks_2" | "months_3" export type ShortcutBinding = { id: string; name: string; description: string; default_binding: string; current_binding: string } diff --git a/src/components/settings/PasteMethod.tsx b/src/components/settings/PasteMethod.tsx index 10297ac..40da991 100644 --- a/src/components/settings/PasteMethod.tsx +++ b/src/components/settings/PasteMethod.tsx @@ -11,21 +11,23 @@ interface PasteMethodProps { } const getPasteMethodOptions = (osType: string) => { - const baseOptions = [ - { value: "ctrl_v", label: "Clipboard (Ctrl+V)" }, + const mod = osType === "macos" ? "Cmd" : "Ctrl"; + + const options = [ + { value: "ctrl_v", label: `Clipboard (${mod}+V)` }, { value: "direct", label: "Direct" }, { value: "none", label: "None" }, ]; - // Add Shift+Insert option for Windows and Linux only + // Add Shift+Insert and Ctrl+Shift+V options for Windows and Linux only if (osType === "windows" || osType === "linux") { - baseOptions.push({ - value: "shift_insert", - label: "Clipboard (Shift+Insert)", - }); + options.push( + { value: "ctrl_shift_v", label: "Clipboard (Ctrl+Shift+V)" }, + { value: "shift_insert", label: "Clipboard (Shift+Insert)" }, + ); } - return baseOptions; + return options; }; export const PasteMethodSetting: React.FC = React.memo( @@ -45,7 +47,7 @@ export const PasteMethodSetting: React.FC = React.memo( return (