Skip to content

Commit

Permalink
Implement the .set_html() method (#69)
Browse files Browse the repository at this point in the history
Implement the `.set_html()` method
  • Loading branch information
breard-r committed Oct 16, 2022
1 parent 4be2045 commit 9243631
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 49 deletions.
19 changes: 19 additions & 0 deletions examples/set_html.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use arboard::Clipboard;
use simple_logger::SimpleLogger;
use std::{thread, time::Duration};

fn main() {
SimpleLogger::new().init().unwrap();
let mut ctx = Clipboard::new().unwrap();

let html = r#"<h1>Hello, World!</h1>
<b>Lorem ipsum</b> dolor sit amet,<br>
<i>consectetur adipiscing elit</i>."#;

let alt_text = r#"Hello, World!
Lorem ipsum dolor sit amet,
consectetur adipiscing elit."#;

ctx.set_html(html, Some(alt_text)).unwrap();
thread::sleep(Duration::from_secs(5));
}
46 changes: 46 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ impl Clipboard {
self.set().text(text)
}

/// Places the HTML as well as a plain-text alternative onto the clipboard.
///
/// Any valid utf-8 string is accepted.
pub fn set_html<'a, T: Into<Cow<'a, str>>>(
&mut self,
html: T,
alt_text: Option<T>,
) -> Result<(), Error> {
self.set().html(html, alt_text)
}

/// Fetches image data from the clipboard, and returns the decoded pixels.
///
/// Any image data placed on the clipboard with `set_image` will be possible read back, using
Expand Down Expand Up @@ -142,6 +153,20 @@ impl Set<'_> {
self.platform.text(text)
}

/// Completes the "set" operation by placing HTML as well as a plain-text alternative onto the
/// clipboard.
///
/// Any valid UTF-8 string is accepted.
pub fn html<'a, T: Into<Cow<'a, str>>>(
self,
html: T,
alt_text: Option<T>,
) -> Result<(), Error> {
let html = html.into();
let alt_text = alt_text.map(|e| e.into());
self.platform.html(html, alt_text)
}

/// Completes the "set" operation by placing an image onto the clipboard.
///
/// The chosen output format, depending on the platform is the following:
Expand Down Expand Up @@ -219,6 +244,27 @@ fn all_tests() {
// confirm it is OK to clear when already empty.
ctx.clear().unwrap();
}
{
let mut ctx = Clipboard::new().unwrap();
let html = "<b>hello</b> <i>world</i>!";

ctx.set_html(html, None).unwrap();

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();

let html = "<b>hello</b> <i>world</i>!";
let alt_text = "hello world!";

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();
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 @@ -159,6 +159,14 @@ impl<'clipboard> Set<'clipboard> {
}
}

pub(crate) fn html(self, html: Cow<'_, str>, alt: Option<Cow<'_, str>>) -> Result<(), Error> {
match self.clipboard {
Clipboard::X11(clipboard) => clipboard.set_html(html, alt, self.selection, self.wait),
#[cfg(feature = "wayland-data-control")]
Clipboard::WlDataControl(clipboard) => clipboard.set_html(html, alt, self.selection, self.wait),
}
}

