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}