diff --git a/CHANGELOG.md b/CHANGELOG.md
index 817a011e..ece51e2f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## 3.2.0
+
+### Changed
+- The Windows clipboard now behaves consistently with the other
+platform implementations again.
+
## 3.1.1 on 2022-17-10
### Added
diff --git a/src/lib.rs b/src/lib.rs
index 25c58646..fbbd0b86 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 a85ff989..39bdc2ef 100644
--- a/src/platform/windows.rs
+++ b/src/platform/windows.rs
@@ -12,8 +12,6 @@ use std::{borrow::Cow, marker::PhantomData};
#[cfg(feature = "image-data")]
use std::{convert::TryInto, mem::size_of};
-use clipboard_win::Clipboard as SystemClipboard;
-
#[cfg(feature = "image-data")]
use winapi::{
shared::minwindef::DWORD,
@@ -35,10 +33,8 @@ use crate::common::{private, Error};
#[cfg(feature = "image-data")]
use crate::common::{ImageData, ScopeGuard};
-const MAX_OPEN_ATTEMPTS: usize = 5;
-
#[cfg(feature = "image-data")]
-fn add_cf_dibv5(image: ImageData) -> Result<(), Error> {
+fn add_cf_dibv5(_open_clipboard: OpenClipboard, image: ImageData) -> Result<(), Error> {
use std::intrinsics::copy_nonoverlapping;
use winapi::um::{
winbase::{GlobalAlloc, GHND},
@@ -46,7 +42,7 @@ fn add_cf_dibv5(image: ImageData) -> Result<(), Error> {
winuser::CF_DIBV5,
};
- let header_size = std::mem::size_of::();
+ let header_size = size_of::();
let header = BITMAPV5HEADER {
bV5Size: header_size as u32,
bV5Width: image.width as LONG,
@@ -355,39 +351,88 @@ unsafe fn convert_bytes_to_u32s(bytes: &mut [u8]) -> ImageDataCow<'_> {
}
}
-pub(crate) struct Clipboard {
- _inner: clipboard_win::Clipboard,
+/// A shim clipboard type that can have operations performed with it, but
+/// does not represent an open clipboard itself.
+///
+/// Windows only allows one thread on the entire system to have the clipboard
+/// open at once, so we have to open it very sparingly or risk causing the rest
+/// of the system to be unresponsive. Instead, the clipboard is opened for
+/// every operation and then closed afterwards.
+pub(crate) struct Clipboard(());
+
+// The other platforms have `Drop` implementation on their
+// clipboard, so Windows should too for consistently.
+impl Drop for Clipboard {
+ fn drop(&mut self) {}
+}
- // The Windows clipboard can't be passed between threads when opened.
+struct OpenClipboard<'clipboard> {
+ _inner: clipboard_win::Clipboard,
+ // The Windows clipboard can not be sent between threads once
+ // open.
_marker: PhantomData<*const ()>,
+ _for_shim: &'clipboard mut Clipboard,
}
impl Clipboard {
+ const DEFAULT_OPEN_ATTEMPTS: usize = 5;
+
pub(crate) fn new() -> Result {
+ Ok(Self(()))
+ }
+
+ fn open(&mut self) -> Result {
// Attempt to open the clipboard multiple times. On Windows, its common for something else to temporarily
// be using it during attempts.
//
// For past work/evidence, see Firefox(https://searchfox.org/mozilla-central/source/widget/windows/nsClipboard.cpp#421) and
// Chromium(https://source.chromium.org/chromium/chromium/src/+/main:ui/base/clipboard/clipboard_win.cc;l=86).
- let clipboard = SystemClipboard::new_attempts(MAX_OPEN_ATTEMPTS)
- .map_err(|_| Error::ClipboardOccupied)?;
+ //
+ // Note: This does not use `Clipboard::new_attempts` because its implementation sleeps for `0ms`, which can
+ // cause race conditions between closing/opening the clipboard in single-threaded apps.
+ let mut attempts = Self::DEFAULT_OPEN_ATTEMPTS;
+ let clipboard = loop {
+ match clipboard_win::Clipboard::new() {
+ Ok(this) => break Ok(this),
+ Err(err) => match attempts {
+ 0 => break Err(err),
+ _ => attempts -= 1,
+ },
+ }
- Ok(Self { _inner: clipboard, _marker: PhantomData })
+ // The default value matches Chromium's implementation, but could be tweaked later.
+ // Safety: This is safe to call with any integer.
+ unsafe { winapi::um::synchapi::Sleep(5) };
+ }
+ .map_err(|_| Error::ClipboardOccupied)?;
+
+ Ok(OpenClipboard { _inner: clipboard, _marker: PhantomData, _for_shim: self })
}
}
+// Note: In all of the builders, a clipboard opening result is stored.
+// This is done for a few reasons:
+// 1. consistently with the other platforms which can have an occupied clipboard.
+// It is better if the operation fails at the most similar place on all platforms.
+// 2. `{Get, Set, Clear}::new()` don't return a `Result`. Windows is the only case that
+// needs this kind of handling, so it doesn't need to affect the other APIs.
+// 3. Due to how the clipboard works on Windows, we need to open it for every operation
+// and keep it open until its finished. This approach allows RAII to still be applicable.
+
pub(crate) struct Get<'clipboard> {
- clipboard: PhantomData<&'clipboard mut Clipboard>,
+ clipboard: Result, Error>,
}
impl<'clipboard> Get<'clipboard> {
- pub(crate) fn new(_clipboard: &'clipboard mut Clipboard) -> Self {
- Self { clipboard: PhantomData }
+ pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
+ Self { clipboard: clipboard.open() }
}
pub(crate) fn text(self) -> Result {
const FORMAT: u32 = clipboard_win::formats::CF_UNICODETEXT;
+ let _clipboard_assertion = self.clipboard?;
+
// XXX: ToC/ToU race conditions are not possible because we are the sole owners of the clipboard currently.
if !clipboard_win::is_format_avail(FORMAT) {
return Err(Error::ContentNotAvailable);
@@ -431,6 +476,8 @@ impl<'clipboard> Get<'clipboard> {
pub(crate) fn image(self) -> Result, Error> {
const FORMAT: u32 = clipboard_win::formats::CF_DIBV5;
+ let _clipboard_assertion = self.clipboard?;
+
if !clipboard_win::is_format_avail(FORMAT) {
return Err(Error::ContentNotAvailable);
}
@@ -446,30 +493,29 @@ impl<'clipboard> Get<'clipboard> {
}
pub(crate) struct Set<'clipboard> {
- clipboard: PhantomData<&'clipboard mut Clipboard>,
+ clipboard: Result, Error>,
exclude_from_cloud: bool,
exclude_from_history: bool,
}
impl<'clipboard> Set<'clipboard> {
- /// `set` should be called with the registered format and a DWORD value of 0.
- ///
- /// See https://docs.microsoft.com/en-us/windows/win32/dataxchg/clipboard-formats#cloud-clipboard-and-clipboard-history-formats
- const CLIPBOARD_EXCLUSION_DATA: &'static [u8] = &0u32.to_ne_bytes();
-
- pub(crate) fn new(_clipboard: &'clipboard mut Clipboard) -> Self {
- Self { clipboard: PhantomData, exclude_from_cloud: false, exclude_from_history: false }
+ pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
+ Self { clipboard: clipboard.open(), exclude_from_cloud: false, exclude_from_history: false }
}
pub(crate) fn text(self, data: Cow<'_, str>) -> Result<(), Error> {
+ let open_clipboard = self.clipboard?;
+
clipboard_win::raw::set_string(&data).map_err(|_| Error::Unknown {
description: "Could not place the specified text to the clipboard".into(),
})?;
- self.add_clipboard_exclusions()?;
- Ok(())
+
+ add_clipboard_exclusions(open_clipboard, self.exclude_from_cloud, self.exclude_from_history)
}
pub(crate) fn html(self, html: Cow<'_, str>, alt: Option>) -> Result<(), Error> {
+ let open_clipboard = self.clipboard?;
+
let alt = match alt {
Some(s) => s.into(),
None => String::new(),
@@ -477,58 +523,72 @@ impl<'clipboard> Set<'clipboard> {
clipboard_win::raw::set_string(&alt).map_err(|_| Error::Unknown {
description: "Could not place the specified text to the clipboard".into(),
})?;
+
if let Some(format) = clipboard_win::register_format("HTML Format") {
let html = wrap_html(&html);
clipboard_win::raw::set_without_clear(format.get(), html.as_bytes())
.map_err(|e| Error::Unknown { description: e.to_string() })?;
}
- self.add_clipboard_exclusions()?;
- Ok(())
+
+ add_clipboard_exclusions(open_clipboard, self.exclude_from_cloud, self.exclude_from_history)
}
#[cfg(feature = "image-data")]
pub(crate) fn image(self, image: ImageData) -> Result<(), Error> {
+ let open_clipboard = self.clipboard?;
+
if let Err(e) = clipboard_win::raw::empty() {
return Err(Error::Unknown {
description: format!("Failed to empty the clipboard. Got error code: {}", e),
});
};
- add_cf_dibv5(image)
+ add_cf_dibv5(open_clipboard, image)
}
+}
- fn add_clipboard_exclusions(&self) -> Result<(), Error> {
- // Clipboard exclusions are applied retroactively to the item that is currently in the clipboard.
- // See the MS docs on `CLIPBOARD_EXCLUSION_DATA` for specifics. Once the item is added to the clipboard,
- // tell Windows to remove it from cloud syncing and history.
-
- if self.exclude_from_cloud {
- if let Some(format) = clipboard_win::register_format("CanUploadToCloudClipboard") {
- // We believe that it would be a logic error if this call failed, since we've validated the format is supported,
- // we still have full ownership of the clipboard and aren't moving it to another thread, and this is a well-documented operation.
- // Due to these reasons, `Error::Unknown` is used because we never expect the error path to be taken.
- clipboard_win::raw::set_without_clear(format.get(), Self::CLIPBOARD_EXCLUSION_DATA)
- .map_err(|_| Error::Unknown {
- description: "Failed to exclude data from cloud clipboard".into(),
- })?;
- }
+fn add_clipboard_exclusions(
+ _open_clipboard: OpenClipboard<'_>,
+ exclude_from_cloud: bool,
+ exclude_from_history: bool,
+) -> Result<(), Error> {
+ /// `set` should be called with the registered format and a DWORD value of 0.
+ ///
+ /// See https://docs.microsoft.com/en-us/windows/win32/dataxchg/clipboard-formats#cloud-clipboard-and-clipboard-history-formats
+ const CLIPBOARD_EXCLUSION_DATA: &[u8] = &0u32.to_ne_bytes();
+
+ // Clipboard exclusions are applied retroactively to the item that is currently in the clipboard.
+ // See the MS docs on `CLIPBOARD_EXCLUSION_DATA` for specifics. Once the item is added to the clipboard,
+ // tell Windows to remove it from cloud syncing and history.
+
+ if exclude_from_cloud {
+ if let Some(format) = clipboard_win::register_format("CanUploadToCloudClipboard") {
+ // We believe that it would be a logic error if this call failed, since we've validated the format is supported,
+ // we still have full ownership of the clipboard and aren't moving it to another thread, and this is a well-documented operation.
+ // Due to these reasons, `Error::Unknown` is used because we never expect the error path to be taken.
+ clipboard_win::raw::set_without_clear(format.get(), CLIPBOARD_EXCLUSION_DATA).map_err(
+ |_| Error::Unknown {
+ description: "Failed to exclude data from cloud clipboard".into(),
+ },
+ )?;
}
+ }
- if self.exclude_from_history {
- if let Some(format) = clipboard_win::register_format("CanIncludeInClipboardHistory") {
- // See above for reasoning about using `Error::Unknown`.
- clipboard_win::raw::set_without_clear(format.get(), Self::CLIPBOARD_EXCLUSION_DATA)
- .map_err(|_| Error::Unknown {
- description: "Failed to exclude data from clipboard history".into(),
- })?;
- }
+ if exclude_from_history {
+ if let Some(format) = clipboard_win::register_format("CanIncludeInClipboardHistory") {
+ // See above for reasoning about using `Error::Unknown`.
+ clipboard_win::raw::set_without_clear(format.get(), CLIPBOARD_EXCLUSION_DATA).map_err(
+ |_| Error::Unknown {
+ description: "Failed to exclude data from clipboard history".into(),
+ },
+ )?;
}
-
- Ok(())
}
+
+ 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].
@@ -556,15 +616,16 @@ impl SetExtWindows for crate::Set<'_> {
}
pub(crate) struct Clear<'clipboard> {
- clipboard: PhantomData<&'clipboard mut Clipboard>,
+ clipboard: Result, Error>,
}
impl<'clipboard> Clear<'clipboard> {
- pub(crate) fn new(_clipboard: &'clipboard mut Clipboard) -> Self {
- Self { clipboard: PhantomData }
+ pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
+ Self { clipboard: clipboard.open() }
}
pub(crate) fn clear(self) -> Result<(), Error> {
+ let _clipboard_assertion = self.clipboard?;
clipboard_win::empty()
.map_err(|_| Error::Unknown { description: "failed to clear clipboard".into() })
}