diff --git a/Cargo.lock b/Cargo.lock index 4c85082..26fbeba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -253,17 +253,6 @@ dependencies = [ "x11-clipboard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "clipboard-ext" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "clipboard 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", - "which 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "x11-clipboard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "clipboard-win" version = "2.2.0" @@ -600,7 +589,7 @@ dependencies = [ "chbs 0.0.9 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", - "clipboard-ext 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "clipboard 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "colored 1.9.2 (registry+https://github.com/rust-lang/crates.io-index)", "derive_builder 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "directories 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -623,6 +612,7 @@ dependencies = [ "toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "urlshortener 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "version-compare 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "which 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2734,7 +2724,6 @@ dependencies = [ "checksum chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01" "checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" "checksum clipboard 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" -"checksum clipboard-ext 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2513f2e8ada5da8d9a368f764092cd35eeb4e0c2d56fc43948c2962645324b22" "checksum clipboard-win 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" "checksum colored 1.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8815e2ab78f3a59928fc32e141fbeece88320a240e43f47b2fd64ea3a88a5b3d" diff --git a/Cargo.toml b/Cargo.toml index 49969d0..b80045a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ exclude = [ "/SECURITY.md", ] edition = "2018" +build = "build.rs" [package.metadata.deb] section = "utility" @@ -71,7 +72,7 @@ default = [ archive = ["tar"] # Support for putting share URLs in clipboard -clipboard = ["clipboard-ext"] +clipboard = ["clip", "which"] # Compile with file history support history = [] @@ -94,11 +95,16 @@ infer-command = [] # Compile without colored output support no-color = ["colored/no-color"] +# Automatic using build.rs: use xclip/xsel binary method for clipboard support +clipboard-bin = ["clipboard"] + +# Automatic using build.rs: use native clipboard crate for clipboard support +clipboard-crate = ["clipboard"] + [dependencies] chbs = "0.0.9" chrono = "0.4" clap = "2.33" -clipboard-ext = { version = "0.1", optional = true } colored = "1.9" derive_builder = "0.9" directories = "2.0" @@ -121,3 +127,10 @@ tempfile = "3" toml = "0.5" urlshortener = { version = "2", optional = true } version-compare = "0.0.10" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "openbsd", target_os = "netbsd"))'.dependencies] +which = { version = "3.1", optional = true } + +[target.'cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "openbsd", target_os = "netbsd")))'.dependencies] +# Aliased to clip to prevent name collision with clipboard feature +clip = { version = "0.5", optional = true, package = "clipboard" } diff --git a/README.md b/README.md index 54ed8af..0d8ec0a 100644 --- a/README.md +++ b/README.md @@ -574,8 +574,8 @@ Some environment variables may be set at compile time to tweak some defaults. | Variable | Description | | :----------- | :------------------------------------------------------------------------- | -| `XCLIP_PATH` | Set fixed `xclip` binary path when using `clipboard` (Linux, *BSD) | -| `XSEL_PATH` | Set fixed `xsel` binary path when using `clipboard` (Linux, *BSD) | +| `XCLIP_PATH` | Set fixed `xclip` binary path when using `clipboard-bin` (Linux, *BSD) | +| `XSEL_PATH` | Set fixed `xsel` binary path when using `clipboard-bin` (Linux, *BSD) | At this time, no configuration or _dotfile_ file support is available. This will be something added in a later release. diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..32b3d32 --- /dev/null +++ b/build.rs @@ -0,0 +1,37 @@ +fn main() { + #[cfg(all( + feature = "clipboard", + any( + target_os = "linux", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + ) + ))] + { + // Select clipboard binary method + #[cfg(not(feature = "clipboard-crate"))] + println!("cargo:rustc-cfg=feature=\"clipboard-bin\""); + + // xclip and xsel paths are inserted at compile time + println!("cargo:rerun-if-env-changed=XCLIP_PATH"); + println!("cargo:rerun-if-env-changed=XSEL_PATH"); + } + + #[cfg(all( + feature = "clipboard", + not(any( + target_os = "linux", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + )) + ))] + { + // Select clipboard crate method + #[cfg(not(feature = "clipboard-bin"))] + println!("cargo:rustc-cfg=feature=\"clipboard-crate\""); + } +} diff --git a/src/action/debug.rs b/src/action/debug.rs index faf3150..7953079 100644 --- a/src/action/debug.rs +++ b/src/action/debug.rs @@ -6,6 +6,8 @@ use prettytable::{format::FormatBuilder, Cell, Row, Table}; use crate::client::to_duration; use crate::cmd::matcher::{debug::DebugMatcher, main::MainMatcher, Matcher}; use crate::error::ActionError; +#[cfg(feature = "clipboard-bin")] +use crate::util::ClipboardType; use crate::util::{api_version_list, features_list, format_bool, format_duration}; /// A file debug action. @@ -96,6 +98,13 @@ impl<'a> Debug<'a> { Cell::new(&api_version_list().join(", ")), ])); + // Clipboard information + #[cfg(feature = "clipboard-bin")] + table.add_row(Row::new(vec![ + Cell::new("Clipboard:"), + Cell::new(&format!("{}", ClipboardType::select())), + ])); + // Show whether quiet is used table.add_row(Row::new(vec![ Cell::new("Quiet:"), diff --git a/src/action/upload.rs b/src/action/upload.rs index 331b4ca..df76221 100644 --- a/src/action/upload.rs +++ b/src/action/upload.rs @@ -444,10 +444,9 @@ impl<'a> Upload<'a> { { if let Some(copy_mode) = matcher_upload.copy() { if let Err(err) = set_clipboard(copy_mode.build(url.as_str())) { - print_error_msg(format!( - "failed to copy the share link to the clipboard, ignoring: {}", - err, - )); + print_error( + err.context("failed to copy the share link to the clipboard, ignoring"), + ); } } } diff --git a/src/util.rs b/src/util.rs index 51238f1..f4a02ab 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,28 +1,38 @@ +#[cfg(feature = "clipboard-crate")] +extern crate clip; extern crate colored; extern crate directories; extern crate fs2; extern crate open; extern crate regex; +#[cfg(feature = "clipboard-bin")] +extern crate which; use std::borrow::Borrow; use std::env::{self, current_exe, var_os}; -#[cfg(feature = "clipboard")] -use std::error::Error as StdError; use std::ffi::OsStr; +#[cfg(feature = "clipboard")] +use std::fmt; use std::fmt::{Debug, Display}; +#[cfg(feature = "clipboard-bin")] +use std::io::ErrorKind as IoErrorKind; use std::io::{stderr, stdin, Error as IoError, Write}; use std::path::Path; use std::path::PathBuf; use std::process::{exit, ExitStatus}; +#[cfg(feature = "clipboard-bin")] +use std::process::{Command, Stdio}; +#[cfg(feature = "clipboard-crate")] +use self::clip::{ClipboardContext, ClipboardProvider}; use self::colored::*; #[cfg(feature = "history")] use self::directories::ProjectDirs; use self::fs2::available_space; use chrono::Duration; -#[cfg(feature = "clipboard")] -use clipboard_ext::{prelude::*, x11_bin::ClipboardContext}; use failure::{err_msg, Fail}; +#[cfg(feature = "clipboard-crate")] +use failure::{Compat, Error}; use ffsend_api::{ api::request::{ensure_success, ResponseError}, client::Client, @@ -31,6 +41,8 @@ use ffsend_api::{ }; use regex::Regex; use rpassword::prompt_password_stderr; +#[cfg(feature = "clipboard-bin")] +use which::which; use crate::cmd::matcher::MainMatcher; @@ -291,10 +303,202 @@ pub fn open_path(path: &str) -> Result { open::that(path) } -/// Set the clipboard of the user to the given `contents` string. +/// Set the clipboard of the user to the given `content` string. #[cfg(feature = "clipboard")] -pub fn set_clipboard(contents: String) -> Result<(), Box> { - Ok(ClipboardContext::new()?.set_contents(contents)?) +pub fn set_clipboard(content: String) -> Result<(), ClipboardError> { + ClipboardType::select().set(content) +} + +/// Clipboard management enum. +/// +/// Defines which method of setting the clipboard is used. +/// Invoke `ClipboardType::select()` to select the best variant to use determined at runtime. +/// +/// Usually, the `Native` variant is used. However, on Linux system a different variant will be +/// selected which will call a system binary to set the clipboard. This must be done because the +/// native clipboard interface only has a lifetime of the application. This means that the +/// clipboard is instantly cleared as soon as this application quits, which is always immediately. +/// This limitation is due to security reasons as defined by X11. The alternative binaries we set +/// the clipboard with spawn a daemon in the background to keep the clipboad alive until it's +/// flushed. +#[cfg(feature = "clipboard")] +#[derive(Clone, Eq, PartialEq)] +pub enum ClipboardType { + /// Native operating system clipboard. + #[cfg(feature = "clipboard-crate")] + Native, + + /// Manage clipboard through `xclip` on Linux. + /// + /// May contain a binary path if specified at compile time through the `XCLIP_PATH` variable. + #[cfg(feature = "clipboard-bin")] + Xclip(Option), + + /// Manage clipboard through `xsel` on Linux. + /// + /// May contain a binary path if specified at compile time through the `XSEL_PATH` variable. + #[cfg(feature = "clipboard-bin")] + Xsel(Option), +} + +#[cfg(feature = "clipboard")] +impl ClipboardType { + /// Select the clipboard type to use, depending on the runtime system. + pub fn select() -> Self { + #[cfg(feature = "clipboard-crate")] + { + ClipboardType::Native + } + + #[cfg(feature = "clipboard-bin")] + { + if let Some(path) = option_env!("XCLIP_PATH") { + ClipboardType::Xclip(Some(path.to_owned())) + } else if let Some(path) = option_env!("XSEL_PATH") { + ClipboardType::Xsel(Some(path.to_owned())) + } else if which("xclip").is_ok() { + ClipboardType::Xclip(None) + } else if which("xsel").is_ok() { + ClipboardType::Xsel(None) + } else { + // TODO: should we error here instead, as no clipboard binary was found? + ClipboardType::Xclip(None) + } + } + } + + /// Set clipboard contents through the selected clipboard type. + pub fn set(&self, content: String) -> Result<(), ClipboardError> { + match self { + #[cfg(feature = "clipboard-crate")] + ClipboardType::Native => Self::native_set(content), + #[cfg(feature = "clipboard-bin")] + ClipboardType::Xclip(path) => Self::xclip_set(path.clone(), &content), + #[cfg(feature = "clipboard-bin")] + ClipboardType::Xsel(path) => Self::xsel_set(path.clone(), &content), + } + } + + /// Set the clipboard through a native interface. + /// + /// This is used on non-Linux systems. + #[cfg(feature = "clipboard-crate")] + fn native_set(content: String) -> Result<(), ClipboardError> { + ClipboardProvider::new() + .and_then(|mut context: ClipboardContext| context.set_contents(content)) + .map_err(|err| format_err!("{}", err).compat()) + .map_err(ClipboardError::Native) + } + + #[cfg(feature = "clipboard-bin")] + fn xclip_set(path: Option, content: &str) -> Result<(), ClipboardError> { + Self::sys_cmd_set( + "xclip", + Command::new(path.unwrap_or_else(|| "xclip".into())) + .arg("-sel") + .arg("clip"), + content, + ) + } + + #[cfg(feature = "clipboard-bin")] + fn xsel_set(path: Option, content: &str) -> Result<(), ClipboardError> { + Self::sys_cmd_set( + "xsel", + Command::new(path.unwrap_or_else(|| "xsel".into())).arg("--clipboard"), + content, + ) + } + + #[cfg(feature = "clipboard-bin")] + fn sys_cmd_set( + bin: &'static str, + command: &mut Command, + content: &str, + ) -> Result<(), ClipboardError> { + // Spawn the command process for setting the clipboard + let mut process = match command.stdin(Stdio::piped()).stdout(Stdio::null()).spawn() { + Ok(process) => process, + Err(err) => { + return Err(match err.kind() { + IoErrorKind::NotFound => ClipboardError::NoBinary, + _ => ClipboardError::BinaryIo(bin, err), + }); + } + }; + + // Write the contents to the xclip process + process + .stdin + .as_mut() + .unwrap() + .write_all(content.as_bytes()) + .map_err(|err| ClipboardError::BinaryIo(bin, err))?; + + // Wait for xclip to exit + let status = process + .wait() + .map_err(|err| ClipboardError::BinaryIo(bin, err))?; + if !status.success() { + return Err(ClipboardError::BinaryStatus( + bin, + status.code().unwrap_or(0), + )); + } + + Ok(()) + } +} + +#[cfg(feature = "clipboard")] +impl fmt::Display for ClipboardType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + #[cfg(feature = "clipboard-crate")] + ClipboardType::Native => write!(f, "native"), + #[cfg(feature = "clipboard-bin")] + ClipboardType::Xclip(path) => match path { + None => write!(f, "xclip"), + Some(path) => write!(f, "xclip ({})", path), + }, + #[cfg(feature = "clipboard-bin")] + ClipboardType::Xsel(path) => match path { + None => write!(f, "xsel"), + Some(path) => write!(f, "xsel ({})", path), + }, + } + } +} + +#[cfg(feature = "clipboard")] +#[derive(Debug, Fail)] +pub enum ClipboardError { + /// A generic error occurred while setting the clipboard contents. + /// + /// This is for non-Linux systems, using a native clipboard interface. + #[cfg(feature = "clipboard-crate")] + #[fail(display = "failed to access clipboard")] + Native(#[cause] Compat), + + /// The `xclip` or `xsel` binary could not be found on the system, required for clipboard support. + #[cfg(feature = "clipboard-bin")] + #[fail(display = "failed to access clipboard, xclip or xsel is not installed")] + NoBinary, + + /// An error occurred while using `xclip` or `xsel` to set the clipboard contents. + /// This problem probably occurred when starting, or while piping the clipboard contents to + /// the process. + #[cfg(feature = "clipboard-bin")] + #[fail(display = "failed to access clipboard using {}", _0)] + BinaryIo(&'static str, #[cause] IoError), + + /// `xclip` or `xsel` unexpectetly exited with a non-successful status code. + #[cfg(feature = "clipboard-bin")] + #[fail( + display = "failed to use clipboard, {} exited with status code {}", + _0, _1 + )] + BinaryStatus(&'static str, i32), } /// Check for an emtpy password in the given `password`. @@ -845,6 +1049,10 @@ pub fn features_list() -> Vec<&'static str> { features.push("archive"); #[cfg(feature = "clipboard")] features.push("clipboard"); + #[cfg(feature = "clipboard-bin")] + features.push("clipboard-bin"); + #[cfg(feature = "clipboard-crate")] + features.push("clipboard-crate"); #[cfg(feature = "history")] features.push("history"); #[cfg(feature = "qrcode")]