Files
transcript/src-tauri/src/injection/mod.rs
T

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
);
}
}