1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
/*
SPDX-License-Identifier: Apache-2.0 OR MIT

Copyright 2022 The Arboard contributors

The project to which this file belongs is licensed under either of
the Apache 2.0 or the MIT license at the licensee's choice. The terms
and conditions of the chosen license apply to this file.
*/
#![warn(unreachable_pub)]

mod common;
use std::borrow::Cow;

pub use common::Error;
#[cfg(feature = "image-data")]
pub use common::ImageData;

mod platform;

#[cfg(all(
	unix,
	not(any(target_os = "macos", target_os = "android", target_os = "emscripten")),
))]
pub use platform::{ClearExtLinux, GetExtLinux, LinuxClipboardKind, SetExtLinux};

#[cfg(windows)]
pub use platform::SetExtWindows;

/// The OS independent struct for accessing the clipboard.
///
/// Any number of `Clipboard` instances are allowed to exist at a single point in time. Note however
/// that all `Clipboard`s must be 'dropped' before the program exits. In most scenarios this happens
/// automatically but there are frameworks (for example, `winit`) that take over the execution
/// and where the objects don't get dropped when the application exits. In these cases you have to
/// make sure the object is dropped by taking ownership of it in a confined scope when detecting
/// that your application is about to quit.
///
/// It is also valid to have these multiple `Clipboards` on separate threads at once but note that
/// executing multiple clipboard operations in parallel might fail with a `ClipboardOccupied` error.
///
/// # Platform-specific behavior
///
/// `arboard` does its best to abstract over different platforms, but sometimes the platform-specific
/// behavior leaks through unsolvably. These differences, depending on which platforms are being targeted,
/// may affect your app's clipboard architecture (ex, opening and closing a [`Clipboard`] every time
/// or keeping one open in some application/global state).
///
/// ## Linux
///
/// Using either Wayland and X11, the clipboard and its content is "hosted" inside of the application
/// that last put data onto it. This means that when the last `Clipboard` instance is dropped, the contents
/// may become unavailable to other apps. See [SetExtLinux] for more details.
///
/// ## Windows
///
/// The clipboard on Windows is a global object, which may only be opened on one thread at once.
/// This means that `arboard` only truly opens the clipboard during each operation to prevent
/// multiple `Clipboard`s from existing at once.
///
/// This means that attempting operations in parallel has a high likelihood to return an error or
/// deadlock. As such, it is recommended to avoid creating/operating clipboard objects on >1 thread.
#[allow(rustdoc::broken_intra_doc_links)]
pub struct Clipboard {
	pub(crate) platform: platform::Clipboard,
}

impl Clipboard {
	/// Creates an instance of the clipboard.
	///
	/// # Errors
	///
	/// On some platforms or desktop environments, an error can be returned if clipboards are not
	/// supported. This may be retried.
	pub fn new() -> Result<Self, Error> {
		Ok(Clipboard { platform: platform::Clipboard::new()? })
	}

	/// Fetches UTF-8 text from the clipboard and returns it.
	///
	/// # Errors
	///
	/// Returns error if clipboard is empty or contents are not UTF-8 text.
	pub fn get_text(&mut self) -> Result<String, Error> {
		self.get().text()
	}

	/// Places the text onto the clipboard. Any valid UTF-8 string is accepted.
	///
	/// # Errors
	///
	/// Returns error if `text` failed to be stored on the clipboard.
	pub fn set_text<'a, T: Into<Cow<'a, str>>>(&mut self, text: T) -> Result<(), Error> {
		self.set().text(text)
	}

	/// Places the HTML as well as a plain-text alternative onto the clipboard.
	///
	/// Any valid UTF-8 string is accepted.
	///
	/// # Errors
	///
	/// Returns error if both `html` and `alt_text` failed to be stored on the clipboard.
	pub fn set_html<'a, T: Into<Cow<'a, str>>>(
		&mut self,
		html: T,
		alt_text: Option<T>,
	) -> Result<(), Error> {
		self.set().html(html, alt_text)
	}

