1#![warn(unreachable_pub)]
11
12mod common;
13use std::{borrow::Cow, path::PathBuf};
14
15pub use common::Error;
16#[cfg(feature = "image-data")]
17pub use common::ImageData;
18
19mod platform;
20
21#[cfg(all(
22 unix,
23 not(any(target_os = "macos", target_os = "android", target_os = "emscripten")),
24))]
25pub use platform::{ClearExtLinux, GetExtLinux, LinuxClipboardKind, SetExtLinux};
26
27#[cfg(windows)]
28pub use platform::SetExtWindows;
29
30#[cfg(target_os = "macos")]
31pub use platform::SetExtApple;
32
33#[allow(rustdoc::broken_intra_doc_links)]
67pub struct Clipboard {
68 pub(crate) platform: platform::Clipboard,
69}
70
71impl Clipboard {
72 pub fn new() -> Result<Self, Error> {
79 Ok(Clipboard { platform: platform::Clipboard::new()? })
80 }
81
82 pub fn get_text(&mut self) -> Result<String, Error> {
88 self.get().text()
89 }
90
91 pub fn set_text<'a, T: Into<Cow<'a, str>>>(&mut self, text: T) -> Result<(), Error> {
97 self.set().text(text)
98 }
99
100 pub fn set_html<'a, T: Into<Cow<'a, str>>>(
108 &mut self,
109 html: T,
110 alt_text: Option<T>,
111 ) -> Result<(), Error> {
112 self.set().html(html, alt_text)
113 }
114
115 #[cfg(feature = "image-data")]
126 pub fn get_image(&mut self) -> Result<ImageData<'static>, Error> {
127 self.get().image()
128 }
129
130 #[cfg(feature = "image-data")]
143 pub fn set_image(&mut self, image: ImageData) -> Result<(), Error> {
144 self.set().image(image)
145 }
146
147 pub fn clear(&mut self) -> Result<(), Error> {
154 self.clear_with().default()
155 }
156
157 pub fn clear_with(&mut self) -> Clear<'_> {
159 Clear { platform: platform::Clear::new(&mut self.platform) }
160 }
161
162 pub fn get(&mut self) -> Get<'_> {
164 Get { platform: platform::Get::new(&mut self.platform) }
165 }
166
167 pub fn set(&mut self) -> Set<'_> {
169 Set { platform: platform::Set::new(&mut self.platform) }
170 }
171}
172
173#[must_use]
175pub struct Get<'clipboard> {
176 pub(crate) platform: platform::Get<'clipboard>,
177}
178
179impl Get<'_> {
180 pub fn text(self) -> Result<String, Error> {
182 self.platform.text()
183 }
184
185 #[cfg(feature = "image-data")]
192 pub fn image(self) -> Result<ImageData<'static>, Error> {
193 self.platform.image()
194 }
195
196 pub fn html(self) -> Result<String, Error> {
198 self.platform.html()
199 }
200
201 pub fn file_list(self) -> Result<Vec<PathBuf>, Error> {
203 self.platform.file_list()
204 }
205}
206
207#[must_use]
209pub struct Set<'clipboard> {
210 pub(crate) platform: platform::Set<'clipboard>,
211}
212
213impl Set<'_> {
214 pub fn text<'a, T: Into<Cow<'a, str>>>(self, text: T) -> Result<(), Error> {
217 let text = text.into();
218 self.platform.text(text)
219 }
220
221 pub fn html<'a, T: Into<Cow<'a, str>>>(
226 self,
227 html: T,
228 alt_text: Option<T>,
229 ) -> Result<(), Error> {
230 let html = html.into();
231 let alt_text = alt_text.map(|e| e.into());
232 self.platform.html(html, alt_text)
233 }
234
235 #[cfg(feature = "image-data")]
243 pub fn image(self, image: ImageData) -> Result<(), Error> {
244 self.platform.image(image)
245 }
246}
247
248#[must_use]
250pub struct Clear<'clipboard> {
251 pub(crate) platform: platform::Clear<'clipboard>,
252}
253
254impl Clear<'_> {
255 pub fn default(self) -> Result<(), Error> {
258 self.platform.clear()
259 }
260}
261
262#[cfg(test)]
265mod tests {
266 use super::*;
267 use std::{sync::Arc, thread, time::Duration};
268
269 #[test]
270 fn all_tests() {
271 let _ = env_logger::builder().is_test(true).try_init();
272 {
273 let mut ctx = Clipboard::new().unwrap();
274 let text = "some string";
275 ctx.set_text(text).unwrap();
276 assert_eq!(ctx.get_text().unwrap(), text);
277
278 drop(ctx);
281
282 thread::sleep(Duration::from_millis(300));
286
287 let mut ctx = Clipboard::new().unwrap();
288 assert_eq!(ctx.get_text().unwrap(), text);
289 }
290 {
291 let mut ctx = Clipboard::new().unwrap();
292 let text = "Some utf8: 🤓 ∑φ(n)<ε 🐔";
293 ctx.set_text(text).unwrap();
294 assert_eq!(ctx.get_text().unwrap(), text);
295 }
296 {
297 let mut ctx = Clipboard::new().unwrap();
298 let text = "hello world";
299
300 ctx.set_text(text).unwrap();
301 assert_eq!(ctx.get_text().unwrap(), text);
302
303 ctx.clear().unwrap();
304
305 match ctx.get_text() {
306 Ok(text) => assert!(text.is_empty()),
307 Err(Error::ContentNotAvailable) => {}
308 Err(e) => panic!("unexpected error: {e}"),
309 };
310
311 ctx.clear().unwrap();
313 }
314 {
315 let mut ctx = Clipboard::new().unwrap();
316 let html = "<b>hello</b> <i>world</i>!";
317
318 ctx.set_html(html, None).unwrap();
319
320 match ctx.get_text() {
321 Ok(text) => assert!(text.is_empty()),
322 Err(Error::ContentNotAvailable) => {}
323 Err(e) => panic!("unexpected error: {e}"),
324 };
325 }
326 {
327 let mut ctx = Clipboard::new().unwrap();
328
329 let html = "<b>hello</b> <i>world</i>!";
330 let alt_text = "hello world!";
331
332 ctx.set_html(html, Some(alt_text)).unwrap();
333 assert_eq!(ctx.get_text().unwrap(), alt_text);
334 }
335 {
336 let mut ctx = Clipboard::new().unwrap();
337
338 let html = "<b>hello</b> <i>world</i>!";
339
340 ctx.set().html(html, None).unwrap();
341
342 if cfg!(target_os = "macos") {
343 let content = ctx.get().html().unwrap();
348 assert!(content.ends_with(&format!("{html}</body></html>")));
349 } else {
350 assert_eq!(ctx.get().html().unwrap(), html);
351 }
352 }
353 #[cfg(feature = "image-data")]
354 {
355 let mut ctx = Clipboard::new().unwrap();
356 #[rustfmt::skip]
357 let bytes = [
358 255, 100, 100, 255,
359 100, 255, 100, 100,
360 100, 100, 255, 100,
361 0, 0, 0, 255,
362 ];
363 let img_data = ImageData { width: 2, height: 2, bytes: bytes.as_ref().into() };
364
365 ctx.set_image(img_data.clone()).unwrap();
367 assert!(matches!(ctx.get_text(), Err(Error::ContentNotAvailable)));
368
369 ctx.set_text("clipboard test").unwrap();
370 assert!(matches!(ctx.get_image(), Err(Error::ContentNotAvailable)));
371
372 ctx.set_image(img_data.clone()).unwrap();
374 let got = ctx.get_image().unwrap();
375 assert_eq!(img_data.bytes, got.bytes);
376
377 #[rustfmt::skip]
378 let big_bytes = vec![
379 255, 100, 100, 255,
380 100, 255, 100, 100,
381 100, 100, 255, 100,
382
383 0, 1, 2, 255,
384 0, 1, 2, 255,
385 0, 1, 2, 255,
386 ];
387 let bytes_cloned = big_bytes.clone();
388 let big_img_data = ImageData { width: 3, height: 2, bytes: big_bytes.into() };
389 ctx.set_image(big_img_data).unwrap();
390 let got = ctx.get_image().unwrap();
391 assert_eq!(bytes_cloned.as_slice(), got.bytes.as_ref());
392 }
393 #[cfg(all(
394 unix,
395 not(any(target_os = "macos", target_os = "android", target_os = "emscripten")),
396 ))]
397 {
398 use crate::{LinuxClipboardKind, SetExtLinux};
399 use std::sync::atomic::{self, AtomicBool};
400
401 let mut ctx = Clipboard::new().unwrap();
402
403 const TEXT1: &str = "I'm a little teapot,";
404 const TEXT2: &str = "short and stout,";
405 const TEXT3: &str = "here is my handle";
406
407 ctx.set().clipboard(LinuxClipboardKind::Clipboard).text(TEXT1.to_string()).unwrap();
408
409 ctx.set().clipboard(LinuxClipboardKind::Primary).text(TEXT2.to_string()).unwrap();
410
411 if !cfg!(feature = "wayland-data-control")
413 || std::env::var_os("WAYLAND_DISPLAY").is_none()
414 {
415 ctx.set().clipboard(LinuxClipboardKind::Secondary).text(TEXT3.to_string()).unwrap();
416 }
417
418 assert_eq!(TEXT1, &ctx.get().clipboard(LinuxClipboardKind::Clipboard).text().unwrap());
419
420 assert_eq!(TEXT2, &ctx.get().clipboard(LinuxClipboardKind::Primary).text().unwrap());
421
422 if !cfg!(feature = "wayland-data-control")
424 || std::env::var_os("WAYLAND_DISPLAY").is_none()
425 {
426 assert_eq!(
427 TEXT3,
428 &ctx.get().clipboard(LinuxClipboardKind::Secondary).text().unwrap()
429 );
430 }
431
432 let was_replaced = Arc::new(AtomicBool::new(false));
433
434 let setter = thread::spawn({
435 let was_replaced = was_replaced.clone();
436 move || {
437 thread::sleep(Duration::from_millis(100));
438 let mut ctx = Clipboard::new().unwrap();
439 ctx.set_text("replacement text".to_owned()).unwrap();
440 was_replaced.store(true, atomic::Ordering::Release);
441 }
442 });
443
444 ctx.set().wait().text("initial text".to_owned()).unwrap();
445
446 assert!(was_replaced.load(atomic::Ordering::Acquire));
447
448 setter.join().unwrap();
449 }
450 }
451
452 #[test]
455 fn multiple_clipboards_at_once() {
456 const THREAD_COUNT: usize = 100;
457
458 let mut handles = Vec::with_capacity(THREAD_COUNT);
459 let barrier = Arc::new(std::sync::Barrier::new(THREAD_COUNT));
460
461 for _ in 0..THREAD_COUNT {
462 let barrier = barrier.clone();
463 handles.push(thread::spawn(move || {
464 let _ctx = Clipboard::new().unwrap();
467
468 thread::sleep(Duration::from_millis(10));
469
470 barrier.wait();
471 }));
472 }
473
474 for thread_handle in handles {
475 thread_handle.join().unwrap();
476 }
477 }
478
479 #[test]
480 fn clipboard_trait_consistently() {
481 fn assert_send_sync<T: Send + Sync + 'static>() {}
482
483 assert_send_sync::<Clipboard>();
484 assert!(std::mem::needs_drop::<Clipboard>());
485 }
486}