From 41b72acdf1d733331fad72d62f91df68f097418f Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 21 Nov 2021 19:38:43 +0100 Subject: [PATCH] feat(clipboard): add clipboard operation options and alternative x11 xclip backend. #882 --- espanso-clipboard/src/cocoa/mod.rs | 19 ++- espanso-clipboard/src/lib.rs | 30 +++- espanso-clipboard/src/wayland/fallback/mod.rs | 19 ++- espanso-clipboard/src/win32/mod.rs | 19 ++- espanso-clipboard/src/x11/mod.rs | 64 +++++++- espanso-clipboard/src/x11/native/mod.rs | 19 ++- espanso-clipboard/src/x11/xclip/mod.rs | 139 ++++++++++++++++++ 7 files changed, 282 insertions(+), 27 deletions(-) create mode 100644 espanso-clipboard/src/x11/xclip/mod.rs diff --git a/espanso-clipboard/src/cocoa/mod.rs b/espanso-clipboard/src/cocoa/mod.rs index d842592..6802851 100644 --- a/espanso-clipboard/src/cocoa/mod.rs +++ b/espanso-clipboard/src/cocoa/mod.rs @@ -24,7 +24,7 @@ use std::{ path::PathBuf, }; -use crate::Clipboard; +use crate::{Clipboard, ClipboardOperationOptions}; use anyhow::Result; use log::error; use thiserror::Error; @@ -38,7 +38,7 @@ impl CocoaClipboard { } impl Clipboard for CocoaClipboard { - fn get_text(&self) -> Option { + fn get_text(&self, _: &ClipboardOperationOptions) -> Option { let mut buffer: [i8; 2048] = [0; 2048]; let native_result = unsafe { ffi::clipboard_get_text(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) }; @@ -50,7 +50,7 @@ impl Clipboard for CocoaClipboard { } } - fn set_text(&self, text: &str) -> anyhow::Result<()> { + fn set_text(&self, text: &str, _: &ClipboardOperationOptions) -> anyhow::Result<()> { let string = CString::new(text)?; let native_result = unsafe { ffi::clipboard_set_text(string.as_ptr()) }; if native_result > 0 { @@ -60,7 +60,11 @@ impl Clipboard for CocoaClipboard { } } - fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> { + fn set_image( + &self, + image_path: &std::path::Path, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { if !image_path.exists() || !image_path.is_file() { return Err(CocoaClipboardError::ImageNotFound(image_path.to_path_buf()).into()); } @@ -75,7 +79,12 @@ impl Clipboard for CocoaClipboard { } } - fn set_html(&self, html: &str, fallback_text: Option<&str>) -> anyhow::Result<()> { + fn set_html( + &self, + html: &str, + fallback_text: Option<&str>, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { let html_string = CString::new(html)?; let fallback_string = CString::new(fallback_text.unwrap_or_default())?; let fallback_ptr = if fallback_text.is_some() { diff --git a/espanso-clipboard/src/lib.rs b/espanso-clipboard/src/lib.rs index 077728b..7416ce4 100644 --- a/espanso-clipboard/src/lib.rs +++ b/espanso-clipboard/src/lib.rs @@ -37,10 +37,28 @@ mod wayland; mod cocoa; pub trait Clipboard { - fn get_text(&self) -> Option; - fn set_text(&self, text: &str) -> Result<()>; - fn set_image(&self, image_path: &Path) -> Result<()>; - fn set_html(&self, html: &str, fallback_text: Option<&str>) -> Result<()>; + fn get_text(&self, options: &ClipboardOperationOptions) -> Option; + fn set_text(&self, text: &str, options: &ClipboardOperationOptions) -> Result<()>; + fn set_image(&self, image_path: &Path, options: &ClipboardOperationOptions) -> Result<()>; + fn set_html( + &self, + html: &str, + fallback_text: Option<&str>, + options: &ClipboardOperationOptions, + ) -> Result<()>; +} + +#[allow(dead_code)] +pub struct ClipboardOperationOptions { + pub use_xclip_backend: bool, +} + +impl Default for ClipboardOperationOptions { + fn default() -> Self { + Self { + use_xclip_backend: false, + } + } } #[allow(dead_code)] @@ -74,8 +92,8 @@ pub fn get_clipboard(_: ClipboardOptions) -> Result> { #[cfg(target_os = "linux")] #[cfg(not(feature = "wayland"))] pub fn get_clipboard(_: ClipboardOptions) -> Result> { - info!("using X11NativeClipboard"); - Ok(Box::new(x11::native::X11NativeClipboard::new()?)) + info!("using X11Clipboard"); + Ok(Box::new(x11::X11Clipboard::new()?)) } #[cfg(target_os = "linux")] diff --git a/espanso-clipboard/src/wayland/fallback/mod.rs b/espanso-clipboard/src/wayland/fallback/mod.rs index c916dac..f566a41 100644 --- a/espanso-clipboard/src/wayland/fallback/mod.rs +++ b/espanso-clipboard/src/wayland/fallback/mod.rs @@ -24,7 +24,7 @@ use std::{ process::Stdio, }; -use crate::{Clipboard, ClipboardOptions}; +use crate::{Clipboard, ClipboardOperationOptions, ClipboardOptions}; use anyhow::Result; use log::{error, warn}; use std::process::Command; @@ -74,7 +74,7 @@ impl WaylandFallbackClipboard { } impl Clipboard for WaylandFallbackClipboard { - fn get_text(&self) -> Option { + fn get_text(&self, _: &ClipboardOperationOptions) -> Option { let timeout = std::time::Duration::from_millis(self.command_timeout); match Command::new("wl-paste") .arg("--no-newline") @@ -116,11 +116,15 @@ impl Clipboard for WaylandFallbackClipboard { } } - fn set_text(&self, text: &str) -> anyhow::Result<()> { + fn set_text(&self, text: &str, _: &ClipboardOperationOptions) -> anyhow::Result<()> { self.invoke_command_with_timeout(&mut Command::new("wl-copy"), text.as_bytes(), "wl-copy") } - fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> { + fn set_image( + &self, + image_path: &std::path::Path, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { if !image_path.exists() || !image_path.is_file() { return Err(WaylandFallbackClipboardError::ImageNotFound(image_path.to_path_buf()).into()); } @@ -137,7 +141,12 @@ impl Clipboard for WaylandFallbackClipboard { ) } - fn set_html(&self, html: &str, _fallback_text: Option<&str>) -> anyhow::Result<()> { + fn set_html( + &self, + html: &str, + _fallback_text: Option<&str>, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { self.invoke_command_with_timeout( &mut Command::new("wl-copy").arg("--type").arg("text/html"), html.as_bytes(), diff --git a/espanso-clipboard/src/win32/mod.rs b/espanso-clipboard/src/win32/mod.rs index c4a3143..5d8023d 100644 --- a/espanso-clipboard/src/win32/mod.rs +++ b/espanso-clipboard/src/win32/mod.rs @@ -21,7 +21,7 @@ mod ffi; use std::{ffi::CString, path::PathBuf}; -use crate::Clipboard; +use crate::{Clipboard, ClipboardOperationOptions}; use anyhow::Result; use log::error; use thiserror::Error; @@ -36,7 +36,7 @@ impl Win32Clipboard { } impl Clipboard for Win32Clipboard { - fn get_text(&self) -> Option { + fn get_text(&self, _: &ClipboardOperationOptions) -> Option { let mut buffer: [u16; 2048] = [0; 2048]; let native_result = unsafe { ffi::clipboard_get_text(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) }; @@ -48,7 +48,7 @@ impl Clipboard for Win32Clipboard { } } - fn set_text(&self, text: &str) -> anyhow::Result<()> { + fn set_text(&self, text: &str, _: &ClipboardOperationOptions) -> anyhow::Result<()> { let string = U16CString::from_str(text)?; let native_result = unsafe { ffi::clipboard_set_text(string.as_ptr()) }; if native_result > 0 { @@ -58,7 +58,11 @@ impl Clipboard for Win32Clipboard { } } - fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> { + fn set_image( + &self, + image_path: &std::path::Path, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { if !image_path.exists() || !image_path.is_file() { return Err(Win32ClipboardError::ImageNotFound(image_path.to_path_buf()).into()); } @@ -73,7 +77,12 @@ impl Clipboard for Win32Clipboard { } } - fn set_html(&self, html: &str, fallback_text: Option<&str>) -> anyhow::Result<()> { + fn set_html( + &self, + html: &str, + fallback_text: Option<&str>, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { let html_descriptor = generate_html_descriptor(html); let html_string = CString::new(html_descriptor)?; let fallback_string = U16CString::from_str(fallback_text.unwrap_or_default())?; diff --git a/espanso-clipboard/src/x11/mod.rs b/espanso-clipboard/src/x11/mod.rs index cccb648..9e658e3 100644 --- a/espanso-clipboard/src/x11/mod.rs +++ b/espanso-clipboard/src/x11/mod.rs @@ -17,4 +17,66 @@ * along with espanso. If not, see . */ -pub(crate) mod native; +use anyhow::Result; + +use crate::{Clipboard, ClipboardOperationOptions}; + +mod native; +mod xclip; + +pub(crate) struct X11Clipboard { + native_backend: native::X11NativeClipboard, + xclip_backend: xclip::XClipClipboard, +} + +impl X11Clipboard { + pub fn new() -> Result { + Ok(Self { + native_backend: native::X11NativeClipboard::new()?, + xclip_backend: xclip::XClipClipboard::new(), + }) + } +} + +impl Clipboard for X11Clipboard { + fn get_text(&self, options: &ClipboardOperationOptions) -> Option { + if options.use_xclip_backend { + self.xclip_backend.get_text(options) + } else { + self.native_backend.get_text(options) + } + } + + fn set_text(&self, text: &str, options: &ClipboardOperationOptions) -> anyhow::Result<()> { + if options.use_xclip_backend { + self.xclip_backend.set_text(text, options) + } else { + self.native_backend.set_text(text, options) + } + } + + fn set_image( + &self, + image_path: &std::path::Path, + options: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { + if options.use_xclip_backend { + self.xclip_backend.set_image(image_path, options) + } else { + self.native_backend.set_image(image_path, options) + } + } + + fn set_html( + &self, + html: &str, + fallback_text: Option<&str>, + options: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { + if options.use_xclip_backend { + self.xclip_backend.set_html(html, fallback_text, options) + } else { + self.native_backend.set_html(html, fallback_text, options) + } + } +} diff --git a/espanso-clipboard/src/x11/native/mod.rs b/espanso-clipboard/src/x11/native/mod.rs index de5a1f1..6cba718 100644 --- a/espanso-clipboard/src/x11/native/mod.rs +++ b/espanso-clipboard/src/x11/native/mod.rs @@ -23,7 +23,7 @@ use std::{ path::PathBuf, }; -use crate::Clipboard; +use crate::{Clipboard, ClipboardOperationOptions}; use anyhow::Result; use std::os::raw::c_char; use thiserror::Error; @@ -39,7 +39,7 @@ impl X11NativeClipboard { } impl Clipboard for X11NativeClipboard { - fn get_text(&self) -> Option { + fn get_text(&self, _: &ClipboardOperationOptions) -> Option { let mut buffer: [c_char; 2048] = [0; 2048]; let native_result = unsafe { ffi::clipboard_x11_get_text(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) }; @@ -51,7 +51,7 @@ impl Clipboard for X11NativeClipboard { } } - fn set_text(&self, text: &str) -> anyhow::Result<()> { + fn set_text(&self, text: &str, _: &ClipboardOperationOptions) -> anyhow::Result<()> { let string = CString::new(text)?; let native_result = unsafe { ffi::clipboard_x11_set_text(string.as_ptr()) }; if native_result > 0 { @@ -61,7 +61,11 @@ impl Clipboard for X11NativeClipboard { } } - fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> { + fn set_image( + &self, + image_path: &std::path::Path, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { if !image_path.exists() || !image_path.is_file() { return Err(X11NativeClipboardError::ImageNotFound(image_path.to_path_buf()).into()); } @@ -80,7 +84,12 @@ impl Clipboard for X11NativeClipboard { } } - fn set_html(&self, html: &str, fallback_text: Option<&str>) -> anyhow::Result<()> { + fn set_html( + &self, + html: &str, + fallback_text: Option<&str>, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { let html_string = CString::new(html)?; let fallback_string = CString::new(fallback_text.unwrap_or_default())?; let fallback_ptr = if fallback_text.is_some() { diff --git a/espanso-clipboard/src/x11/xclip/mod.rs b/espanso-clipboard/src/x11/xclip/mod.rs new file mode 100644 index 0000000..1136dff --- /dev/null +++ b/espanso-clipboard/src/x11/xclip/mod.rs @@ -0,0 +1,139 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use anyhow::bail; +use log::error; +use std::io::Write; +use std::process::{Command, Stdio}; + +use crate::{Clipboard, ClipboardOperationOptions}; + +pub struct XClipClipboard { + is_xclip_available: bool, +} + +impl XClipClipboard { + pub fn new() -> Self { + let command = Command::new("xclipz").arg("-h").output(); + let is_xclip_available = command + .map(|output| output.status.success()) + .unwrap_or(false); + + Self { is_xclip_available } + } +} + +impl Clipboard for XClipClipboard { + fn get_text(&self, _: &ClipboardOperationOptions) -> Option { + if !self.is_xclip_available { + error!("attempted to use XClipClipboard, but `xclip` command can't be called"); + return None; + } + + match Command::new("xclip").args(&["-o", "-sel", "clip"]).output() { + Ok(output) => { + if output.status.success() { + let s = String::from_utf8_lossy(&output.stdout); + return Some(s.to_string()); + } + } + Err(error) => { + error!("xclip reported an error: {}", error); + } + } + + None + } + + fn set_text(&self, text: &str, _: &ClipboardOperationOptions) -> anyhow::Result<()> { + if !self.is_xclip_available { + bail!("attempted to use XClipClipboard, but `xclip` command can't be called"); + } + + let mut child = Command::new("xclip") + .args(&["-sel", "clip"]) + .stdin(Stdio::piped()) + .spawn()?; + + let stdin = child.stdin.as_mut(); + if let Some(input) = stdin { + input.write_all(text.as_bytes())?; + child.wait()?; + } + + Ok(()) + } + + fn set_image( + &self, + image_path: &std::path::Path, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { + if !self.is_xclip_available { + bail!("attempted to use XClipClipboard, but `xclip` command can't be called"); + } + + let extension = image_path.extension(); + let mime = match extension { + Some(ext) => { + let ext = ext.to_string_lossy().to_lowercase(); + match ext.as_ref() { + "png" => "image/png", + "jpg" | "jpeg" => "image/jpeg", + "gif" => "image/gif", + "svg" => "image/svg", + _ => "image/png", + } + } + None => "image/png", + }; + + let image_path = image_path.to_string_lossy(); + + Command::new("xclip") + .args(&["-selection", "clipboard", "-t", mime, "-i", &image_path]) + .spawn()?; + + Ok(()) + } + + fn set_html( + &self, + html: &str, + _: Option<&str>, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { + if !self.is_xclip_available { + bail!("attempted to use XClipClipboard, but `xclip` command can't be called"); + } + + let mut child = Command::new("xclip") + .args(&["-sel", "clip", "-t", "text/html"]) + .stdin(Stdio::piped()) + .spawn()?; + + let stdin = child.stdin.as_mut(); + if let Some(input) = stdin { + input.write_all(html.as_bytes())?; + child.wait()?; + } + + Ok(()) + } +}