Browse Source

Add support for xsel clipboard as well, dynamically select at runtime

Fixes timvisee/ffsend#76
timvisee 6 years ago
parent
commit
982027f33c
5 changed files with 158 additions and 19 deletions
  1. 10 0
      Cargo.lock
  2. 13 2
      Cargo.toml
  3. 7 0
      README.md
  4. 3 5
      src/action/debug.rs
  5. 125 12
      src/util.rs

+ 10 - 0
Cargo.lock

@@ -557,6 +557,7 @@ dependencies = [
  "pbr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "prettytable-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "qr2term 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quale 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "rpassword 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1246,6 +1247,14 @@ dependencies = [
  "checked_int_cast 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "quale"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "quote"
 version = "0.5.2"
@@ -2280,6 +2289,7 @@ dependencies = [
 "checksum proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)" = "4d317f9caece796be1980837fd5cb3dfec5613ebdb04ad0956deea83ce168915"
 "checksum qr2term 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "358175087ab058962f8f4457864ece0db9685ca5a8938385b5c9e279aa781349"
 "checksum qrcode 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3553d8614cd59cede2a00db69bc96ea72dfdd92c041600f06b100f6f2699b26"
+"checksum quale 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c46811c513cc64cceb23adc27892b3b3470c1790de117610221b97071de8bc8c"
 "checksum quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8"
 "checksum quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd8e04bd9c52e0342b406469d494fcb033be4bdbe5c606016defbb1681411e1"
 "checksum rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9"

+ 13 - 2
Cargo.toml

@@ -50,7 +50,17 @@ name = "ffsend"
 path = "src/main.rs"
 
 [features]
-default = ["archive", "clipboard", "history", "infer-command", "qrcode", "send2", "send3", "urlshorten"]
+default = [
+    "archive",
+    "clipboard",
+    "history",
+    "infer-command",
+    "qrcode",
+    "quale",
+    "send2",
+    "send3",
+    "urlshorten"
+]
 
 # Compile with file archiving support
 archive = ["tar"]
@@ -92,14 +102,15 @@ openssl-probe = "0.1"
 pbr = "1"
 prettytable-rs = "0.8"
 qr2term = { version = "0.1", optional = true }
+quale = { version = "1.0", optional = true }
 rpassword = "3.0"
 serde = "1.0"
 serde_derive = "1.0"
 tar = { version = "0.4", optional = true }
 tempfile = "3"
 toml = "0.5"
-version-compare = "0.0.6"
 urlshortener = { version = "0.10", default-features = false, optional = true }
+version-compare = "0.0.6"
 
 [target.'cfg(not(target_os = "linux"))'.dependencies]
 clipboard = { version = "0.5", optional = true }

+ 7 - 0
README.md

@@ -490,6 +490,13 @@ empty.
 | `FFSEND_QUIET`       | `--quiet`       | Log quiet information              |
 | `FFSEND_VERBOSE`     | `--verbose`     | Log verbose information            |
 
+Some environment variables may be set at compile time to tweak some defaults.
+
+| Variable     | Description                            |
+| :----------- | :------------------------------------- |
+| `XCLIP_PATH` | Set fixed `xclip` binary path on Linux |
+| `XSEL_PATH`  | Set fixed `xsel` binary path on Linux  |
+
 At this time, no configuration or _dotfile_ file support is available.
 This will be something added in a later release.
 

+ 3 - 5
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(all(feature = "clipboard", target_os = "linux"))]
+use crate::util::ClipboardType;
 use crate::util::{api_version_list, features_list, format_bool, format_duration};
 
 /// A file debug action.
@@ -100,11 +102,7 @@ impl<'a> Debug<'a> {
         #[cfg(all(feature = "clipboard", target_os = "linux"))]
         table.add_row(Row::new(vec![
             Cell::new("Clipboard:"),
-            Cell::new(
-                &option_env!("XCLIP_PATH")
-                    .map(|path| format!("xclip ({})", path))
-                    .unwrap_or_else(|| "xclip".into()),
-            ),
+            Cell::new(&format!("{}", ClipboardType::select())),
         ]));
 
         // Show whether quiet is used

+ 125 - 12
src/util.rs

@@ -4,12 +4,16 @@ extern crate colored;
 extern crate directories;
 extern crate fs2;
 extern crate open;
+#[cfg(all(feature = "clipboard", target_os = "linux"))]
+extern crate quale;
 
 use std::borrow::Borrow;
 use std::env::{self, current_exe, var_os};
 use std::ffi::OsStr;
+#[cfg(all(feature = "clipboard", target_os = "linux"))]
+use std::fmt;
 use std::fmt::{Debug, Display};
-#[cfg(all(target_os = "linux", feature = "clipboard"))]
+#[cfg(all(feature = "clipboard", target_os = "linux"))]
 use std::io::ErrorKind as IoErrorKind;
 use std::io::{stderr, stdin, Error as IoError, Write};
 use std::path::Path;
@@ -284,11 +288,81 @@ pub fn open_path(path: &str) -> Result<ExitStatus, IoError> {
     open::that(path)
 }
 
-/// Set the clipboard of the user to the given `content` string.
+/// 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")]
-pub fn set_clipboard(content: String) -> Result<(), ClipboardError> {
+#[derive(Clone, Eq, PartialEq)]
+pub enum ClipboardType {
+    /// Native operating system clipboard.
+    #[cfg(not(target_os = "linux"))]
+    Native,
+
+    /// Manage clipboard through `xclip` on Linux.
+    ///
+    /// May contain a binary path if specified at compile time through the `XCLIP_PATH` variable.
+    #[cfg(target_os = "linux")]
+    Xclip(Option<String>),
+
+    /// Manage clipboard through `xsel` on Linux.
+    ///
+    /// May contain a binary path if specified at compile time through the `XSEL_PATH` variable.
+    #[cfg(target_os = "linux")]
+    Xsel(Option<String>),
+}
+
+#[cfg(feature = "clipboard")]
+impl ClipboardType {
+    /// Select the clipboard type to use, depending on the runtime system.
+    pub fn select() -> Self {
+        #[cfg(not(target_os = "linux"))]
+        {
+            ClipboardType::Native
+        }
+
+        #[cfg(target_os = "linux")]
+        {
+            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 quale::which("xclip").is_some() {
+                ClipboardType::Xclip(None)
+            } else if quale::which("xsel").is_some() {
+                ClipboardType::Xsel(None)
+            } else {
+                // TODO: should we error here instead, as no clipboard binary was found?
+                ClipboardType::Xclip(None)
+            }
+        }
+    }
+
+    /// Set clipboar contents through the selected clipboard type.
+    pub fn set(&self, content: String) -> Result<(), ClipboardError> {
+        match self {
+            #[cfg(not(target_os = "linux"))]
+            ClipboardType::Native => Self::native_set(content),
+            #[cfg(target_os = "linux")]
+            ClipboardType::Xclip(path) => Self::xclip_set(path.clone(), content),
+            #[cfg(target_os = "linux")]
+            ClipboardType::Xsel(path) => Self::xsel_set(path.clone(), content),
+        }
+    }
+
+    /// Set the clipboard through a native interface.
+    ///
+    /// This is used on non-Linux systems.
     #[cfg(not(target_os = "linux"))]
-    {
+    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())
@@ -296,14 +370,27 @@ pub fn set_clipboard(content: String) -> Result<(), ClipboardError> {
     }
 
     #[cfg(target_os = "linux")]
-    {
-        // Open an xclip process
-        let mut process = match Command::new(option_env!("XCLIP_PATH").unwrap_or("xclip"))
-            .arg("-sel")
-            .arg("clip")
-            .stdin(Stdio::piped())
-            .spawn()
-        {
+    fn xclip_set(path: Option<String>, content: String) -> Result<(), ClipboardError> {
+        Self::sys_cmd_set(
+            Command::new(path.unwrap_or_else(|| "xclip".into()))
+                .arg("-sel")
+                .arg("clip"),
+            content,
+        )
+    }
+
+    #[cfg(target_os = "linux")]
+    fn xsel_set(path: Option<String>, content: String) -> Result<(), ClipboardError> {
+        Self::sys_cmd_set(
+            Command::new(path.unwrap_or_else(|| "xsel".into())).arg("--clipboard"),
+            content,
+        )
+    }
+
+    #[cfg(target_os = "linux")]
+    fn sys_cmd_set(command: &mut Command, content: String) -> Result<(), ClipboardError> {
+        // Spawn the command process for setting the clipboard
+        let mut process = match command.stdin(Stdio::piped()).spawn() {
             Ok(process) => process,
             Err(err) => {
                 return Err(match err.kind() {
@@ -331,6 +418,32 @@ pub fn set_clipboard(content: String) -> Result<(), ClipboardError> {
     }
 }
 
+#[cfg(feature = "clipboard")]
+impl fmt::Display for ClipboardType {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            #[cfg(not(target_os = "linux"))]
+            ClipboardType::Native => write!(f, "native"),
+            #[cfg(target_os = "linux")]
+            ClipboardType::Xclip(path) => match path {
+                None => write!(f, "xclip"),
+                Some(path) => write!(f, "xclip ({})", path),
+            },
+            #[cfg(target_os = "linux")]
+            ClipboardType::Xsel(path) => match path {
+                None => write!(f, "xsel"),
+                Some(path) => write!(f, "xsel ({})", path),
+            },
+        }
+    }
+}
+
+/// Set the clipboard of the user to the given `content` string.
+#[cfg(feature = "clipboard")]
+pub fn set_clipboard(content: String) -> Result<(), ClipboardError> {
+    ClipboardType::select().set(content)
+}
+
 #[cfg(feature = "clipboard")]
 #[derive(Debug, Fail)]
 pub enum ClipboardError {