#[cfg(feature = "image-data")]
use crate::common::ImageData;
use crate::common::{private, Error};
use objc2::{
msg_send,
rc::{autoreleasepool, Retained},
runtime::ProtocolObject,
ClassType,
};
use objc2_app_kit::{
NSPasteboard, NSPasteboardTypeHTML, NSPasteboardTypeString,
NSPasteboardURLReadingFileURLsOnlyKey,
};
use objc2_foundation::{ns_string, NSArray, NSDictionary, NSNumber, NSString, NSURL};
use std::{
borrow::Cow,
panic::{RefUnwindSafe, UnwindSafe},
path::{Path, PathBuf},
};
#[cfg(feature = "image-data")]
fn image_from_pixels(
pixels: Vec<u8>,
width: usize,
height: usize,
) -> Retained<objc2_app_kit::NSImage> {
use objc2::AllocAnyThread;
use objc2_app_kit::NSImage;
use objc2_core_foundation::CGFloat;
use objc2_core_graphics::{
CGBitmapInfo, CGColorRenderingIntent, CGColorSpaceCreateDeviceRGB,
CGDataProviderCreateWithData, CGImageAlphaInfo, CGImageCreate,
};
use objc2_foundation::NSSize;
use std::{
ffi::c_void,
ptr::{self, NonNull},
};
unsafe extern "C-unwind" fn release(_info: *mut c_void, data: NonNull<c_void>, size: usize) {
let data = data.cast::<u8>();
let slice = NonNull::slice_from_raw_parts(data, size);
drop(unsafe { Box::from_raw(slice.as_ptr()) })
}
let provider = {
let pixels = pixels.into_boxed_slice();
let len = pixels.len();
let pixels: *mut [u8] = Box::into_raw(pixels);
let data_ptr = pixels.cast::<c_void>();
unsafe { CGDataProviderCreateWithData(ptr::null_mut(), data_ptr, len, Some(release)) }
}
.unwrap();
let colorspace = unsafe { CGColorSpaceCreateDeviceRGB() }.unwrap();
let cg_image = unsafe {
CGImageCreate(
width,
height,
8,
32,
4 * width,
Some(&colorspace),
CGBitmapInfo::ByteOrderDefault | CGBitmapInfo(CGImageAlphaInfo::Last.0),
Some(&provider),
ptr::null_mut(),
false,
CGColorRenderingIntent::RenderingIntentDefault,
)
}
.unwrap();
let size = NSSize { width: width as CGFloat, height: height as CGFloat };
unsafe { NSImage::initWithCGImage_size(NSImage::alloc(), &cg_image, size) }
}
pub(crate) struct Clipboard {
pasteboard: Retained<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<Retained<NSPasteboard>> =
unsafe { msg_send![NSPasteboard::class(), generalPasteboard] };
if let Some(pasteboard) = pasteboard {
Ok(Clipboard { pasteboard })
} else {
Err(Error::ClipboardNotSupported)
}
}
fn clear(&mut self) {
unsafe { self.pasteboard.clearContents() };
}
fn string_from_type(&self, type_: &'static NSString) -> Result<String, Error> {
autoreleasepool(|_| {
let contents = unsafe { self.pasteboard.pasteboardItems() }
.ok_or_else(|| Error::unknown("NSPasteboard#pasteboardItems errored"))?;
for item in contents {
if let Some(string) = unsafe { item.stringForType(type_) } {
return Ok(string.to_string());
}
}
Err(Error::ContentNotAvailable)
})
}
}
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> {
unsafe { self.clipboard.string_from_type(NSPasteboardTypeString) }
}
pub(crate) fn html(self) -> Result<String, Error> {
unsafe { self.clipboard.string_from_type(NSPasteboardTypeHTML) }
}
#[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(unsafe { image_data.as_bytes_unchecked() });
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) fn file_list(self) -> Result<Vec<PathBuf>, Error> {
autoreleasepool(|_| {
let class_array = NSArray::from_slice(&[NSURL::class()]);
let options = NSDictionary::from_slices(
&[unsafe { NSPasteboardURLReadingFileURLsOnlyKey }],
&[NSNumber::new_bool(true).as_ref()],
);
let objects = unsafe {
self.clipboard
.pasteboard
.readObjectsForClasses_options(&class_array, Some(&options))
};
objects
.map(|array| {
array
.iter()
.filter_map(|obj| {
obj.downcast::<NSURL>().ok().and_then(|url| {
unsafe { url.path() }.map(|p| PathBuf::from(p.to_string()))
})
})
.collect::<Vec<_>>()
})
.filter(|file_list| !file_list.is_empty())
.ok_or(Error::ContentNotAvailable)
})
}
}
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_retained_slice(&[ProtocolObject::from_retained(
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("NSPasteboard#writeObjects: returned false"))
}
}
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("NSPasteboard#writeObjects: returned false"))
}
}
#[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);
self.clipboard.clear();
let image_array = NSArray::from_retained_slice(&[ProtocolObject::from_retained(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(
"Failed to write the image to the pasteboard (`writeObjects` returned NO).",
))
}
}
pub(crate) fn file_list(self, file_list: &[impl AsRef<Path>]) -> Result<(), Error> {
self.clipboard.clear();
let uri_list = file_list
.iter()
.filter_map(|path| {
path.as_ref().canonicalize().ok().and_then(|abs_path| {
abs_path.to_str().map(|str| {
let url = unsafe { NSURL::fileURLWithPath(&NSString::from_str(str)) };
ProtocolObject::from_retained(url)
})
})
})
.collect::<Vec<_>>();
if uri_list.is_empty() {
return Err(Error::ConversionFailure);
}
let objects = NSArray::from_retained_slice(&uri_list);
let success = unsafe { self.clipboard.pasteboard.writeObjects(&objects) };
add_clipboard_exclusions(self.clipboard, self.exclude_from_history);
if success {
Ok(())
} else {
Err(Error::unknown("NSPasteboard#writeObjects: returned false"))
}
}
}
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
}
}