blob: 1942ac43e24e7d281330cf52ef58c3f48978dfb2 [file] [log] [blame]
//! 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_id;
extern crate objc_foundation;
#[link(name = "AppKit", kind = "framework")]
extern {}
use std::mem;
use objc::runtime::{Class, Object};
use self::objc_id::{Id, Owned};
use self::objc_foundation::{NSArray, NSObject, NSDictionary, NSString};
use self::objc_foundation::{INSString, INSArray, INSObject};
/// Rust API for NSPasteboard
pub struct Pasteboard(Id<Object>);
/// 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<T> {
type Err;
fn read_object(&self) -> Result<T, Self::Err>;
}
/// A trait for writing contents to the pasteboard
pub trait PasteboardWriteObject<T> {
type Err;
fn write_object(&mut self, T) -> Result<(), Self::Err>;
}
impl PasteboardReadObject<String> for Pasteboard {
type Err = ReadStringError;
fn read_object(&self) -> Result<String, ReadStringError> {
// 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<Object> = 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<NSArray<NSObject, Owned>> = unsafe {
// I think this transmute is valid. It's going from an
// Id<Object> to an Id<NSObject>. 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<Object>.
//
// 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<NSObject, NSObject>> = NSDictionary::new();
// call [pasteboard readObjectsForClasses:options:]
let copied_items = unsafe {
let copied_items: *mut NSArray<NSString> = 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<NSArray<NSString>>
}
};
// Ok, this is great. We have an NSArray<NSString>, 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<String> 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<Pasteboard, NewPasteboardError> {
// 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<ns::NewPasteboardError> for Error {
fn from(val: ns::NewPasteboardError) -> Error {
Error::CreatePasteboard(val)
}
}
impl From<ns::ReadStringError> for Error {
fn from(val: ns::ReadStringError) -> Error {
Error::ReadString(val)
}
}
impl From<ns::WriteStringError> 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<Self, Error> {
Ok(Clipboard(ns::Pasteboard::new()?))
}
fn load_primary(&self) -> Result<String, Self::Err> {
use self::ns::PasteboardReadObject;
self.0.read_object()
.map_err(::std::convert::From::from)
}
}
impl super::Store for Clipboard {
fn store_primary<S>(&mut self, contents: S) -> Result<(), Self::Err>
where S: Into<String>
{
use self::ns::PasteboardWriteObject;
self.0.write_object(contents.into())
.map_err(::std::convert::From::from)
}
fn store_selection<S>(&mut self, _contents: S) -> Result<(), Self::Err>
where S: Into<String>
{
// 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);
}
}