use core::slice;
use std::ffi::c_void;
use cocoa::{
appkit::{NSPasteboard, NSPasteboardTypePNG, NSPasteboardTypeString, NSPasteboardTypeTIFF},
base::{id, nil},
foundation::NSData,
};
use objc::{msg_send, runtime::Object, sel, sel_impl};
use strum::IntoEnumIterator as _;
use crate::{
ClipboardEntry, ClipboardItem, ClipboardString, Image, ImageFormat, asset_cache::hash,
platform::mac::ns_string,
};
pub struct Pasteboard {
inner: id,
text_hash_type: id,
metadata_type: id,
}
impl Pasteboard {
pub fn general() -> Self {
unsafe { Self::new(NSPasteboard::generalPasteboard(nil)) }
}
pub fn find() -> Self {
unsafe { Self::new(NSPasteboard::pasteboardWithName(nil, NSPasteboardNameFind)) }
}
#[cfg(test)]
pub fn unique() -> Self {
unsafe { Self::new(NSPasteboard::pasteboardWithUniqueName(nil)) }
}
unsafe fn new(inner: id) -> Self {
Self {
inner,
text_hash_type: unsafe { ns_string("zed-text-hash") },
metadata_type: unsafe { ns_string("zed-metadata") },
}
}
pub fn read(&self) -> Option<ClipboardItem> {
unsafe {
let pasteboard_types: id = self.inner.types();
let string_type: id = ns_string("public.utf8-plain-text");
if msg_send![pasteboard_types, containsObject: string_type] {
let data = self.inner.dataForType(string_type);
if data == nil {
return None;
} else if data.bytes().is_null() {
return Some(self.read_string(&[]));
} else {
let bytes =
slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize);
return Some(self.read_string(bytes));
}
}
for format in ImageFormat::iter() {
if let Some(item) = self.read_image(format) {
return Some(item);
}
}
}
None
}
fn read_image(&self, format: ImageFormat) -> Option<ClipboardItem> {
let mut ut_type: UTType = format.into();
unsafe {
let types: id = self.inner.types();
if msg_send![types, containsObject: ut_type.inner()] {
self.data_for_type(ut_type.inner_mut()).map(|bytes| {
let bytes = bytes.to_vec();
let id = hash(&bytes);
ClipboardItem {
entries: vec![ClipboardEntry::Image(Image { format, bytes, id })],
}
})
} else {
None
}
}
}
fn read_string(&self, text_bytes: &[u8]) -> ClipboardItem {
unsafe {
let text = String::from_utf8_lossy(text_bytes).to_string();
let metadata = self
.data_for_type(self.text_hash_type)
.and_then(|hash_bytes| {
let hash_bytes = hash_bytes.try_into().ok()?;
let hash = u64::from_be_bytes(hash_bytes);
let metadata = self.data_for_type(self.metadata_type)?;
if hash == ClipboardString::text_hash(&text) {
String::from_utf8(metadata.to_vec()).ok()
} else {
None
}
});
ClipboardItem {
entries: vec![ClipboardEntry::String(ClipboardString { text, metadata })],
}
}
}
unsafe fn data_for_type(&self, kind: id) -> Option<&[u8]> {
unsafe {
let data = self.inner.dataForType(kind);
if data == nil {
None
} else {
Some(slice::from_raw_parts(
data.bytes() as *mut u8,
data.length() as usize,
))
}
}
}
pub fn write(&self, item: ClipboardItem) {
unsafe {
match item.entries.as_slice() {
[] => {
self.inner.clearContents();
}
[ClipboardEntry::String(string)] => {
self.write_plaintext(string);
}
[ClipboardEntry::Image(image)] => {
self.write_image(image);
}
[ClipboardEntry::ExternalPaths(_)] => {}
_ => {
let mut combined = ClipboardString {
text: String::new(),
metadata: None,
};
for entry in item.entries {
match entry {
ClipboardEntry::String(text) => {
combined.text.push_str(&text.text());
if combined.metadata.is_none() {
combined.metadata = text.metadata;
}
}
_ => {}
}
}
self.write_plaintext(&combined);
}
}
}
}
fn write_plaintext(&self, string: &ClipboardString) {
unsafe {
self.inner.clearContents();
let text_bytes = NSData::dataWithBytes_length_(
nil,
string.text.as_ptr() as *const c_void,
string.text.len() as u64,
);
self.inner
.setData_forType(text_bytes, NSPasteboardTypeString);
if let Some(metadata) = string.metadata.as_ref() {
let hash_bytes = ClipboardString::text_hash(&string.text).to_be_bytes();
let hash_bytes = NSData::dataWithBytes_length_(
nil,
hash_bytes.as_ptr() as *const c_void,
hash_bytes.len() as u64,
);
self.inner.setData_forType(hash_bytes, self.text_hash_type);
let metadata_bytes = NSData::dataWithBytes_length_(
nil,
metadata.as_ptr() as *const c_void,
metadata.len() as u64,
);
self.inner
.setData_forType(metadata_bytes, self.metadata_type);
}
}
}
unsafe fn write_image(&self, image: &Image) {
unsafe {
self.inner.clearContents();
let bytes = NSData::dataWithBytes_length_(
nil,
image.bytes.as_ptr() as *const c_void,
image.bytes.len() as u64,
);
self.inner
.setData_forType(bytes, Into::<UTType>::into(image.format).inner_mut());
}
}
}
#[link(name = "AppKit", kind = "framework")]
unsafe extern "C" {
pub static NSPasteboardNameFind: id;
}
impl From<ImageFormat> for UTType {
fn from(value: ImageFormat) -> Self {
match value {
ImageFormat::Png => Self::png(),
ImageFormat::Jpeg => Self::jpeg(),
ImageFormat::Tiff => Self::tiff(),
ImageFormat::Webp => Self::webp(),
ImageFormat::Gif => Self::gif(),
ImageFormat::Bmp => Self::bmp(),
ImageFormat::Svg => Self::svg(),
ImageFormat::Ico => Self::ico(),
}
}
}
pub struct UTType(id);
impl UTType {
pub fn png() -> Self {
Self(unsafe { NSPasteboardTypePNG }) }
pub fn jpeg() -> Self {
Self(unsafe { ns_string("public.jpeg") })
}
pub fn gif() -> Self {
Self(unsafe { ns_string("com.compuserve.gif") })
}
pub fn webp() -> Self {
Self(unsafe { ns_string("org.webmproject.webp") })
}
pub fn bmp() -> Self {
Self(unsafe { ns_string("com.microsoft.bmp") })
}
pub fn svg() -> Self {
Self(unsafe { ns_string("public.svg-image") })
}
pub fn ico() -> Self {
Self(unsafe { ns_string("com.microsoft.ico") })
}
pub fn tiff() -> Self {
Self(unsafe { NSPasteboardTypeTIFF }) }
fn inner(&self) -> *const Object {
self.0
}
pub fn inner_mut(&self) -> *mut Object {
self.0 as *mut _
}
}
#[cfg(test)]
mod tests {
use cocoa::{appkit::NSPasteboardTypeString, foundation::NSData};
use crate::{ClipboardEntry, ClipboardItem, ClipboardString};
use super::*;
#[test]
fn test_string() {
let pasteboard = Pasteboard::unique();
assert_eq!(pasteboard.read(), None);
let item = ClipboardItem::new_string("1".to_string());
pasteboard.write(item.clone());
assert_eq!(pasteboard.read(), Some(item));
let item = ClipboardItem {
entries: vec![ClipboardEntry::String(
ClipboardString::new("2".to_string()).with_json_metadata(vec![3, 4]),
)],
};
pasteboard.write(item.clone());
assert_eq!(pasteboard.read(), Some(item));
let text_from_other_app = "text from other app";
unsafe {
let bytes = NSData::dataWithBytes_length_(
nil,
text_from_other_app.as_ptr() as *const c_void,
text_from_other_app.len() as u64,
);
pasteboard
.inner
.setData_forType(bytes, NSPasteboardTypeString);
}
assert_eq!(
pasteboard.read(),
Some(ClipboardItem::new_string(text_from_other_app.to_string()))
);
}
}