arboard/
lib.rs

1/*
2SPDX-License-Identifier: Apache-2.0 OR MIT
3
4Copyright 2022 The Arboard contributors
5
6The project to which this file belongs is licensed under either of
7the Apache 2.0 or the MIT license at the licensee's choice. The terms
8and conditions of the chosen license apply to this file.
9*/
10#![warn(unreachable_pub)]
11
12mod common;
13use std::{borrow::Cow, path::PathBuf};
14
15pub use common::Error;
16#[cfg(feature = "image-data")]
17pub use common::ImageData;
18
19mod platform;
20
21#[cfg(all(
22	unix,
23	not(any(target_os = "macos", target_os = "android", target_os = "emscripten")),
24))]
25pub use platform::{ClearExtLinux, GetExtLinux, LinuxClipboardKind, SetExtLinux};
26
27#[cfg(windows)]
28pub use platform::SetExtWindows;
29
30#[cfg(target_os = "macos")]
31pub use platform::SetExtApple;
32
33/// The OS independent struct for accessing the clipboard.
34///
35/// Any number of `Clipboard` instances are allowed to exist at a single point in time. Note however
36/// that all `Clipboard`s must be 'dropped' before the program exits. In most scenarios this happens
37/// automatically but there are frameworks (for example, `winit`) that take over the execution
38/// and where the objects don't get dropped when the application exits. In these cases you have to
39/// make sure the object is dropped by taking ownership of it in a confined scope when detecting
40/// that your application is about to quit.
41///
42/// It is also valid to have these multiple `Clipboards` on separate threads at once but note that
43/// executing multiple clipboard operations in parallel might fail with a `ClipboardOccupied` error.
44///
45/// # Platform-specific behavior
46///
47/// `arboard` does its best to abstract over different platforms, but sometimes the platform-specific
48/// behavior leaks through unsolvably. These differences, depending on which platforms are being targeted,
49/// may affect your app's clipboard architecture (ex, opening and closing a [`Clipboard`] every time
50/// or keeping one open in some application/global state).
51///
52/// ## Linux
53///
54/// Using either Wayland and X11, the clipboard and its content is "hosted" inside of the application
55/// that last put data onto it. This means that when the last `Clipboard` instance is dropped, the contents
56/// may become unavailable to other apps. See [SetExtLinux] for more details.
57///
58/// ## Windows
59///
60/// The clipboard on Windows is a global object, which may only be opened on one thread at once.
61/// This means that `arboard` only truly opens the clipboard during each operation to prevent
62/// multiple `Clipboard`s from existing at once.
63///
64/// This means that attempting operations in parallel has a high likelihood to return an error or
65/// deadlock. As such, it is recommended to avoid creating/operating clipboard objects on >1 thread.
66#[allow(rustdoc::broken_intra_doc_links)]
67pub struct Clipboard {
68	pub(crate) platform: platform::Clipboard,
69}
70
71impl Clipboard {
72	/// Creates an instance of the clipboard.
73	///
74	/// # Errors
75	///
76	/// On some platforms or desktop environments, an error can be returned if clipboards are not
77	/// supported. This may be retried.
78	pub fn new() -> Result<Self, Error> {
79		Ok(Clipboard { platform: platform::Clipboard::new()? })
80	}
81
82	/// Fetches UTF-8 text from the clipboard and returns it.
83	///
84	/// # Errors
85	///
86	/// Returns error if clipboard is empty or contents are not UTF-8 text.
87	pub fn get_text(&mut self) -> Result<String, Error> {
88		self.get().text()
89	}
90
91	/// Places the text onto the clipboard. Any valid UTF-8 string is accepted.
92	///
93	/// # Errors
94	///
95	/// Returns error if `text` failed to be stored on the clipboard.
96	pub fn set_text<'a, T: Into<Cow<'a, str>>>(&mut self, text: T) -> Result<(), Error> {
97		self.set().text(text)
98	}
99
100	/// Places the HTML as well as a plain-text alternative onto the clipboard.
101	///
102	/// Any valid UTF-8 string is accepted.
103	///
104	/// # Errors
105	///
106	/// Returns error if both `html` and `alt_text` failed to be stored on the clipboard.
107	pub fn set_html<'a, T: Into<Cow<'a, str>>>(
108		&mut self,
109		html: T,
110		alt_text: Option<T>,
111	) -> Result<(), Error> {
112		self.set().html(html, alt_text)
113	}
114
115	/// Fetches image data from the clipboard, and returns the decoded pixels.
116	///
117	/// Any image data placed on the clipboard with `set_image` will be possible read back, using
118	/// this function. However it's of not guaranteed that an image placed on the clipboard by any
119	/// other application will be of a supported format.
120	///
121	/// # Errors
122	///
123	/// Returns error if clipboard is empty, contents are not an image, or the contents cannot be
124	/// converted to an appropriate format and stored in the [`ImageData`] type.
125	#[cfg(feature = "image-data")]
126	pub fn get_image(&mut self) -> Result<ImageData<'static>, Error> {
127		self.get().image()
128	}
129
130	/// Places an image to the clipboard.
131	///
132	/// The chosen output format, depending on the platform is the following:
133	///
134	/// - On macOS: `NSImage` object
135	/// - On Linux: PNG, under the atom `image/png`
136	/// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP`
137	///
138	/// # Errors
139	///
140	/// Returns error if `image` cannot be converted to an appropriate format or if it failed to be
141	/// stored on the clipboard.
142	#[cfg(feature = "image-data")]
143	pub fn set_image(&mut self, image: ImageData) -> Result<(), Error> {
144		self.set().image(image)
145	}
146
147	/// Clears any contents that may be present from the platform's default clipboard,
148	/// regardless of the format of the data.
149	///
150	/// # Errors
151	///
152	/// Returns error on Windows or Linux if clipboard cannot be cleared.
153	pub fn clear(&mut self) -> Result<(), Error> {
154		self.clear_with().default()
155	}
156
157	/// Begins a "clear" option to remove data from the clipboard.
158	pub fn clear_with(&mut self) -> Clear<'_> {
159		Clear { platform: platform::Clear::new(&mut self.platform) }
160	}
161
162	/// Begins a "get" operation to retrieve data from the clipboard.
163	pub fn get(&mut self) -> Get<'_> {
164		Get { platform: platform::Get::new(&mut self.platform) }
165	}
166
167	/// Begins a "set" operation to set the clipboard's contents.
168	pub fn set(&mut self) -> Set<'_> {
169		Set { platform: platform::Set::new(&mut self.platform) }
170	}
171}
172
173/// A builder for an operation that gets a value from the clipboard.
174#[must_use]
175pub struct Get<'clipboard> {
176	pub(crate) platform: platform::Get<'clipboard>,
177}
178
179impl Get<'_> {
180	/// Completes the "get" operation by fetching UTF-8 text from the clipboard.
181	pub fn text(self) -> Result<String, Error> {
182		self.platform.text()
183	}
184
185	/// Completes the "get" operation by fetching image data from the clipboard and returning the
186	/// decoded pixels.
187	///
188	/// Any image data placed on the clipboard with `set_image` will be possible read back, using
189	/// this function. However it's of not guaranteed that an image placed on the clipboard by any
190	/// other application will be of a supported format.
191	#[cfg(feature = "image-data")]
192	pub fn image(self) -> Result<ImageData<'static>, Error> {
193		self.platform.image()
194	}
195
196	/// Completes the "get" operation by fetching HTML from the clipboard.
197	pub fn html(self) -> Result<String, Error> {
198		self.platform.html()
199	}
200
201	/// Completes the "get" operation by fetching a list of file paths from the clipboard.
202	pub fn file_list(self) -> Result<Vec<PathBuf>, Error> {
203		self.platform.file_list()
204	}
205}
206
207/// A builder for an operation that sets a value to the clipboard.
208#[must_use]
209pub struct Set<'clipboard> {
210	pub(crate) platform: platform::Set<'clipboard>,
211}
212
213impl Set<'_> {
214	/// Completes the "set" operation by placing text onto the clipboard. Any valid UTF-8 string
215	/// is accepted.
216	pub fn text<'a, T: Into<Cow<'a, str>>>(self, text: T) -> Result<(), Error> {
217		let text = text.into();
218		self.platform.text(text)
219	}
220
221	/// Completes the "set" operation by placing HTML as well as a plain-text alternative onto the
222	/// clipboard.
223	///
224	/// Any valid UTF-8 string is accepted.
225	pub fn html<'a, T: Into<Cow<'a, str>>>(
226		self,
227		html: T,
228		alt_text: Option<T>,
229	) -> Result<(), Error> {
230		let html = html.into();
231		let alt_text = alt_text.map(|e| e.into());
232		self.platform.html(html, alt_text)
233	}
234
235	/// Completes the "set" operation by placing an image onto the clipboard.
236	///
237	/// The chosen output format, depending on the platform is the following:
238	///
239	/// - On macOS: `NSImage` object
240	/// - On Linux: PNG, under the atom `image/png`
241	/// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP`
242	#[cfg(feature = "image-data")]
243	pub fn image(self, image: ImageData) -> Result<(), Error> {
244		self.platform.image(image)
245	}
246}
247
248/// A builder for an operation that clears the data from the clipboard.
249#[must_use]
250pub struct Clear<'clipboard> {
251	pub(crate) platform: platform::Clear<'clipboard>,
252}
253
254impl Clear<'_> {
255	/// Completes the "clear" operation by deleting any existing clipboard data,
256	/// regardless of the format.
257	pub fn default(self) -> Result<(), Error> {
258		self.platform.clear()
259	}
260}
261
262/// All tests grouped in one because the windows clipboard cannot be open on
263/// multiple threads at once.
264#[cfg(test)]
265mod tests {
266	use super::*;
267	use std::{sync::Arc, thread, time::Duration};
268
269	#[test]
270	fn all_tests() {
271		let _ = env_logger::builder().is_test(true).try_init();
272		{
273			let mut ctx = Clipboard::new().unwrap();
274			let text = "some string";
275			ctx.set_text(text).unwrap();
276			assert_eq!(ctx.get_text().unwrap(), text);
277
278			// We also need to check that the content persists after the drop; this is
279			// especially important on X11
280			drop(ctx);
281
282			// Give any external mechanism a generous amount of time to take over
283			// responsibility for the clipboard, in case that happens asynchronously
284			// (it appears that this is the case on X11 plus Mutter 3.34+, see #4)
285			thread::sleep(Duration::from_millis(300));
286
287			let mut ctx = Clipboard::new().unwrap();
288			assert_eq!(ctx.get_text().unwrap(), text);
289		}
290		{
291			let mut ctx = Clipboard::new().unwrap();
292			let text = "Some utf8: 🤓 ∑φ(n)<ε 🐔";
293			ctx.set_text(text).unwrap();
294			assert_eq!(ctx.get_text().unwrap(), text);
295		}
296		{
297			let mut ctx = Clipboard::new().unwrap();
298			let text = "hello world";
299
300			ctx.set_text(text).unwrap();
301			assert_eq!(ctx.get_text().unwrap(), text);
302
303			ctx.clear().unwrap();
304
305			match ctx.get_text() {
306				Ok(text) => assert!(text.is_empty()),
307				Err(Error::ContentNotAvailable) => {}
308				Err(e) => panic!("unexpected error: {e}"),
309			};
310
311			// confirm it is OK to clear when already empty.
312			ctx.clear().unwrap();
313		}
314		{
315			let mut ctx = Clipboard::new().unwrap();
316			let html = "<b>hello</b> <i>world</i>!";
317
318			ctx.set_html(html, None).unwrap();
319
320			match ctx.get_text() {
321				Ok(text) => assert!(text.is_empty()),
322				Err(Error::ContentNotAvailable) => {}
323				Err(e) => panic!("unexpected error: {e}"),
324			};
325		}
326		{
327			let mut ctx = Clipboard::new().unwrap();
328
329			let html = "<b>hello</b> <i>world</i>!";
330			let alt_text = "hello world!";
331
332			ctx.set_html(html, Some(alt_text)).unwrap();
333			assert_eq!(ctx.get_text().unwrap(), alt_text);
334		}
335		{
336			let mut ctx = Clipboard::new().unwrap();
337
338			let html = "<b>hello</b> <i>world</i>!";
339
340			ctx.set().html(html, None).unwrap();
341
342			if cfg!(target_os = "macos") {
343				// Copying HTML on macOS adds wrapper content to work around
344				// historical platform bugs. We control this wrapper, so we are
345				// able to check that the full user data still appears and at what
346				// position in the final copy contents.
347				let content = ctx.get().html().unwrap();
348				assert!(content.ends_with(&format!("{html}</body></html>")));
349			} else {
350				assert_eq!(ctx.get().html().unwrap(), html);
351			}
352		}
353		#[cfg(feature = "image-data")]
354		{
355			let mut ctx = Clipboard::new().unwrap();
356			#[rustfmt::skip]
357			let bytes = [
358				255, 100, 100, 255,
359				100, 255, 100, 100,
360				100, 100, 255, 100,
361				0, 0, 0, 255,
362			];
363			let img_data = ImageData { width: 2, height: 2, bytes: bytes.as_ref().into() };
364
365			// Make sure that setting one format overwrites the other.
366			ctx.set_image(img_data.clone()).unwrap();
367			assert!(matches!(ctx.get_text(), Err(Error::ContentNotAvailable)));
368
369			ctx.set_text("clipboard test").unwrap();
370			assert!(matches!(ctx.get_image(), Err(Error::ContentNotAvailable)));
371
372			// Test if we get the same image that we put onto the clipboard
373			ctx.set_image(img_data.clone()).unwrap();
374			let got = ctx.get_image().unwrap();
375			assert_eq!(img_data.bytes, got.bytes);
376
377			#[rustfmt::skip]
378			let big_bytes = vec![
379				255, 100, 100, 255,
380				100, 255, 100, 100,
381				100, 100, 255, 100,
382
383				0, 1, 2, 255,
384				0, 1, 2, 255,
385				0, 1, 2, 255,
386			];
387			let bytes_cloned = big_bytes.clone();
388			let big_img_data = ImageData { width: 3, height: 2, bytes: big_bytes.into() };
389			ctx.set_image(big_img_data).unwrap();
390			let got = ctx.get_image().unwrap();
391			assert_eq!(bytes_cloned.as_slice(), got.bytes.as_ref());
392		}
393		#[cfg(all(
394			unix,
395			not(any(target_os = "macos", target_os = "android", target_os = "emscripten")),
396		))]
397		{
398			use crate::{LinuxClipboardKind, SetExtLinux};
399			use std::sync::atomic::{self, AtomicBool};
400
401			let mut ctx = Clipboard::new().unwrap();
402
403			const TEXT1: &str = "I'm a little teapot,";
404			const TEXT2: &str = "short and stout,";
405			const TEXT3: &str = "here is my handle";
406
407			ctx.set().clipboard(LinuxClipboardKind::Clipboard).text(TEXT1.to_string()).unwrap();
408
409			ctx.set().clipboard(LinuxClipboardKind::Primary).text(TEXT2.to_string()).unwrap();
410
411			// The secondary clipboard is not available under wayland
412			if !cfg!(feature = "wayland-data-control")
413				|| std::env::var_os("WAYLAND_DISPLAY").is_none()
414			{
415				ctx.set().clipboard(LinuxClipboardKind::Secondary).text(TEXT3.to_string()).unwrap();
416			}
417
418			assert_eq!(TEXT1, &ctx.get().clipboard(LinuxClipboardKind::Clipboard).text().unwrap());
419
420			assert_eq!(TEXT2, &ctx.get().clipboard(LinuxClipboardKind::Primary).text().unwrap());
421
422			// The secondary clipboard is not available under wayland
423			if !cfg!(feature = "wayland-data-control")
424				|| std::env::var_os("WAYLAND_DISPLAY").is_none()
425			{
426				assert_eq!(
427					TEXT3,
428					&ctx.get().clipboard(LinuxClipboardKind::Secondary).text().unwrap()
429				);
430			}
431
432			let was_replaced = Arc::new(AtomicBool::new(false));
433
434			let setter = thread::spawn({
435				let was_replaced = was_replaced.clone();
436				move || {
437					thread::sleep(Duration::from_millis(100));
438					let mut ctx = Clipboard::new().unwrap();
439					ctx.set_text("replacement text".to_owned()).unwrap();
440					was_replaced.store(true, atomic::Ordering::Release);
441				}
442			});
443
444			ctx.set().wait().text("initial text".to_owned()).unwrap();
445
446			assert!(was_replaced.load(atomic::Ordering::Acquire));
447
448			setter.join().unwrap();
449		}
450	}
451
452	// The cross-platform abstraction should allow any number of clipboards
453	// to be open at once without issue, as documented under [Clipboard].
454	#[test]
455	fn multiple_clipboards_at_once() {
456		const THREAD_COUNT: usize = 100;
457
458		let mut handles = Vec::with_capacity(THREAD_COUNT);
459		let barrier = Arc::new(std::sync::Barrier::new(THREAD_COUNT));
460
461		for _ in 0..THREAD_COUNT {
462			let barrier = barrier.clone();
463			handles.push(thread::spawn(move || {
464				// As long as the clipboard isn't used multiple times at once, multiple instances
465				// are perfectly fine.
466				let _ctx = Clipboard::new().unwrap();
467
468				thread::sleep(Duration::from_millis(10));
469
470				barrier.wait();
471			}));
472		}
473
474		for thread_handle in handles {
475			thread_handle.join().unwrap();
476		}
477	}
478
479	#[test]
480	fn clipboard_trait_consistently() {
481		fn assert_send_sync<T: Send + Sync + 'static>() {}
482
483		assert_send_sync::<Clipboard>();
484		assert!(std::mem::needs_drop::<Clipboard>());
485	}
486}