	/// Fetches image data from the clipboard, and returns the decoded pixels.
	///
	/// Any image data placed on the clipboard with `set_image` will be possible read back, using
	/// this function. However it's of not guaranteed that an image placed on the clipboard by any
	/// other application will be of a supported format.
	///
	/// # Errors
	///
	/// Returns error if clipboard is empty, contents are not an image, or the contents cannot be
	/// converted to an appropriate format and stored in the [`ImageData`] type.
	#[cfg(feature = "image-data")]
	pub fn get_image(&mut self) -> Result<ImageData<'static>, Error> {
		self.get().image()
	}

	/// Places an image to the clipboard.
	///
	/// The chosen output format, depending on the platform is the following:
	///
	/// - On macOS: `NSImage` object
	/// - On Linux: PNG, under the atom `image/png`
	/// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP`
	///
	/// # Errors
	///
	/// Returns error if `image` cannot be converted to an appropriate format or if it failed to be
	/// stored on the clipboard.
	#[cfg(feature = "image-data")]
	pub fn set_image(&mut self, image: ImageData) -> Result<(), Error> {
		self.set().image(image)
	}

	/// Clears any contents that may be present from the platform's default clipboard,
	/// regardless of the format of the data.
	///
	/// # Errors
	///
	/// Returns error on Windows or Linux if clipboard cannot be cleared.
	pub fn clear(&mut self) -> Result<(), Error> {
		self.clear_with().default()
	}

	/// Begins a "clear" option to remove data from the clipboard.
	pub fn clear_with(&mut self) -> Clear<'_> {
		Clear { platform: platform::Clear::new(&mut self.platform) }
	}

	/// Begins a "get" operation to retrieve data from the clipboard.
	pub fn get(&mut self) -> Get<'_> {
		Get { platform: platform::Get::new(&mut self.platform) }
	}

	/// Begins a "set" operation to set the clipboard's contents.
	pub fn set(&mut self) -> Set<'_> {
		Set { platform: platform::Set::new(&mut self.platform) }
	}
}

/// A builder for an operation that gets a value from the clipboard.
#[must_use]
pub struct Get<'clipboard> {
	pub(crate) platform: platform::Get<'clipboard>,
}

impl Get<'_> {
	/// Completes the "get" operation by fetching UTF-8 text from the clipboard.
	pub fn text(self) -> Result<String, Error> {
		self.platform.text()
	}

	/// Completes the "get" operation by fetching image data from the clipboard and returning the
	/// decoded pixels.
	///
	/// Any image data placed on the clipboard with `set_image` will be possible read back, using
	/// this function. However it's of not guaranteed that an image placed on the clipboard by any
	/// other application will be of a supported format.
	#[cfg(feature = "image-data")]
	pub fn image(self) -> Result<ImageData<'static>, Error> {
		self.platform.image()
	}
}

/// A builder for an operation that sets a value to the clipboard.
#[must_use]
pub struct Set<'clipboard> {
	pub(crate) platform: platform::Set<'clipboard>,
}

impl Set<'_> {
	/// Completes the "set" operation by placing text onto the clipboard. Any valid UTF-8 string
	/// is accepted.
	pub fn text<'a, T: Into<Cow<'a, str>>>(self, text: T) -> Result<(), Error> {
		let text = text.into();
		self.platform.text(text)
	}

	/// Completes the "set" operation by placing HTML as well as a plain-text alternative onto the
	/// clipboard.
	///
	/// Any valid UTF-8 string is accepted.
	pub fn html<'a, T: Into<Cow<'a, str>>>(
		self,
		html: T,
		alt_text: Option<T>,
	) -> Result<(), Error> {
		let html = html.into();
		let alt_text = alt_text.map(|e| e.into());
		self.platform.html(html, alt_text)
	}

	/// Completes the "set" operation by placing an image onto the clipboard.
	///
	/// The chosen output format, depending on the platform is the following:
	///
	/// - On macOS: `NSImage` object
	/// - On Linux: PNG, under the atom `image/png`
	/// - On Windows: In order of priority `CF_DIB` and `CF_BITMAP`
	#[cfg(feature = "image-data")]
	pub fn image(self, image: ImageData) -> Result<(), Error> {
		self.platform.image(image)
	}
}

