Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 58 additions & 40 deletions src-tauri/src/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -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> {
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_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> {
/// 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_shift_insert()?;

send_paste()?;
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))?;
Expand Down Expand Up @@ -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()),
};

Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ pub enum PasteMethod {
Direct,
None,
ShiftInsert,
CtrlShiftV,
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/shortcut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
20 changes: 11 additions & 9 deletions src/components/settings/PasteMethod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<PasteMethodProps> = React.memo(
Expand All @@ -45,7 +47,7 @@ export const PasteMethodSetting: React.FC<PasteMethodProps> = React.memo(
return (
<SettingContainer
title="Paste Method"
description="Ctrl+V: 'pastes' via ctrl+v. Direct: type text directly. Shift+Insert: pastes via shift+insert. None: skip paste; just update history/clipboard."
description="Choose how text is inserted. Direct: simulates typing via system input. None: skips paste, only updates history/clipboard."
descriptionMode={descriptionMode}
grouped={grouped}
tooltipPosition="bottom"
Expand Down