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]
183pub struct Get<'clipboard> {
184 pub(crate) platform: platform::Get<'clipboard>,
185}
186
187impl Get<'_> {
188 pub fn text(self) -> Result<String, Error> {
190 self.platform.text()
191 }
192
193 #[cfg(feature = "image-data")]
200 pub fn image(self) -> Result<ImageData<'static>, Error> {
201 self.platform.image()
202 }
203
204 pub fn html(self) -> Result<String, Error> {
206 self.platform.html()
207 }
208
209 pub fn file_list(self) -> Result<Vec<PathBuf>, Error> {
211 self.platform.file_list()
212 }
213}
214
215#[must_use]
222pub struct Set<'clipboard> {
223 pub(crate) platform: platform::Set<'clipboard>,
224}
225
226impl Set<'_> {
227 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 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 #[cfg(feature = "image-data")]
256 pub fn image(self, image: ImageData) -> Result<(), Error> {
257 self.platform.image(image)
258 }
259
260 pub fn file_list(self, file_list: &[impl AsRef<Path>]) -> Result<(), Error> {
262 self.platform.file_list(file_list)
263 }
264}
265
266#[must_use]
268pub struct Clear<'clipboard> {
269 pub(crate) platform: platform::Clear<'clipboard>,
270}
271
272impl Clear<'_> {
273 pub fn default(self) -> Result<(), Error> {
276 self.platform.clear()
277 }
278}
279
280#[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 drop(ctx);
299
300 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 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 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 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 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 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 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 #[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 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}