clippers 0.1.2

Cross-platform clipboard management library
Documentation
//! Cross-platform clipboard management library powered by [`clip`](https://github.com/dacap/clip).
//!
//! # Features
//!
//! * Read and write UTF-8 text to/from the clipboard
//! * Read and write RGBA images to/from the clipboard
//! * Clipboard clearing
//!
//! # Platform support
//!
//! | **Platform**    | Clear | Text (R) | Text (W) | Images (R) | Images (W) |
//! |-----------------|:-----:|:--------:|:--------:|:----------:|:----------:|
//! | **Windows**     |   ✅   |     ✅    |     ✅    |      ✅     |      ✅     |
//! | **macOS**       |   ✅   |     ✅    |     ✅    |      ✅     |      ✅     |
//! | **Linux (X11)** |   ✅   |     ✅    |     ✅    |      ✅     |      ✅     |
//!
//! ### Linux
//!
//! Requires the `libx11-dev`/`libX11-devel` and `libpng-dev`/`libpng-devel` packages to be installed.
//!
//! # Thread Safety
//!
//! Not all OS clipboard APIs are thread-safe, so whilst the functions in this crate do their best to be thread-safe
//! by synchronising using an internal mutex, using other clipboard libraries or calling OS clipboard APIs directly
//! may cause undefined behaviour.
//!
//! # Examples
//!
//! ### Reading data
//!
//! ```rust
//! let mut clipboard = clippers::Clipboard::get();
//! match clipboard.read() {
//!     Some(clippers::ClipperData::Text(text)) => {
//!         println!("Clipboard text: {:?}", text);
//!     }
//!
//!     Some(clippers::ClipperData::Image(image)) => {
//!         println!("Clipboard image: {}x{} RGBA", image.width(), image.height());
//!     }
//!
//!     Some(data) => {
//!         println!("Clipboard data is unknown: {data:?}");
//!     }
//!
//!     None => {
//!         println!("Clipboard is empty");
//!     }
//! }
//! ```
//!
//! ### Writing text
//!
//! ```rust
//! let mut clipboard = clippers::Clipboard::get();
//! clipboard.write_text("Hello, world!").unwrap();
//! assert_eq!(clipboard.read().unwrap().into_text().unwrap(), "Hello, world!");
//! ```
//!
//! ### Writing an image
//!
//! ```rust
//! let mut clipboard = clippers::Clipboard::get();
//! let image = image::ImageBuffer::from_fn(8, 8, |x, y| {
//!     if (x * y) % 2 == 0 {
//!         image::Rgba([255, 0, 0, 255])
//!     } else {
//!         image::Rgba([0, 255, 0, 255])
//!     }
//! });
//! clipboard.write_image(image.width(), image.height(), image.as_raw()).unwrap();
//!
//! let clipboard_image = clipboard.read().unwrap();
//! assert_eq!(clipboard_image.into_image().unwrap().as_raw(), image.as_ref());
//! ```

#![deny(missing_docs)]
#![cfg_attr(any(not(debug_assertions), doc), deny(warnings))]
#![cfg_attr(any(not(debug_assertions), doc), deny(unused))]

mod ffi;
mod img;
mod text;

#[cfg(test)]
mod tests;

use std::{mem::MaybeUninit, os::raw::c_char};

pub use img::ClipperImage;
pub use text::ClipperText;

static CLIPBOARD_LOCK: parking_lot::Mutex<()> = parking_lot::const_mutex(());

#[derive(thiserror::Error, Debug)]
/// The errors that can occur when interacting with the clipboard through this crate.
pub enum Error {
	#[error("generic clipboard failure")]
	/// A generic clipboard failure. This can be caused by a variety of platform-specific reasons.
	GenericFailure,

	#[error("clipboard is locked")]
	/// The clipboard is locked by another thread. This should never happen with correct use of the library.
	Locked,
}
impl From<ffi::SetClipboardResult> for Result<(), Error> {
	fn from(result: ffi::SetClipboardResult) -> Self {
		match result {
			ffi::SetClipboardResult::Ok => Ok(()),
			ffi::SetClipboardResult::GenericFailure => Err(Error::GenericFailure),
			ffi::SetClipboardResult::Locked => Err(Error::Locked),
		}
	}
}

#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
/// Types of clipboard data that can be returned when reading from the clipboard.
///
/// Because we may add new types in the future, this enum is non-exhaustive.
pub enum ClipperData {
	/// UTF-8 text
	Text(ClipperText),

