arboard/platform/linux/
mod.rs

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