use crate::compat::String;
use crate::platform::clipboard::{ClipboardContent, RichClipboardBackend};
const PLAIN_TEXT: &str = "text/plain";
const HTML: &str = "text/html";
const HTML_INFO: u32 = 1;
const PLAIN_INFO: u32 = 2;
fn with_clipboard<R>(body: impl FnOnce(>k::Clipboard) -> R) -> Option<R> {
if !gtk::is_initialized_main_thread() {
return None;
}
let display = gtk::gdk::Display::default()?;
let clipboard = gtk::Clipboard::default(&display)?;
Some(body(&clipboard))
}
pub(crate) fn available() -> bool {
with_clipboard(|_| ()).is_some()
}
pub(crate) fn set_text(text: &str) -> bool {
with_clipboard(|clipboard| {
clipboard.set_text(text);
clipboard.store();
match clipboard.wait_for_text() {
Some(stored) => stored.as_str() == text,
None => text.is_empty(),
}
})
.unwrap_or(false)
}
pub(crate) fn text() -> Option<String> {
with_clipboard(|clipboard| clipboard.wait_for_text().map(|text| text.to_string())).flatten()
}
pub(crate) fn set_html(html: &str, plain: &str) -> bool {
let html_owned: String = html.into();
let plain_owned: String = plain.into();
let owned = with_clipboard(|clipboard| {
let html_atom = gtk::gdk::Atom::intern(HTML);
let targets = [
gtk::TargetEntry::new(HTML, gtk::TargetFlags::empty(), HTML_INFO),
gtk::TargetEntry::new("UTF8_STRING", gtk::TargetFlags::empty(), PLAIN_INFO),
gtk::TargetEntry::new(PLAIN_TEXT, gtk::TargetFlags::empty(), PLAIN_INFO),
];
let offered = clipboard.set_with_data(&targets, move |_clipboard, selection, info| {
match info {
HTML_INFO => selection.set(&html_atom, 8, html_owned.as_bytes()),
PLAIN_INFO => {
selection.set_text(&plain_owned);
}
_ => {}
}
});
if offered {
clipboard.store();
}
offered
})
.unwrap_or(false);
if !owned {
return false;
}
let markup = self::html().is_some_and(|stored| stored == html);
if !markup {
log::warn!(
"[linux clipboard] the text/html target did not come back from the display; the \
plain-text channel is what a receiver will read"
);
}
markup || text().is_some_and(|stored| stored == plain)
}
pub(crate) fn html() -> Option<String> {
with_clipboard(|clipboard| {
let selection = clipboard.wait_for_contents(>k::gdk::Atom::intern(HTML))?;
let bytes = selection.data();
if bytes.is_empty() {
return None;
}
String::from_utf8(bytes).ok()
})
.flatten()
}
pub(crate) fn has_format(content_type: &str) -> bool {
match content_type {
PLAIN_TEXT => text().is_some(),
HTML => html().is_some(),
other => holds_target(other),
}
}
fn holds_target(content_type: &str) -> bool {
with_clipboard(|clipboard| {
clipboard.wait_for_contents(>k::gdk::Atom::intern(content_type)).is_some()
})
.unwrap_or(false)
}
pub(crate) fn set_typed(content_type: &str, data: &[u8]) -> bool {
let type_owned: String = content_type.into();
let data_owned = data.to_vec();
let offered = with_clipboard(|clipboard| {
let atom = gtk::gdk::Atom::intern(content_type);
let targets = [gtk::TargetEntry::new(content_type, gtk::TargetFlags::empty(), 1)];
let offered = clipboard.set_with_data(&targets, move |_clipboard, selection, _info| {
selection.set(&atom, 8, &data_owned);
});
if offered {
clipboard.store();
}
offered
})
.unwrap_or(false);
if !offered {
return false;
}
let stored = typed(content_type);
if stored.as_deref() != Some(data) {
log::warn!(
"[linux clipboard] the '{type_owned}' target did not come back from the display; the \
write is reported as refused rather than pretended"
);
return false;
}
true
}
pub(crate) fn typed(content_type: &str) -> Option<Vec<u8>> {
with_clipboard(|clipboard| {
let selection = clipboard.wait_for_contents(>k::gdk::Atom::intern(content_type))?;
Some(selection.data())
})
.flatten()
}
pub struct GdkClipboard;
impl GdkClipboard {
pub fn new() -> Self {
Self
}
}
impl Default for GdkClipboard {
fn default() -> Self {
Self::new()
}
}
impl RichClipboardBackend for GdkClipboard {
fn set_contents(&self, content: ClipboardContent) -> bool {
match &content {
ClipboardContent::Text(text) => set_text(text),
ClipboardContent::Html { html, plain } => set_html(html, plain),
ClipboardContent::Custom { content_type, data } => set_typed(content_type, data),
other => {
log::warn!(
"[linux clipboard] {} is not carried by this backend; the write is reported \
as refused rather than pretended",
other.content_type()
);
false
}
}
}
fn get_contents(&self) -> Option<ClipboardContent> {
if let Some(html) = html() {
return Some(ClipboardContent::Html { html, plain: text().unwrap_or_default() });
}
text().map(ClipboardContent::Text)
}
fn has_format(&self, content_type: &str) -> bool {
has_format(content_type)
}
fn get_contents_for_type(&self, content_type: &str) -> Option<Vec<u8>> {
match content_type {
PLAIN_TEXT => text().map(String::into_bytes),
HTML => html().map(String::into_bytes),
other => typed(other),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_non_main_thread_is_answered_instead_of_touching_gtk() {
let answer = std::thread::spawn(|| (text(), html(), has_format(PLAIN_TEXT), set_text("x")))
.join()
.expect("the probe thread must not panic");
assert_eq!(answer, (None, None, false, false));
}
}