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