use crate::input::{self, EnigoState}; #[cfg(target_os = "linux")] use crate::settings::TypingTool; use crate::settings::{get_settings, AutoSubmitKey, ClipboardHandling, PasteMethod}; use enigo::{Direction, Enigo, Key, Keyboard}; use log::info; use std::process::Command; use std::time::Duration; use tauri::{AppHandle, Manager}; use tauri_plugin_clipboard_manager::ClipboardExt; #[cfg(target_os = "linux")] use crate::utils::{is_kde_wayland, is_wayland}; /// Pastes text using the clipboard: saves current content, writes text, sends paste keystroke, restores clipboard. fn paste_via_clipboard( enigo: &mut Enigo, 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(); // Write text to clipboard first // On Wayland, prefer wl-copy for better compatibility (especially with umlauts) #[cfg(target_os = "linux")] let write_result = if is_wayland() && is_wl_copy_available() { info!("Using wl-copy for clipboard write on Wayland"); write_clipboard_via_wl_copy(text) } else { clipboard .write_text(text) .map_err(|e| format!("Failed to write to clipboard: {}", e)) }; #[cfg(not(target_os = "linux"))] let write_result = clipboard .write_text(text) .map_err(|e| format!("Failed to write to clipboard: {}", e)); write_result?; std::thread::sleep(Duration::from_millis(paste_delay_ms)); // Send paste key combo #[cfg(target_os = "linux")] let key_combo_sent = try_send_key_combo_linux(paste_method)?; #[cfg(not(target_os = "linux"))] let key_combo_sent = false; // Fall back to enigo if no native tool handled it if !key_combo_sent { match paste_method { PasteMethod::CtrlV => input::send_paste_ctrl_v(enigo)?, PasteMethod::CtrlShiftV => input::send_paste_ctrl_shift_v(enigo)?, PasteMethod::ShiftInsert => input::send_paste_shift_insert(enigo)?, _ => return Err("Invalid paste method for clipboard paste".into()), } } std::thread::sleep(std::time::Duration::from_millis(50)); // Restore original clipboard content // On Wayland, prefer wl-copy for better compatibility #[cfg(target_os = "linux")] if is_wayland() && is_wl_copy_available() { let _ = write_clipboard_via_wl_copy(&clipboard_content); } else { let _ = clipboard.write_text(&clipboard_content); } #[cfg(not(target_os = "linux"))] let _ = clipboard.write_text(&clipboard_content); Ok(()) } /// Attempts to send a key combination using Linux-native tools. /// Returns `Ok(true)` if a native tool handled it, `Ok(false)` to fall back to enigo. #[cfg(target_os = "linux")] fn try_send_key_combo_linux(paste_method: &PasteMethod) -> Result { if is_wayland() { // Wayland: prefer wtype (but not on KDE), then dotool, then ydotool // Note: wtype doesn't work on KDE (no zwp_virtual_keyboard_manager_v1 support) if !is_kde_wayland() && is_wtype_available() { info!("Using wtype for key combo"); send_key_combo_via_wtype(paste_method)?; return Ok(true); } if is_dotool_available() { info!("Using dotool for key combo"); send_key_combo_via_dotool(paste_method)?; return Ok(true); } if is_ydotool_available() { info!("Using ydotool for key combo"); send_key_combo_via_ydotool(paste_method)?; return Ok(true); } } else { // X11: prefer xdotool, then ydotool if is_xdotool_available() { info!("Using xdotool for key combo"); send_key_combo_via_xdotool(paste_method)?; return Ok(true); } if is_ydotool_available() { info!("Using ydotool for key combo"); send_key_combo_via_ydotool(paste_method)?; return Ok(true); } } Ok(false) } /// Attempts to type text directly using Linux-native tools. /// Returns `Ok(true)` if a native tool handled it, `Ok(false)` to fall back to enigo. #[cfg(target_os = "linux")] fn try_direct_typing_linux(text: &str, preferred_tool: TypingTool) -> Result { // If user specified a tool, try only that one if preferred_tool != TypingTool::Auto { return match preferred_tool { TypingTool::Wtype if is_wtype_available() => { info!("Using user-specified wtype"); type_text_via_wtype(text)?; Ok(true) } TypingTool::Kwtype if is_kwtype_available() => { info!("Using user-specified kwtype"); type_text_via_kwtype(text)?; Ok(true) } TypingTool::Dotool if is_dotool_available() => { info!("Using user-specified dotool"); type_text_via_dotool(text)?; Ok(true) } TypingTool::Ydotool if is_ydotool_available() => { info!("Using user-specified ydotool"); type_text_via_ydotool(text)?; Ok(true) } TypingTool::Xdotool if is_xdotool_available() => { info!("Using user-specified xdotool"); type_text_via_xdotool(text)?; Ok(true) } _ => Err(format!( "Typing tool {:?} is not available on this system", preferred_tool )), }; } // Auto mode - existing fallback chain if is_wayland() { // KDE Wayland: prefer kwtype (uses KDE Fake Input protocol, supports umlauts) if is_kde_wayland() && is_kwtype_available() { info!("Using kwtype for direct text input on KDE Wayland"); type_text_via_kwtype(text)?; return Ok(true); } // Wayland: prefer wtype, then dotool, then ydotool // Note: wtype doesn't work on KDE (no zwp_virtual_keyboard_manager_v1 support) if !is_kde_wayland() && is_wtype_available() { info!("Using wtype for direct text input"); type_text_via_wtype(text)?; return Ok(true); } if is_dotool_available() { info!("Using dotool for direct text input"); type_text_via_dotool(text)?; return Ok(true); } if is_ydotool_available() { info!("Using ydotool for direct text input"); type_text_via_ydotool(text)?; return Ok(true); } } else { // X11: prefer xdotool, then ydotool if is_xdotool_available() { info!("Using xdotool for direct text input"); type_text_via_xdotool(text)?; return Ok(true); } if is_ydotool_available() { info!("Using ydotool for direct text input"); type_text_via_ydotool(text)?; return Ok(true); } } Ok(false) } /// Returns the list of available typing tools on this system. /// Always includes "auto" as the first entry. #[cfg(target_os = "linux")] pub fn get_available_typing_tools() -> Vec { let mut tools = vec!["auto".to_string()]; if is_wtype_available() { tools.push("wtype".to_string()); } if is_kwtype_available() { tools.push("kwtype".to_string()); } if is_dotool_available() { tools.push("dotool".to_string()); } if is_ydotool_available() { tools.push("ydotool".to_string()); } if is_xdotool_available() { tools.push("xdotool".to_string()); } tools } /// Check if wtype is available (Wayland text input tool) #[cfg(target_os = "linux")] fn is_wtype_available() -> bool { Command::new("which") .arg("wtype") .output() .map(|output| output.status.success()) .unwrap_or(false) } /// Check if dotool is available (another Wayland text input tool) #[cfg(target_os = "linux")] fn is_dotool_available() -> bool { Command::new("which") .arg("dotool") .output() .map(|output| output.status.success()) .unwrap_or(false) } /// Check if ydotool is available (uinput-based, works on both Wayland and X11) #[cfg(target_os = "linux")] fn is_ydotool_available() -> bool { Command::new("which") .arg("ydotool") .output() .map(|output| output.status.success()) .unwrap_or(false) } #[cfg(target_os = "linux")] fn is_xdotool_available() -> bool { Command::new("which") .arg("xdotool") .output() .map(|output| output.status.success()) .unwrap_or(false) } /// Check if kwtype is available (KDE Wayland virtual keyboard input tool) #[cfg(target_os = "linux")] fn is_kwtype_available() -> bool { Command::new("which") .arg("kwtype") .output() .map(|output| output.status.success()) .unwrap_or(false) } /// Check if wl-copy is available (Wayland clipboard tool) #[cfg(target_os = "linux")] fn is_wl_copy_available() -> bool { Command::new("which") .arg("wl-copy") .output() .map(|output| output.status.success()) .unwrap_or(false) } /// Type text directly via wtype on Wayland. #[cfg(target_os = "linux")] fn type_text_via_wtype(text: &str) -> Result<(), String> { let output = Command::new("wtype") .arg("--") // Protect against text starting with - .arg(text) .output() .map_err(|e| format!("Failed to execute wtype: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("wtype failed: {}", stderr)); } Ok(()) } /// Type text directly via xdotool on X11. #[cfg(target_os = "linux")] fn type_text_via_xdotool(text: &str) -> Result<(), String> { let output = Command::new("xdotool") .arg("type") .arg("--clearmodifiers") .arg("--") .arg(text) .output() .map_err(|e| format!("Failed to execute xdotool: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("xdotool failed: {}", stderr)); } Ok(()) } /// Type text directly via dotool (works on both Wayland and X11 via uinput). #[cfg(target_os = "linux")] fn type_text_via_dotool(text: &str) -> Result<(), String> { use std::io::Write; use std::process::Stdio; let mut child = Command::new("dotool") .stdin(Stdio::piped()) .spawn() .map_err(|e| format!("Failed to spawn dotool: {}", e))?; if let Some(mut stdin) = child.stdin.take() { // dotool uses "type " command writeln!(stdin, "type {}", text) .map_err(|e| format!("Failed to write to dotool stdin: {}", e))?; } let output = child .wait_with_output() .map_err(|e| format!("Failed to wait for dotool: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("dotool failed: {}", stderr)); } Ok(()) } /// Type text directly via ydotool (uinput-based, requires ydotoold daemon). #[cfg(target_os = "linux")] fn type_text_via_ydotool(text: &str) -> Result<(), String> { let output = Command::new("ydotool") .arg("type") .arg("--") .arg(text) .output() .map_err(|e| format!("Failed to execute ydotool: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("ydotool failed: {}", stderr)); } Ok(()) } /// Type text directly via kwtype (KDE Wayland virtual keyboard, uses KDE Fake Input protocol). #[cfg(target_os = "linux")] fn type_text_via_kwtype(text: &str) -> Result<(), String> { let output = Command::new("kwtype") .arg("--") .arg(text) .output() .map_err(|e| format!("Failed to execute kwtype: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("kwtype failed: {}", stderr)); } Ok(()) } /// Write text to clipboard via wl-copy (Wayland clipboard tool). /// Uses Stdio::null() to avoid blocking on repeated calls — wl-copy forks a /// daemon that inherits piped fds, causing read_to_end to hang indefinitely. #[cfg(target_os = "linux")] fn write_clipboard_via_wl_copy(text: &str) -> Result<(), String> { use std::process::Stdio; let status = Command::new("wl-copy") .arg("--") .arg(text) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map_err(|e| format!("Failed to execute wl-copy: {}", e))?; if !status.success() { return Err("wl-copy failed".into()); } Ok(()) } /// Send a key combination (e.g., Ctrl+V) via wtype on Wayland. #[cfg(target_os = "linux")] fn send_key_combo_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()), }; let output = Command::new("wtype") .args(&args) .output() .map_err(|e| format!("Failed to execute wtype: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("wtype failed: {}", stderr)); } Ok(()) } /// Send a key combination (e.g., Ctrl+V) via dotool. #[cfg(target_os = "linux")] fn send_key_combo_via_dotool(paste_method: &PasteMethod) -> Result<(), String> { let command; 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()), } use std::process::Stdio; let status = Command::new("sh") .arg("-c") .arg(command) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map_err(|e| format!("Failed to execute dotool: {}", e))?; if !status.success() { return Err("dotool failed".into()); } Ok(()) } /// Send a key combination (e.g., Ctrl+V) via ydotool (requires ydotoold daemon). #[cfg(target_os = "linux")] fn send_key_combo_via_ydotool(paste_method: &PasteMethod) -> Result<(), String> { // ydotool uses Linux input event keycodes with format : // where pressed is 1 for down, 0 for up. Keycodes: ctrl=29, shift=42, v=47, insert=110 let args: Vec<&str> = match paste_method { PasteMethod::CtrlV => vec!["key", "29:1", "47:1", "47:0", "29:0"], PasteMethod::ShiftInsert => vec!["key", "42:1", "110:1", "110:0", "42:0"], PasteMethod::CtrlShiftV => vec!["key", "29:1", "42:1", "47:1", "47:0", "42:0", "29:0"], _ => return Err("Unsupported paste method".into()), }; let output = Command::new("ydotool") .args(&args) .output() .map_err(|e| format!("Failed to execute ydotool: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("ydotool failed: {}", stderr)); } Ok(()) } /// Send a key combination (e.g., Ctrl+V) via xdotool on X11. #[cfg(target_os = "linux")] fn send_key_combo_via_xdotool(paste_method: &PasteMethod) -> Result<(), String> { let key_combo = match paste_method { PasteMethod::CtrlV => "ctrl+v", PasteMethod::CtrlShiftV => "ctrl+shift+v", PasteMethod::ShiftInsert => "shift+Insert", _ => return Err("Unsupported paste method".into()), }; let output = Command::new("xdotool") .arg("key") .arg("--clearmodifiers") .arg(key_combo) .output() .map_err(|e| format!("Failed to execute xdotool: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("xdotool failed: {}", stderr)); } Ok(()) } /// Pastes text by invoking an external script. /// The script receives the text to paste as a single argument. fn paste_via_external_script(text: &str, script_path: &str) -> Result<(), String> { info!("Pasting via external script: {}", script_path); let output = Command::new(script_path) .arg(text) .output() .map_err(|e| format!("Failed to execute external script '{}': {}", script_path, e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); return Err(format!( "External script '{}' failed with exit code {:?}. stderr: {}, stdout: {}", script_path, output.status.code(), stderr.trim(), stdout.trim() )); } Ok(()) } /// Types text directly by simulating individual key presses. fn paste_direct( enigo: &mut Enigo, text: &str, #[cfg(target_os = "linux")] typing_tool: TypingTool, ) -> Result<(), String> { #[cfg(target_os = "linux")] { if try_direct_typing_linux(text, typing_tool)? { return Ok(()); } info!("Falling back to enigo for direct text input"); } input::paste_text_direct(enigo, text) } fn send_return_key(enigo: &mut Enigo, key_type: AutoSubmitKey) -> Result<(), String> { match key_type { AutoSubmitKey::Enter => { enigo .key(Key::Return, Direction::Press) .map_err(|e| format!("Failed to press Return key: {}", e))?; enigo .key(Key::Return, Direction::Release) .map_err(|e| format!("Failed to release Return key: {}", e))?; } AutoSubmitKey::CtrlEnter => { enigo .key(Key::Control, Direction::Press) .map_err(|e| format!("Failed to press Control key: {}", e))?; enigo .key(Key::Return, Direction::Press) .map_err(|e| format!("Failed to press Return key: {}", e))?; enigo .key(Key::Return, Direction::Release) .map_err(|e| format!("Failed to release Return key: {}", e))?; enigo .key(Key::Control, Direction::Release) .map_err(|e| format!("Failed to release Control key: {}", e))?; } AutoSubmitKey::CmdEnter => { enigo .key(Key::Meta, Direction::Press) .map_err(|e| format!("Failed to press Meta/Cmd key: {}", e))?; enigo .key(Key::Return, Direction::Press) .map_err(|e| format!("Failed to press Return key: {}", e))?; enigo .key(Key::Return, Direction::Release) .map_err(|e| format!("Failed to release Return key: {}", e))?; enigo .key(Key::Meta, Direction::Release) .map_err(|e| format!("Failed to release Meta/Cmd key: {}", e))?; } } Ok(()) } fn should_send_auto_submit(auto_submit: bool, paste_method: PasteMethod) -> bool { auto_submit && paste_method != PasteMethod::None } 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 { format!("{} ", text) } else { text }; info!( "Using paste method: {:?}, delay: {}ms", paste_method, paste_delay_ms ); // Get the managed Enigo instance let enigo_state = app_handle .try_state::() .ok_or("Enigo state not initialized")?; let mut enigo = enigo_state .0 .lock() .map_err(|e| format!("Failed to lock Enigo: {}", e))?; // Perform the paste operation match paste_method { PasteMethod::None => { info!("PasteMethod::None selected - skipping paste action"); } PasteMethod::Direct => { paste_direct( &mut enigo, &text, #[cfg(target_os = "linux")] settings.typing_tool, )?; } PasteMethod::CtrlV | PasteMethod::CtrlShiftV | PasteMethod::ShiftInsert => { paste_via_clipboard( &mut enigo, &text, &app_handle, &paste_method, paste_delay_ms, )? } PasteMethod::ExternalScript => { let script_path = settings .external_script_path .as_ref() .filter(|p| !p.is_empty()) .ok_or("External script path is not configured")?; paste_via_external_script(&text, script_path)?; } } if should_send_auto_submit(settings.auto_submit, paste_method) { std::thread::sleep(Duration::from_millis(50)); send_return_key(&mut enigo, settings.auto_submit_key)?; } // After pasting, optionally copy to clipboard based on settings if settings.clipboard_handling == ClipboardHandling::CopyToClipboard { let clipboard = app_handle.clipboard(); clipboard .write_text(&text) .map_err(|e| format!("Failed to copy to clipboard: {}", e))?; } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn auto_submit_requires_setting_enabled() { assert!(!should_send_auto_submit(false, PasteMethod::CtrlV)); assert!(!should_send_auto_submit(false, PasteMethod::Direct)); } #[test] fn auto_submit_skips_none_paste_method() { assert!(!should_send_auto_submit(true, PasteMethod::None)); } #[test] fn auto_submit_runs_for_active_paste_methods() { assert!(should_send_auto_submit(true, PasteMethod::CtrlV)); assert!(should_send_auto_submit(true, PasteMethod::Direct)); assert!(should_send_auto_submit(true, PasteMethod::CtrlShiftV)); assert!(should_send_auto_submit(true, PasteMethod::ShiftInsert)); } }