/// A builder for an operation that clears the data from the clipboard.
#[must_use]
pub struct Clear<'clipboard> {
	pub(crate) platform: platform::Clear<'clipboard>,
}

impl Clear<'_> {
	/// Completes the "clear" operation by deleting any existing clipboard data,
	/// regardless of the format.
	pub fn default(self) -> Result<(), Error> {
		self.platform.clear()
	}
}

/// All tests grouped in one because the windows clipboard cannot be open on
/// multiple threads at once.
#[cfg(test)]
mod tests {
	use super::*;
	use std::{sync::Arc, thread, time::Duration};

	#[test]
	fn all_tests() {
		let _ = env_logger::builder().is_test(true).try_init();
		{
			let mut ctx = Clipboard::new().unwrap();
			let text = "some string";
			ctx.set_text(text).unwrap();
			assert_eq!(ctx.get_text().unwrap(), text);

			// We also need to check that the content persists after the drop; this is
			// especially important on X11
			drop(ctx);

			// Give any external mechanism a generous amount of time to take over
			// responsibility for the clipboard, in case that happens asynchronously
			// (it appears that this is the case on X11 plus Mutter 3.34+, see #4)
			thread::sleep(Duration::from_millis(300));

			let mut ctx = Clipboard::new().unwrap();
			assert_eq!(ctx.get_text().unwrap(), text);
		}
		{
			let mut ctx = Clipboard::new().unwrap();
			let text = "Some utf8: 🤓 ∑φ(n)<ε 🐔";
			ctx.set_text(text).unwrap();
			assert_eq!(ctx.get_text().unwrap(), text);
		}
		{
			let mut ctx = Clipboard::new().unwrap();
			let text = "hello world";

			ctx.set_text(text).unwrap();
			assert_eq!(ctx.get_text().unwrap(), text);

			ctx.clear().unwrap();

			match ctx.get_text() {
				Ok(text) => assert!(text.is_empty()),
				Err(Error::ContentNotAvailable) => {}
				Err(e) => panic!("unexpected error: {}", e),
			};

			// confirm it is OK to clear when already empty.
			ctx.clear().unwrap();
		}
		{
			let mut ctx = Clipboard::new().unwrap();
			let html = "<b>hello</b> <i>world</i>!";

			ctx.set_html(html, None).unwrap();

			match ctx.get_text() {
				Ok(text) => assert!(text.is_empty()),
				Err(Error::ContentNotAvailable) => {}
				Err(e) => panic!("unexpected error: {}", e),
			};
		}
		{
			let mut ctx = Clipboard::new().unwrap();

			let html = "<b>hello</b> <i>world</i>!";
			let alt_text = "hello world!";

			ctx.set_html(html, Some(alt_text)).unwrap();
			assert_eq!(ctx.get_text().unwrap(), alt_text);
		}
		#[cfg(feature = "image-data")]
		{
			let mut ctx = Clipboard::new().unwrap();
			#[rustfmt::skip]
			let bytes = [
				255, 100, 100, 255,
				100, 255, 100, 100,
				100, 100, 255, 100,
				0, 0, 0, 255,
			];
			let img_data = ImageData { width: 2, height: 2, bytes: bytes.as_ref().into() };

			// Make sure that setting one format overwrites the other.
			ctx.set_image(img_data.clone()).unwrap();
			assert!(matches!(ctx.get_text(), Err(Error::ContentNotAvailable)));

			ctx.set_text("clipboard test").unwrap();
			assert!(matches!(ctx.get_image(), Err(Error::ContentNotAvailable)));

			// Test if we get the same image that we put onto the clipboard
			ctx.set_image(img_data.clone()).unwrap();
			let got = ctx.get_image().unwrap();
			assert_eq!(img_data.bytes, got.bytes);

			#[rustfmt::skip]
			let big_bytes = vec![
				255, 100, 100, 255,
				100, 255, 100, 100,
				100, 100, 255, 100,

				0, 1, 2, 255,
				0, 1, 2, 255,
				0, 1, 2, 255,
			];
			let bytes_cloned = big_bytes.clone();
			let big_img_data = ImageData { width: 3, height: 2, bytes: big_bytes.into() };
			ctx.set_image(big_img_data).unwrap();
			let got = ctx.get_image().unwrap();
			assert_eq!(bytes_cloned.as_slice(), got.bytes.as_ref());
		}
		#[cfg(all(
			unix,
			not(any(target_os = "macos", target_os = "android", target_os = "emscripten")),
		))]
		{
			use crate::{LinuxClipboardKind, SetExtLinux};
			use std::sync::atomic::{self, AtomicBool};

			let mut ctx = Clipboard::new().unwrap();

			const TEXT1: &str = "I'm a little teapot,";
			const TEXT2: &str = "short and stout,";
			const TEXT3: &str = "here is my handle";

			ctx.set().clipboard(LinuxClipboardKind::Clipboard).text(TEXT1.to_string()).unwrap();

			ctx.set().clipboard(LinuxClipboardKind::Primary).text(TEXT2.to_string()).unwrap();

			// The secondary clipboard is not available under wayland
			if !cfg!(feature = "wayland-data-control")
				|| std::env::var_os("WAYLAND_DISPLAY").is_none()
			{
				ctx.set().clipboard(LinuxClipboardKind::Secondary).text(TEXT3.to_string()).unwrap();
			}

			assert_eq!(TEXT1, &ctx.get().clipboard(LinuxClipboardKind::Clipboard).text().unwrap());

			assert_eq!(TEXT2, &ctx.get().clipboard(LinuxClipboardKind::Primary).text().unwrap());

			// The secondary clipboard is not available under wayland
			if !cfg!(feature = "wayland-data-control")
				|| std::env::var_os("WAYLAND_DISPLAY").is_none()
			{
				assert_eq!(
					TEXT3,
					&ctx.get().clipboard(LinuxClipboardKind::Secondary).text().unwrap()
				);
			}

			let was_replaced = Arc::new(AtomicBool::new(false));

			let setter = thread::spawn({
				let was_replaced = was_replaced.clone();
				move || {
					thread::sleep(Duration::from_millis(100));
					let mut ctx = Clipboard::new().unwrap();
					ctx.set_text("replacement text".to_owned()).unwrap();
					was_replaced.store(true, atomic::Ordering::Release);
				}
			});

			ctx.set().wait().text("initial text".to_owned()).unwrap();

			assert!(was_replaced.load(atomic::Ordering::Acquire));

			setter.join().unwrap();
		}
	}

	// The cross-platform abstraction should allow any number of clipboards
	// to be open at once without issue, as documented under [Clipboard].
	#[test]
	fn multiple_clipboards_at_once() {
		const THREAD_COUNT: usize = 100;

		let mut handles = Vec::with_capacity(THREAD_COUNT);
		let barrier = Arc::new(std::sync::Barrier::new(THREAD_COUNT));

		for _ in 0..THREAD_COUNT {
			let barrier = barrier.clone();
			handles.push(thread::spawn(move || {
				// As long as the clipboard isn't used multiple times at once, multiple instances
				// are perfectly fine.
				let _ctx = Clipboard::new().unwrap();

				thread::sleep(Duration::from_millis(10));

				barrier.wait();
			}));
		}

		for thread_handle in handles {
			thread_handle.join().unwrap();
		}
	}

	#[test]
	fn clipboard_trait_consistently() {
		fn assert_send_sync<T: Send + Sync + 'static>() {}

		assert_send_sync::<Clipboard>();
		assert!(std::mem::needs_drop::<Clipboard>());
	}
}