diff --git a/examples/set_html.rs b/examples/set_get_html.rs similarity index 78% rename from examples/set_html.rs rename to examples/set_get_html.rs index 125f6b1..96ab58a 100644 --- a/examples/set_html.rs +++ b/examples/set_get_html.rs @@ -15,4 +15,7 @@ consectetur adipiscing elit."#; ctx.set_html(html, Some(alt_text)).unwrap(); thread::sleep(Duration::from_secs(5)); + + let success = ctx.get().html().unwrap() == html; + println!("Set and Get html operations were successful: {success}"); } diff --git a/src/lib.rs b/src/lib.rs index 01eac45..c002bdc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -192,6 +192,11 @@ impl Get<'_> { pub fn image(self) -> Result, Error> { self.platform.image() } + + /// Completes the "get" operation by fetching HTML from the clipboard. + pub fn html(self) -> Result { + self.platform.html() + } } /// A builder for an operation that sets a value to the clipboard. @@ -322,6 +327,24 @@ mod tests { ctx.set_html(html, Some(alt_text)).unwrap(); assert_eq!(ctx.get_text().unwrap(), alt_text); } + { + let mut ctx = Clipboard::new().unwrap(); + + let html = "hello world!"; + + ctx.set().html(html, None).unwrap(); + + if cfg!(target_os = "macos") { + // Copying HTML on macOS adds wrapper content to work around + // historical platform bugs. We control this wrapper, so we are + // able to check that the full user data still appears and at what + // position in the final copy contents. + let content = ctx.get().html().unwrap(); + assert!(content.ends_with(&format!("{html}"))); + } else { + assert_eq!(ctx.get().html().unwrap(), html); + } + } #[cfg(feature = "image-data")] { let mut ctx = Clipboard::new().unwrap(); diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index d8207e3..4ce13c1 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -122,6 +122,14 @@ impl<'clipboard> Get<'clipboard> { Clipboard::WlDataControl(clipboard) => clipboard.get_image(self.selection), } } + + pub(crate) fn html(self) -> Result { + match self.clipboard { + Clipboard::X11(clipboard) => clipboard.get_html(self.selection), + #[cfg(feature = "wayland-data-control")] + Clipboard::WlDataControl(clipboard) => clipboard.get_html(self.selection), + } + } } /// Linux-specific extensions to the [`Get`](super::Get) builder. diff --git a/src/platform/linux/wayland.rs b/src/platform/linux/wayland.rs index b3916a6..9cd12bf 100644 --- a/src/platform/linux/wayland.rs +++ b/src/platform/linux/wayland.rs @@ -53,10 +53,12 @@ impl Clipboard { Ok(Self {}) } - pub(crate) fn get_text(&mut self, selection: LinuxClipboardKind) -> Result { - use wl_clipboard_rs::paste::MimeType; - - let result = get_contents(selection.try_into()?, Seat::Unspecified, MimeType::Text); + fn string_for_mime( + &mut self, + selection: LinuxClipboardKind, + mime: paste::MimeType, + ) -> Result { + let result = get_contents(selection.try_into()?, Seat::Unspecified, mime); match result { Ok((mut pipe, _)) => { let mut contents = vec![]; @@ -74,6 +76,10 @@ impl Clipboard { } } + pub(crate) fn get_text(&mut self, selection: LinuxClipboardKind) -> Result { + self.string_for_mime(selection, paste::MimeType::Text) + } + pub(crate) fn set_text( &self, text: Cow<'_, str>, @@ -91,6 +97,10 @@ impl Clipboard { Ok(()) } + pub(crate) fn get_html(&mut self, selection: LinuxClipboardKind) -> Result { + self.string_for_mime(selection, paste::MimeType::Specific("text/html")) + } + pub(crate) fn set_html( &self, html: Cow<'_, str>, diff --git a/src/platform/linux/x11.rs b/src/platform/linux/x11.rs index 406b43f..c221c2d 100644 --- a/src/platform/linux/x11.rs +++ b/src/platform/linux/x11.rs @@ -885,6 +885,12 @@ impl Clipboard { self.inner.write(data, selection, wait) } + pub(crate) fn get_html(&self, selection: LinuxClipboardKind) -> Result { + let formats = [self.inner.atoms.HTML]; + let result = self.inner.read(&formats, selection)?; + String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure) + } + pub(crate) fn set_html( &self, html: Cow<'_, str>, diff --git a/src/platform/osx.rs b/src/platform/osx.rs index ec5f881..1df7222 100644 --- a/src/platform/osx.rs +++ b/src/platform/osx.rs @@ -121,6 +121,27 @@ impl Clipboard { unsafe { self.pasteboard.clearContents() }; } + fn string_from_type(&self, type_: &'static NSString) -> Result { + // XXX: There does not appear to be an alternative for obtaining text without the need for + // autorelease behavior. + autoreleasepool(|_| { + // XXX: We explicitly use `pasteboardItems` and not `stringForType` since the latter will concat + // multiple strings, if present, into one and return it instead of reading just the first which is `arboard`'s + // historical behavior. + let contents = unsafe { self.pasteboard.pasteboardItems() }.ok_or_else(|| { + Error::Unknown { description: String::from("NSPasteboard#pasteboardItems errored") } + })?; + + for item in contents { + if let Some(string) = unsafe { item.stringForType(type_) } { + return Ok(string.to_string()); + } + } + + Err(Error::ContentNotAvailable) + }) + } + // fn get_binary_contents(&mut self) -> Result, Box> { // let string_class: Id = { // let cls: Id = unsafe { Id::from_ptr(class("NSString")) }; @@ -182,27 +203,11 @@ impl<'clipboard> Get<'clipboard> { } pub(crate) fn text(self) -> Result { - // XXX: There does not appear to be an alternative for obtaining text without the need for - // autorelease behavior. - autoreleasepool(|_| { - // XXX: We explicitly use `pasteboardItems` and not `stringForType` since the latter will concat - // multiple strings, if present, into one and return it instead of reading just the first which is `arboard`'s - // historical behavior. - let contents = - unsafe { self.clipboard.pasteboard.pasteboardItems() }.ok_or_else(|| { - Error::Unknown { - description: String::from("NSPasteboard#pasteboardItems errored"), - } - })?; - - for item in contents { - if let Some(string) = unsafe { item.stringForType(NSPasteboardTypeString) } { - return Ok(string.to_string()); - } - } + unsafe { self.clipboard.string_from_type(NSPasteboardTypeString) } + } - Err(Error::ContentNotAvailable) - }) + pub(crate) fn html(self) -> Result { + unsafe { self.clipboard.string_from_type(NSPasteboardTypeHTML) } } #[cfg(feature = "image-data")] diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 6544b92..79dca54 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -567,6 +567,19 @@ impl<'clipboard> Get<'clipboard> { String::from_utf16(&out[..bytes_read]).map_err(|_| Error::ConversionFailure) } + pub(crate) fn html(self) -> Result { + let _clipboard_assertion = self.clipboard?; + + let format = clipboard_win::register_format("HTML Format") + .ok_or_else(|| Error::unknown("unable to register HTML format"))?; + + let mut out: Vec = Vec::new(); + clipboard_win::raw::get_html(format.get(), &mut out) + .map_err(|_| Error::unknown("failed to read clipboard string"))?; + + String::from_utf8(out).map_err(|_| Error::ConversionFailure) + } + #[cfg(feature = "image-data")] pub(crate) fn image(self) -> Result, Error> { const FORMAT: u32 = clipboard_win::formats::CF_DIBV5;