#![warn(unreachable_pub)]
mod common;
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
pub use common::Error;
#[cfg(feature = "image-data")]
pub use common::ImageData;
mod platform;
#[cfg(all(
unix,
not(any(target_os = "macos", target_os = "android", target_os = "emscripten")),
))]
pub use platform::{ClearExtLinux, GetExtLinux, LinuxClipboardKind, SetExtLinux};
#[cfg(windows)]
pub use platform::SetExtWindows;
#[cfg(target_os = "macos")]
pub use platform::SetExtApple;
#[allow(rustdoc::broken_intra_doc_links)]
pub struct Clipboard {
pub(crate) platform: platform::Clipboard,
}
impl Clipboard {
pub fn new() -> Result<Self, Error> {
Ok(Clipboard { platform: platform::Clipboard::new()? })
}
pub fn get_text(&mut self) -> Result<String, Error> {
self.get().text()
}
pub fn set_text<'a, T: Into<Cow<'a, str>>>(&mut self, text: T) -> Result<(), Error> {
self.set().text(text)
}
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)
}
#[cfg(feature = "image-data")]
pub fn get_image(&mut self) -> Result<ImageData<'static>, Error> {
self.get().image()
}
#[cfg(feature = "image-data")]
pub fn set_image(&mut self, image: ImageData) -> Result<(), Error> {
self.set().image(image)
}
pub fn clear(&mut self) -> Result<(), Error> {
self.clear_with().default()
}
pub fn clear_with(&mut self) -> Clear<'_> {
Clear { platform: platform::Clear::new(&mut self.platform) }
}
pub fn get(&mut self) -> Get<'_> {
Get { platform: platform::Get::new(&mut self.platform) }
}
pub fn set(&mut self) -> Set<'_> {
Set { platform: platform::Set::new(&mut self.platform) }
}
}
#[must_use]
pub struct Get<'clipboard> {
pub(crate) platform: platform::Get<'clipboard>,
}
impl Get<'_> {
pub fn text(self) -> Result<String, Error> {
self.platform.text()
}
#[cfg(feature = "image-data")]
pub fn image(self) -> Result<ImageData<'static>, Error> {
self.platform.image()
}
pub fn html(self) -> Result<String, Error> {
self.platform.html()
}
pub fn file_list(self) -> Result<Vec<PathBuf>, Error> {
self.platform.file_list()
}
}
#[must_use]
pub struct Set<'clipboard> {
pub(crate) platform: platform::Set<'clipboard>,
}
impl Set<'_> {
pub fn text<'a, T: Into<Cow<'a, str>>>(self, text: T) -> Result<(), Error> {
let text = text.into();
self.platform.text(text)
}
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)
}
#[cfg(feature = "image-data")]
pub fn image(self, image: ImageData) -> Result<(), Error> {
self.platform.image(image)
}
pub fn file_list(self, file_list: &[impl AsRef<Path>]) -> Result<(), Error> {
self.platform.file_list(file_list)
}
}
#[must_use]
pub struct Clear<'clipboard> {
pub(crate) platform: platform::Clear<'clipboard>,
}
impl Clear<'_> {
pub fn default(self) -> Result<(), Error> {
self.platform.clear()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{sync::Arc, thread, time::Duration};
#[test]
fn all_tests() {
let _ = env_logger::builder().is_test(true).try_init();
{
let mut ctx = Clipboard::new().unwrap();
let text = "some string";
ctx.set_text(text).unwrap();
assert_eq!(ctx.get_text().unwrap(), text);
drop(ctx);
thread::sleep(Duration::from_millis(300));
let mut ctx = Clipboard::new().unwrap();
assert_eq!(ctx.get_text().unwrap(), text);
}
{
let mut ctx = Clipboard::new().unwrap();
let text = "Some utf8: 🤓 ∑φ(n)<ε 🐔";
ctx.set_text(text).unwrap();
assert_eq!(ctx.get_text().unwrap(), text);
}
{
let mut ctx = Clipboard::new().unwrap();
let text = "hello world";
ctx.set_text(text).unwrap();
assert_eq!(ctx.get_text().unwrap(), text);
ctx.clear().unwrap();
match ctx.get_text() {
Ok(text) => assert!(text.is_empty()),
Err(Error::ContentNotAvailable) => {}
Err(e) => panic!("unexpected error: {e}"),
};
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);
}
{
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") {
let content = ctx.get().html().unwrap();
assert!(content.ends_with(&format!("{html}</body></html>")));
} else {
assert_eq!(ctx.get().html().unwrap(), html);
}
}
{
let mut ctx = Clipboard::new().unwrap();
let this_dir = env!("CARGO_MANIFEST_DIR");
let paths = &[
PathBuf::from(this_dir).join("README.md"),
PathBuf::from(this_dir).join("Cargo.toml"),
];
ctx.set().file_list(paths).unwrap();
assert_eq!(ctx.get().file_list().unwrap().as_slice(), paths);
}
#[cfg(feature = "image-data")]
{
let mut ctx = Clipboard::new().unwrap();
#[rustfmt::skip]
let bytes = [
255, 100, 100, 255,
100, 255, 100, 100,
100, 100, 255, 100,
0, 0, 0, 255,
];
let img_data = ImageData { width: 2, height: 2, bytes: bytes.as_ref().into() };
ctx.set_image(img_data.clone()).unwrap();
assert!(matches!(ctx.get_text(), Err(Error::ContentNotAvailable)));
ctx.set_text("clipboard test").unwrap();
assert!(matches!(ctx.get_image(), Err(Error::ContentNotAvailable)));
ctx.set_image(img_data.clone()).unwrap();
let got = ctx.get_image().unwrap();
assert_eq!(img_data.bytes, got.bytes);
#[rustfmt::skip]
let big_bytes = vec![
255, 100, 100, 255,
100, 255, 100, 100,
100, 100, 255, 100,
0, 1, 2, 255,
0, 1, 2, 255,
0, 1, 2, 255,
];
let bytes_cloned = big_bytes.clone();
let big_img_data = ImageData { width: 3, height: 2, bytes: big_bytes.into() };
ctx.set_image(big_img_data).unwrap();
let got = ctx.get_image().unwrap();
assert_eq!(bytes_cloned.as_slice(), got.bytes.as_ref());
}
#[cfg(all(
unix,
not(any(target_os = "macos", target_os = "android", target_os = "emscripten")),
))]
{
use crate::{LinuxClipboardKind, SetExtLinux};
use std::sync::atomic::{self, AtomicBool};
let mut ctx = Clipboard::new().unwrap();
const TEXT1: &str = "I'm a little teapot,";
const TEXT2: &str = "short and stout,";
const TEXT3: &str = "here is my handle";
ctx.set().clipboard(LinuxClipboardKind::Clipboard).text(TEXT1.to_string()).unwrap();
ctx.set().clipboard(LinuxClipboardKind::Primary).text(TEXT2.to_string()).unwrap();
if !cfg!(feature = "wayland-data-control")
|| std::env::var_os("WAYLAND_DISPLAY").is_none()
{
ctx.set().clipboard(LinuxClipboardKind::Secondary).text(TEXT3.to_string()).unwrap();
}
assert_eq!(TEXT1, &ctx.get().clipboard(LinuxClipboardKind::Clipboard).text().unwrap());
assert_eq!(TEXT2, &ctx.get().clipboard(LinuxClipboardKind::Primary).text().unwrap());
if !cfg!(feature = "wayland-data-control")
|| std::env::var_os("WAYLAND_DISPLAY").is_none()
{
assert_eq!(
TEXT3,
&ctx.get().clipboard(LinuxClipboardKind::Secondary).text().unwrap()
);
}
let was_replaced = Arc::new(AtomicBool::new(false));
let setter = thread::spawn({
let was_replaced = was_replaced.clone();
move || {
thread::sleep(Duration::from_millis(100));
let mut ctx = Clipboard::new().unwrap();
ctx.set_text("replacement text".to_owned()).unwrap();
was_replaced.store(true, atomic::Ordering::Release);
}
});
ctx.set().wait().text("initial text".to_owned()).unwrap();
assert!(was_replaced.load(atomic::Ordering::Acquire));
setter.join().unwrap();
}
}
#[test]
fn multiple_clipboards_at_once() {
const THREAD_COUNT: usize = 100;
let mut handles = Vec::with_capacity(THREAD_COUNT);
let barrier = Arc::new(std::sync::Barrier::new(THREAD_COUNT));
for _ in 0..THREAD_COUNT {
let barrier = barrier.clone();
handles.push(thread::spawn(move || {
let _ctx = Clipboard::new().unwrap();
thread::sleep(Duration::from_millis(10));
barrier.wait();
}));
}
for thread_handle in handles {
thread_handle.join().unwrap();
}
}
#[test]
fn clipboard_trait_consistently() {
fn assert_send_sync<T: Send + Sync + 'static>() {}
assert_send_sync::<Clipboard>();
assert!(std::mem::needs_drop::<Clipboard>());
}
}