//! Clipboard access on macOS //! //! Implemented according to //! https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/PasteboardGuide106/Articles/pbReading.html#//apple_ref/doc/uid/TP40008123-SW1 mod ns { extern crate objc_foundation; extern crate objc_id; #[link(name = "AppKit", kind = "framework")] extern "C" {} use std::mem; use self::objc_foundation::{INSArray, INSObject, INSString}; use self::objc_foundation::{NSArray, NSDictionary, NSObject, NSString}; use self::objc_id::{Id, Owned}; use objc::runtime::{Class, Object}; /// Rust API for NSPasteboard pub struct Pasteboard(Id); /// Errors occurring when creating a Pasteboard #[derive(Debug)] pub enum NewPasteboardError { GetPasteboardClass, LoadGeneralPasteboard, } /// Errors occurring when reading a string from the pasteboard #[derive(Debug)] pub enum ReadStringError { GetStringClass, ReadObjectsForClasses, } /// Errors from writing strings to the pasteboard #[derive(Debug)] pub struct WriteStringError; /// A trait for reading contents from the pasteboard /// /// This is intended to reflect the underlying objective C API /// `readObjectsForClasses:options:`. pub trait PasteboardReadObject { type Err; fn read_object(&self) -> Result; } /// A trait for writing contents to the pasteboard pub trait PasteboardWriteObject { type Err; fn write_object(&mut self, T) -> Result<(), Self::Err>; } impl PasteboardReadObject for Pasteboard { type Err = ReadStringError; fn read_object(&self) -> Result { // Get string class; need this for passing to readObjectsForClasses let ns_string_class = match Class::get("NSString") { Some(class) => class, None => return Err(ReadStringError::GetStringClass), }; let ns_string: Id = unsafe { let ptr: *mut Object = msg_send![ns_string_class, class]; if ptr.is_null() { return Err(ReadStringError::GetStringClass); } else { Id::from_ptr(ptr) } }; let classes: Id> = unsafe { // I think this transmute is valid. It's going from an // Id to an Id. From transmute's perspective, // the only thing that matters is that they both have the same // size (they do for now since the generic is phantom data). In // both cases, the underlying pointer is an id (from `[NSString // class]`), so again, this should be valid. There's just // nothing implemented in objc_id or objc_foundation to do this // "safely". By the way, the only reason this is necessary is // because INSObject isn't implemented for Id. // // And if that argument isn't convincing, my final reasoning is // that "it seems to work". NSArray::from_vec(vec![mem::transmute(ns_string)]) }; // No options // // Apparently this doesn't compile without a type hint, so it maps // objects to objects! let options: Id> = NSDictionary::new(); // call [pasteboard readObjectsForClasses:options:] let copied_items = unsafe { let copied_items: *mut NSArray = msg_send![ self.0, readObjectsForClasses:&*classes options:&*options ]; if copied_items.is_null() { return Err(ReadStringError::ReadObjectsForClasses); } else { Id::from_ptr(copied_items) as Id> } }; // Ok, this is great. We have an NSArray, and these have // decent bindings. Use the first item returned (if an item was // returned) or just return an empty string // // XXX Should this return an error if no items were returned? let contents = copied_items .first_object() .map(|ns_string| ns_string.as_str().to_owned()) .unwrap_or_else(String::new); Ok(contents) } } impl PasteboardWriteObject for Pasteboard { type Err = WriteStringError; fn write_object(&mut self, object: String) -> Result<(), Self::Err> { let objects = NSArray::from_vec(vec![NSString::from_str(&object)]); self.clear_contents(); // The writeObjects method returns true in case of success, and // false otherwise. let ok: bool = unsafe { msg_send![self.0, writeObjects: objects] }; if ok { Ok(()) } else { Err(WriteStringError) } } } impl ::std::error::Error for WriteStringError { fn description(&self) -> &str { "Failed writing string to the NSPasteboard (writeContents returned false)" } } impl ::std::fmt::Display for WriteStringError { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { f.write_str(::std::error::Error::description(self)) } } impl ::std::error::Error for ReadStringError { fn description(&self) -> &str { match *self { ReadStringError::GetStringClass => "NSString class not found", ReadStringError::ReadObjectsForClasses => "readObjectsForClasses:options: failed", } } } impl ::std::fmt::Display for ReadStringError { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { f.write_str(::std::error::Error::description(self)) } } impl ::std::error::Error for NewPasteboardError { fn description(&self) -> &str { match *self { NewPasteboardError::GetPasteboardClass => "NSPasteboard class not found", NewPasteboardError::LoadGeneralPasteboard => { "[NSPasteboard generalPasteboard] failed" }, } } } impl ::std::fmt::Display for NewPasteboardError { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { f.write_str(::std::error::Error::description(self)) } } impl Pasteboard { pub fn new() -> Result { // NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; let ns_pasteboard_class = match Class::get("NSPasteboard") { Some(class) => class, None => return Err(NewPasteboardError::GetPasteboardClass), }; let ptr = unsafe { let ptr: *mut Object = msg_send![ns_pasteboard_class, generalPasteboard]; if ptr.is_null() { return Err(NewPasteboardError::LoadGeneralPasteboard); } else { ptr } }; let id = unsafe { Id::from_ptr(ptr) }; Ok(Pasteboard(id)) } /// Clears the existing contents of the pasteboard, preparing it for new /// contents. /// /// This is the first step in providing data on the pasteboard. The /// return value is the change count of the pasteboard pub fn clear_contents(&mut self) -> usize { unsafe { msg_send![self.0, clearContents] } } } } #[derive(Debug)] pub enum Error { CreatePasteboard(ns::NewPasteboardError), ReadString(ns::ReadStringError), WriteString(ns::WriteStringError), } impl ::std::error::Error for Error { fn cause(&self) -> Option<&::std::error::Error> { match *self { Error::CreatePasteboard(ref err) => Some(err), Error::ReadString(ref err) => Some(err), Error::WriteString(ref err) => Some(err), } } fn description(&self) -> &str { match *self { Error::CreatePasteboard(ref _err) => "Failed to create pasteboard", Error::ReadString(ref _err) => "Failed to read string from pasteboard", Error::WriteString(ref _err) => "Failed to write string to pasteboard", } } } impl ::std::fmt::Display for Error { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { match *self { Error::CreatePasteboard(ref err) => write!(f, "Failed to create pasteboard: {}", err), Error::ReadString(ref err) => { write!(f, "Failed to read string from pasteboard: {}", err) }, Error::WriteString(ref err) => { write!(f, "Failed to write string to pasteboard: {}", err) }, } } } impl From for Error { fn from(val: ns::NewPasteboardError) -> Error { Error::CreatePasteboard(val) } } impl From for Error { fn from(val: ns::ReadStringError) -> Error { Error::ReadString(val) } } impl From for Error { fn from(val: ns::WriteStringError) -> Error { Error::WriteString(val) } } pub struct Clipboard(ns::Pasteboard); impl super::Load for Clipboard { type Err = Error; fn new() -> Result { Ok(Clipboard(ns::Pasteboard::new()?)) } fn load_primary(&self) -> Result { use self::ns::PasteboardReadObject; self.0.read_object().map_err(::std::convert::From::from) } } impl super::Store for Clipboard { fn store_primary(&mut self, contents: S) -> Result<(), Self::Err> where S: Into, { use self::ns::PasteboardWriteObject; self.0.write_object(contents.into()).map_err(::std::convert::From::from) } fn store_selection(&mut self, _contents: S) -> Result<(), Self::Err> where S: Into, { // No such thing on macOS Ok(()) } } #[cfg(test)] mod tests { use super::Clipboard; use {Load, Store}; #[test] fn create_clipboard_save_load_contents() { let mut clipboard = Clipboard::new().unwrap(); clipboard.store_primary("arst").unwrap(); let loaded = clipboard.load_primary().unwrap(); assert_eq!("arst", loaded); } }