From 8df15bba3f89f5613a18cfade61fbcadfcbe8624 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 17 Mar 2021 11:35:50 +0100 Subject: [PATCH] feat(clipboard): add macOS clipboard implementation --- espanso-clipboard/build.rs | 8 +-- espanso-clipboard/src/cocoa/ffi.rs | 28 ++++++++ espanso-clipboard/src/cocoa/mod.rs | 100 ++++++++++++++++++++++++++ espanso-clipboard/src/cocoa/native.h | 30 ++++++++ espanso-clipboard/src/cocoa/native.mm | 87 ++++++++++++++++++++++ espanso-clipboard/src/lib.rs | 8 +-- 6 files changed, 253 insertions(+), 8 deletions(-) create mode 100644 espanso-clipboard/src/cocoa/ffi.rs create mode 100644 espanso-clipboard/src/cocoa/mod.rs create mode 100644 espanso-clipboard/src/cocoa/native.h create mode 100644 espanso-clipboard/src/cocoa/native.mm diff --git a/espanso-clipboard/build.rs b/espanso-clipboard/build.rs index 325e9ac..3d50903 100644 --- a/espanso-clipboard/build.rs +++ b/espanso-clipboard/build.rs @@ -61,12 +61,12 @@ fn cc_config() { #[cfg(target_os = "macos")] fn cc_config() { - println!("cargo:rerun-if-changed=src/mac/native.mm"); - println!("cargo:rerun-if-changed=src/mac/native.h"); + println!("cargo:rerun-if-changed=src/cocoa/native.mm"); + println!("cargo:rerun-if-changed=src/cocoa/native.h"); cc::Build::new() .cpp(true) - .include("src/mac/native.h") - .file("src/mac/native.mm") + .include("src/cocoa/native.h") + .file("src/cocoa/native.mm") .compile("espansoclipboard"); println!("cargo:rustc-link-lib=dylib=c++"); println!("cargo:rustc-link-lib=static=espansoclipboard"); diff --git a/espanso-clipboard/src/cocoa/ffi.rs b/espanso-clipboard/src/cocoa/ffi.rs new file mode 100644 index 0000000..9528786 --- /dev/null +++ b/espanso-clipboard/src/cocoa/ffi.rs @@ -0,0 +1,28 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use std::os::raw::c_char; + +#[link(name = "espansoclipboard", kind = "static")] +extern "C" { + pub fn clipboard_get_text(buffer: *mut c_char, buffer_size: i32) -> i32; + pub fn clipboard_set_text(text: *const c_char) -> i32; + pub fn clipboard_set_image(image_path: *const c_char) -> i32; + pub fn clipboard_set_html(html_descriptor: *const c_char, fallback_text: *const c_char) -> i32; +} diff --git a/espanso-clipboard/src/cocoa/mod.rs b/espanso-clipboard/src/cocoa/mod.rs new file mode 100644 index 0000000..3add22e --- /dev/null +++ b/espanso-clipboard/src/cocoa/mod.rs @@ -0,0 +1,100 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +mod ffi; + +use std::{ffi::{CStr, CString}, path::PathBuf}; + +use crate::Clipboard; +use anyhow::Result; +use log::error; +use thiserror::Error; + +pub struct CocoaClipboard {} + +impl CocoaClipboard { + pub fn new() -> Result { + Ok(Self {}) + } +} + +impl Clipboard for CocoaClipboard { + fn get_text(&self) -> Option { + let mut buffer: [i8; 2048] = [0; 2048]; + let native_result = + unsafe { ffi::clipboard_get_text(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) }; + if native_result > 0 { + let string = unsafe { CStr::from_ptr(buffer.as_ptr()) }; + Some(string.to_string_lossy().to_string()) + } else { + None + } + } + + fn set_text(&self, text: &str) -> anyhow::Result<()> { + let string = CString::new(text)?; + let native_result = unsafe { ffi::clipboard_set_text(string.as_ptr()) }; + if native_result > 0 { + Ok(()) + } else { + Err(CocoaClipboardError::SetOperationFailed().into()) + } + } + + fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> { + if !image_path.exists() || !image_path.is_file() { + return Err(CocoaClipboardError::ImageNotFound(image_path.to_path_buf()).into()); + } + + let path = CString::new(image_path.to_string_lossy().to_string())?; + let native_result = unsafe { ffi::clipboard_set_image(path.as_ptr()) }; + + if native_result > 0 { + Ok(()) + } else { + Err(CocoaClipboardError::SetOperationFailed().into()) + } + } + + fn set_html(&self, html: &str, fallback_text: Option<&str>) -> anyhow::Result<()> { + let html_string = CString::new(html)?; + let fallback_string = CString::new(fallback_text.unwrap_or_default())?; + let fallback_ptr = if fallback_text.is_some() { + fallback_string.as_ptr() + } else { + std::ptr::null() + }; + + let native_result = unsafe { ffi::clipboard_set_html(html_string.as_ptr(), fallback_ptr) }; + if native_result > 0 { + Ok(()) + } else { + Err(CocoaClipboardError::SetOperationFailed().into()) + } + } +} + +#[derive(Error, Debug)] +pub enum CocoaClipboardError { + #[error("clipboard set operation failed")] + SetOperationFailed(), + + #[error("image not found: `{0}`")] + ImageNotFound(PathBuf), +} diff --git a/espanso-clipboard/src/cocoa/native.h b/espanso-clipboard/src/cocoa/native.h new file mode 100644 index 0000000..f22c0e8 --- /dev/null +++ b/espanso-clipboard/src/cocoa/native.h @@ -0,0 +1,30 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +#ifndef ESPANSO_CLIPBOARD_H +#define ESPANSO_CLIPBOARD_H + +#include + +extern "C" int32_t clipboard_get_text(char * buffer, int32_t buffer_size); +extern "C" int32_t clipboard_set_text(char * text); +extern "C" int32_t clipboard_set_image(char * image_path); +extern "C" int32_t clipboard_set_html(char * html, char * fallback_text); + +#endif //ESPANSO_CLIPBOARD_H \ No newline at end of file diff --git a/espanso-clipboard/src/cocoa/native.mm b/espanso-clipboard/src/cocoa/native.mm new file mode 100644 index 0000000..b60f56d --- /dev/null +++ b/espanso-clipboard/src/cocoa/native.mm @@ -0,0 +1,87 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +#include "native.h" +#import +#import +#include + +int32_t clipboard_get_text(char * buffer, int32_t buffer_size) { + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + for (id element in pasteboard.pasteboardItems) { + NSString *string = [element stringForType: NSPasteboardTypeString]; + if (string != NULL) { + const char * text = [string UTF8String]; + strncpy(buffer, text, buffer_size); + + [string release]; + + return 1; + } + } + return 0; +} + +int32_t clipboard_set_text(char * text) { + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + NSArray *array = @[NSPasteboardTypeString]; + [pasteboard declareTypes:array owner:nil]; + + NSString *nsText = [NSString stringWithUTF8String:text]; + [pasteboard setString:nsText forType:NSPasteboardTypeString]; +} + +int32_t clipboard_set_image(char * image_path) { + NSString *pathString = [NSString stringWithUTF8String:image_path]; + NSImage *image = [[NSImage alloc] initWithContentsOfFile:pathString]; + int result = 0; + + if (image != nil) { + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard clearContents]; + NSArray *copiedObjects = [NSArray arrayWithObject:image]; + [pasteboard writeObjects:copiedObjects]; + result = 1; + } + [image release]; + + return result; +} + +int32_t clipboard_set_html(char * html, char * fallback_text) { + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + NSArray *array = @[NSRTFPboardType, NSPasteboardTypeString]; + [pasteboard declareTypes:array owner:nil]; + + NSString *nsHtml = [NSString stringWithUTF8String:html]; + NSDictionary *documentAttributes = [NSDictionary dictionaryWithObjectsAndKeys:NSHTMLTextDocumentType, NSDocumentTypeDocumentAttribute, NSCharacterEncodingDocumentAttribute,[NSNumber numberWithInt:NSUTF8StringEncoding], nil]; + NSAttributedString* atr = [[NSAttributedString alloc] initWithData:[nsHtml dataUsingEncoding:NSUTF8StringEncoding] options:documentAttributes documentAttributes:nil error:nil]; + + NSData *rtf = [atr RTFFromRange:NSMakeRange(0, [atr length]) + documentAttributes:nil]; + + [pasteboard setData:rtf forType:NSRTFPboardType]; + + if (fallback_text) { + NSString *nsText = [NSString stringWithUTF8String:fallback_text]; + [pasteboard setString:nsText forType:NSPasteboardTypeString]; + } + + return 1; +} \ No newline at end of file diff --git a/espanso-clipboard/src/lib.rs b/espanso-clipboard/src/lib.rs index c60cb69..efa1adf 100644 --- a/espanso-clipboard/src/lib.rs +++ b/espanso-clipboard/src/lib.rs @@ -34,7 +34,7 @@ mod x11; mod wayland; #[cfg(target_os = "macos")] -mod mac; +mod cocoa; pub trait Clipboard { fn get_text(&self) -> Option; @@ -66,9 +66,9 @@ pub fn get_clipboard(_: ClipboardOptions) -> Result> { } #[cfg(target_os = "macos")] -pub fn get_injector(_options: InjectorCreationOptions) -> Result> { - info!("using MacInjector"); - Ok(Box::new(mac::MacInjector::new())) +pub fn get_clipboard(_: ClipboardOptions) -> Result> { + info!("using CocoaClipboard"); + Ok(Box::new(cocoa::CocoaClipboard::new()?)) } #[cfg(target_os = "linux")]