1use fission_core::env::Clipboard;
2use fission_core::{
3 ClipboardContent, ClipboardError, ClipboardText, ClipboardWriteTextRequest, CLEAR_CLIPBOARD,
4 READ_CLIPBOARD_CONTENT, READ_CLIPBOARD_TEXT, WRITE_CLIPBOARD_CONTENT, WRITE_CLIPBOARD_TEXT,
5};
6use fission_shell::async_host::AsyncRegistry;
7#[cfg(target_os = "ios")]
8use objc::{class, msg_send, sel, sel_impl};
9#[cfg(target_os = "ios")]
10use std::ffi::CStr;
11#[cfg(target_os = "ios")]
12use std::os::raw::{c_char, c_void};
13use std::sync::{Arc, Mutex};
14
15#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
16use arboard::Clipboard as Arboard;
17
18#[cfg(target_os = "ios")]
19#[link(name = "UIKit", kind = "framework")]
20extern "C" {}
21
22pub struct DesktopClipboard {
23 #[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
24 system: Arc<Mutex<Option<Arboard>>>,
25 memory: Arc<Mutex<String>>,
26}
27
28impl DesktopClipboard {
29 pub fn new() -> Self {
30 Self {
31 #[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
32 system: Arc::new(Mutex::new(Arboard::new().ok())),
33 memory: Arc::new(Mutex::new(String::new())),
34 }
35 }
36}
37
38impl Clipboard for DesktopClipboard {
39 fn get_text(&self) -> Option<String> {
40 #[cfg(target_os = "ios")]
41 if let Some(text) = ios_clipboard_text() {
42 return Some(text);
43 }
44
45 #[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
46 if let Ok(mut lock) = self.system.lock() {
47 if let Some(cb) = lock.as_mut() {
48 if let Ok(text) = cb.get_text() {
49 return Some(text);
50 }
51 }
52 }
53 self.memory.lock().ok().map(|text| text.clone())
54 }
55
56 fn set_text(&self, text: &str) {
57 if let Ok(mut memory) = self.memory.lock() {
58 *memory = text.to_string();
59 }
60 #[cfg(target_os = "ios")]
61 ios_set_clipboard_text(text);
62
63 #[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
64 if let Ok(mut lock) = self.system.lock() {
65 if let Some(cb) = lock.as_mut() {
66 let _ = cb.set_text(text);
67 }
68 }
69 }
70}
71
72#[cfg(target_os = "ios")]
73fn ios_clipboard_text() -> Option<String> {
74 unsafe {
75 let pasteboard: *mut objc::runtime::Object =
76 msg_send![class!(UIPasteboard), generalPasteboard];
77 if pasteboard.is_null() {
78 return None;
79 }
80 let string: *mut objc::runtime::Object = msg_send![pasteboard, string];
81 if string.is_null() {
82 return None;
83 }
84 let c_string: *const c_char = msg_send![string, UTF8String];
85 if c_string.is_null() {
86 return None;
87 }
88 CStr::from_ptr(c_string)
89 .to_str()
90 .ok()
91 .map(ToOwned::to_owned)
92 }
93}
94
95#[cfg(target_os = "ios")]
96fn ios_set_clipboard_text(text: &str) {
97 unsafe {
98 let pasteboard: *mut objc::runtime::Object =
99 msg_send![class!(UIPasteboard), generalPasteboard];
100 if pasteboard.is_null() {
101 return;
102 }
103 let string: *mut objc::runtime::Object = msg_send![class!(NSString), alloc];
104 let string: *mut objc::runtime::Object = msg_send![
105 string,
106 initWithBytes: text.as_ptr() as *const c_void
107 length: text.len()
108 encoding: 4usize
109 ];
110 let _: () = msg_send![pasteboard, setString: string];
111 }
112}
113
114pub trait ClipboardHost: Send + Sync + 'static {
116 fn read_text(&self) -> Result<ClipboardText, ClipboardError>;
118 fn write_text(&self, request: ClipboardWriteTextRequest) -> Result<(), ClipboardError>;
120 fn read_content(&self) -> Result<ClipboardContent, ClipboardError>;
122 fn write_content(&self, request: ClipboardContent) -> Result<(), ClipboardError>;
124 fn clear(&self) -> Result<(), ClipboardError>;
126}
127
128impl ClipboardHost for DesktopClipboard {
129 fn read_text(&self) -> Result<ClipboardText, ClipboardError> {
130 Ok(ClipboardText {
131 text: self.get_text(),
132 })
133 }
134
135 fn write_text(&self, request: ClipboardWriteTextRequest) -> Result<(), ClipboardError> {
136 self.set_text(&request.text);
137 Ok(())
138 }
139
140 fn read_content(&self) -> Result<ClipboardContent, ClipboardError> {
141 let text = self.get_text().unwrap_or_default();
142 Ok(ClipboardContent {
143 items: if text.is_empty() {
144 Vec::new()
145 } else {
146 vec![fission_core::ClipboardItem {
147 content_type: "text/plain".into(),
148 bytes: text.into_bytes(),
149 suggested_name: None,
150 }]
151 },
152 })
153 }
154
155 fn write_content(&self, request: ClipboardContent) -> Result<(), ClipboardError> {
156 if let Some(item) = request
157 .items
158 .iter()
159 .find(|item| item.content_type.starts_with("text/plain"))
160 {
161 if let Ok(text) = String::from_utf8(item.bytes.clone()) {
162 self.set_text(&text);
163 return Ok(());
164 }
165 }
166 Err(ClipboardError::unsupported("write_content_non_text"))
167 }
168
169 fn clear(&self) -> Result<(), ClipboardError> {
170 self.set_text("");
171 Ok(())
172 }
173}
174
175#[derive(Debug, Default)]
177pub struct MemoryClipboardHost {
178 content: Arc<Mutex<ClipboardContent>>,
179}
180
181impl ClipboardHost for MemoryClipboardHost {
182 fn read_text(&self) -> Result<ClipboardText, ClipboardError> {
183 let content = self.content.lock().map_err(|_| {
184 ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
185 })?;
186 let text = content
187 .items
188 .iter()
189 .find(|item| item.content_type.starts_with("text/plain"))
190 .and_then(|item| String::from_utf8(item.bytes.clone()).ok());
191 Ok(ClipboardText { text })
192 }
193
194 fn write_text(&self, request: ClipboardWriteTextRequest) -> Result<(), ClipboardError> {
195 let mut content = self.content.lock().map_err(|_| {
196 ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
197 })?;
198 *content = ClipboardContent {
199 items: vec![fission_core::ClipboardItem {
200 content_type: "text/plain".into(),
201 bytes: request.text.into_bytes(),
202 suggested_name: None,
203 }],
204 };
205 Ok(())
206 }
207
208 fn read_content(&self) -> Result<ClipboardContent, ClipboardError> {
209 self.content
210 .lock()
211 .map(|content| content.clone())
212 .map_err(|_| ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned"))
213 }
214
215 fn write_content(&self, request: ClipboardContent) -> Result<(), ClipboardError> {
216 let mut content = self.content.lock().map_err(|_| {
217 ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
218 })?;
219 *content = request;
220 Ok(())
221 }
222
223 fn clear(&self) -> Result<(), ClipboardError> {
224 let mut content = self.content.lock().map_err(|_| {
225 ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
226 })?;
227 content.items.clear();
228 Ok(())
229 }
230}
231
232pub(crate) fn register_clipboard_capabilities(
233 async_registry: &mut AsyncRegistry,
234 host: Arc<dyn ClipboardHost>,
235) {
236 let read_text_host = host.clone();
237 async_registry.register_operation_capability(READ_CLIPBOARD_TEXT, move |(), _| {
238 let host = read_text_host.clone();
239 async move { host.read_text() }
240 });
241
242 let write_text_host = host.clone();
243 async_registry.register_operation_capability(WRITE_CLIPBOARD_TEXT, move |request, _| {
244 let host = write_text_host.clone();
245 async move { host.write_text(request) }
246 });
247
248 let read_content_host = host.clone();
249 async_registry.register_operation_capability(READ_CLIPBOARD_CONTENT, move |(), _| {
250 let host = read_content_host.clone();
251 async move { host.read_content() }
252 });
253
254 let write_content_host = host.clone();
255 async_registry.register_operation_capability(WRITE_CLIPBOARD_CONTENT, move |request, _| {
256 let host = write_content_host.clone();
257 async move { host.write_content(request) }
258 });
259
260 async_registry.register_operation_capability(CLEAR_CLIPBOARD, move |(), _| {
261 let host = host.clone();
262 async move { host.clear() }
263 });
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn memory_clipboard_reads_and_writes_text() {
272 let host = MemoryClipboardHost::default();
273 host.write_text(ClipboardWriteTextRequest {
274 text: "copied".into(),
275 })
276 .unwrap();
277 assert_eq!(host.read_text().unwrap().text.as_deref(), Some("copied"));
278 host.clear().unwrap();
279 assert_eq!(host.read_text().unwrap().text, None);
280 }
281
282 #[test]
283 fn desktop_clipboard_host_supports_text_content() {
284 let host = DesktopClipboard::new();
285 host.write_text(ClipboardWriteTextRequest {
286 text: "copied".into(),
287 })
288 .unwrap();
289 let content = ClipboardHost::read_content(&host).unwrap();
290 assert_eq!(content.items[0].content_type, "text/plain");
291 }
292}