diff --git a/src/lib.rs b/src/lib.rs
index 25c5864..fbbd0b8 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -8,9 +8,6 @@ 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.
*/
-#![crate_name = "arboard"]
-#![crate_type = "lib"]
-
mod common;
use std::borrow::Cow;
@@ -33,13 +30,34 @@ pub use platform::SetExtWindows;
///
/// 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
+/// 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 multiple `Clipboards` on separate threads at once but note that
+/// 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 ensure that
+/// multiple `Clipboard`'s may exist at once. This also means that attempting operations in parallel
+/// has a high likelyhood to return an error instead.
+#[allow(rustdoc::broken_intra_doc_links)]
pub struct Clipboard {
pub(crate) platform: platform::Clipboard,
}
@@ -197,167 +215,207 @@ impl Clear<'_> {
/// All tests grouped in one because the windows clipboard cannot be open on
/// multiple threads at once.
#[cfg(test)]
-#[test]
-fn all_tests() {
- use std::{thread, time::Duration};
-
- 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";
+mod tests {
+ use super::*;
+ use std::{sync::Arc, thread, time::Duration};
- ctx.set_text(text).unwrap();
- assert_eq!(ctx.get_text().unwrap(), text);
+ #[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.clear().unwrap();
+ ctx.set_text(text).unwrap();
+ assert_eq!(ctx.get_text().unwrap(), text);
- match ctx.get_text() {
- Ok(text) => assert!(text.is_empty()),
- Err(Error::ContentNotAvailable) => {}
- Err(e) => panic!("unexpected error: {}", e),
- };
+ ctx.clear().unwrap();
- // confirm it is OK to clear when already empty.
- ctx.clear().unwrap();
- }
- {
- let mut ctx = Clipboard::new().unwrap();
- let html = "hello world!";
+ match ctx.get_text() {
+ Ok(text) => assert!(text.is_empty()),
+ Err(Error::ContentNotAvailable) => {}
+ Err(e) => panic!("unexpected error: {}", e),
+ };
- ctx.set_html(html, None).unwrap();
+ // confirm it is OK to clear when already empty.
+ ctx.clear().unwrap();
+ }
+ {
+ let mut ctx = Clipboard::new().unwrap();
+ let html = "hello world!";
- 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();
+ ctx.set_html(html, None).unwrap();
- let html = "hello world!";
- let alt_text = "hello world!";
+ 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();
- 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 clibboard
- 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},
- Arc,
- };
+ let html = "hello world!";
+ let alt_text = "hello world!";
- let mut ctx = Clipboard::new().unwrap();
+ 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 clibboard
+ 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};
- const TEXT1: &str = "I'm a little teapot,";
- const TEXT2: &str = "short and stout,";
- const TEXT3: &str = "here is my handle";
+ let mut ctx = Clipboard::new().unwrap();
- ctx.set().clipboard(LinuxClipboardKind::Clipboard).text(TEXT1.to_string()).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::Primary).text(TEXT2.to_string()).unwrap();
+ ctx.set().clipboard(LinuxClipboardKind::Clipboard).text(TEXT1.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();
- }
+ ctx.set().clipboard(LinuxClipboardKind::Primary).text(TEXT2.to_string()).unwrap();
- assert_eq!(TEXT1, &ctx.get().clipboard(LinuxClipboardKind::Clipboard).text().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!(TEXT2, &ctx.get().clipboard(LinuxClipboardKind::Primary).text().unwrap());
+ assert_eq!(TEXT1, &ctx.get().clipboard(LinuxClipboardKind::Clipboard).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());
+ 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();
}
+ }
- let was_replaced = Arc::new(AtomicBool::new(false));
+ // 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 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);
- }
- });
+ 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();
- ctx.set().wait().text("initial text".to_owned()).unwrap();
+ thread::sleep(Duration::from_millis(10));
+
+ barrier.wait();
+ }));
+ }
+
+ for thread_handle in handles {
+ thread_handle.join().unwrap();
+ }
+ }
- assert!(was_replaced.load(atomic::Ordering::Acquire));
+ #[test]
+ fn clipboard_trait_consistently() {
+ fn assert_send_sync() {}
- setter.join().unwrap();
+ assert_send_sync::();
+ assert!(std::mem::needs_drop::());
}
}
diff --git a/src/platform/windows.rs b/src/platform/windows.rs
index 9928ee6..39bdc2e 100644
--- a/src/platform/windows.rs
+++ b/src/platform/windows.rs
@@ -588,7 +588,7 @@ fn add_clipboard_exclusions(
Ok(())
}
-/// Windows-specific extensions to the [`Set`](super::Set) builder.
+/// Windows-specific extensions to the [`Set`](crate::Set) builder.
pub trait SetExtWindows: private::Sealed {
/// Excludes the data which will be set on the clipboard from being uploaded to
/// the Windows 10/11 [cloud clipboard].