use std::ffi::{CStr, CString, c_char, c_void};
use std::sync::OnceLock;
unsafe extern "C" {
fn objc_autoreleasePoolPush() -> *mut c_void;
fn objc_autoreleasePoolPop(pool: *mut c_void);
}
struct AutoreleasePool {
token: *mut c_void,
}
impl Drop for AutoreleasePool {
fn drop(&mut self) {
unsafe { objc_autoreleasePoolPop(self.token) }
}
}
fn pool() -> AutoreleasePool {
AutoreleasePool {
token: unsafe { objc_autoreleasePoolPush() },
}
}
use crate::{ClipboardError, MimeType, Selection};
use super::Backend;
type Id = *mut c_void;
type Class = *mut c_void;
type Sel = *const c_void;
type NSUInteger = usize;
#[link(name = "AppKit", kind = "framework")]
unsafe extern "C" {}
#[link(name = "Foundation", kind = "framework")]
unsafe extern "C" {}
#[link(name = "objc")]
unsafe extern "C" {
fn sel_registerName(name: *const c_char) -> Sel;
fn objc_getClass(name: *const c_char) -> Class;
fn objc_msgSend();
}
trait MsgAbi {}
impl<T> MsgAbi for *const T {}
impl<T> MsgAbi for *mut T {}
impl MsgAbi for bool {}
impl MsgAbi for usize {}
impl MsgAbi for isize {}
impl MsgAbi for u8 {}
impl MsgAbi for i8 {}
impl MsgAbi for u16 {}
impl MsgAbi for i16 {}
impl MsgAbi for u32 {}
impl MsgAbi for i32 {}
impl MsgAbi for u64 {}
impl MsgAbi for i64 {}
unsafe fn msg0<R: MsgAbi>(obj: Id, sel: Sel) -> R {
let f: unsafe extern "C" fn(Id, Sel) -> R =
unsafe { std::mem::transmute(objc_msgSend as *const ()) };
unsafe { f(obj, sel) }
}
unsafe fn msg1<A: MsgAbi, R: MsgAbi>(obj: Id, sel: Sel, a: A) -> R {
let f: unsafe extern "C" fn(Id, Sel, A) -> R =
unsafe { std::mem::transmute(objc_msgSend as *const ()) };
unsafe { f(obj, sel, a) }
}
unsafe fn msg2<A: MsgAbi, B: MsgAbi, R: MsgAbi>(obj: Id, sel: Sel, a: A, b: B) -> R {
let f: unsafe extern "C" fn(Id, Sel, A, B) -> R =
unsafe { std::mem::transmute(objc_msgSend as *const ()) };
unsafe { f(obj, sel, a, b) }
}
macro_rules! sel_cached {
($fn_name:ident, $name:literal) => {
fn $fn_name() -> Sel {
static S: OnceLock<usize> = OnceLock::new();
*S.get_or_init(|| unsafe {
sel_registerName(concat!($name, "\0").as_ptr().cast()) as usize
}) as Sel
}
};
}
sel_cached!(sel_general_pasteboard, "generalPasteboard");
sel_cached!(sel_clear_contents, "clearContents");
sel_cached!(sel_set_data_for_type, "setData:forType:");
sel_cached!(sel_data_for_type, "dataForType:");
sel_cached!(sel_types, "types");
sel_cached!(sel_count, "count");
sel_cached!(sel_object_at_index, "objectAtIndex:");
sel_cached!(sel_utf8_string, "UTF8String");
sel_cached!(sel_length, "length");
sel_cached!(sel_bytes, "bytes");
sel_cached!(sel_data_with_bytes_length, "dataWithBytes:length:");
sel_cached!(sel_string_with_utf8_string, "stringWithUTF8String:");
macro_rules! class_cached {
($fn_name:ident, $name:literal) => {
fn $fn_name() -> Class {
static C: OnceLock<usize> = OnceLock::new();
*C.get_or_init(|| unsafe {
objc_getClass(concat!($name, "\0").as_ptr().cast()) as usize
}) as Class
}
};
}
class_cached!(class_nspasteboard, "NSPasteboard");
class_cached!(class_nsdata, "NSData");
class_cached!(class_nsstring, "NSString");
unsafe fn general_pasteboard() -> Id {
unsafe { msg0::<Id>(class_nspasteboard(), sel_general_pasteboard()) }
}
unsafe fn nsstring_from_str(s: &str) -> Id {
let cstr = CString::new(s).expect("NUL byte in clipboard type string");
unsafe {
msg1::<*const c_char, Id>(
class_nsstring(),
sel_string_with_utf8_string(),
cstr.as_ptr(),
)
}
}
unsafe fn nsstring_to_string(s: Id) -> Option<String> {
if s.is_null() {
return None;
}
let utf8: *const c_char = unsafe { msg0::<*const c_char>(s, sel_utf8_string()) };
if utf8.is_null() {
return None;
}
unsafe { CStr::from_ptr(utf8) }
.to_str()
.ok()
.map(String::from)
}
unsafe fn nsdata_from_bytes(bytes: &[u8]) -> Id {
unsafe {
msg2::<*const c_void, NSUInteger, Id>(
class_nsdata(),
sel_data_with_bytes_length(),
bytes.as_ptr().cast(),
bytes.len(),
)
}
}
unsafe fn nsdata_to_vec(data: Id) -> Vec<u8> {
let len: NSUInteger = unsafe { msg0(data, sel_length()) };
let ptr: *const c_void = unsafe { msg0(data, sel_bytes()) };
if ptr.is_null() || len == 0 {
return Vec::new();
}
let slice = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
slice.to_vec()
}
fn mime_to_uti(mime: &MimeType) -> Option<String> {
match mime {
MimeType::Text => Some("public.utf8-plain-text".into()),
MimeType::Html => Some("public.html".into()),
MimeType::Rtf => Some("public.rtf".into()),
MimeType::UriList => Some("text/uri-list".into()),
MimeType::Png => Some("public.png".into()),
MimeType::Custom(s) => Some(s.clone()),
#[allow(unreachable_patterns)]
_ => None,
}
}
fn uti_to_mime(name: &str) -> Option<MimeType> {
match name {
"public.utf8-plain-text" | "NSStringPboardType" => Some(MimeType::Text),
"public.html" => Some(MimeType::Html),
"public.rtf" => Some(MimeType::Rtf),
"text/uri-list" => Some(MimeType::UriList),
"public.png" => Some(MimeType::Png),
_ => None,
}
}
pub struct MacosBackend;
impl MacosBackend {
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) fn new() -> Self {
Self
}
}
impl Backend for MacosBackend {
fn kind(&self) -> crate::BackendKind {
crate::BackendKind::MacOs
}
fn capabilities(&self) -> crate::Capabilities {
crate::Capabilities::WRITE
| crate::Capabilities::READ
| crate::Capabilities::CLEAR
| crate::Capabilities::AVAILABLE
| crate::Capabilities::IMAGE
| crate::Capabilities::RICH_TEXT
| crate::Capabilities::URI_LIST
}
fn set(&self, sel: Selection, mime: MimeType, bytes: &[u8]) -> Result<(), ClipboardError> {
let _pool = pool();
if sel != Selection::Clipboard {
return Err(ClipboardError::UnsupportedMime);
}
let uti = mime_to_uti(&mime).ok_or(ClipboardError::UnsupportedMime)?;
unsafe {
let pb = general_pasteboard();
if pb.is_null() {
return Err(ClipboardError::io_other("generalPasteboard returned nil"));
}
let _change: isize = msg0(pb, sel_clear_contents());
let data = nsdata_from_bytes(bytes);
let ty = nsstring_from_str(&uti);
let ok: bool = msg2(pb, sel_set_data_for_type(), data, ty);
if !ok {
return Err(ClipboardError::io_other("setData:forType: returned NO"));
}
}
Ok(())
}
fn get(&self, sel: Selection, mime: MimeType) -> Result<Vec<u8>, ClipboardError> {
let _pool = pool();
if sel != Selection::Clipboard {
return Err(ClipboardError::UnsupportedMime);
}
let uti = mime_to_uti(&mime).ok_or(ClipboardError::UnsupportedMime)?;
unsafe {
let pb = general_pasteboard();
if pb.is_null() {
return Err(ClipboardError::io_other("generalPasteboard returned nil"));
}
let ty = nsstring_from_str(&uti);
let data: Id = msg1(pb, sel_data_for_type(), ty);
if data.is_null() {
return Err(ClipboardError::UnsupportedMime);
}
Ok(nsdata_to_vec(data))
}
}
fn clear(&self, sel: Selection) -> Result<(), ClipboardError> {
let _pool = pool();
if sel != Selection::Clipboard {
return Err(ClipboardError::UnsupportedMime);
}
unsafe {
let pb = general_pasteboard();
if pb.is_null() {
return Err(ClipboardError::io_other("generalPasteboard returned nil"));
}
let _change: isize = msg0(pb, sel_clear_contents());
}
Ok(())
}
fn available(&self, sel: Selection) -> Result<Vec<MimeType>, ClipboardError> {
let _pool = pool();
if sel != Selection::Clipboard {
return Err(ClipboardError::UnsupportedMime);
}
unsafe {
let pb = general_pasteboard();
if pb.is_null() {
return Ok(vec![]);
}
let types: Id = msg0(pb, sel_types());
if types.is_null() {
return Ok(vec![]);
}
let count: NSUInteger = msg0(types, sel_count());
let mut out: Vec<MimeType> = Vec::new();
for i in 0..count {
let s: Id = msg1(types, sel_object_at_index(), i);
let Some(name) = nsstring_to_string(s) else {
continue;
};
if let Some(mime) = uti_to_mime(&name)
&& !out.contains(&mime)
{
out.push(mime);
}
}
Ok(out)
}
}
}