	/// RGBA image
	Image(image::ImageBuffer<image::Rgba<u8>, ClipperImage>),
}
impl ClipperData {
	/// If the clipboard data is text, returns a reference to the text.
	pub fn as_text(&self) -> Option<&str> {
		match self {
			Self::Text(text) => Some(text.as_str()),
			_ => None,
		}
	}

	/// If the clipboard data is text, returns the text.
	pub fn into_text(self) -> Option<ClipperText> {
		match self {
			Self::Text(text) => Some(text),
			_ => None,
		}
	}

	/// If the clipboard data is an image, returns a reference to the image.
	pub fn as_image(&self) -> Option<&image::ImageBuffer<image::Rgba<u8>, ClipperImage>> {
		match self {
			Self::Image(image) => Some(image),
			_ => None,
		}
	}

	/// If the clipboard data is an image, returns the image.
	pub fn into_image(self) -> Option<image::ImageBuffer<image::Rgba<u8>, ClipperImage>> {
		match self {
			Self::Image(image) => Some(image),
			_ => None,
		}
	}
}

/// The clipboard interface.
///
/// This represents a lock on the clipboard, which is released when this is dropped.
///
/// With this lock, you can read, write and clear the clipboard. See the [crate-level documentation](index.html) for examples.
pub struct Clipboard(parking_lot::MutexGuard<'static, ()>);
impl Clipboard {
	/// Acquires a lock to the clipboard interface.
	///
	/// This function will block until the clipboard is available.
	pub fn get() -> Self {
		Self(CLIPBOARD_LOCK.lock())
	}

	/// Tries to acquire a lock to the clipboard interface.
	///
	/// This function will return `None` if the clipboard is already locked.
	pub fn try_get() -> Option<Self> {
		CLIPBOARD_LOCK.try_lock().map(Self)
	}

	/// Returns the current clipboard data, if the format is supported.
	pub fn read(&mut self) -> Option<ClipperData> {
		let tagged = unsafe { ffi::clipper_get_tagged_data() };
		match tagged.tag {
			ffi::ClipperTag::Text => Some(ClipperData::Text(ClipperText(unsafe {
				tagged.data.assume_init_ref().text
			}))),

			ffi::ClipperTag::Image => Some(ClipperData::Image(unsafe {
				let data = tagged.data.assume_init_ref();
				image::ImageBuffer::<image::Rgba<u8>, _>::from_raw(
					data.image.width,
					data.image.height,
					ClipperImage(data.image),
				)
				.unwrap()
			})),

			ffi::ClipperTag::Empty => None,
		}
	}

	/// Clears the current clipboard data.
	pub fn clear(&mut self) -> Result<(), Error> {
		unsafe {
			ffi::clipper_set_tagged_data(ffi::TaggedClipperData {
				tag: ffi::ClipperTag::Empty,
				data: MaybeUninit::uninit(),
			})
		}
		.into()
	}

	/// Set the current clipboard data to UTF-8 text.
	pub fn write_text(&mut self, text: impl AsRef<str>) -> Result<(), Error> {
		let text = text.as_ref();
		if text.is_empty() {
			self.clear()
		} else {
			unsafe {
				ffi::clipper_set_tagged_data(ffi::TaggedClipperData {
					tag: ffi::ClipperTag::Text,
					data: MaybeUninit::new(ffi::ClipperData {
						text: ffi::ClipperText {
							text: text.as_bytes().as_ptr() as *const c_char as *mut _,
							length: text.len(),
						},
					}),
				})
				.into()
			}
		}
	}

	/// Set the current clipboard data to RGBA image data in row-major order.
	///
	/// # Panics
	///
	/// This function will panic if the length of `rgba` is not equal to `width * height * 4`.
	pub fn write_image(&mut self, width: u32, height: u32, rgba: &[u8]) -> Result<(), Error> {
		let len = width as usize * height as usize * 4;
		if len != rgba.len() {
			panic!(
				"expected an RGBA pixel slice of length {len} but got {}",
				rgba.len()
			);
		}

		unsafe {
			ffi::clipper_set_tagged_data(ffi::TaggedClipperData {
				tag: ffi::ClipperTag::Image,
				data: MaybeUninit::new(ffi::ClipperData {
					image: ffi::ClipperImage {
						width,
						height,
						rgba: rgba.as_ptr() as *mut _,
					},
				}),
			})
			.into()
		}
	}
}