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