diff --git a/src/app.rs b/src/app.rs index 69f2789..37e279d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -105,31 +105,20 @@ impl TesseraApp { if command && let Key::Character(value) = &event.logical_key { match value.to_lowercase().as_str() { "a" => self.developer_tools.selection = true, - "v" => { - #[cfg(target_os = "macos")] - if let Ok(output) = std::process::Command::new("pbpaste").output() - && output.status.success() - { - self.developer_tools - .insert(&String::from_utf8_lossy(&output.stdout)); - } - } + "v" => match crate::clipboard::read() { + Ok(value) => self.developer_tools.insert(&value), + Err(error) => self.developer_tools.input_error = format!("Clipboard: {error}"), + }, "c" | "x" => { - #[cfg(target_os = "macos")] - { - use std::io::Write; - let selected = self.developer_tools.selected_text(); - if !selected.is_empty() - && let Ok(mut child) = std::process::Command::new("pbcopy") - .stdin(std::process::Stdio::piped()) - .spawn() - { - if let Some(mut stdin) = child.stdin.take() { - let _ = stdin.write_all(selected.as_bytes()); + let selected = self.developer_tools.selected_text(); + if !selected.is_empty() { + match crate::clipboard::write(&selected) { + Ok(()) if value.eq_ignore_ascii_case("x") => { + self.developer_tools.insert("") } - let _ = child.wait(); - if value.eq_ignore_ascii_case("x") { - self.developer_tools.insert(""); + Ok(()) => {} + Err(error) => { + self.developer_tools.input_error = format!("Clipboard: {error}") } } } diff --git a/src/clipboard.rs b/src/clipboard.rs new file mode 100644 index 0000000..71f60d5 --- /dev/null +++ b/src/clipboard.rs @@ -0,0 +1,52 @@ +//! Clipboard text is always UTF-8, including Finder/LaunchServices launches without LANG. +use anyhow::{Result, bail}; +#[cfg(target_os = "macos")] +fn command(program: &str) -> std::process::Command { + let mut cmd = std::process::Command::new(program); + cmd.env("LC_ALL", "en_US.UTF-8") + .env("LC_CTYPE", "en_US.UTF-8") + .env("LANG", "en_US.UTF-8"); + cmd +} +pub fn read() -> Result { + #[cfg(target_os = "macos")] + { + let output = command("/usr/bin/pbpaste") + .arg("-Prefer") + .arg("txt") + .output()?; + if !output.status.success() { + bail!("system clipboard read failed"); + } + // Never silently replace invalid bytes in the user's source. + Ok(String::from_utf8(output.stdout)?) + } + #[cfg(not(target_os = "macos"))] + { + bail!("system clipboard adapter is not available on this platform") + } +} +pub fn write(value: &str) -> Result<()> { + #[cfg(target_os = "macos")] + { + use std::io::Write; + let mut child = command("/usr/bin/pbcopy") + .stdin(std::process::Stdio::piped()) + .spawn()?; + let mut stdin = child + .stdin + .take() + .ok_or_else(|| anyhow::anyhow!("clipboard stdin is unavailable"))?; + stdin.write_all(value.as_bytes())?; + drop(stdin); + if !child.wait()?.success() { + bail!("system clipboard write failed"); + } + Ok(()) + } + #[cfg(not(target_os = "macos"))] + { + let _ = value; + bail!("system clipboard adapter is not available on this platform") + } +} diff --git a/src/main.rs b/src/main.rs index fff12f9..5b9cd94 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod activity; mod api; mod app; +mod clipboard; mod developer_tools; mod display_list; mod document;