fission_shell_winit/
clipboard.rs1use 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;
7use std::sync::{Arc, Mutex};
8
9#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
10use arboard::Clipboard as Arboard;
11
12pub struct DesktopClipboard {
13 #[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
14 system: Arc<Mutex<Option<Arboard>>>,
15 memory: Arc<Mutex<String>>,
16}
17
18impl DesktopClipboard {
19 pub fn new() -> Self {
20 Self {
21 #[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
22 system: Arc::new(Mutex::new(Arboard::new().ok())),
23 memory: Arc::new(Mutex::new(String::new())),
24 }
25 }
26}
27
28impl Clipboard for DesktopClipboard {
29 fn get_text(&self) -> Option<String> {
30 #[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
31 if let Ok(mut lock) = self.system.lock() {
32 if let Some(cb) = lock.as_mut() {
33 if let Ok(text) = cb.get_text() {
34 return Some(text);
35 }
36 }
37 }
38 self.memory.lock().ok().map(|text| text.clone())
39 }
40
41 fn set_text(&self, text: &str) {
42 if let Ok(mut memory) = self.memory.lock() {
43 *memory = text.to_string();
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 let _ = cb.set_text(text);
49 }
50 }
51 }
52}
53
54pub trait ClipboardHost: Send + Sync + 'static {
56 fn read_text(&self) -> Result<ClipboardText, ClipboardError>;
58 fn write_text(&self, request: ClipboardWriteTextRequest) -> Result<(), ClipboardError>;
60 fn read_content(&self) -> Result<ClipboardContent, ClipboardError>;
62 fn write_content(&self, request: ClipboardContent) -> Result<(), ClipboardError>;
64 fn clear(&self) -> Result<(), ClipboardError>;
66}
67
68impl ClipboardHost for DesktopClipboard {
69 fn read_text(&self) -> Result<ClipboardText, ClipboardError> {
70 Ok(ClipboardText {
71 text: self.get_text(),
72 })
73 }
74
75 fn write_text(&self, request: ClipboardWriteTextRequest) -> Result<(), ClipboardError> {
76 self.set_text(&request.text);
77 Ok(())
78 }
79
80 fn read_content(&self) -> Result<ClipboardContent, ClipboardError> {
81 let text = self.get_text().unwrap_or_default();
82 Ok(ClipboardContent {
83 items: if text.is_empty() {
84 Vec::new()
85 } else {
86 vec![fission_core::ClipboardItem {
87 content_type: "text/plain".into(),
88 bytes: text.into_bytes(),
89 suggested_name: None,
90 }]
91 },
92 })
93 }
94
95 fn write_content(&self, request: ClipboardContent) -> Result<(), ClipboardError> {
96 if let Some(item) = request
97 .items
98 .iter()
99 .find(|item| item.content_type.starts_with("text/plain"))
100 {
101 if let Ok(text) = String::from_utf8(item.bytes.clone()) {
102 self.set_text(&text);
103 return Ok(());
104 }
105 }
106 Err(ClipboardError::unsupported("write_content_non_text"))
107 }
108
109 fn clear(&self) -> Result<(), ClipboardError> {
110 self.set_text("");
111 Ok(())
112 }
113}
114
115#[derive(Debug, Default)]
117pub struct MemoryClipboardHost {
118 content: Arc<Mutex<ClipboardContent>>,
119}
120
121impl ClipboardHost for MemoryClipboardHost {
122 fn read_text(&self) -> Result<ClipboardText, ClipboardError> {
123 let content = self.content.lock().map_err(|_| {
124 ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
125 })?;
126 let text = content
127 .items
128 .iter()
129 .find(|item| item.content_type.starts_with("text/plain"))
130 .and_then(|item| String::from_utf8(item.bytes.clone()).ok());
131 Ok(ClipboardText { text })
132 }
133
134 fn write_text(&self, request: ClipboardWriteTextRequest) -> Result<(), ClipboardError> {
135 let mut content = self.content.lock().map_err(|_| {
136 ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
137 })?;
138 *content = ClipboardContent {
139 items: vec![fission_core::ClipboardItem {
140 content_type: "text/plain".into(),
141 bytes: request.text.into_bytes(),
142 suggested_name: None,
143 }],
144 };
145 Ok(())
146 }
147
148 fn read_content(&self) -> Result<ClipboardContent, ClipboardError> {
149 self.content
150 .lock()
151 .map(|content| content.clone())
152 .map_err(|_| ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned"))
153 }
154
155 fn write_content(&self, request: ClipboardContent) -> Result<(), ClipboardError> {
156 let mut content = self.content.lock().map_err(|_| {
157 ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
158 })?;
159 *content = request;
160 Ok(())
161 }
162
163 fn clear(&self) -> Result<(), ClipboardError> {
164 let mut content = self.content.lock().map_err(|_| {
165 ClipboardError::new("lock_poisoned", "memory clipboard lock was poisoned")
166 })?;
167 content.items.clear();
168 Ok(())
169 }
170}
171
172pub(crate) fn register_clipboard_capabilities(
173 async_registry: &mut AsyncRegistry,
174 host: Arc<dyn ClipboardHost>,
175) {
176 let read_text_host = host.clone();
177 async_registry.register_operation_capability(READ_CLIPBOARD_TEXT, move |(), _| {
178 let host = read_text_host.clone();
179 async move { host.read_text() }
180 });
181
182 let write_text_host = host.clone();
183 async_registry.register_operation_capability(WRITE_CLIPBOARD_TEXT, move |request, _| {
184 let host = write_text_host.clone();
185 async move { host.write_text(request) }
186 });
187
188 let read_content_host = host.clone();
189 async_registry.register_operation_capability(READ_CLIPBOARD_CONTENT, move |(), _| {
190 let host = read_content_host.clone();
191 async move { host.read_content() }
192 });
193
194 let write_content_host = host.clone();
195 async_registry.register_operation_capability(WRITE_CLIPBOARD_CONTENT, move |request, _| {
196 let host = write_content_host.clone();
197 async move { host.write_content(request) }
198 });
199
200 async_registry.register_operation_capability(CLEAR_CLIPBOARD, move |(), _| {
201 let host = host.clone();
202 async move { host.clear() }
203 });
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn memory_clipboard_reads_and_writes_text() {
212 let host = MemoryClipboardHost::default();
213 host.write_text(ClipboardWriteTextRequest {
214 text: "copied".into(),
215 })
216 .unwrap();
217 assert_eq!(host.read_text().unwrap().text.as_deref(), Some("copied"));
218 host.clear().unwrap();
219 assert_eq!(host.read_text().unwrap().text, None);
220 }
221
222 #[test]
223 fn desktop_clipboard_host_supports_text_content() {
224 let host = DesktopClipboard::new();
225 host.write_text(ClipboardWriteTextRequest {
226 text: "copied".into(),
227 })
228 .unwrap();
229 let content = ClipboardHost::read_content(&host).unwrap();
230 assert_eq!(content.items[0].content_type, "text/plain");
231 }
232}