205 lines
6.1 KiB
Rust
205 lines
6.1 KiB
Rust
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<String>,
|
|
pub process_name: Option<String>,
|
|
}
|
|
|
|
#[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<FocusTarget>,
|
|
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<String, String> {
|
|
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
|
|
);
|
|
}
|
|
}
|