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}