use serde::{Deserialize, Serialize}; use std::process::Command; use arboard::Clipboard; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct FocusTarget { pub platform: Platform, pub display_server: DisplayServer, pub window_title: Option, pub process_name: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum Platform { Windows, Linux, Unknown, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum DisplayServer { Windows, X11, Wayland, Unknown, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum InsertCapability { Full, Partial, Unsupported, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum InsertMethod { ClipboardPaste, KeyTyping, ManualCopy, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct InsertRequest { pub text: String, pub target: Option, pub method: InsertMethod, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct InsertResult { pub capability: InsertCapability, pub method_used: InsertMethod, pub message: String, } pub trait TextInjector { fn can_insert(&self, target: Option<&FocusTarget>) -> InsertCapability; fn insert_text(&self, request: InsertRequest) -> InsertResult; } #[derive(Debug, Default, Clone)] pub struct FallbackInjector; impl TextInjector for FallbackInjector { fn can_insert(&self, target: Option<&FocusTarget>) -> InsertCapability { let display_server = target .map(|target| target.display_server.clone()) .unwrap_or_else(current_display_server); match display_server { DisplayServer::Windows | DisplayServer::X11 => InsertCapability::Full, DisplayServer::Wayland => InsertCapability::Partial, DisplayServer::Unknown => InsertCapability::Unsupported, } } fn insert_text(&self, request: InsertRequest) -> InsertResult { let capability = self.can_insert(request.target.as_ref()); let clipboard_message = copy_to_clipboard(&request.text); let message = match (&request.method, &capability) { (InsertMethod::ManualCopy, _) => clipboard_message.unwrap_or_else(|error| error), (_, InsertCapability::Full) => match clipboard_message { Ok(_) => match trigger_paste() { Ok(_) => "Texto copiado e Ctrl+V acionado no app em foco.".to_string(), Err(error) => format!( "Texto copiado. Não consegui acionar Ctrl+V automaticamente: {error}" ), }, Err(error) => error, }, (_, InsertCapability::Partial) => match clipboard_message { Ok(_) => "Texto copiado. No Wayland, cole manualmente com Ctrl+V.".to_string(), Err(error) => error, }, (_, InsertCapability::Unsupported) => match clipboard_message { Ok(_) => "Texto copiado. Cole manualmente no aplicativo desejado.".to_string(), Err(error) => error, }, }; InsertResult { capability, method_used: request.method, message, } } } fn current_display_server() -> DisplayServer { #[cfg(target_os = "windows")] { DisplayServer::Windows } #[cfg(target_os = "linux")] { let session_type = std::env::var("XDG_SESSION_TYPE") .unwrap_or_default() .to_lowercase(); if session_type == "wayland" || std::env::var_os("WAYLAND_DISPLAY").is_some() { DisplayServer::Wayland } else if session_type == "x11" || std::env::var_os("DISPLAY").is_some() { DisplayServer::X11 } else { DisplayServer::Unknown } } #[cfg(not(any(target_os = "windows", target_os = "linux")))] { DisplayServer::Unknown } } fn copy_to_clipboard(text: &str) -> Result { let mut clipboard = Clipboard::new().map_err(|error| format!("Falha ao acessar clipboard: {error}"))?; clipboard .set_text(text.to_string()) .map_err(|error| format!("Falha ao copiar texto: {error}"))?; Ok("Texto copiado para o clipboard.".to_string()) } fn trigger_paste() -> Result<(), String> { #[cfg(target_os = "windows")] { let status = Command::new("powershell") .args([ "-NoProfile", "-Command", "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('^v')", ]) .status() .map_err(|error| error.to_string())?; return status .success() .then_some(()) .ok_or_else(|| format!("powershell retornou {status}")); } #[cfg(target_os = "linux")] { let status = Command::new("xdotool") .args(["key", "--clearmodifiers", "ctrl+v"]) .status() .map_err(|error| error.to_string())?; return status .success() .then_some(()) .ok_or_else(|| format!("xdotool retornou {status}")); } #[allow(unreachable_code)] Err("paste automático não suportado neste sistema".to_string()) } #[cfg(test)] mod tests { use super::*; #[test] fn wayland_reports_partial_insert_capability() { let injector = FallbackInjector; let target = FocusTarget { platform: Platform::Linux, display_server: DisplayServer::Wayland, window_title: Some("Editor".to_string()), process_name: Some("app".to_string()), }; assert_eq!( injector.can_insert(Some(&target)), InsertCapability::Partial ); } }