Skip to main content

arboard/platform/linux/
wayland.rs

1use std::{
2	borrow::Cow,
3	io::Read,
4	path::{Path, PathBuf},
5};
6
7use wl_clipboard_rs::{
8	copy::{self, Error as CopyError, MimeSource, MimeType, Options, ServeRequests, Source},
9	paste::{self, get_contents, Error as PasteError, Seat},
10	utils::is_primary_selection_supported,
11};
12
13#[cfg(feature = "image-data")]
14use super::encode_as_png;
15use super::{
16	into_unknown, paths_from_uri_list, paths_to_uri_list, LinuxClipboardKind, WaitConfig,
17	KDE_EXCLUSION_HINT, KDE_EXCLUSION_MIME,
18};
19use crate::common::Error;
20#[cfg(feature = "image-data")]
21use crate::common::ImageData;
22
23#[cfg(feature = "image-data")]
24const MIME_PNG: &str = "image/png";
25
26const MIME_URI: &str = "text/uri-list";
27
28pub(crate) struct Clipboard {}
29
30impl TryInto<copy::ClipboardType> for LinuxClipboardKind {
31	type Error = Error;
32
33	fn try_into(self) -> Result<copy::ClipboardType, Self::Error> {
34		match self {
35			LinuxClipboardKind::Clipboard => Ok(copy::ClipboardType::Regular),
36			LinuxClipboardKind::Primary => Ok(copy::ClipboardType::Primary),
37			LinuxClipboardKind::Secondary => Err(Error::ClipboardNotSupported),
38		}
39	}
40}
41
42impl TryInto<paste::ClipboardType> for LinuxClipboardKind {
43	type Error = Error;
44
45	fn try_into(self) -> Result<paste::ClipboardType, Self::Error> {
46		match self {
47			LinuxClipboardKind::Clipboard => Ok(paste::ClipboardType::Regular),
48			LinuxClipboardKind::Primary => Ok(paste::ClipboardType::Primary),
49			LinuxClipboardKind::Secondary => Err(Error::ClipboardNotSupported),
50		}
51	}
52}
53
54fn add_clipboard_exclusions(exclude_from_history: bool, sources: &mut Vec<MimeSource>) {
55	if exclude_from_history {
56		sources.push(MimeSource {
57			source: Source::Bytes(Box::from(KDE_EXCLUSION_HINT)),
58			mime_type: MimeType::Specific(String::from(KDE_EXCLUSION_MIME)),
59		});
60	}
61}
62
63fn options(wait: WaitConfig, selection: LinuxClipboardKind) -> Result<Options, Error> {
64	let mut opts = Options::new();
65	opts.foreground(matches!(wait, WaitConfig::Forever));
66	if matches!(wait, WaitConfig::Once) {
67		opts.serve_requests(ServeRequests::Only(1));
68	}
69	opts.clipboard(selection.try_into()?);
70	Ok(opts)
71}
72
73fn handle_copy_error(e: copy::Error) -> Error {
74	match e {
75		CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported,
76		other => into_unknown(other),
77	}
78}
79
80fn handle_paste_error(e: paste::Error) -> Error {
81	match e {
82		PasteError::PrimarySelectionUnsupported => Error::ClipboardNotSupported,
83		other => into_unknown(other),
84	}
85}
86
87fn handle_clipboard_read<T, F: FnOnce(Vec<u8>) -> Result<T, Error>>(
88	selection: LinuxClipboardKind,
89	mime: paste::MimeType,
90	into_requested_data: F,
91) -> Result<T, Error> {
92	let result = get_contents(selection.try_into()?, Seat::Unspecified, mime);
93	match result {
94		Ok((mut pipe, _)) => {
95			let mut buffer = vec![];
96			pipe.read_to_end(&mut buffer).map_err(into_unknown)?;
97			into_requested_data(buffer)
98		}
99		Err(PasteError::ClipboardEmpty) | Err(PasteError::NoMimeType) => {
100			Err(Error::ContentNotAvailable)
101		}
102		Err(err) => Err(handle_paste_error(err)),
103	}
104}
105
106impl Clipboard {
107	pub(crate) fn new() -> Result<Self, Error> {
108		// Check if it's possible to communicate with the wayland compositor
109		match is_primary_selection_supported() {
110			// We don't care if the primary clipboard is supported or not, `wl-clipboard-rs` will fail
111			// if not and we don't want to duplicate more of their logic.
112			Ok(_) => Ok(Self {}),
113			Err(e) => Err(into_unknown(e)),
114		}
115	}
116
117	pub(crate) fn clear(&mut self, selection: LinuxClipboardKind) -> Result<(), Error> {
118		let selection = selection.try_into()?;
119		copy::clear(selection, copy::Seat::All).map_err(handle_copy_error)
120	}
121
122	pub(crate) fn get_text(&mut self, selection: LinuxClipboardKind) -> Result<String, Error> {
123		handle_clipboard_read(selection, paste::MimeType::Text, |contents| {
124			String::from_utf8(contents).map_err(|_| Error::ConversionFailure)
125		})
126	}
127
128	pub(crate) fn set_text(
129		&self,
130		text: Cow<'_, str>,
131		selection: LinuxClipboardKind,
132		wait: WaitConfig,
133		exclude_from_history: bool,
134	) -> Result<(), Error> {
135		let opts = options(wait, selection)?;
136		let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
137
138		sources.push(MimeSource {
139			source: Source::Bytes(text.into_owned().into_bytes().into_boxed_slice()),
140			mime_type: MimeType::Text,
141		});
142
143		add_clipboard_exclusions(exclude_from_history, &mut sources);
144
145		opts.copy_multi(sources).map_err(handle_copy_error)
146	}
147
148	pub(crate) fn get_html(&mut self, selection: LinuxClipboardKind) -> Result<String, Error> {
149		handle_clipboard_read(selection, paste::MimeType::Specific("text/html"), |contents| {
150			String::from_utf8(contents).map_err(|_| Error::ConversionFailure)
151		})
152	}
153
154	pub(crate) fn set_html(
155		&self,
156		html: Cow<'_, str>,
157		alt: Option<Cow<'_, str>>,
158		selection: LinuxClipboardKind,
159		wait: WaitConfig,
160		exclude_from_history: bool,
161	) -> Result<(), Error> {
162		let opts = options(wait, selection)?;
163		let mut sources = {
164			let cap = [true, alt.is_some(), exclude_from_history]
165				.map(|v| usize::from(v as u8))
166				.iter()
167				.sum();
168			Vec::with_capacity(cap)
169		};
170
171		if let Some(alt) = alt {
172			sources.push(MimeSource {
173				source: Source::Bytes(alt.into_owned().into_bytes().into_boxed_slice()),
174				mime_type: MimeType::Text,
175			});
176		}
177
178		sources.push(MimeSource {
179			source: Source::Bytes(html.into_owned().into_bytes().into_boxed_slice()),
180			mime_type: MimeType::Specific(String::from("text/html")),
181		});
182
183		add_clipboard_exclusions(exclude_from_history, &mut sources);
184
185		opts.copy_multi(sources).map_err(handle_copy_error)
186	}
187
188	#[cfg(feature = "image-data")]
189	pub(crate) fn get_image(
190		&mut self,
191		selection: LinuxClipboardKind,
192	) -> Result<ImageData<'static>, Error> {
193		use std::io::Cursor;
194
195		handle_clipboard_read(selection, paste::MimeType::Specific(MIME_PNG), |buffer| {
196			let image = image::io::Reader::new(Cursor::new(buffer))
197				.with_guessed_format()
198				.map_err(|_| Error::ConversionFailure)?
199				.decode()
200				.map_err(|_| Error::ConversionFailure)?;
201			let image = image.into_rgba8();
202
203			Ok(ImageData {
204				width: image.width() as usize,
205				height: image.height() as usize,
206				bytes: image.into_raw().into(),
207			})
208		})
209	}
210
211	#[cfg(feature = "image-data")]
212	pub(crate) fn set_image(
213		&mut self,
214		image: ImageData,
215		selection: LinuxClipboardKind,
216		wait: WaitConfig,
217		exclude_from_history: bool,
218	) -> Result<(), Error> {
219		let opts = options(wait, selection)?;
220		let image = encode_as_png(&image)?;
221
222		let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
223
224		sources.push(MimeSource {
225			source: Source::Bytes(image.into()),
226			mime_type: MimeType::Specific(String::from(MIME_PNG)),
227		});
228
229		add_clipboard_exclusions(exclude_from_history, &mut sources);
230
231		opts.copy_multi(sources).map_err(handle_copy_error)
232	}
233
234	pub(crate) fn get_file_list(
235		&mut self,
236		selection: LinuxClipboardKind,
237	) -> Result<Vec<PathBuf>, Error> {
238		handle_clipboard_read(selection, paste::MimeType::Specific(MIME_URI), |contents| {
239			Ok(paths_from_uri_list(contents))
240		})
241	}
242
243	pub(crate) fn set_file_list(
244		&self,
245		file_list: &[impl AsRef<Path>],
246		selection: LinuxClipboardKind,
247		wait: WaitConfig,
248		exclude_from_history: bool,
249	) -> Result<(), Error> {
250		let opts = options(wait, selection)?;
251		let files = paths_to_uri_list(file_list)?;
252		let mut sources = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
253		sources.push(MimeSource {
254			source: Source::Bytes(files.into_bytes().into_boxed_slice()),
255			mime_type: MimeType::Specific(String::from(MIME_URI)),
256		});
257
258		add_clipboard_exclusions(exclude_from_history, &mut sources);
259
260		opts.copy_multi(sources).map_err(handle_copy_error)
261	}
262}