#[cfg(feature = "image-data")]
use crate::common::ImageData;
use crate::common::{private, Error};
use objc2::{
msg_send_id,
rc::{autoreleasepool, Id},
runtime::ProtocolObject,
ClassType,
};
use objc2_app_kit::{NSPasteboard, NSPasteboardTypeHTML, NSPasteboardTypeString};
use objc2_foundation::{ns_string, NSArray, NSString};
use std::{
borrow::Cow,
panic::{RefUnwindSafe, UnwindSafe},
};
#[cfg(feature = "image-data")]
fn image_from_pixels(
pixels: Vec<u8>,
width: usize,
height: usize,
) -> Result<Id<objc2_app_kit::NSImage>, Box<dyn std::error::Error>> {
use core_graphics::{
base::{kCGBitmapByteOrderDefault, kCGImageAlphaLast, kCGRenderingIntentDefault, CGFloat},
color_space::CGColorSpace,
data_provider::{CGDataProvider, CustomData},
image::{CGImage, CGImageRef},
};
use objc2_app_kit::NSImage;
use objc2_foundation::NSSize;
use std::ffi::c_void;
#[derive(Debug)]
struct PixelArray {
data: Vec<u8>,
}
impl CustomData for PixelArray {
unsafe fn ptr(&self) -> *const u8 {
self.data.as_ptr()
}
unsafe fn len(&self) -> usize {
self.data.len()
}
}
let colorspace = CGColorSpace::create_device_rgb();
let pixel_data: Box<Box<dyn CustomData>> = Box::new(Box::new(PixelArray { data: pixels }));
let provider = unsafe { CGDataProvider::from_custom_data(pixel_data) };
let cg_image = CGImage::new(
width,
height,
8,
32,
4 * width,
&colorspace,
kCGBitmapByteOrderDefault | kCGImageAlphaLast,
&provider,
false,
kCGRenderingIntentDefault,
);
let cg_image: *const CGImageRef = &*cg_image;
let cg_image: *const c_void = cg_image.cast();
let size = NSSize { width: width as CGFloat, height: height as CGFloat };
let image: Id<NSImage> =
unsafe { msg_send_id![NSImage::alloc(), initWithCGImage: cg_image, size:size] };
Ok(image)
}
pub(crate) struct Clipboard {
pasteboard: Id<NSPasteboard>,
}
unsafe impl Send for Clipboard {}
unsafe impl Sync for Clipboard {}
impl UnwindSafe for Clipboard {}
impl RefUnwindSafe for Clipboard {}
impl Clipboard {
pub(crate) fn new() -> Result<Clipboard, Error> {
let pasteboard: Option<Id<NSPasteboard>> =
unsafe { msg_send_id![NSPasteboard::class(), generalPasteboard] };
if let Some(pasteboard) = pasteboard {
Ok(Clipboard { pasteboard })
} else {
Err(Error::ClipboardNotSupported)
}
}
fn clear(&mut self) {
unsafe { self.pasteboard.clearContents() };
}
}
pub(crate) struct Get<'clipboard> {
clipboard: &'clipboard Clipboard,
}
impl<'clipboard> Get<'clipboard> {
pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
Self { clipboard }
}
pub(crate) fn text(self) -> Result<String, Error> {
autoreleasepool(|_| {
let contents =
unsafe { self.clipboard.pasteboard.pasteboardItems() }.ok_or_else(|| {
Error::Unknown {
description: String::from("NSPasteboard#pasteboardItems errored"),
}
})?;
for item in contents {
if let Some(string) = unsafe { item.stringForType(NSPasteboardTypeString) } {
return Ok(string.to_string());
}
}
Err(Error::ContentNotAvailable)
})
}
#[cfg(feature = "image-data")]
pub(crate) fn image(self) -> Result<ImageData<'static>, Error> {
use objc2_app_kit::NSPasteboardTypeTIFF;
use std::io::Cursor;
let image = autoreleasepool(|_| {
let image_data = unsafe { self.clipboard.pasteboard.dataForType(NSPasteboardTypeTIFF) }
.ok_or(Error::ContentNotAvailable)?;
let data = Cursor::new(image_data.bytes());
let reader = image::io::Reader::with_format(data, image::ImageFormat::Tiff);
reader.decode().map_err(|_| Error::ConversionFailure)
})?;
let rgba = image.into_rgba8();
let (width, height) = rgba.dimensions();
Ok(ImageData {
width: width as usize,
height: height as usize,
bytes: rgba.into_raw().into(),
})
}
}
pub(crate) struct Set<'clipboard> {
clipboard: &'clipboard mut Clipboard,
exclude_from_history: bool,
}
impl<'clipboard> Set<'clipboard> {
pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
Self { clipboard, exclude_from_history: false }
}
pub(crate) fn text(self, data: Cow<'_, str>) -> Result<(), Error> {
self.clipboard.clear();
let string_array =
NSArray::from_vec(vec![ProtocolObject::from_id(NSString::from_str(&data))]);
let success = unsafe { self.clipboard.pasteboard.writeObjects(&string_array) };
add_clipboard_exclusions(self.clipboard, self.exclude_from_history);
if success {
Ok(())
} else {
Err(Error::Unknown { description: "NSPasteboard#writeObjects: returned false".into() })
}
}
pub(crate) fn html(self, html: Cow<'_, str>, alt: Option<Cow<'_, str>>) -> Result<(), Error> {
self.clipboard.clear();
let html = format!(
r#"<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"></head><body>{html}</body></html>"#,
);
let html_nss = NSString::from_str(&html);
let mut success =
unsafe { self.clipboard.pasteboard.setString_forType(&html_nss, NSPasteboardTypeHTML) };
if success {
if let Some(alt_text) = alt {
let alt_nss = NSString::from_str(&alt_text);
success = unsafe {
self.clipboard.pasteboard.setString_forType(&alt_nss, NSPasteboardTypeString)
};
}
}
add_clipboard_exclusions(self.clipboard, self.exclude_from_history);
if success {
Ok(())
} else {
Err(Error::Unknown { description: "NSPasteboard#writeObjects: returned false".into() })
}
}
#[cfg(feature = "image-data")]
pub(crate) fn image(self, data: ImageData) -> Result<(), Error> {
let pixels = data.bytes.into();
let image = image_from_pixels(pixels, data.width, data.height)
.map_err(|_| Error::ConversionFailure)?;
self.clipboard.clear();
let image_array = NSArray::from_vec(vec![ProtocolObject::from_id(image)]);
let success = unsafe { self.clipboard.pasteboard.writeObjects(&image_array) };
add_clipboard_exclusions(self.clipboard, self.exclude_from_history);
if success {
Ok(())
} else {
Err(Error::Unknown {
description:
"Failed to write the image to the pasteboard (`writeObjects` returned NO)."
.into(),
})
}
}
}
pub(crate) struct Clear<'clipboard> {
clipboard: &'clipboard mut Clipboard,
}
impl<'clipboard> Clear<'clipboard> {
pub(crate) fn new(clipboard: &'clipboard mut Clipboard) -> Self {
Self { clipboard }
}
pub(crate) fn clear(self) -> Result<(), Error> {
self.clipboard.clear();
Ok(())
}
}
fn add_clipboard_exclusions(clipboard: &mut Clipboard, exclude_from_history: bool) {
if exclude_from_history {
unsafe {
clipboard
.pasteboard
.setString_forType(ns_string!(""), ns_string!("org.nspasteboard.ConcealedType"));
}
}
}
pub trait SetExtApple: private::Sealed {
fn exclude_from_history(self) -> Self;
}
impl SetExtApple for crate::Set<'_> {
fn exclude_from_history(mut self) -> Self {
self.platform.exclude_from_history = true;
self
}
}