use fission_core::env::Clipboard;
use fission_core::{
ClipboardContent, ClipboardError, ClipboardText, ClipboardWriteTextRequest, CLEAR_CLIPBOARD,
READ_CLIPBOARD_CONTENT, READ_CLIPBOARD_TEXT, WRITE_CLIPBOARD_CONTENT, WRITE_CLIPBOARD_TEXT,
};
use fission_shell::async_host::AsyncRegistry;
#[cfg(target_os = "ios")]
use objc::{class, msg_send, sel, sel_impl};
#[cfg(target_os = "ios")]
use std::ffi::CStr;
#[cfg(target_os = "ios")]
use std::os::raw::{c_char, c_void};
use std::sync::{Arc, Mutex};
#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
use arboard::Clipboard as Arboard;
#[cfg(target_os = "ios")]
#[link(name = "UIKit", kind = "framework")]
extern "C" {}
pub struct DesktopClipboard {
#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
system: Arc<Mutex<Option<Arboard>>>,
memory: Arc<Mutex<String>>,
}
impl DesktopClipboard {
pub fn new() -> Self {
Self {
#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
system: Arc::new(Mutex::new(Arboard::new().ok())),
memory: Arc::new(Mutex::new(String::new())),
}
}
}
impl Clipboard for DesktopClipboard {
fn get_text(&self) -> Option<String> {
#[cfg(target_os = "ios")]
if let Some(text) = ios_clipboard_text() {
return Some(text);
}
#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
if let Ok(mut lock) = self.system.lock() {
if let Some(cb) = lock.as_mut() {
if let Ok(text) = cb.get_text() {
return Some(text);
}
}
}
self.memory.lock().ok().map(|text| text.clone())
}
fn set_text(&self, text: &str) {
if let Ok(mut memory) = self.memory.lock() {
*memory = text.to_string();
}
#[cfg(target_os = "ios")]
ios_set_clipboard_text(text);
#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
if let Ok(mut lock) = self.system.lock() {
if let Some(cb) = lock.as_mut() {
let _ = cb.set_text(text);
}
}
}
}
#[cfg(target_os = "ios")]
fn ios_clipboard_text() -> Option<String> {
unsafe {
let pasteboard: *mut objc::runtime::Object =
msg_send![class!(UIPasteboard), generalPasteboard];
if pasteboard.is_null() {
return None;
}
let string: *mut objc::runtime::Object = msg_send![pasteboard, string];
if string.is_null() {
return None;
}
let c_string: *const c_char = msg_send![string, UTF8String];
if c_string.is_null() {
return None;
}
CStr::from_ptr(c_string)
.to_str()
.ok()
.map(ToOwned::to_owned)
}
}
#[cfg(target_os = "ios")]
fn ios_set_clipboard_text(text: &str) {
unsafe {
let pasteboard: *mut objc::runtime::Object =
msg_send![class!(UIPasteboard), generalPasteboard];
if pasteboard.is_null() {
return;
}
let string: *mut objc::runtime::Object = msg_send![class!(NSString), alloc];
let string: *mut objc::runtime::Object = msg_send![
string,
initWithBytes: text.as_ptr() as *const c_void
length: text.len()
encoding: 4usize
];
let _: () = msg_send![pasteboard, setString: string];
}
}
pub trait ClipboardHost: Send + Sync + 'static {
fn read_text(&self) -> Result<ClipboardText, ClipboardError>;
fn write_text(&self, request: ClipboardWriteTextRequest) -> Result<(), ClipboardError>;
fn read_content(&self) -> Result<ClipboardContent, ClipboardError>;
fn write_content(&self, request: ClipboardContent) -> Result<(), ClipboardError>;
fn clear(&self) -> Result<(), ClipboardError>;
}
impl ClipboardHost for DesktopClipboard {
fn read_text(&self) -> Result<ClipboardText, ClipboardError> {
Ok(ClipboardText {
text: self.get_text(),
})
}
fn write_text(&self, request: ClipboardWriteTextRequest) -> Result<(), ClipboardError> {
self.set_text(&request.text);
Ok(())
}
fn read_content(&self) -> Result<ClipboardContent, ClipboardError> {
let text = self.get_text().unwrap_or_default();
Ok(ClipboardContent {
items: if text.is_empty() {
Vec::new()
} else {
vec![fission_core::ClipboardItem {
content_type: "text/plain".into(),
bytes: text.into_bytes(),
suggested_name: None,
}]
},
})
}
fn write_content(&self, request: ClipboardContent) -> Result<(), ClipboardError> {
if let Some(item) = request
.items
.iter()
.find(|item| item.content_type.starts_with("text/plain"))
{
if let Ok(text) = String::from_utf8(item.bytes.clone()) {
self.set_text(&text);
return Ok(());
}
}
Err(ClipboardError::unsupported("write_content_non_text"))
}
fn clear(&self) -> Result<(), ClipboardError> {
self.set_text("");
Ok(())
}
}
#[derive(Debug, Default)]
pub struct MemoryClipboardHost {
content: Arc<Mutex<ClipboardContent>>,
}
impl ClipboardHost for MemoryClipboardHost {
fn read_text(&self) -> Result<ClipboardText, ClipboardError> {
let content = self.content.lock().map_err(|_| {
ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
})?;
let text = content
.items
.iter()
.find(|item| item.content_type.starts_with("text/plain"))
.and_then(|item| String::from_utf8(item.bytes.clone()).ok());
Ok(ClipboardText { text })
}
fn write_text(&self, request: ClipboardWriteTextRequest) -> Result<(), ClipboardError> {
let mut content = self.content.lock().map_err(|_| {
ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
})?;
*content = ClipboardContent {
items: vec![fission_core::ClipboardItem {
content_type: "text/plain".into(),
bytes: request.text.into_bytes(),
suggested_name: None,
}],
};
Ok(())
}
fn read_content(&self) -> Result<ClipboardContent, ClipboardError> {
self.content
.lock()
.map(|content| content.clone())
.map_err(|_| ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned"))
}
fn write_content(&self, request: ClipboardContent) -> Result<(), ClipboardError> {
let mut content = self.content.lock().map_err(|_| {
ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
})?;
*content = request;
Ok(())
}
fn clear(&self) -> Result<(), ClipboardError> {
let mut content = self.content.lock().map_err(|_| {
ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
})?;
content.items.clear();
Ok(())
}
}
pub(crate) fn register_clipboard_capabilities(
async_registry: &mut AsyncRegistry,
host: Arc<dyn ClipboardHost>,
) {
let read_text_host = host.clone();
async_registry.register_operation_capability(READ_CLIPBOARD_TEXT, move |(), _| {
let host = read_text_host.clone();
async move { host.read_text() }
});
let write_text_host = host.clone();
async_registry.register_operation_capability(WRITE_CLIPBOARD_TEXT, move |request, _| {
let host = write_text_host.clone();
async move { host.write_text(request) }
});
let read_content_host = host.clone();
async_registry.register_operation_capability(READ_CLIPBOARD_CONTENT, move |(), _| {
let host = read_content_host.clone();
async move { host.read_content() }
});
let write_content_host = host.clone();
async_registry.register_operation_capability(WRITE_CLIPBOARD_CONTENT, move |request, _| {
let host = write_content_host.clone();
async move { host.write_content(request) }
});
async_registry.register_operation_capability(CLEAR_CLIPBOARD, move |(), _| {
let host = host.clone();
async move { host.clear() }
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn memory_clipboard_reads_and_writes_text() {
let host = MemoryClipboardHost::default();
host.write_text(ClipboardWriteTextRequest {
text: "copied".into(),
})
.unwrap();
assert_eq!(host.read_text().unwrap().text.as_deref(), Some("copied"));
host.clear().unwrap();
assert_eq!(host.read_text().unwrap().text, None);
}
#[test]
fn desktop_clipboard_host_supports_text_content() {
let host = DesktopClipboard::new();
host.write_text(ClipboardWriteTextRequest {
text: "copied".into(),
})
.unwrap();
let content = ClipboardHost::read_content(&host).unwrap();
assert_eq!(content.items[0].content_type, "text/plain");
}
}