From 5ddf5257474e5e123fd6a10cc59dba17b8d11524 Mon Sep 17 00:00:00 2001 From: Joe Wilm Date: Sat, 8 Oct 2016 20:57:30 -0700 Subject: [PATCH] Implement copypasta::Load for macos::Clipboard --- copypasta/Cargo.lock | 56 ++++++++++ copypasta/Cargo.toml | 5 + copypasta/src/lib.rs | 4 + copypasta/src/macos.rs | 238 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 296 insertions(+), 7 deletions(-) diff --git a/copypasta/Cargo.lock b/copypasta/Cargo.lock index 53d49794..bf56c1ce 100644 --- a/copypasta/Cargo.lock +++ b/copypasta/Cargo.lock @@ -1,4 +1,60 @@ [root] name = "copypasta" version = "0.0.1" +dependencies = [ + "objc 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "objc-foundation 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "objc_id 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "objc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "objc 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "objc_id 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "objc_id" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "objc 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +"checksum libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)" = "408014cace30ee0f767b1c4517980646a573ec61a57957aeeabcac8ac0a02e8d" +"checksum malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +"checksum objc 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7c9311aa5acd7bee14476afa0f0557f564e9d0d61218a8b833d9b1f871fa5fba" +"checksum objc-foundation 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +"checksum objc_id 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e4730aa1c64d722db45f7ccc4113a3e2c465d018de6db4d3e7dfe031e8c8a297" diff --git a/copypasta/Cargo.toml b/copypasta/Cargo.toml index 1f07645a..1b887072 100644 --- a/copypasta/Cargo.toml +++ b/copypasta/Cargo.toml @@ -7,3 +7,8 @@ description = "Forthcoming clipboard library" keywords = ["clipboard", "copy", "paste"] [dependencies] + +[target.'cfg(target_os = "macos")'.dependencies] +objc = "0.2" +objc_id = "0.1" +objc-foundation = "0.1" diff --git a/copypasta/src/lib.rs b/copypasta/src/lib.rs index 722142aa..9fe354f6 100644 --- a/copypasta/src/lib.rs +++ b/copypasta/src/lib.rs @@ -1,5 +1,9 @@ //! A cross-platform clipboard library +// This has to be here due to macro_use +#[cfg(target_os = "macos")] +#[macro_use] extern crate objc; + /// Types that can get the system clipboard contents pub trait Load : Sized { /// Errors encountered when working with a clipboard. Each implementation is diff --git a/copypasta/src/macos.rs b/copypasta/src/macos.rs index 5ef7630e..4910790d 100644 --- a/copypasta/src/macos.rs +++ b/copypasta/src/macos.rs @@ -1,19 +1,243 @@ //! 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 -//! -//! FIXME implement this :) -struct Clipboard; +mod ns { + extern crate objc_id; + extern crate objc_foundation; -impl Load for Clipboard { - type Err = (); + #[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); + + /// 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, + } + + /// 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; + } + + 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 ::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)) + } + } +} + +#[derive(Debug)] +pub enum Error { + CreatePasteboard(ns::NewPasteboardError), + ReadString(ns::ReadStringError), +} + + +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), + } + } + + fn description(&self) -> &str { + match *self { + Error::CreatePasteboard(ref _err) => "Failed to create pasteboard", + Error::ReadString(ref _err) => "Failed to read string from 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) + }, + } + } +} + +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) + } +} + +pub struct Clipboard(ns::Pasteboard); + +impl super::Load for Clipboard { + type Err = Error; fn new() -> Result { - Ok(Clipboard) + Ok(Clipboard(try!(ns::Pasteboard::new()))) } fn load_primary(&self) -> Result { - Ok(String::new()) + Ok(try!(self::ns::PasteboardReadObject::::read_object(&self.0))) + } +} + +#[cfg(test)] +mod tests { + use super::Clipboard; + use ::Load; + + #[test] + fn create_clipboard_and_load_contents() { + let clipboard = Clipboard::new().unwrap(); + println!("{:?}", clipboard.load_primary()); } }