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