Skip to main content

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