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}