clippers/
lib.rs

1//! Cross-platform clipboard management library powered by [`clip`](https://github.com/dacap/clip).
2//!
3//! # Features
4//!
5//! * Read and write UTF-8 text to/from the clipboard
6//! * Read and write RGBA images to/from the clipboard
7//! * Clipboard clearing
8//!
9//! # Platform support
10//!
11//! | **Platform**    | Clear | Text (R) | Text (W) | Images (R) | Images (W) |
12//! |-----------------|:-----:|:--------:|:--------:|:----------:|:----------:|
13//! | **Windows**     |   ✅   |     ✅    |     ✅    |      ✅     |      ✅     |
14//! | **macOS**       |   ✅   |     ✅    |     ✅    |      ✅     |      ✅     |
15//! | **Linux (X11)** |   ✅   |     ✅    |     ✅    |      ✅     |      ✅     |
16//!
17//! ### Linux
18//!
19//! Requires the `libx11-dev`/`libX11-devel` and `libpng-dev`/`libpng-devel` packages to be installed.
20//!
21//! # Thread Safety
22//!
23//! Not all OS clipboard APIs are thread-safe, so whilst the functions in this crate do their best to be thread-safe
24//! by synchronising using an internal mutex, using other clipboard libraries or calling OS clipboard APIs directly
25//! may cause undefined behaviour.
26//!
27//! # Examples
28//!
29//! ### Reading data
30//!
31//! ```rust
32//! let mut clipboard = clippers::Clipboard::get();
33//! match clipboard.read() {
34//!     Some(clippers::ClipperData::Text(text)) => {
35//!         println!("Clipboard text: {:?}", text);
36//!     }
37//!
38//!     Some(clippers::ClipperData::Image(image)) => {
39//!         println!("Clipboard image: {}x{} RGBA", image.width(), image.height());
40//!     }
41//!
42//!     Some(data) => {
43//!         println!("Clipboard data is unknown: {data:?}");
44//!     }
45//!
46//!     None => {
47//!         println!("Clipboard is empty");
48//!     }
49//! }
50//! ```
51//!
52//! ### Writing text
53//!
54//! ```rust
55//! let mut clipboard = clippers::Clipboard::get();
56//! clipboard.write_text("Hello, world!").unwrap();
57//! assert_eq!(clipboard.read().unwrap().into_text().unwrap(), "Hello, world!");
58//! ```
59//!
60//! ### Writing an image
61//!
62//! ```rust
63//! let mut clipboard = clippers::Clipboard::get();
64//! let image = image::ImageBuffer::from_fn(8, 8, |x, y| {
65//!     if (x * y) % 2 == 0 {
66//!         image::Rgba([255, 0, 0, 255])
67//!     } else {
68//!         image::Rgba([0, 255, 0, 255])
69//!     }
70//! });
71//! clipboard.write_image(image.width(), image.height(), image.as_raw()).unwrap();
72//!
73//! let clipboard_image = clipboard.read().unwrap();
74//! assert_eq!(clipboard_image.into_image().unwrap().as_raw(), image.as_ref());
75//! ```
76
77#![deny(missing_docs)]
78#![cfg_attr(any(not(debug_assertions), doc), deny(warnings))]
79#![cfg_attr(any(not(debug_assertions), doc), deny(unused))]
80
81mod ffi;
82mod img;
83mod text;
84
85#[cfg(test)]
86mod tests;
87
88use std::{mem::MaybeUninit, os::raw::c_char};
89
90pub use img::ClipperImage;
91pub use text::ClipperText;
92
93static CLIPBOARD_LOCK: parking_lot::Mutex<()> = parking_lot::const_mutex(());
94
95#[derive(thiserror::Error, Debug)]
96/// The errors that can occur when interacting with the clipboard through this crate.
97pub enum Error {
98	#[error("generic clipboard failure")]
99	/// A generic clipboard failure. This can be caused by a variety of platform-specific reasons.
100	GenericFailure,
101
102	#[error("clipboard is locked")]
103	/// The clipboard is locked by another thread. This should never happen with correct use of the library.
104	Locked,
105}
106impl From<ffi::SetClipboardResult> for Result<(), Error> {
107	fn from(result: ffi::SetClipboardResult) -> Self {
108		match result {
109			ffi::SetClipboardResult::Ok => Ok(()),
110			ffi::SetClipboardResult::GenericFailure => Err(Error::GenericFailure),
111			ffi::SetClipboardResult::Locked => Err(Error::Locked),
112		}
113	}
114}
115
116#[derive(Debug, PartialEq, Eq)]
117#[non_exhaustive]
118/// Types of clipboard data that can be returned when reading from the clipboard.
119///
120/// Because we may add new types in the future, this enum is non-exhaustive.
121pub enum ClipperData {
122	/// UTF-8 text
123	Text(ClipperText),
124
125	/// RGBA image
126	Image(image::ImageBuffer<image::Rgba<u8>, ClipperImage>),
127}
128impl ClipperData {
129	/// If the clipboard data is text, returns a reference to the text.
130	pub fn as_text(&self) -> Option<&str> {
131		match self {
132			Self::Text(text) => Some(text.as_str()),
133			_ => None,
134		}
135	}
136
137	/// If the clipboard data is text, returns the text.
138	pub fn into_text(self) -> Option<ClipperText> {
139		match self {
140			Self::Text(text) => Some(text),
141			_ => None,
142		}
143	}
144
145	/// If the clipboard data is an image, returns a reference to the image.
146	pub fn as_image(&self) -> Option<&image::ImageBuffer<image::Rgba<u8>, ClipperImage>> {
147		match self {
148			Self::Image(image) => Some(image),
149			_ => None,
150		}
151	}
152
153	/// If the clipboard data is an image, returns the image.
154	pub fn into_image(self) -> Option<image::ImageBuffer<image::Rgba<u8>, ClipperImage>> {
155		match self {
156			Self::Image(image) => Some(image),
157			_ => None,
158		}
159	}
160}
161
162/// The clipboard interface.
163///
164/// This represents a lock on the clipboard, which is released when this is dropped.
165///
166/// With this lock, you can read, write and clear the clipboard. See the [crate-level documentation](index.html) for examples.
167pub struct Clipboard(parking_lot::MutexGuard<'static, ()>);
168impl Clipboard {
169	/// Acquires a lock to the clipboard interface.
170	///
171	/// This function will block until the clipboard is available.
172	pub fn get() -> Self {
173		Self(CLIPBOARD_LOCK.lock())
174	}
175
176	/// Tries to acquire a lock to the clipboard interface.
177	///
178	/// This function will return `None` if the clipboard is already locked.
179	pub fn try_get() -> Option<Self> {
180		CLIPBOARD_LOCK.try_lock().map(Self)
181	}
182
183	/// Returns the current clipboard data, if the format is supported.
184	pub fn read(&mut self) -> Option<ClipperData> {
185		let tagged = unsafe { ffi::clipper_get_tagged_data() };
186		match tagged.tag {
187			ffi::ClipperTag::Text => Some(ClipperData::Text(ClipperText(unsafe {
188				tagged.data.assume_init_ref().text
189			}))),
190
191			ffi::ClipperTag::Image => Some(ClipperData::Image(unsafe {
192				let data = tagged.data.assume_init_ref();
193				image::ImageBuffer::<image::Rgba<u8>, _>::from_raw(
194					data.image.width,
195					data.image.height,
196					ClipperImage(data.image),
197				)
198				.unwrap()
199			})),
200
201			ffi::ClipperTag::Empty => None,
202		}
203	}
204
205	/// Clears the current clipboard data.
206	pub fn clear(&mut self) -> Result<(), Error> {
207		unsafe {
208			ffi::clipper_set_tagged_data(ffi::TaggedClipperData {
209				tag: ffi::ClipperTag::Empty,
210				data: MaybeUninit::uninit(),
211			})
212		}
213		.into()
214	}
215
216	/// Set the current clipboard data to UTF-8 text.
217	pub fn write_text(&mut self, text: impl AsRef<str>) -> Result<(), Error> {
218		let text = text.as_ref();
219		if text.is_empty() {
220			self.clear()
221		} else {
222			unsafe {
223				ffi::clipper_set_tagged_data(ffi::TaggedClipperData {
224					tag: ffi::ClipperTag::Text,
225					data: MaybeUninit::new(ffi::ClipperData {
226						text: ffi::ClipperText {
227							text: text.as_bytes().as_ptr() as *const c_char as *mut _,
228							length: text.len(),
229						},
230					}),
231				})
232				.into()
233			}
234		}
235	}
236
237	/// Set the current clipboard data to RGBA image data in row-major order.
238	///
239	/// # Panics
240	///
241	/// This function will panic if the length of `rgba` is not equal to `width * height * 4`.
242	pub fn write_image(&mut self, width: u32, height: u32, rgba: &[u8]) -> Result<(), Error> {
243		let len = width as usize * height as usize * 4;
244		if len != rgba.len() {
245			panic!(
246				"expected an RGBA pixel slice of length {len} but got {}",
247				rgba.len()
248			);
249		}
250
251		unsafe {
252			ffi::clipper_set_tagged_data(ffi::TaggedClipperData {
253				tag: ffi::ClipperTag::Image,
254				data: MaybeUninit::new(ffi::ClipperData {
255					image: ffi::ClipperImage {
256						width,
257						height,
258						rgba: rgba.as_ptr() as *mut _,
259					},
260				}),
261			})
262			.into()
263		}
264	}
265}