1#![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#[allow(rustdoc::broken_intra_doc_links)]
70pub struct Clipboard {
71	pub(crate) platform: platform::Clipboard,
72}
73
74impl Clipboard {
75	pub fn new() -> Result<Self, Error> {
82		Ok(Clipboard { platform: platform::Clipboard::new()? })
83	}
84
85	pub fn get_text(&mut self) -> Result<String, Error> {
91		self.get().text()
92	}
93
94	pub fn set_text<'a, T: Into<Cow<'a, str>>>(&mut self, text: T) -> Result<(), Error> {
100		self.set().text(text)
101	}
102
103	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	#[cfg(feature = "image-data")]
129	pub fn get_image(&mut self) -> Result<ImageData<'static>, Error> {
130		self.get().image()
131	}
132
133	#[cfg(feature = "image-data")]
146	pub fn set_image(&mut self, image: ImageData) -> Result<(), Error> {
147		self.set().image(image)
148	}
149
150	pub fn clear(&mut self) -> Result<(), Error> {
157		self.clear_with().default()
158	}
159
160	pub fn clear_with(&mut self) -> Clear<'_> {
162		Clear { platform: platform::Clear::new(&mut self.platform) }
163	}
164
165	pub fn get(&mut self) -> Get<'_> {
167		Get { platform: platform::Get::new(&mut self.platform) }
168	}
169
170	pub fn set(&mut self) -> Set<'_> {
172		Set { platform: platform::Set::new(&mut self.platform) }
173	}
174}
175
176#[must_use]
178pub struct Get<'clipboard> {
179	pub(crate) platform: platform::Get<'clipboard>,
180}
181
182impl Get<'_> {
183	pub fn text(self) -> Result<String, Error> {
185		self.platform.text()
186	}
187
188	#[cfg(feature = "image-data")]
195	pub fn image(self) -> Result<ImageData<'static>, Error> {
196		self.platform.image()
197	}
198
199	pub fn html(self) -> Result<String, Error> {
201		self.platform.html()
202	}
203
204	pub fn file_list(self) -> Result<Vec<PathBuf>, Error> {
206		self.platform.file_list()
207	}
208}
209
210#[must_use]
212pub struct Set<'clipboard> {
213	pub(crate) platform: platform::Set<'clipboard>,
214}
215
216impl Set<'_> {
217	pub fn text<'a, T: Into<Cow<'a, str>>>(self, text: T) -> Result<(), Error> {
220		let text = text.into();
221		self.platform.text(text)
222	}
223
224	pub fn html<'a, T: Into<Cow<'a, str>>>(
229		self,
230		html: T,
231		alt_text: Option<T>,
232	) -> Result<(), Error> {
233		let html = html.into();
234		let alt_text = alt_text.map(|e| e.into());
235		self.platform.html(html, alt_text)
236	}
237
238	#[cfg(feature = "image-data")]
246	pub fn image(self, image: ImageData) -> Result<(), Error> {
247		self.platform.image(image)
248	}
249
250	pub fn file_list(self, file_list: &[impl AsRef<Path>]) -> Result<(), Error> {
252		self.platform.file_list(file_list)
253	}
254}
255
256#[must_use]
258pub struct Clear<'clipboard> {
259	pub(crate) platform: platform::Clear<'clipboard>,
260}
261
262impl Clear<'_> {
263	pub fn default(self) -> Result<(), Error> {
266		self.platform.clear()
267	}
268}
269
270#[cfg(test)]
273mod tests {
274	use super::*;
275	use std::{sync::Arc, thread, time::Duration};
276
277	#[test]
278	fn all_tests() {
279		let _ = env_logger::builder().is_test(true).try_init();
280		{
281			let mut ctx = Clipboard::new().unwrap();
282			let text = "some string";
283			ctx.set_text(text).unwrap();
284			assert_eq!(ctx.get_text().unwrap(), text);
285
286			drop(ctx);
289
290			thread::sleep(Duration::from_millis(300));
294
295			let mut ctx = Clipboard::new().unwrap();
296			assert_eq!(ctx.get_text().unwrap(), text);
297		}
298		{
299			let mut ctx = Clipboard::new().unwrap();
300			let text = "Some utf8: 🤓 ∑φ(n)<ε 🐔";
301			ctx.set_text(text).unwrap();
302			assert_eq!(ctx.get_text().unwrap(), text);
303		}
304		{
305			let mut ctx = Clipboard::new().unwrap();
306			let text = "hello world";
307
308			ctx.set_text(text).unwrap();
309			assert_eq!(ctx.get_text().unwrap(), text);
310
311			ctx.clear().unwrap();
312
313			match ctx.get_text() {
314				Ok(text) => assert!(text.is_empty()),
315				Err(Error::ContentNotAvailable) => {}
316				Err(e) => panic!("unexpected error: {e}"),
317			};
318
319			ctx.clear().unwrap();
321		}
322		{
323			let mut ctx = Clipboard::new().unwrap();
324			let html = "<b>hello</b> <i>world</i>!";
325
326			ctx.set_html(html, None).unwrap();
327
328			match ctx.get_text() {
329				Ok(text) => assert!(text.is_empty()),
330				Err(Error::ContentNotAvailable) => {}
331				Err(e) => panic!("unexpected error: {e}"),
332			};
333		}
334		{
335			let mut ctx = Clipboard::new().unwrap();
336
337			let html = "<b>hello</b> <i>world</i>!";
338			let alt_text = "hello world!";
339
340			ctx.set_html(html, Some(alt_text)).unwrap();
341			assert_eq!(ctx.get_text().unwrap(), alt_text);
342		}
343		{
344			let mut ctx = Clipboard::new().unwrap();
345
346			let html = "<b>hello</b> <i>world</i>!";
347
348			ctx.set().html(html, None).unwrap();
349
350			if cfg!(target_os = "macos") {
351				let content = ctx.get().html().unwrap();
356				assert!(content.ends_with(&format!("{html}</body></html>")));
357			} else {
358				assert_eq!(ctx.get().html().unwrap(), html);
359			}
360		}
361		{
362			let mut ctx = Clipboard::new().unwrap();
363
364			let this_dir = env!("CARGO_MANIFEST_DIR");
365
366			let paths = &[
367				PathBuf::from(this_dir).join("README.md"),
368				PathBuf::from(this_dir).join("Cargo.toml"),
369			];
370
371			ctx.set().file_list(paths).unwrap();
372			assert_eq!(ctx.get().file_list().unwrap().as_slice(), paths);
373		}
374		#[cfg(feature = "image-data")]
375		{
376			let mut ctx = Clipboard::new().unwrap();
377			#[rustfmt::skip]
378			let bytes = [
379				255, 100, 100, 255,
380				100, 255, 100, 100,
381				100, 100, 255, 100,
382				0, 0, 0, 255,
383			];
384			let img_data = ImageData { width: 2, height: 2, bytes: bytes.as_ref().into() };
385
386			ctx.set_image(img_data.clone()).unwrap();
388			assert!(matches!(ctx.get_text(), Err(Error::ContentNotAvailable)));
389
390			ctx.set_text("clipboard test").unwrap();
391			assert!(matches!(ctx.get_image(), Err(Error::ContentNotAvailable)));
392
393			ctx.set_image(img_data.clone()).unwrap();
395			let got = ctx.get_image().unwrap();
396			assert_eq!(img_data.bytes, got.bytes);
397
398			#[rustfmt::skip]
399			let big_bytes = vec![
400				255, 100, 100, 255,
401				100, 255, 100, 100,
402				100, 100, 255, 100,
403
404				0, 1, 2, 255,
405				0, 1, 2, 255,
406				0, 1, 2, 255,
407			];
408			let bytes_cloned = big_bytes.clone();
409			let big_img_data = ImageData { width: 3, height: 2, bytes: big_bytes.into() };
410			ctx.set_image(big_img_data).unwrap();
411			let got = ctx.get_image().unwrap();
412			assert_eq!(bytes_cloned.as_slice(), got.bytes.as_ref());
413		}
414		#[cfg(all(
415			unix,
416			not(any(target_os = "macos", target_os = "android", target_os = "emscripten")),
417		))]
418		{
419			use crate::{LinuxClipboardKind, SetExtLinux};
420			use std::sync::atomic::{self, AtomicBool};
421
422			let mut ctx = Clipboard::new().unwrap();
423
424			const TEXT1: &str = "I'm a little teapot,";
425			const TEXT2: &str = "short and stout,";
426			const TEXT3: &str = "here is my handle";
427
428			ctx.set().clipboard(LinuxClipboardKind::Clipboard).text(TEXT1.to_string()).unwrap();
429
430			ctx.set().clipboard(LinuxClipboardKind::Primary).text(TEXT2.to_string()).unwrap();
431
432			if !cfg!(feature = "wayland-data-control")
434				|| std::env::var_os("WAYLAND_DISPLAY").is_none()
435			{
436				ctx.set().clipboard(LinuxClipboardKind::Secondary).text(TEXT3.to_string()).unwrap();
437			}
438
439			assert_eq!(TEXT1, &ctx.get().clipboard(LinuxClipboardKind::Clipboard).text().unwrap());
440
441			assert_eq!(TEXT2, &ctx.get().clipboard(LinuxClipboardKind::Primary).text().unwrap());
442
443			if !cfg!(feature = "wayland-data-control")
445				|| std::env::var_os("WAYLAND_DISPLAY").is_none()
446			{
447				assert_eq!(
448					TEXT3,
449					&ctx.get().clipboard(LinuxClipboardKind::Secondary).text().unwrap()
450				);
451			}
452
453			let was_replaced = Arc::new(AtomicBool::new(false));
454
455			let setter = thread::spawn({
456				let was_replaced = was_replaced.clone();
457				move || {
458					thread::sleep(Duration::from_millis(100));
459					let mut ctx = Clipboard::new().unwrap();
460					ctx.set_text("replacement text".to_owned()).unwrap();
461					was_replaced.store(true, atomic::Ordering::Release);
462				}
463			});
464
465			ctx.set().wait().text("initial text".to_owned()).unwrap();
466
467			assert!(was_replaced.load(atomic::Ordering::Acquire));
468
469			setter.join().unwrap();
470		}
471	}
472
473	#[test]
476	fn multiple_clipboards_at_once() {
477		const THREAD_COUNT: usize = 100;
478
479		let mut handles = Vec::with_capacity(THREAD_COUNT);
480		let barrier = Arc::new(std::sync::Barrier::new(THREAD_COUNT));
481
482		for _ in 0..THREAD_COUNT {
483			let barrier = barrier.clone();
484			handles.push(thread::spawn(move || {
485				let _ctx = Clipboard::new().unwrap();
488
489				thread::sleep(Duration::from_millis(10));
490
491				barrier.wait();
492			}));
493		}
494
495		for thread_handle in handles {
496			thread_handle.join().unwrap();
497		}
498	}
499
500	#[test]
501	fn clipboard_trait_consistently() {
502		fn assert_send_sync<T: Send + Sync + 'static>() {}
503
504		assert_send_sync::<Clipboard>();
505		assert!(std::mem::needs_drop::<Clipboard>());
506	}
507}