1#[cfg(all(
2 feature = "clipboard",
3 feature = "clipboard-images",
4 not(target_arch = "wasm32")
5))]
6use std::io::Cursor;
7
8use crate::clipboard::error::{ClipboardError, ClipboardOperation};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ImageFormat {
13 Png,
15 Jpeg,
17}
18
19impl ImageFormat {
20 pub const fn mime_type(&self) -> &'static str {
22 match self {
23 Self::Png => "image/png",
24 Self::Jpeg => "image/jpeg",
25 }
26 }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct ImageContent {
32 pub data: String,
34 pub mime: &'static str,
36 pub filename: Option<String>,
38}
39
40impl ImageContent {
41 pub fn from_bytes(bytes: &[u8], format: ImageFormat) -> Self {
43 use base64::{Engine as _, engine::general_purpose};
44 Self {
45 data: general_purpose::STANDARD.encode(bytes),
46 mime: format.mime_type(),
47 filename: None,
48 }
49 }
50
51 pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
53 self.filename = Some(filename.into());
54 self
55 }
56
57 pub fn to_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> {
59 use base64::{Engine as _, engine::general_purpose};
60 general_purpose::STANDARD.decode(&self.data)
61 }
62}
63
64pub trait ClipboardProvider {
66 fn read_clipboard_text(&mut self) -> Result<String, ClipboardError>;
68 fn write_clipboard_text(&mut self, text: &str) -> Result<(), ClipboardError>;
70
71 fn set_clipboard_text_cache(&mut self, _text: String) {}
73
74 fn read_primary_selection_text(&mut self) -> Result<String, ClipboardError> {
76 Err(ClipboardError::unsupported(
77 ClipboardOperation::ReadPrimarySelection,
78 ))
79 }
80
81 fn write_primary_selection_text(&mut self, _text: &str) -> Result<(), ClipboardError> {
83 Err(ClipboardError::unsupported(
84 ClipboardOperation::WritePrimarySelection,
85 ))
86 }
87
88 fn supports_primary_selection(&self) -> bool {
90 false
91 }
92
93 fn read_clipboard_image(&mut self) -> Result<ImageContent, ClipboardError> {
96 Err(ClipboardError::unsupported(
97 ClipboardOperation::ReadImageClipboard,
98 ))
99 }
100
101 fn write_clipboard_image(&mut self, _content: &ImageContent) -> Result<(), ClipboardError> {
104 Err(ClipboardError::unsupported(
105 ClipboardOperation::WriteImageClipboard,
106 ))
107 }
108}
109
110#[cfg(not(feature = "clipboard"))]
114pub(crate) struct NoOpClipboardProvider;
115
116#[cfg(not(feature = "clipboard"))]
117impl ClipboardProvider for NoOpClipboardProvider {
118 fn read_clipboard_text(&mut self) -> Result<String, ClipboardError> {
119 Err(ClipboardError::unsupported(
120 ClipboardOperation::ReadClipboard,
121 ))
122 }
123
124 fn write_clipboard_text(&mut self, _text: &str) -> Result<(), ClipboardError> {
125 Err(ClipboardError::unsupported(
126 ClipboardOperation::WriteClipboard,
127 ))
128 }
129}
130
131#[cfg(all(feature = "clipboard", not(target_arch = "wasm32")))]
133pub(crate) struct SystemClipboardProvider {
134 clipboard: Option<arboard::Clipboard>,
135}
136
137#[cfg(all(feature = "clipboard", not(target_arch = "wasm32")))]
138impl SystemClipboardProvider {
139 pub fn new() -> Self {
140 Self { clipboard: None }
141 }
142
143 fn ensure_clipboard(
144 &mut self,
145 operation: ClipboardOperation,
146 ) -> Result<&mut arboard::Clipboard, ClipboardError> {
147 if self.clipboard.is_none() {
148 self.clipboard = Some(
149 arboard::Clipboard::new()
150 .map_err(|err| ClipboardError::provider(operation, err.to_string()))?,
151 );
152 }
153
154 self.clipboard
155 .as_mut()
156 .ok_or_else(|| ClipboardError::provider(operation, "init"))
157 }
158}
159
160#[cfg(all(feature = "clipboard", not(target_arch = "wasm32")))]
161impl Default for SystemClipboardProvider {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167#[cfg(all(feature = "clipboard", not(target_arch = "wasm32")))]
168impl ClipboardProvider for SystemClipboardProvider {
169 fn read_clipboard_text(&mut self) -> Result<String, ClipboardError> {
170 let clipboard = self.ensure_clipboard(ClipboardOperation::ReadClipboard)?;
171 clipboard.get_text().map_err(|err| {
172 ClipboardError::provider(ClipboardOperation::ReadClipboard, err.to_string())
173 })
174 }
175
176 fn write_clipboard_text(&mut self, text: &str) -> Result<(), ClipboardError> {
177 let clipboard = self.ensure_clipboard(ClipboardOperation::WriteClipboard)?;
178 clipboard.set_text(text.to_string()).map_err(|err| {
179 ClipboardError::provider(ClipboardOperation::WriteClipboard, err.to_string())
180 })
181 }
182
183 fn read_primary_selection_text(&mut self) -> Result<String, ClipboardError> {
184 #[cfg(target_os = "linux")]
185 {
186 use arboard::{GetExtLinux, LinuxClipboardKind};
187 let clipboard = self.ensure_clipboard(ClipboardOperation::ReadPrimarySelection)?;
188 clipboard
189 .get()
190 .clipboard(LinuxClipboardKind::Primary)
191 .text()
192 .map_err(|err| {
193 ClipboardError::provider(
194 ClipboardOperation::ReadPrimarySelection,
195 err.to_string(),
196 )
197 })
198 }
199
200 #[cfg(not(target_os = "linux"))]
201 {
202 Err(ClipboardError::unsupported(
203 ClipboardOperation::ReadPrimarySelection,
204 ))
205 }
206 }
207
208 fn write_primary_selection_text(&mut self, _text: &str) -> Result<(), ClipboardError> {
209 #[cfg(target_os = "linux")]
210 {
211 use arboard::{LinuxClipboardKind, SetExtLinux};
212 let clipboard = self.ensure_clipboard(ClipboardOperation::WritePrimarySelection)?;
213 clipboard
214 .set()
215 .clipboard(LinuxClipboardKind::Primary)
216 .text(_text.to_string())
217 .map_err(|err| {
218 ClipboardError::provider(
219 ClipboardOperation::WritePrimarySelection,
220 err.to_string(),
221 )
222 })
223 }
224
225 #[cfg(not(target_os = "linux"))]
226 {
227 Err(ClipboardError::unsupported(
228 ClipboardOperation::WritePrimarySelection,
229 ))
230 }
231 }
232
233 fn supports_primary_selection(&self) -> bool {
234 cfg!(target_os = "linux")
235 }
236
237 #[cfg(feature = "clipboard-images")]
238 fn read_clipboard_image(&mut self) -> Result<ImageContent, ClipboardError> {
239 #[cfg(target_os = "linux")]
240 if let Some(content) = read_wayland_png_clipboard()? {
241 return Ok(content);
242 }
243
244 let clipboard = self.ensure_clipboard(ClipboardOperation::ReadImageClipboard)?;
245
246 let image_data = clipboard.get_image().map_err(|err| {
247 ClipboardError::provider(ClipboardOperation::ReadImageClipboard, err.to_string())
248 })?;
249
250 let width = image_data.width;
251 let height = image_data.height;
252 let rgba_bytes = image_data.bytes.into_owned();
253
254 let mut png_buffer = Cursor::new(Vec::new());
255 {
256 let image_buffer = image::ImageBuffer::<image::Rgba<u8>, Vec<u8>>::from_raw(
257 width as u32,
258 height as u32,
259 rgba_bytes,
260 )
261 .ok_or_else(|| {
262 ClipboardError::provider(
263 ClipboardOperation::ReadImageClipboard,
264 "invalid image buffer dimensions",
265 )
266 })?;
267
268 image_buffer
269 .write_to(&mut png_buffer, image::ImageFormat::Png)
270 .map_err(|err| {
271 ClipboardError::provider(
272 ClipboardOperation::ReadImageClipboard,
273 format!("PNG encode error: {}", err),
274 )
275 })?;
276 }
277
278 Ok(ImageContent::from_bytes(
279 png_buffer.into_inner().as_slice(),
280 ImageFormat::Png,
281 ))
282 }
283
284 #[cfg(feature = "clipboard-images")]
285 fn write_clipboard_image(&mut self, content: &ImageContent) -> Result<(), ClipboardError> {
286 let clipboard = self.ensure_clipboard(ClipboardOperation::WriteImageClipboard)?;
287
288 let bytes = content.to_bytes().map_err(|err| {
289 ClipboardError::provider(
290 ClipboardOperation::WriteImageClipboard,
291 format!("base64 decode error: {}", err),
292 )
293 })?;
294
295 let img = image::load_from_memory(&bytes).map_err(|err| {
296 ClipboardError::provider(
297 ClipboardOperation::WriteImageClipboard,
298 format!("image decode error: {}", err),
299 )
300 })?;
301
302 let rgba = img.to_rgba8();
303 let width = rgba.width() as usize;
304 let height = rgba.height() as usize;
305 let pixels: Vec<u8> = rgba.into_raw();
306
307 clipboard
308 .set_image(arboard::ImageData {
309 width,
310 height,
311 bytes: std::borrow::Cow::Owned(pixels),
312 })
313 .map_err(|err| {
314 ClipboardError::provider(ClipboardOperation::WriteImageClipboard, err.to_string())
315 })
316 }
317}
318
319#[cfg(all(
320 feature = "clipboard",
321 feature = "clipboard-images",
322 target_os = "linux"
323))]
324fn read_wayland_png_clipboard() -> Result<Option<ImageContent>, ClipboardError> {
325 use std::io::Read as _;
326
327 use wl_clipboard_rs::paste::{ClipboardType, Error, MimeType, Seat, get_contents};
328
329 if std::env::var_os("WAYLAND_DISPLAY").is_none() {
330 return Ok(None);
331 }
332
333 let (mut reader, _) = match get_contents(
334 ClipboardType::Regular,
335 Seat::Unspecified,
336 MimeType::Specific(ImageFormat::Png.mime_type()),
337 ) {
338 Ok(result) => result,
339 Err(
340 Error::NoSeats
341 | Error::ClipboardEmpty
342 | Error::NoMimeType
343 | Error::SocketOpenError(_)
344 | Error::WaylandConnection(_)
345 | Error::MissingProtocol { .. }
346 | Error::PrimarySelectionUnsupported
347 | Error::SeatNotFound,
348 ) => return Ok(None),
349 Err(err) => {
350 return Err(ClipboardError::provider(
351 ClipboardOperation::ReadImageClipboard,
352 err.to_string(),
353 ));
354 }
355 };
356
357 let mut bytes = Vec::new();
358 reader.read_to_end(&mut bytes).map_err(|err| {
359 ClipboardError::provider(ClipboardOperation::ReadImageClipboard, err.to_string())
360 })?;
361
362 if bytes.is_empty() {
363 return Ok(None);
364 }
365
366 Ok(Some(ImageContent::from_bytes(&bytes, ImageFormat::Png)))
367}
368
369#[cfg(all(target_arch = "wasm32", feature = "web"))]
370#[derive(Default)]
371pub(crate) struct WebClipboardProvider {
372 cache: std::rc::Rc<std::cell::RefCell<Option<String>>>,
373}
374
375#[cfg(all(target_arch = "wasm32", feature = "web"))]
376impl WebClipboardProvider {
377 pub fn new() -> Self {
378 Self::default()
379 }
380}
381
382#[cfg(all(target_arch = "wasm32", feature = "web"))]
383impl ClipboardProvider for WebClipboardProvider {
384 fn read_clipboard_text(&mut self) -> Result<String, ClipboardError> {
385 if let Some(cached) = self.cache.borrow().clone() {
386 return Ok(cached);
387 }
388
389 Err(ClipboardError::provider(
390 ClipboardOperation::ReadClipboard,
391 "web clipboard read requires a primed cache from a paste gesture",
392 ))
393 }
394
395 fn write_clipboard_text(&mut self, text: &str) -> Result<(), ClipboardError> {
396 let window = web_sys::window().ok_or_else(|| {
397 ClipboardError::provider(ClipboardOperation::WriteClipboard, "window is unavailable")
398 })?;
399 let navigator = window.navigator();
400 let clipboard = navigator.clipboard();
401 let promise = clipboard.write_text(text);
402
403 let cache = std::rc::Rc::clone(&self.cache);
404 let text = text.to_string();
405 let fut = wasm_bindgen_futures::JsFuture::from(promise);
406 wasm_bindgen_futures::spawn_local(async move {
407 match fut.await {
408 Ok(_) => {
409 *cache.borrow_mut() = Some(text);
410 }
411 Err(err) => {
412 web_sys::console::warn_1(&err);
413 }
414 }
415 });
416 Ok(())
417 }
418
419 fn set_clipboard_text_cache(&mut self, text: String) {
420 *self.cache.borrow_mut() = Some(text);
421 }
422
423 fn supports_primary_selection(&self) -> bool {
424 false
425 }
426}