Skip to content

Commit

Permalink
Implement Get::html() for all platforms (#163)
Browse files Browse the repository at this point in the history
* implement get html operation

Signed-off-by: Gae24 <[email protected]>
  • Loading branch information
Gae24 authored Feb 13, 2025
1 parent e458e1a commit 4b91bfe
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 24 deletions.
3 changes: 3 additions & 0 deletions examples/set_html.rs → examples/set_get_html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
}
23 changes: 23 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ impl Get<'_> {
pub fn image(self) -> Result<ImageData<'static>, Error> {
self.platform.image()
}

/// Completes the "get" operation by fetching HTML from the clipboard.
pub fn html(self) -> Result<String, Error> {
self.platform.html()
}
}

/// A builder for an operation that sets a value to the clipboard.
Expand Down Expand Up @@ -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 = "<b>hello</b> <i>world</i>!";

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}</body></html>")));
} else {
assert_eq!(ctx.get().html().unwrap(), html);
}
}
#[cfg(feature = "image-data")]
{
let mut ctx = Clipboard::new().unwrap();
Expand Down
8 changes: 8 additions & 0 deletions src/platform/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ impl<'clipboard> Get<'clipboard> {
Clipboard::WlDataControl(clipboard) => clipboard.get_image(self.selection),
}
}

pub(crate) fn html(self) -> Result<String, Error> {
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.
Expand Down
18 changes: 14 additions & 4 deletions src/platform/linux/wayland.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ impl Clipboard {
Ok(Self {})
}

pub(crate) fn get_text(&mut self, selection: LinuxClipboardKind) -> Result<String, Error> {
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<String, Error> {
let result = get_contents(selection.try_into()?, Seat::Unspecified, mime);
match result {
Ok((mut pipe, _)) => {
let mut contents = vec![];
Expand All @@ -74,6 +76,10 @@ impl Clipboard {
}
}

pub(crate) fn get_text(&mut self, selection: LinuxClipboardKind) -> Result<String, Error> {
self.string_for_mime(selection, paste::MimeType::Text)
}

pub(crate) fn set_text(
&self,
text: Cow<'_, str>,
Expand All @@ -91,6 +97,10 @@ impl Clipboard {
Ok(())
}

pub(crate) fn get_html(&mut self, selection: LinuxClipboardKind) -> Result<String, Error> {
self.string_for_mime(selection, paste::MimeType::Specific("text/html"))
}

pub(crate) fn set_html(
&self,
html: Cow<'_, str>,
Expand Down
6 changes: 6 additions & 0 deletions src/platform/linux/x11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,12 @@ impl Clipboard {
self.inner.write(data, selection, wait)
}

pub(crate) fn get_html(&self, selection: LinuxClipboardKind) -> Result<String> {
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>,
Expand Down
45 changes: 25 additions & 20 deletions src/platform/osx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,27 @@ impl Clipboard {
unsafe { self.pasteboard.clearContents() };
}

fn string_from_type(&self, type_: &'static NSString) -> Result<String, Error> {
// 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<Option<ClipboardContent>, Box<dyn std::error::Error>> {
// let string_class: Id<NSObject> = {
// let cls: Id<Class> = unsafe { Id::from_ptr(class("NSString")) };
Expand Down Expand Up @@ -182,27 +203,11 @@ impl<'clipboard> Get<'clipboard> {
}

pub(crate) fn text(self) -> Result<String, Error> {
// 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<String, Error> {
unsafe { self.clipboard.string_from_type(NSPasteboardTypeHTML) }
}

#[cfg(feature = "image-data")]
Expand Down
13 changes: 13 additions & 0 deletions src/platform/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,19 @@ impl<'clipboard> Get<'clipboard> {
String::from_utf16(&out[..bytes_read]).map_err(|_| Error::ConversionFailure)
}

pub(crate) fn html(self) -> Result<String, Error> {
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<u8> = 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<ImageData<'static>, Error> {
const FORMAT: u32 = clipboard_win::formats::CF_DIBV5;
Expand Down

0 comments on commit 4b91bfe

Please sign in to comment.