use std::cell::RefCell;
use std::rc::Rc;
pub trait ClipboardBackend {
fn get_text(&mut self) -> Result<String, String>;
fn set_text(&mut self, text: &str) -> Result<(), String>;
fn has_text(&mut self) -> bool {
self.get_text().map(|s| !s.is_empty()).unwrap_or(false)
}
fn get_html(&mut self) -> Result<String, String> {
Err("unsupported".into())
}
fn set_html(&mut self, _html: &str, plain_fallback: &str) -> Result<(), String> {
self.set_text(plain_fallback)
}
fn has_html(&mut self) -> bool {
false
}
}
#[derive(Clone)]
pub struct ClipboardHandle {
inner: Rc<RefCell<dyn ClipboardBackend>>,
}
impl ClipboardHandle {
pub fn new<B: ClipboardBackend + 'static>(backend: B) -> Self {
Self {
inner: Rc::new(RefCell::new(backend)),
}
}
pub fn get_text(&self) -> Result<String, String> {
self.inner.borrow_mut().get_text()
}
pub fn set_text(&self, text: &str) -> Result<(), String> {
self.inner.borrow_mut().set_text(text)
}
pub fn has_text(&self) -> bool {
self.inner.borrow_mut().has_text()
}
pub fn get_html(&self) -> Result<String, String> {
self.inner.borrow_mut().get_html()
}
pub fn set_html(&self, html: &str, plain_fallback: &str) -> Result<(), String> {
self.inner.borrow_mut().set_html(html, plain_fallback)
}
pub fn has_html(&self) -> bool {
self.inner.borrow_mut().has_html()
}
}
impl std::fmt::Debug for ClipboardHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClipboardHandle").finish_non_exhaustive()
}
}
#[derive(Debug, Default)]
pub struct MemoryClipboard {
text: Option<String>,
html: Option<String>,
}
impl MemoryClipboard {
pub fn new() -> Self {
Self::default()
}
}
impl ClipboardBackend for MemoryClipboard {
fn get_text(&mut self) -> Result<String, String> {
Ok(self.text.clone().unwrap_or_default())
}
fn set_text(&mut self, text: &str) -> Result<(), String> {
self.text = Some(text.to_string());
self.html = None;
Ok(())
}
fn has_text(&mut self) -> bool {
self.text.as_deref().map(|s| !s.is_empty()).unwrap_or(false)
}
fn get_html(&mut self) -> Result<String, String> {
match self.html.as_deref() {
Some(h) if !h.is_empty() => Ok(h.to_string()),
_ => Err("no html payload".into()),
}
}
fn set_html(&mut self, html: &str, plain_fallback: &str) -> Result<(), String> {
self.html = Some(html.to_string());
self.text = Some(plain_fallback.to_string());
Ok(())
}
fn has_html(&mut self) -> bool {
self.html.as_deref().map(|s| !s.is_empty()).unwrap_or(false)
}
}
#[cfg(feature = "clipboard")]
pub use arboard_backend::ArboardClipboard;
#[cfg(feature = "clipboard")]
mod arboard_backend {
use super::ClipboardBackend;
use arboard::{Clipboard, Error};
pub struct ArboardClipboard {
inner: Clipboard,
}
impl ArboardClipboard {
pub fn new() -> Result<Self, String> {
Clipboard::new()
.map(|inner| Self { inner })
.map_err(|e| e.to_string())
}
}
impl ClipboardBackend for ArboardClipboard {
fn get_text(&mut self) -> Result<String, String> {
self.inner.get_text().map_err(|e| e.to_string())
}
fn set_text(&mut self, text: &str) -> Result<(), String> {
self.inner
.set_text(text.to_string())
.map_err(|e| e.to_string())
}
fn get_html(&mut self) -> Result<String, String> {
self.inner.get().html().map_err(|e| e.to_string())
}
fn set_html(&mut self, html: &str, plain_fallback: &str) -> Result<(), String> {
self.inner
.set_html(html.to_string(), Some(plain_fallback.to_string()))
.map_err(|e| e.to_string())
}
fn has_html(&mut self) -> bool {
match self.inner.get().html() {
Ok(s) => !s.is_empty(),
Err(Error::ContentNotAvailable) => false,
Err(_) => false,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn memory_backend_roundtrip() {
let handle = ClipboardHandle::new(MemoryClipboard::new());
assert!(!handle.has_text());
handle.set_text("hello").unwrap();
assert!(handle.has_text());
assert_eq!(handle.get_text().unwrap(), "hello");
handle.set_text("").unwrap();
assert!(!handle.has_text());
}
#[test]
fn handle_is_cloneable_and_shares_state() {
let a = ClipboardHandle::new(MemoryClipboard::new());
let b = a.clone();
a.set_text("shared").unwrap();
assert_eq!(b.get_text().unwrap(), "shared");
}
#[test]
fn memory_backend_html_roundtrip() {
let handle = ClipboardHandle::new(MemoryClipboard::new());
assert!(!handle.has_html(), "empty clipboard has no html");
handle.set_html("<p>a</p>", "a").unwrap();
assert!(handle.has_html(), "set_html must flip has_html");
assert_eq!(handle.get_html().unwrap(), "<p>a</p>");
assert_eq!(
handle.get_text().unwrap(),
"a",
"set_html must also install the plain-text alternative"
);
}
#[test]
fn memory_backend_set_text_invalidates_html() {
let handle = ClipboardHandle::new(MemoryClipboard::new());
handle.set_html("<b>old</b>", "old").unwrap();
assert!(handle.has_html());
handle.set_text("new").unwrap();
assert!(
!handle.has_html(),
"plain-text overwrite must invalidate stale html"
);
assert!(handle.get_html().is_err());
}
#[test]
fn handle_html_shared_state() {
let a = ClipboardHandle::new(MemoryClipboard::new());
let b = a.clone();
a.set_html("<p>shared</p>", "shared").unwrap();
assert_eq!(b.get_html().unwrap(), "<p>shared</p>");
assert_eq!(b.get_text().unwrap(), "shared");
}
#[test]
fn default_set_html_falls_back_to_plain_text() {
struct PlainOnly {
text: Option<String>,
}
impl ClipboardBackend for PlainOnly {
fn get_text(&mut self) -> Result<String, String> {
Ok(self.text.clone().unwrap_or_default())
}
fn set_text(&mut self, text: &str) -> Result<(), String> {
self.text = Some(text.to_string());
Ok(())
}
}
let handle = ClipboardHandle::new(PlainOnly { text: None });
assert!(!handle.has_html());
assert!(handle.get_html().is_err());
handle.set_html("<p>ignored</p>", "fallback").unwrap();
assert_eq!(handle.get_text().unwrap(), "fallback");
assert!(!handle.has_html(), "plain-only backend never reports html");
}
}