Skip to main content

arboard/platform/linux/
mod.rs

1use std::{
2	borrow::Cow,
3	os::unix::ffi::OsStrExt,
4	path::{Path, PathBuf},
5	time::Instant,
6};
7
8#[cfg(feature = "wayland-data-control")]
9use log::{trace, warn};
10use percent_encoding::{percent_decode, percent_encode, AsciiSet, CONTROLS};
11
12#[cfg(feature = "image-data")]
13use crate::ImageData;
14use crate::{common::private, Error};
15
16// Magic strings used in `Set::exclude_from_history()` on linux
17const KDE_EXCLUSION_MIME: &str = "x-kde-passwordManagerHint";
18const KDE_EXCLUSION_HINT: &[u8] = b"secret";
19
20mod x11;
21
22#[cfg(feature = "wayland-data-control")]
23mod wayland;
24
25fn into_unknown<E: std::fmt::Display>(error: E) -> Error {
26	Error::Unknown { description: error.to_string() }
27}
28
29#[cfg(feature = "image-data")]
30fn encode_as_png(image: &ImageData) -> Result<Vec<u8>, Error> {
31	use image::ImageEncoder as _;
32
33	if image.bytes.is_empty() || image.width == 0 || image.height == 0 {
34		return Err(Error::ConversionFailure);
35	}
36
37	let mut png_bytes = Vec::new();
38	let encoder = image::codecs::png::PngEncoder::new(&mut png_bytes);
39	encoder
40		.write_image(
41			image.bytes.as_ref(),
42			image.width as u32,
43			image.height as u32,
44			image::ExtendedColorType::Rgba8,
45		)
46		.map_err(|_| Error::ConversionFailure)?;
47
48	Ok(png_bytes)
49}
50
51fn paths_from_uri_list(uri_list: Vec<u8>) -> Vec<PathBuf> {
52	uri_list
53		.split(|char| *char == b'\n')
54		.map(|line| line.strip_suffix(b"\r").unwrap_or(line))
55		.filter_map(|line| line.strip_prefix(b"file://"))
56		.filter_map(|s| percent_decode(s).decode_utf8().ok())
57		.map(|decoded| PathBuf::from(decoded.as_ref()))
58		.collect()
59}
60
61fn paths_to_uri_list(file_list: &[impl AsRef<Path>]) -> Result<String, Error> {
62	// The characters that require encoding, which includes £ and € but they can't be added to the set.
63	const ASCII_SET: &AsciiSet = &CONTROLS
64		.add(b'#')
65		.add(b';')
66		.add(b'?')
67		.add(b'[')
68		.add(b']')
69		.add(b' ')
70		.add(b'\"')
71		.add(b'%')
72		.add(b'<')
73		.add(b'>')
74		.add(b'\\')
75		.add(b'^')
76		.add(b'`')
77		.add(b'{')
78		.add(b'|')
79		.add(b'}');
80
81	file_list
82		.iter()
83		.filter_map(|path| {
84			path.as_ref().canonicalize().ok().map(|path| {
85				format!("file://{}", percent_encode(path.as_os_str().as_bytes(), ASCII_SET))
86			})
87		})
88		// Use CRLF ("\r\n") per RFC 2045 section 2.10:
89		// https://www.rfc-editor.org/rfc/rfc2045#section-2.10
90		// Ensures compliance and future-proofing.
91		.reduce(|uri_list, uri| uri_list + "\r\n" + &uri)
92		.ok_or(Error::ConversionFailure)
93}
94
95/// Clipboard selection
96///
97/// Linux has a concept of clipboard "selections" which tend to be used in different contexts. This
98/// enum provides a way to get/set to a specific clipboard (the default
99/// [`Clipboard`](Self::Clipboard) being used for the common platform API). You can choose which
100/// clipboard to use with [`GetExtLinux::clipboard`] and [`SetExtLinux::clipboard`].
101///
102/// See <https://specifications.freedesktop.org/clipboards-spec/clipboards-0.1.txt> for a better
103/// description of the different clipboards.
104#[derive(Copy, Clone, Debug)]
105pub enum LinuxClipboardKind {
106	/// Typically used selection for explicit cut/copy/paste actions (ie. windows/macos like
107	/// clipboard behavior)
108	Clipboard,
109
110	/// Typically used for mouse selections and/or currently selected text. Accessible via middle
111	/// mouse click.
112	///
113	/// *On Wayland, this may not be available for all systems (requires a compositor supporting
114	/// version 2 or above) and operations using this will return an error if unsupported.*
115	Primary,
116
117	/// The secondary clipboard is rarely used but theoretically available on X11.
118	///
119	/// *On Wayland, this is not be available and operations using this variant will return an
120	/// error.*
121	Secondary,
122}
123
124pub(crate) enum Clipboard {
125	X11(x11::Clipboard),
126
127	#[cfg(feature = "wayland-data-control")]
128	WlDataControl(wayland::Clipboard),
129}
130
131impl Clipboard {
132	pub(crate) fn new() -> Result<Self, Error> {
133		#[cfg(feature = "wayland-data-control")]
134		{
135			if std::env::var_os("WAYLAND_DISPLAY").is_some() {
136				// Wayland is available
137				match wayland::Clipboard::new() {
138					Ok(clipboard) => {
139						trace!("Successfully initialized the Wayland data control clipboard.");
140						return Ok(Self::WlDataControl(clipboard));
141					}
142					Err(e) => warn!(
143						"Tried to initialize the wayland data control protocol clipboard, but failed. Falling back to the X11 clipboard protocol. The error was: {}",
144						e
145					),
146				}
147			}
148		}
149		Ok(Self::X11(x11::Clipboard::new()?))
150	}
151}
152
153pub(crate) struct Get<'clipboard> {
154	clipboard: &'clipboard mut Clipboard,
155	selection: LinuxClipboardKind,
156}
157
158impl<'clipboard> Get<'clipboard> {
159	pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
160		Self { clipboard, selection: LinuxClipboardKind::Clipboard }
161	}
162
163	pub(crate) fn text(self) -> Result<String, Error> {
164		match self.clipboard {
165			Clipboard::X11(clipboard) => clipboard.get_text(self.selection),
166			#[cfg(feature = "wayland-data-control")]
167			Clipboard::WlDataControl(clipboard) => clipboard.get_text(self.selection),
168		}
169	}
170
171	#[cfg(feature = "image-data")]
172	pub(crate) fn image(self) -> Result<ImageData<'static>, Error> {
173		match self.clipboard {
174			Clipboard::X11(clipboard) => clipboard.get_image(self.selection),
175			#[cfg(feature = "wayland-data-control")]
176			Clipboard::WlDataControl(clipboard) => clipboard.get_image(self.selection),
177		}
178	}
179
180	pub(crate) fn html(self) -> Result<String, Error> {
181		match self.clipboard {
182			Clipboard::X11(clipboard) => clipboard.get_html(self.selection),
183			#[cfg(feature = "wayland-data-control")]
184			Clipboard::WlDataControl(clipboard) => clipboard.get_html(self.selection),
185		}
186	}
187
188	pub(crate) fn file_list(self) -> Result<Vec<PathBuf>, Error> {
189		match self.clipboard {
190			Clipboard::X11(clipboard) => clipboard.get_file_list(self.selection),
191			#[cfg(feature = "wayland-data-control")]
192			Clipboard::WlDataControl(clipboard) => clipboard.get_file_list(self.selection),
193		}
194	}
195}
196
197/// Linux-specific extensions to the [`Get`](super::Get) builder.
198pub trait GetExtLinux: private::Sealed {
199	/// Sets the clipboard the operation will retrieve data from.
200	///
201	/// If wayland support is enabled and available, attempting to use the Secondary clipboard will
202	/// return an error.
203	fn clipboard(self, selection: LinuxClipboardKind) -> Self;
204}
205
206impl GetExtLinux for crate::Get<'_> {
207	fn clipboard(mut self, selection: LinuxClipboardKind) -> Self {
208		self.platform.selection = selection;
209		self
210	}
211}
212
213/// Configuration on how long to wait for a new X11 copy event is emitted.
214#[derive(Default)]
215pub(crate) enum WaitConfig {
216	/// Waits until the given [`Instant`] has reached.
217	Until(Instant),
218
219	/// Waits until the clipboard conents have been retrieved once.
220	Once,
221
222	/// Waits forever until a new event is reached.
223	Forever,
224
225	/// It shouldn't wait.
226	#[default]
227	None,
228}
229
230pub(crate) struct Set<'clipboard> {
231	clipboard: &'clipboard mut Clipboard,
232	wait: WaitConfig,
233	selection: LinuxClipboardKind,
234	exclude_from_history: bool,
235}
236
237impl<'clipboard> Set<'clipboard> {
238	pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
239		Self {
240			clipboard,
241			wait: WaitConfig::default(),
242			selection: LinuxClipboardKind::Clipboard,
243			exclude_from_history: false,
244		}
245	}
246
247	pub(crate) fn text(self, text: Cow<'_, str>) -> Result<(), Error> {
248		match self.clipboard {
249			Clipboard::X11(clipboard) => {
250				clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history)
251			}
252
253			#[cfg(feature = "wayland-data-control")]
254			Clipboard::WlDataControl(clipboard) => {
255				clipboard.set_text(text, self.selection, self.wait, self.exclude_from_history)
256			}
257		}
258	}
259
260	pub(crate) fn html(self, html: Cow<'_, str>, alt: Option<Cow<'_, str>>) -> Result<(), Error> {
261		match self.clipboard {
262			Clipboard::X11(clipboard) => {
263				clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history)
264			}
265
266			#[cfg(feature = "wayland-data-control")]
267			Clipboard::WlDataControl(clipboard) => {
268				clipboard.set_html(html, alt, self.selection, self.wait, self.exclude_from_history)
269			}
270		}
271	}
272
273	#[cfg(feature = "image-data")]
274	pub(crate) fn image(self, image: ImageData<'_>) -> Result<(), Error> {
275		match self.clipboard {
276			Clipboard::X11(clipboard) => {
277				clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history)
278			}
279
280			#[cfg(feature = "wayland-data-control")]
281			Clipboard::WlDataControl(clipboard) => {
282				clipboard.set_image(image, self.selection, self.wait, self.exclude_from_history)
283			}
284		}
285	}
286
287	pub(crate) fn file_list(self, file_list: &[impl AsRef<Path>]) -> Result<(), Error> {
288		match self.clipboard {
289			Clipboard::X11(clipboard) => clipboard.set_file_list(
290				file_list,
291				self.selection,
292				self.wait,
293				self.exclude_from_history,
294			),
295
296			#[cfg(feature = "wayland-data-control")]
297			Clipboard::WlDataControl(clipboard) => clipboard.set_file_list(
298				file_list,
299				self.selection,
300				self.wait,
301				self.exclude_from_history,
302			),
303		}
304	}
305}
306
307/// Linux specific extensions to the [`Set`](super::Set) builder.
308pub trait SetExtLinux: private::Sealed {
309	/// Whether to wait for the clipboard's contents to be replaced after setting it.
310	///
311	/// The Wayland and X11 clipboards work by having the clipboard content being, at any given
312	/// time, "owned" by a single process, and that process is expected to reply to all the requests
313	/// from any other system process that wishes to access the clipboard's contents. As a
314	/// consequence, when that process exits the contents of the clipboard will effectively be
315	/// cleared since there is no longer anyone around to serve requests for it.
316	///
317	/// This poses a problem for short-lived programs that just want to copy to the clipboard and
318	/// then exit, since they don't want to wait until the user happens to copy something else just
319	/// to finish. To resolve that, whenever the user copies something you can offload the actual
320	/// work to a newly-spawned daemon process which will run in the background (potentially
321	/// outliving the current process) and serve all the requests. That process will then
322	/// automatically and silently exit once the user copies something else to their clipboard so it
323	/// doesn't take up too many resources.
324	///
325	/// To support that pattern, this method will not only have the contents of the clipboard be
326	/// set, but will also wait and continue to serve requests until the clipboard is overwritten.
327	/// As long as you don't exit the current process until that method has returned, you can avoid
328	/// all surprising situations where the clipboard's contents seemingly disappear from under your
329	/// feet.
330	///
331	/// See the [daemonize example] for a demo of how you could implement this.
332	///
333	/// [daemonize example]: https://github.com/1Password/arboard/blob/master/examples/daemonize.rs
334	fn wait(self) -> Self;
335
336	/// Whether or not to wait for the clipboard's content to be replaced after setting it. This waits until the
337	/// `deadline` has exceeded.
338	///
339	/// This is useful for short-lived programs so it won't block until new contents on the clipboard
340	/// were added.
341	///
342	/// Note: this is a superset of [`wait()`][SetExtLinux::wait] and will overwrite any state
343	/// that was previously set using it.
344	fn wait_until(self, deadline: Instant) -> Self;
345
346	/// Whether to wait for the clipboard's content to be retrieved once after setting it. This
347	/// waits until the clipboard was retrieved once.
348	///
349	/// Note: this is a superset of [`wait()`][SetExtLinux::wait] and will overwrite any state
350	/// that was previously set using it.
351	fn wait_once(self) -> Self;
352
353	/// Sets the clipboard the operation will store its data to.
354	///
355	/// If wayland support is enabled and available, attempting to use the Secondary clipboard will
356	/// return an error.
357	///
358	/// # Examples
359	///
360	/// ```
361	/// use arboard::{Clipboard, SetExtLinux, LinuxClipboardKind};
362	/// # fn main() -> Result<(), arboard::Error> {
363	/// let mut ctx = Clipboard::new()?;
364	///
365	/// let clipboard = "This goes in the traditional (ex. Copy & Paste) clipboard.";
366	/// ctx.set().clipboard(LinuxClipboardKind::Clipboard).text(clipboard.to_owned())?;
367	///
368	/// let primary = "This goes in the primary keyboard. It's typically used via middle mouse click.";
369	/// ctx.set().clipboard(LinuxClipboardKind::Primary).text(primary.to_owned())?;
370	/// # Ok(())
371	/// # }
372	/// ```
373	fn clipboard(self, selection: LinuxClipboardKind) -> Self;
374
375	/// Excludes the data which will be set on the clipboard from being added to
376	/// the desktop clipboard managers' histories by adding the MIME-Type `x-kde-passwordMangagerHint`
377	/// to the clipboard's selection data.
378	///
379	/// This is the most widely adopted convention on Linux.
380	fn exclude_from_history(self) -> Self;
381}
382
383impl SetExtLinux for crate::Set<'_> {
384	fn wait(mut self) -> Self {
385		self.platform.wait = WaitConfig::Forever;
386		self
387	}
388
389	fn clipboard(mut self, selection: LinuxClipboardKind) -> Self {
390		self.platform.selection = selection;
391		self
392	}
393
394	fn wait_until(mut self, deadline: Instant) -> Self {
395		self.platform.wait = WaitConfig::Until(deadline);
396		self
397	}
398
399	fn wait_once(mut self) -> Self {
400		self.platform.wait = WaitConfig::Once;
401		self
402	}
403
404	fn exclude_from_history(mut self) -> Self {
405		self.platform.exclude_from_history = true;
406		self
407	}
408}
409
410pub(crate) struct Clear<'clipboard> {
411	clipboard: &'clipboard mut Clipboard,
412}
413
414impl<'clipboard> Clear<'clipboard> {
415	pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
416		Self { clipboard }
417	}
418
419	pub(crate) fn clear(self) -> Result<(), Error> {
420		self.clear_inner(LinuxClipboardKind::Clipboard)
421	}
422
423	fn clear_inner(self, selection: LinuxClipboardKind) -> Result<(), Error> {
424		match self.clipboard {
425			Clipboard::X11(clipboard) => clipboard.clear(selection),
426			#[cfg(feature = "wayland-data-control")]
427			Clipboard::WlDataControl(clipboard) => clipboard.clear(selection),
428		}
429	}
430}
431
432/// Linux specific extensions to the [Clear] builder.
433pub trait ClearExtLinux: private::Sealed {
434	/// Performs the "clear" operation on the selected clipboard.
435	///
436	/// ### Example
437	///
438	/// ```no_run
439	/// # use arboard::{Clipboard, LinuxClipboardKind, ClearExtLinux, Error};
440	/// # fn main() -> Result<(), Error> {
441	/// let mut clipboard = Clipboard::new()?;
442	///
443	/// clipboard
444	///     .clear_with()
445	///     .clipboard(LinuxClipboardKind::Secondary)?;
446	/// # Ok(())
447	/// # }
448	/// ```
449	///
450	/// If wayland support is enabled and available, attempting to use the Secondary clipboard will
451	/// return an error.
452	fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error>;
453}
454
455impl ClearExtLinux for crate::Clear<'_> {
456	fn clipboard(self, selection: LinuxClipboardKind) -> Result<(), Error> {
457		self.platform.clear_inner(selection)
458	}
459}
460
461#[cfg(test)]
462mod tests {
463	use super::*;
464
465	#[test]
466	fn test_decoding_uri_list() {
467		// Test that paths_from_uri_list correctly decodes
468		// differents percent encoded characters
469		let file_list = [
470			"file:///tmp/bar.log",
471			"file:///tmp/test%5C.txt",
472			"file:///tmp/foo%3F.png",
473			"file:///tmp/white%20space.txt",
474		];
475
476		let paths = vec![
477			PathBuf::from("/tmp/bar.log"),
478			PathBuf::from("/tmp/test\\.txt"),
479			PathBuf::from("/tmp/foo?.png"),
480			PathBuf::from("/tmp/white space.txt"),
481		];
482		assert_eq!(paths_from_uri_list(file_list.join("\r\n").into()), paths);
483	}
484}