Add wayland paste support using wtype or dotool (#376)

This commit is contained in:
Lari Hotari 2025-11-27 12:52:33 +02:00 committed by GitHub
parent dbd8827bb2
commit e72bfb4bc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 103 additions and 0 deletions

View file

@ -1,15 +1,22 @@
use crate::settings::{get_settings, ClipboardHandling, PasteMethod};
use crate::utils::is_wayland;
use enigo::Enigo;
use enigo::Key;
use enigo::Keyboard;
use enigo::Settings;
use log::info;
use std::process::Command;
use tauri::AppHandle;
use tauri_plugin_clipboard_manager::ClipboardExt;
/// Sends a Ctrl+V or Cmd+V paste command using platform-specific virtual key codes.
/// This ensures the paste works regardless of keyboard layout (e.g., Russian, AZERTY, DVORAK).
fn send_paste_ctrl_v() -> Result<(), String> {
#[cfg(target_os = "linux")]
if try_wayland_send_paste(&PasteMethod::CtrlV)? {
return Ok(());
}
// Platform-specific key definitions
#[cfg(target_os = "macos")]
let (modifier_key, v_key_code) = (Key::Meta, Key::Other(9));
@ -41,6 +48,11 @@ fn send_paste_ctrl_v() -> Result<(), String> {
/// 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> {
#[cfg(target_os = "linux")]
if try_wayland_send_paste(&PasteMethod::ShiftInsert)? {
return Ok(());
}
#[cfg(target_os = "windows")]
let insert_key_code = Key::Other(0x2D); // VK_INSERT
#[cfg(not(target_os = "windows"))]
@ -133,6 +145,88 @@ fn paste_via_clipboard_shift_insert(text: &str, app_handle: &AppHandle) -> Resul
Ok(())
}
/// Attempts to paste using Wayland-specific tools (`wtype` or `dotool`).
/// Returns `Ok(true)` if a Wayland tool handled the paste, `Ok(false)` if not applicable,
/// or `Err` on failure from the underlying tool.
#[cfg(target_os = "linux")]
fn try_wayland_send_paste(paste_method: &PasteMethod) -> Result<bool, String> {
if is_wayland() {
if is_wtype_available() {
send_paste_via_wtype(paste_method)?;
return Ok(true);
} else if is_dotool_available() {
send_paste_via_dotool(paste_method)?;
return Ok(true);
}
}
Ok(false)
}
/// 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)
}
/// Paste using wtype and return a friendly error on failure.
#[cfg(target_os = "linux")]
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"],
_ => 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(())
}
/// Paste using dotool and return a friendly error on failure.
#[cfg(target_os = "linux")]
fn send_paste_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",
_ => return Err("Unsupported paste method".into()),
}
let output = Command::new("sh")
.arg("-c")
.arg(command)
.output()
.map_err(|e| format!("Failed to execute dotool: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("dotool failed: {}", stderr));
}
Ok(())
}
pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
let settings = get_settings(&app_handle);
let paste_method = settings.paste_method;

View file

@ -54,3 +54,12 @@ pub fn cancel_current_operation(app: &AppHandle) {
info!("Operation cancellation completed - returned to idle state");
}
/// Check if using the Wayland display server protocol
#[cfg(target_os = "linux")]
pub fn is_wayland() -> bool {
std::env::var("WAYLAND_DISPLAY").is_ok()
|| std::env::var("XDG_SESSION_TYPE")
.map(|v| v.to_lowercase() == "wayland")
.unwrap_or(false)
}