copypasta_ext/
x11_bin.rs

1//! Invokes [`xclip`][xclip]/[`xsel`][xsel] to access clipboard.
2//!
3//! This provider ensures the clipboard contents you set remain available even after your
4//! application exists, unlike [`X11ClipboardContext`][X11ClipboardContext].
5//!
6//! When getting or setting the clipboard, the `xclip` or `xsel` binary is invoked to manage the
7//! contents. When setting the clipboard contents, these binaries internally fork and stay alive
8//! until the clipboard content changes.
9//!
10//! The `xclip` or `xsel` must be in `PATH`. Alternatively the paths of either may be set at
11//! compile time using the `XCLIP_PATH` and `XSEL_PATH` environment variables. If set, the
12//! clipboard context will automatically use those.
13//!
14//! What binary is used is deterimined at runtime on context creation based on the compile time
15//! variables and the runtime environment.
16//!
17//! Use the provided `ClipboardContext` type alias to use this clipboard context on supported
18//! platforms, but fall back to the standard clipboard on others.
19//!
20//! ## Benefits
21//!
22//! - Keeps contents in clipboard even after your application exists.
23//!
24//! ## Drawbacks
25//!
26//! - Requires [`xclip`][xclip] or [`xsel`][xsel] to be available.
27//! - Less performant than alternatives due to binary invocation.
28//! - Set contents may not be immediately available, because they are set in an external binary.
29//! - May have undefined behaviour if `xclip` or `xsel` are modified.
30//!
31//! # Examples
32//!
33//! ```rust,no_run
34//! use copypasta_ext::prelude::*;
35//! use copypasta_ext::x11_bin::X11BinClipboardContext;
36//!
37//! let mut ctx = X11BinClipboardContext::new().unwrap();
38//! println!("{:?}", ctx.get_contents());
39//! ctx.set_contents("some string".into()).unwrap();
40//! ```
41//!
42//! Use `ClipboardContext` alias for better platform compatability:
43//!
44//! ```rust,no_run
45//! use copypasta_ext::prelude::*;
46//! use copypasta_ext::x11_bin::ClipboardContext;
47//!
48//! let mut ctx = ClipboardContext::new().unwrap();
49//! println!("{:?}", ctx.get_contents());
50//! ctx.set_contents("some string".into()).unwrap();
51//! ```
52//!
53//! Use `new_with_x11` to combine with [`X11ClipboardContext`][X11ClipboardContext] for better performance.
54//!
55//! ```rust,no_run
56//! use copypasta_ext::prelude::*;
57//! use copypasta_ext::x11_bin::X11BinClipboardContext;
58//!
59//! let mut ctx = X11BinClipboardContext::new_with_x11().unwrap();
60//! println!("{:?}", ctx.get_contents());
61//! ctx.set_contents("some string".into()).unwrap();
62//! ```
63//!
64//! [X11ClipboardContext]: https://docs.rs/copypasta/*/copypasta/x11_clipboard/struct.X11ClipboardContext.html
65//! [x11_clipboard]: https://docs.rs/copypasta/*/copypasta/x11_clipboard/index.html
66//! [xclip]: https://github.com/astrand/xclip
67//! [xsel]: http://www.vergenet.net/~conrad/software/xsel/
68
69use std::error::Error as StdError;
70use std::fmt;
71use std::io::{Error as IoError, ErrorKind as IoErrorKind, Write};
72use std::process::{Command, Stdio};
73use std::string::FromUtf8Error;
74
75use copypasta::x11_clipboard::X11ClipboardContext;
76use which::which;
77
78use crate::combined::CombinedClipboardContext;
79use crate::display::DisplayServer;
80use crate::prelude::*;
81
82/// Platform specific context.
83///
84/// Alias for `X11BinClipboardContext` on supported platforms, aliases to standard
85/// `ClipboardContext` provided by `rust-clipboard` on other platforms.
86pub type ClipboardContext = X11BinClipboardContext;
87
88/// Invokes [`xclip`][xclip]/[`xsel`][xsel] to access clipboard.
89///
90/// See module documentation for more information.
91///
92/// [xclip]: https://github.com/astrand/xclip
93/// [xsel]: http://www.vergenet.net/~conrad/software/xsel/
94pub struct X11BinClipboardContext(ClipboardType);
95
96impl X11BinClipboardContext {
97    pub fn new() -> crate::ClipResult<Self> {
98        Ok(Self(ClipboardType::select()))
99    }
100
101    /// Construct combined with [`X11ClipboardContext`][X11ClipboardContext].
102    ///
103    /// This clipboard context invokes a binary for getting the clipboard contents. This may
104    /// be considered inefficient and has other drawbacks as noted in the struct documentation.
105    /// This function also constructs a `X11ClipboardContext` for getting clipboard contents and
106    /// combines the two to get the best of both worlds.
107    ///
108    /// [X11ClipboardContext]: https://docs.rs/copypasta/*/copypasta/x11_clipboard/struct.X11ClipboardContext.html
109    pub fn new_with_x11() -> crate::ClipResult<CombinedClipboardContext<X11ClipboardContext, Self>>
110    {
111        Self::new()?.with_x11()
112    }
113
114    /// Combine this context with [`X11ClipboardContext`][X11ClipboardContext].
115    ///
116    /// This clipboard context invokes a binary for getting the clipboard contents. This may
117    /// be considered inefficient and has other drawbacks as noted in the struct documentation.
118    /// This function constructs a `X11ClipboardContext` for getting clipboard contents and
119    /// combines the two to get the best of both worlds.
120    ///
121    /// [X11ClipboardContext]: https://docs.rs/copypasta/*/copypasta/x11_clipboard/struct.X11ClipboardContext.html
122    pub fn with_x11(
123        self,
124    ) -> crate::ClipResult<CombinedClipboardContext<X11ClipboardContext, Self>> {
125        Ok(CombinedClipboardContext(X11ClipboardContext::new()?, self))
126    }
127}
128
129impl ClipboardProvider for X11BinClipboardContext {
130    fn get_contents(&mut self) -> crate::ClipResult<String> {
131        Ok(self.0.get()?)
132    }
133
134    fn set_contents(&mut self, contents: String) -> crate::ClipResult<()> {
135        Ok(self.0.set(&contents)?)
136    }
137}
138
139impl ClipboardProviderExt for X11BinClipboardContext {
140    fn display_server(&self) -> Option<DisplayServer> {
141        Some(DisplayServer::X11)
142    }
143
144    fn has_bin_lifetime(&self) -> bool {
145        false
146    }
147}
148
149/// Available clipboard management binaries.
150///
151/// Invoke `ClipboardType::select()` to select the best variant to use determined at runtime.
152enum ClipboardType {
153    /// Use `xclip`.
154    ///
155    /// May contain a binary path if specified at compile time through the `XCLIP_PATH` variable.
156    Xclip(Option<String>),
157
158    /// Use `xsel`.
159    ///
160    /// May contain a binary path if specified at compile time through the `XSEL_PATH` variable.
161    Xsel(Option<String>),
162}
163
164impl ClipboardType {
165    /// Select the clipboard type to use.
166    pub fn select() -> Self {
167        if let Some(path) = option_env!("XCLIP_PATH") {
168            ClipboardType::Xclip(Some(path.to_owned()))
169        } else if let Some(path) = option_env!("XSEL_PATH") {
170            ClipboardType::Xsel(Some(path.to_owned()))
171        } else if which("xclip").is_ok() {
172            ClipboardType::Xclip(None)
173        } else if which("xsel").is_ok() {
174            ClipboardType::Xsel(None)
175        } else {
176            // TODO: should we error here instead, as no clipboard binary was found?
177            ClipboardType::Xclip(None)
178        }
179    }
180
181    /// Get clipboard contents through the selected clipboard type.
182    pub fn get(&self) -> Result<String, Error> {
183        match self {
184            ClipboardType::Xclip(path) => sys_cmd_get(
185                "xclip",
186                Command::new(path.as_deref().unwrap_or("xclip"))
187                    .arg("-sel")
188                    .arg("clip")
189                    .arg("-out"),
190            ),
191            ClipboardType::Xsel(path) => sys_cmd_get(
192                "xsel",
193                Command::new(path.as_deref().unwrap_or("xsel"))
194                    .arg("--clipboard")
195                    .arg("--output"),
196            ),
197        }
198    }
199
200    /// Set clipboard contents through the selected clipboard type.
201    pub fn set(&self, contents: &str) -> Result<(), Error> {
202        match self {
203            ClipboardType::Xclip(path) => sys_cmd_set(
204                "xclip",
205                Command::new(path.as_deref().unwrap_or("xclip"))
206                    .arg("-sel")
207                    .arg("clip"),
208                contents,
209            ),
210            ClipboardType::Xsel(path) => sys_cmd_set(
211                "xsel",
212                Command::new(path.as_deref().unwrap_or("xsel")).arg("--clipboard"),
213                contents,
214            ),
215        }
216    }
217}
218
219/// Get clipboard contents using a system command.
220fn sys_cmd_get(bin: &'static str, command: &mut Command) -> Result<String, Error> {
221    // Spawn the command process for getting the clipboard
222    let output = match command.output() {
223        Ok(output) => output,
224        Err(err) => {
225            return Err(match err.kind() {
226                IoErrorKind::NotFound => Error::NoBinary,
227                _ => Error::BinaryIo(bin, err),
228            });
229        }
230    };
231
232    // Check process status code
233    if !output.status.success() {
234        return Err(Error::BinaryStatus(bin, output.status.code().unwrap_or(0)));
235    }
236
237    // Get and parse output
238    String::from_utf8(output.stdout).map_err(Error::NoUtf8)
239}
240
241/// Set clipboard contents using a system command.
242fn sys_cmd_set(bin: &'static str, command: &mut Command, contents: &str) -> Result<(), Error> {
243    // Spawn the command process for setting the clipboard
244    let mut process = match command.stdin(Stdio::piped()).stdout(Stdio::null()).spawn() {
245        Ok(process) => process,
246        Err(err) => {
247            return Err(match err.kind() {
248                IoErrorKind::NotFound => Error::NoBinary,
249                _ => Error::BinaryIo(bin, err),
250            });
251        }
252    };
253
254    // Write the contents to the xclip process
255    process
256        .stdin
257        .as_mut()
258        .unwrap()
259        .write_all(contents.as_bytes())
260        .map_err(|err| Error::BinaryIo(bin, err))?;
261
262    // Wait for process to exit
263    let status = process.wait().map_err(|err| Error::BinaryIo(bin, err))?;
264    if !status.success() {
265        return Err(Error::BinaryStatus(bin, status.code().unwrap_or(0)));
266    }
267
268    Ok(())
269}
270
271/// Represents X11 binary related error.
272#[derive(Debug)]
273#[non_exhaustive]
274pub enum Error {
275    /// The `xclip` or `xsel` binary could not be found on the system, required for clipboard support.
276    NoBinary,
277
278    /// An error occurred while using `xclip` or `xsel` to manage the clipboard contents.
279    /// This problem probably occurred when starting, or while piping the clipboard contents
280    /// from/to the process.
281    BinaryIo(&'static str, IoError),
282
283    /// `xclip` or `xsel` unexpectetly exited with a non-successful status code.
284    BinaryStatus(&'static str, i32),
285
286    /// The clipboard contents could not be parsed as valid UTF-8.
287    NoUtf8(FromUtf8Error),
288}
289
290impl fmt::Display for Error {
291    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
292        match self {
293            Error::NoBinary => write!(
294                f,
295                "Could not find xclip or xsel binary for clipboard support"
296            ),
297            Error::BinaryIo(cmd, err) => {
298                write!(f, "Failed to access clipboard using {}: {}", cmd, err)
299            }
300            Error::BinaryStatus(cmd, code) => write!(
301                f,
302                "Failed to use clipboard, {} exited with status code {}",
303                cmd, code
304            ),
305            Error::NoUtf8(err) => write!(
306                f,
307                "Failed to parse clipboard contents as valid UTF-8: {}",
308                err
309            ),
310        }
311    }
312}
313
314impl StdError for Error {
315    fn source(&self) -> Option<&(dyn StdError + 'static)> {
316        match self {
317            Error::BinaryIo(_, err) => Some(err),
318            Error::NoUtf8(err) => Some(err),
319            _ => None,
320        }
321    }
322}