summaryrefslogtreecommitdiffstats
path: root/copypasta
diff options
context:
space:
mode:
authorJoe Wilm <joe@jwilm.com>2016-10-08 20:57:30 -0700
committerJoe Wilm <joe@jwilm.com>2016-10-08 20:57:30 -0700
commit5ddf5257474e5e123fd6a10cc59dba17b8d11524 (patch)
treef4d2490c21de76a304b0423acc4a80c09d7ff90f /copypasta
parent9d491f9f676536634040fea8294dc672f3466e26 (diff)
Implement copypasta::Load for macos::Clipboard
Diffstat (limited to 'copypasta')
-rw-r--r--copypasta/Cargo.lock56
-rw-r--r--copypasta/Cargo.toml5
-rw-r--r--copypasta/src/lib.rs4
-rw-r--r--copypasta/src/macos.rs238
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;
+
+ #[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,
+ }
+
+ /// 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>;
+ }
+
+ 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 ::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)
+ };
-impl Load for Clipboard {
- type Err = ();
+ 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<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)
+ }
+}
+
+pub struct Clipboard(ns::Pasteboard);
+
+impl super::Load for Clipboard {
+ type Err = Error;
fn new() -> Result<Self, Error> {
- Ok(Clipboard)
+ Ok(Clipboard(try!(ns::Pasteboard::new())))
}
fn load_primary(&self) -> Result<String, Self::Err> {
- Ok(String::new())
+ Ok(try!(self::ns::PasteboardReadObject::<String>::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());
}
}