#[cfg(feature = "image-data")]
pub(crate) fn image(self, image: ImageData<'_>) -> Result<(), Error> {
match self.clipboard {
Expand Down
35 changes: 31 additions & 4 deletions src/platform/linux/wayland.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::convert::TryInto;
use std::io::Read;

use wl_clipboard_rs::{
copy::{self, Error as CopyError, Options, Source},
copy::{self, Error as CopyError, MimeSource, MimeType, Options, Source},
paste::{self, get_contents, Error as PasteError, Seat},
utils::is_primary_selection_supported,
};
Expand Down Expand Up @@ -81,7 +81,6 @@ impl Clipboard {
selection: LinuxClipboardKind,
wait: bool,
) -> Result<(), Error> {
use wl_clipboard_rs::copy::MimeType;
let mut opts = Options::new();
opts.foreground(wait);
opts.clipboard(selection.try_into()?);
Expand All @@ -93,6 +92,36 @@ impl Clipboard {
Ok(())
}

pub(crate) fn set_html(
&self,
html: Cow<'_, str>,
alt: Option<Cow<'_, str>>,
selection: LinuxClipboardKind,
wait: bool,
) -> Result<(), Error> {
let html_mime = MimeType::Specific(String::from("text/html"));
let mut opts = Options::new();
opts.foreground(wait);
opts.clipboard(selection.try_into()?);
let html_source = Source::Bytes(html.into_owned().into_bytes().into_boxed_slice());
match alt {
Some(alt_text) => {
let alt_source =
Source::Bytes(alt_text.into_owned().into_bytes().into_boxed_slice());
opts.copy_multi(vec![
MimeSource { source: alt_source, mime_type: MimeType::Text },
MimeSource { source: html_source, mime_type: html_mime },
])
}
None => opts.copy(html_source, html_mime),
}
.map_err(|e| match e {
CopyError::PrimarySelectionUnsupported => Error::ClipboardNotSupported,
other => into_unknown(other),
})?;
Ok(())
}

#[cfg(feature = "image-data")]
pub(crate) fn get_image(
&mut self,
Expand Down Expand Up @@ -140,8 +169,6 @@ impl Clipboard {
selection: LinuxClipboardKind,
wait: bool,
) -> Result<(), Error> {
use wl_clipboard_rs::copy::MimeType;

let image = encode_as_png(&image)?;
let mut opts = Options::new();
opts.foreground(wait);
Expand Down
99 changes: 66 additions & 33 deletions src/platform/linux/x11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ x11rb::atom_manager! {
TEXT,
TEXT_MIME_UNKNOWN: b"text/plain",

HTML: b"text/html",

PNG_MIME: b"image/png",

// This is just some random name for the property on our window, into which
Expand Down Expand Up @@ -172,7 +174,7 @@ impl XContext {

#[derive(Default)]
struct Selection {
data: RwLock<Option<ClipboardData>>,
data: RwLock<Option<Vec<ClipboardData>>>,
/// Mutex around nothing to use with the below condvar.
mutex: Mutex<()>,
/// A condvar that is notified when the contents of this clipboard are changed.
Expand Down Expand Up @@ -213,7 +215,12 @@ impl Inner {
})
}

fn write(&self, data: ClipboardData, selection: LinuxClipboardKind, wait: bool) -> Result<()> {
fn write(
&self,
data: Vec<ClipboardData>,
selection: LinuxClipboardKind,
wait: bool,
) -> Result<()> {
if self.serve_stopped.load(Ordering::Relaxed) {
return Err(Error::Unknown {
description: "The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)".into()
Expand Down Expand Up @@ -262,10 +269,12 @@ impl Inner {
// if we are the current owner, we can get the current clipboard ourselves
if self.is_owner(selection)? {
let data = self.selection_of(selection).data.read();
if let Some(data) = &*data {
for format in formats {
if *format == data.format {
return Ok(data.clone());
if let Some(data_list) = &*data {
for data in data_list {
for format in formats {
if *format == data.format {
return Ok(data.clone());
}
}
}
}
Expand Down Expand Up @@ -571,13 +580,15 @@ impl Inner {
targets.push(self.atoms.TARGETS);
targets.push(self.atoms.SAVE_TARGETS);
let data = self.selection_of(selection).data.read();
if let Some(data) = &*data {
targets.push(data.format);
if data.format == self.atoms.UTF8_STRING {
// When we are storing a UTF8 string,
// add all equivalent formats to the supported targets
targets.push(self.atoms.UTF8_MIME_0);
targets.push(self.atoms.UTF8_MIME_1);
if let Some(data_list) = &*data {
for data in data_list {
targets.push(data.format);
if data.format == self.atoms.UTF8_STRING {
// When we are storing a UTF8 string,
// add all equivalent formats to the supported targets
targets.push(self.atoms.UTF8_MIME_0);
targets.push(self.atoms.UTF8_MIME_1);
}
}
}
self.server
Expand All @@ -596,23 +607,24 @@ impl Inner {
} else {
trace!("Handling request for (probably) the clipboard contents.");
let data = self.selection_of(selection).data.read();
if let Some(data) = &*data {
if data.format == event.target {
self.server
.conn
.change_property8(
PropMode::REPLACE,
event.requestor,
event.property,
event.target,
&data.bytes,
)
.map_err(into_unknown)?;
self.server.conn.flush().map_err(into_unknown)?;
success = true;
} else {
success = false
}
if let Some(data_list) = &*data {
success = match data_list.iter().find(|d| d.format == event.target) {
Some(data) => {
self.server
.conn
.change_property8(
PropMode::REPLACE,
event.requestor,
event.property,
event.target,
&data.bytes,
)
.map_err(into_unknown)?;
self.server.conn.flush().map_err(into_unknown)?;
true
}
None => false,
};
} else {
// This must mean that we lost ownership of the data
// since the other side requested the selection.
Expand Down Expand Up @@ -857,10 +869,31 @@ impl Clipboard {
selection: LinuxClipboardKind,
wait: bool,
) -> Result<()> {
let data = ClipboardData {
let data = vec![ClipboardData {
bytes: message.into_owned().into_bytes(),
format: self.inner.atoms.UTF8_STRING,
};
}];
self.inner.write(data, selection, wait)
}

pub(crate) fn set_html(
&self,
html: Cow<'_, str>,
alt: Option<Cow<'_, str>>,
selection: LinuxClipboardKind,
wait: bool,
) -> Result<()> {
let mut data = vec![];
if let Some(alt_text) = alt {
data.push(ClipboardData {
bytes: alt_text.into_owned().into_bytes(),
format: self.inner.atoms.UTF8_STRING,
});
}
data.push(ClipboardData {
bytes: html.into_owned().into_bytes(),
format: self.inner.atoms.HTML,
});
self.inner.write(data, selection, wait)
}

Expand Down Expand Up @@ -890,7 +923,7 @@ impl Clipboard {
wait: bool,
) -> Result<()> {
let encoded = encode_as_png(&image)?;
let data = ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME };
let data = vec![ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME }];
self.inner.write(data, selection, wait)
}
}
Expand Down
41 changes: 40 additions & 1 deletion src/platform/osx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ use std::borrow::Cow;

// Required to bring NSPasteboard into the path of the class-resolver
#[link(name = "AppKit", kind = "framework")]
extern "C" {}
extern "C" {
static NSPasteboardTypeHTML: *const Object;
static NSPasteboardTypeString: *const Object;
}

static NSSTRING_CLASS: Lazy<&Class> = Lazy::new(|| Class::get("NSString").unwrap());
#[cfg(feature = "image-data")]
Expand Down Expand Up @@ -268,6 +271,42 @@ impl<'clipboard> Set<'clipboard> {
}
}

pub(crate) fn html(self, html: Cow<'_, str>, alt: Option<Cow<'_, str>>) -> Result<(), Error> {
self.clipboard.clear();
// Text goes to the clipboard as UTF-8 but may be interpreted as Windows Latin 1.
// This wrapping forces it to be interpreted as UTF-8.
//
// See:
// https://bugzilla.mozilla.org/show_bug.cgi?id=466599
// https://bugs.chromium.org/p/chromium/issues/detail?id=11957
let html = format!(
r#"<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
</head>
<body>{}</body>
</html>"#,
html
);
let html_nss = NSString::from_str(&html);
let mut success: bool = unsafe {
msg_send![self.clipboard.pasteboard, setString: html_nss forType:NSPasteboardTypeHTML]
};
if success {
if let Some(alt_text) = alt {
let alt_nss = NSString::from_str(&alt_text);
success = unsafe {
msg_send![self.clipboard.pasteboard, setString: alt_nss forType:NSPasteboardTypeString]
};
}
}
if success {
Ok(())
} else {
Err(Error::Unknown { description: "NSPasteboard#writeObjects: returned false".into() })
}
}

#[cfg(feature = "image-data")]
pub(crate) fn image(self, data: ImageData) -> Result<(), Error> {
let pixels = data.bytes.into();
Expand Down
Loading

0 comments on commit 9243631

Please sign in to comment.