#![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)]
pub enum Error {
#[error("generic clipboard failure")]
GenericFailure,
#[error("clipboard is locked")]
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]
pub enum ClipperData {
Text(ClipperText),
Image(image::ImageBuffer<image::Rgba<u8>, ClipperImage>),
}
impl ClipperData {
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(text) => Some(text.as_str()),
_ => None,
}
}
pub fn into_text(self) -> Option<ClipperText> {
match self {
Self::Text(text) => Some(text),
_ => None,
}
}
pub fn as_image(&self) -> Option<&image::ImageBuffer<image::Rgba<u8>, ClipperImage>> {
match self {
Self::Image(image) => Some(image),
_ => None,
}
}
pub fn into_image(self) -> Option<image::ImageBuffer<image::Rgba<u8>, ClipperImage>> {
match self {
Self::Image(image) => Some(image),
_ => None,
}
}
}
pub struct Clipboard(parking_lot::MutexGuard<'static, ()>);
impl Clipboard {
pub fn get() -> Self {
Self(CLIPBOARD_LOCK.lock())
}
pub fn try_get() -> Option<Self> {
CLIPBOARD_LOCK.try_lock().map(Self)
}
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,
}
}
pub fn clear(&mut self) -> Result<(), Error> {
unsafe {
ffi::clipper_set_tagged_data(ffi::TaggedClipperData {
tag: ffi::ClipperTag::Empty,
data: MaybeUninit::uninit(),
})
}
.into()
}
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()
}
}
}
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()
}
}
}