1#[cfg(all(
2 feature = "clipboard",
3 feature = "clipboard-images",
4 not(target_arch = "wasm32")
5))]
6use std::io::Cursor;
7
8use std::path::PathBuf;
9
10use crate::clipboard::error::{ClipboardError, ClipboardOperation};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ImageFormat {
15 Png,
17 Jpeg,
19}
20
21impl ImageFormat {
22 pub const fn mime_type(&self) -> &'static str {
24 match self {
25 Self::Png => "image/png",
26 Self::Jpeg => "image/jpeg",
27 }
28 }
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct ImageContent {
34 pub data: String,
36 pub mime: &'static str,
38 pub filename: Option<String>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum ClipboardPasteContent {
45 Text(String),
47 Rich,
49 Unavailable,
51}
52
53impl ImageContent {
54 pub fn from_bytes(bytes: &[u8], format: ImageFormat) -> Self {
56 use base64::{Engine as _, engine::general_purpose};
57 Self {
58 data: general_purpose::STANDARD.encode(bytes),
59 mime: format.mime_type(),
60 filename: None,
61 }
62 }
63
64 pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
66 self.filename = Some(filename.into());
67 self
68 }
69
70 pub fn to_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> {
72 use base64::{Engine as _, engine::general_purpose};
73 general_purpose::STANDARD.decode(&self.data)
74 }
75}
76
77pub trait ClipboardProvider {
79 fn read_clipboard_text(&mut self) -> Result<String, ClipboardError>;
81 fn write_clipboard_text(&mut self, text: &str) -> Result<(), ClipboardError>;
83
84 fn read_terminal_paste(&mut self) -> Result<ClipboardPasteContent, ClipboardError> {
90 self.read_clipboard_text().map(ClipboardPasteContent::Text)
91 }
92
93 fn set_clipboard_text_cache(&mut self, _text: String) {}
95
96 fn read_primary_selection_text(&mut self) -> Result<String, ClipboardError> {
98 Err(ClipboardError::unsupported(
99 ClipboardOperation::ReadPrimarySelection,
100 ))
101 }
102
103 fn write_primary_selection_text(&mut self, _text: &str) -> Result<(), ClipboardError> {
105 Err(ClipboardError::unsupported(
106 ClipboardOperation::WritePrimarySelection,
107 ))
108 }
109
110 fn supports_primary_selection(&self) -> bool {
112 false
113 }
114
115 fn read_clipboard_image(&mut self) -> Result<ImageContent, ClipboardError> {
118 Err(ClipboardError::unsupported(
119 ClipboardOperation::ReadImageClipboard,
120 ))
121 }
122
123 fn write_clipboard_image(&mut self, _content: &ImageContent) -> Result<(), ClipboardError> {
126 Err(ClipboardError::unsupported(
127 ClipboardOperation::WriteImageClipboard,
128 ))
129 }
130
131 fn read_clipboard_files(&mut self) -> Result<Vec<PathBuf>, ClipboardError> {
136 Err(ClipboardError::unsupported(
137 ClipboardOperation::ReadFileClipboard,
138 ))
139 }
140
141 fn write_clipboard_files(&mut self, _paths: &[PathBuf]) -> Result<(), ClipboardError> {
147 Err(ClipboardError::unsupported(
148 ClipboardOperation::WriteFileClipboard,
149 ))
150 }
151
152 fn supports_file_clipboard(&self) -> bool {
154 false
155 }
156}
157
158#[cfg(not(feature = "clipboard"))]
162pub(crate) struct NoOpClipboardProvider;
163
164#[cfg(not(feature = "clipboard"))]
165impl ClipboardProvider for NoOpClipboardProvider {
166 fn read_clipboard_text(&mut self) -> Result<String, ClipboardError> {
167 Err(ClipboardError::unsupported(
168 ClipboardOperation::ReadClipboard,
169 ))
170 }
171
172 fn write_clipboard_text(&mut self, _text: &str) -> Result<(), ClipboardError> {
173 Err(ClipboardError::unsupported(
174 ClipboardOperation::WriteClipboard,
175 ))
176 }
177}
178
179#[cfg(all(feature = "clipboard", not(target_arch = "wasm32")))]
181pub(crate) struct SystemClipboardProvider {
182 clipboard: Option<arboard::Clipboard>,
183}
184
185#[cfg(all(feature = "clipboard", not(target_arch = "wasm32")))]
186impl SystemClipboardProvider {
187 pub fn new() -> Self {
188 Self { clipboard: None }
189 }
190
191 fn ensure_clipboard(
192 &mut self,
193 operation: ClipboardOperation,
194 ) -> Result<&mut arboard::Clipboard, ClipboardError> {
195 if self.clipboard.is_none() {
196 self.clipboard = Some(
197 arboard::Clipboard::new()
198 .map_err(|err| ClipboardError::provider(operation, err.to_string()))?,
199 );
200 }
201
202 self.clipboard
203 .as_mut()
204 .ok_or_else(|| ClipboardError::provider(operation, "init"))
205 }
206}
207
208#[cfg(all(feature = "clipboard", not(target_arch = "wasm32")))]
209impl Default for SystemClipboardProvider {
210 fn default() -> Self {
211 Self::new()
212 }
213}
214
215#[cfg(all(feature = "clipboard", not(target_arch = "wasm32")))]
216impl ClipboardProvider for SystemClipboardProvider {
217 fn read_clipboard_text(&mut self) -> Result<String, ClipboardError> {
218 let clipboard = self.ensure_clipboard(ClipboardOperation::ReadClipboard)?;
219 clipboard.get_text().map_err(|err| {
220 ClipboardError::provider(ClipboardOperation::ReadClipboard, err.to_string())
221 })
222 }
223
224 fn write_clipboard_text(&mut self, text: &str) -> Result<(), ClipboardError> {
225 let clipboard = self.ensure_clipboard(ClipboardOperation::WriteClipboard)?;
226 clipboard.set_text(text.to_string()).map_err(|err| {
227 ClipboardError::provider(ClipboardOperation::WriteClipboard, err.to_string())
228 })
229 }
230
231 fn read_terminal_paste(&mut self) -> Result<ClipboardPasteContent, ClipboardError> {
232 let clipboard = self.ensure_clipboard(ClipboardOperation::ReadClipboard)?;
233 match clipboard.get().file_list() {
234 Ok(files) if !files.is_empty() => return Ok(ClipboardPasteContent::Rich),
235 Ok(_) | Err(arboard::Error::ContentNotAvailable) => {}
236 Err(err) => {
237 return Err(ClipboardError::provider(
238 ClipboardOperation::ReadClipboard,
239 err.to_string(),
240 ));
241 }
242 }
243
244 #[cfg(feature = "clipboard-images")]
245 {
246 let probe_image_data = true;
247 #[cfg(target_os = "linux")]
248 let probe_image_data = match wayland_clipboard_has_png()? {
249 Some(true) => return Ok(ClipboardPasteContent::Rich),
250 Some(false) => false,
251 None => probe_image_data,
252 };
253
254 if probe_image_data {
255 let clipboard = self.ensure_clipboard(ClipboardOperation::ReadImageClipboard)?;
259 match clipboard.get().image() {
260 Ok(_) => return Ok(ClipboardPasteContent::Rich),
261 Err(arboard::Error::ContentNotAvailable) => {}
262 Err(err) => {
263 return Err(ClipboardError::provider(
264 ClipboardOperation::ReadImageClipboard,
265 err.to_string(),
266 ));
267 }
268 }
269 }
270 }
271
272 let clipboard = self.ensure_clipboard(ClipboardOperation::ReadClipboard)?;
273 match clipboard.get().text() {
274 Ok(text) => Ok(ClipboardPasteContent::Text(text)),
275 Err(arboard::Error::ContentNotAvailable) => Ok(ClipboardPasteContent::Unavailable),
276 Err(err) => Err(ClipboardError::provider(
277 ClipboardOperation::ReadClipboard,
278 err.to_string(),
279 )),
280 }
281 }
282
283 fn read_primary_selection_text(&mut self) -> Result<String, ClipboardError> {
284 #[cfg(target_os = "linux")]
285 {
286 use arboard::{GetExtLinux, LinuxClipboardKind};
287 let clipboard = self.ensure_clipboard(ClipboardOperation::ReadPrimarySelection)?;
288 clipboard
289 .get()
290 .clipboard(LinuxClipboardKind::Primary)
291 .text()
292 .map_err(|err| {
293 ClipboardError::provider(
294 ClipboardOperation::ReadPrimarySelection,
295 err.to_string(),
296 )
297 })
298 }
299
300 #[cfg(not(target_os = "linux"))]
301 {
302 Err(ClipboardError::unsupported(
303 ClipboardOperation::ReadPrimarySelection,
304 ))
305 }
306 }
307
308 fn write_primary_selection_text(&mut self, _text: &str) -> Result<(), ClipboardError> {
309 #[cfg(target_os = "linux")]
310 {
311 use arboard::{LinuxClipboardKind, SetExtLinux};
312 let clipboard = self.ensure_clipboard(ClipboardOperation::WritePrimarySelection)?;
313 clipboard
314 .set()
315 .clipboard(LinuxClipboardKind::Primary)
316 .text(_text.to_string())
317 .map_err(|err| {
318 ClipboardError::provider(
319 ClipboardOperation::WritePrimarySelection,
320 err.to_string(),
321 )
322 })
323 }
324
325 #[cfg(not(target_os = "linux"))]
326 {
327 Err(ClipboardError::unsupported(
328 ClipboardOperation::WritePrimarySelection,
329 ))
330 }
331 }
332
333 fn supports_primary_selection(&self) -> bool {
334 cfg!(target_os = "linux")
335 }
336
337 fn read_clipboard_files(&mut self) -> Result<Vec<PathBuf>, ClipboardError> {
338 let clipboard = self.ensure_clipboard(ClipboardOperation::ReadFileClipboard)?;
339 match clipboard.get().file_list() {
340 Ok(paths) => Ok(paths),
341 Err(arboard::Error::ContentNotAvailable) => Ok(Vec::new()),
343 Err(err) => Err(ClipboardError::provider(
344 ClipboardOperation::ReadFileClipboard,
345 err.to_string(),
346 )),
347 }
348 }
349
350 fn write_clipboard_files(&mut self, paths: &[PathBuf]) -> Result<(), ClipboardError> {
351 let clipboard = self.ensure_clipboard(ClipboardOperation::WriteFileClipboard)?;
352 clipboard.set().file_list(paths).map_err(|err| {
353 ClipboardError::provider(ClipboardOperation::WriteFileClipboard, err.to_string())
354 })
355 }
356
357 fn supports_file_clipboard(&self) -> bool {
358 true
359 }
360
361 #[cfg(feature = "clipboard-images")]
362 fn read_clipboard_image(&mut self) -> Result<ImageContent, ClipboardError> {
363 #[cfg(target_os = "linux")]
364 if let Some(content) = read_wayland_png_clipboard()? {
365 return Ok(content);
366 }
367
368 let clipboard = self.ensure_clipboard(ClipboardOperation::ReadImageClipboard)?;
369
370 let image_data = clipboard.get_image().map_err(|err| {
371 ClipboardError::provider(ClipboardOperation::ReadImageClipboard, err.to_string())
372 })?;
373
374 let width = image_data.width;
375 let height = image_data.height;
376 let rgba_bytes = image_data.bytes.into_owned();
377
378 let mut png_buffer = Cursor::new(Vec::new());
379 {
380 let image_buffer = image::ImageBuffer::<image::Rgba<u8>, Vec<u8>>::from_raw(
381 width as u32,
382 height as u32,
383 rgba_bytes,
384 )
385 .ok_or_else(|| {
386 ClipboardError::provider(
387 ClipboardOperation::ReadImageClipboard,
388 "invalid image buffer dimensions",
389 )
390 })?;
391
392 image_buffer
393 .write_to(&mut png_buffer, image::ImageFormat::Png)
394 .map_err(|err| {
395 ClipboardError::provider(
396 ClipboardOperation::ReadImageClipboard,
397 format!("PNG encode error: {}", err),
398 )
399 })?;
400 }
401
402 Ok(ImageContent::from_bytes(
403 png_buffer.into_inner().as_slice(),
404 ImageFormat::Png,
405 ))
406 }
407
408 #[cfg(feature = "clipboard-images")]
409 fn write_clipboard_image(&mut self, content: &ImageContent) -> Result<(), ClipboardError> {
410 let clipboard = self.ensure_clipboard(ClipboardOperation::WriteImageClipboard)?;
411
412 let bytes = content.to_bytes().map_err(|err| {
413 ClipboardError::provider(
414 ClipboardOperation::WriteImageClipboard,
415 format!("base64 decode error: {}", err),
416 )
417 })?;
418
419 let img = image::load_from_memory(&bytes).map_err(|err| {
420 ClipboardError::provider(
421 ClipboardOperation::WriteImageClipboard,
422 format!("image decode error: {}", err),
423 )
424 })?;
425
426 let rgba = img.to_rgba8();
427 let width = rgba.width() as usize;
428 let height = rgba.height() as usize;
429 let pixels: Vec<u8> = rgba.into_raw();
430
431 clipboard
432 .set_image(arboard::ImageData {
433 width,
434 height,
435 bytes: std::borrow::Cow::Owned(pixels),
436 })
437 .map_err(|err| {
438 ClipboardError::provider(ClipboardOperation::WriteImageClipboard, err.to_string())
439 })
440 }
441}
442
443#[cfg(all(
444 feature = "clipboard",
445 feature = "clipboard-images",
446 target_os = "linux"
447))]
448fn read_wayland_png_clipboard() -> Result<Option<ImageContent>, ClipboardError> {
449 use std::io::Read as _;
450
451 use wl_clipboard_rs::paste::{ClipboardType, Error, MimeType, Seat, get_contents};
452
453 if std::env::var_os("WAYLAND_DISPLAY").is_none() {
454 return Ok(None);
455 }
456
457 let (mut reader, _) = match get_contents(
458 ClipboardType::Regular,
459 Seat::Unspecified,
460 MimeType::Specific(ImageFormat::Png.mime_type()),
461 ) {
462 Ok(result) => result,
463 Err(
464 Error::NoSeats
465 | Error::ClipboardEmpty
466 | Error::NoMimeType
467 | Error::SocketOpenError(_)
468 | Error::WaylandConnection(_)
469 | Error::MissingProtocol { .. }
470 | Error::PrimarySelectionUnsupported
471 | Error::SeatNotFound,
472 ) => return Ok(None),
473 Err(err) => {
474 return Err(ClipboardError::provider(
475 ClipboardOperation::ReadImageClipboard,
476 err.to_string(),
477 ));
478 }
479 };
480
481 let mut bytes = Vec::new();
482 reader.read_to_end(&mut bytes).map_err(|err| {
483 ClipboardError::provider(ClipboardOperation::ReadImageClipboard, err.to_string())
484 })?;
485
486 if bytes.is_empty() {
487 return Ok(None);
488 }
489
490 Ok(Some(ImageContent::from_bytes(&bytes, ImageFormat::Png)))
491}
492
493#[cfg(all(
494 feature = "clipboard",
495 feature = "clipboard-images",
496 target_os = "linux"
497))]
498fn wayland_clipboard_has_png() -> Result<Option<bool>, ClipboardError> {
499 use wl_clipboard_rs::paste::{ClipboardType, Error, Seat, get_mime_types};
500
501 if std::env::var_os("WAYLAND_DISPLAY").is_none() {
502 return Ok(None);
503 }
504
505 match get_mime_types(ClipboardType::Regular, Seat::Unspecified) {
506 Ok(types) => Ok(Some(types.contains(ImageFormat::Png.mime_type()))),
507 Err(Error::ClipboardEmpty | Error::NoMimeType) => Ok(Some(false)),
508 Err(
509 Error::NoSeats
510 | Error::SocketOpenError(_)
511 | Error::WaylandConnection(_)
512 | Error::MissingProtocol { .. }
513 | Error::PrimarySelectionUnsupported
514 | Error::SeatNotFound,
515 ) => Ok(None),
516 Err(err) => Err(ClipboardError::provider(
517 ClipboardOperation::ReadImageClipboard,
518 err.to_string(),
519 )),
520 }
521}
522
523#[cfg(all(target_arch = "wasm32", feature = "web"))]
524#[derive(Default)]
525pub(crate) struct WebClipboardProvider {
526 cache: std::rc::Rc<std::cell::RefCell<Option<String>>>,
527}
528
529#[cfg(all(target_arch = "wasm32", feature = "web"))]
530impl WebClipboardProvider {
531 pub fn new() -> Self {
532 Self::default()
533 }
534}
535
536#[cfg(all(target_arch = "wasm32", feature = "web"))]
537impl ClipboardProvider for WebClipboardProvider {
538 fn read_clipboard_text(&mut self) -> Result<String, ClipboardError> {
539 if let Some(cached) = self.cache.borrow().clone() {
540 return Ok(cached);
541 }
542
543 Err(ClipboardError::provider(
544 ClipboardOperation::ReadClipboard,
545 "web clipboard read requires a primed cache from a paste gesture",
546 ))
547 }
548
549 fn write_clipboard_text(&mut self, text: &str) -> Result<(), ClipboardError> {
550 let window = web_sys::window().ok_or_else(|| {
551 ClipboardError::provider(ClipboardOperation::WriteClipboard, "window is unavailable")
552 })?;
553 let navigator = window.navigator();
554 let clipboard = navigator.clipboard();
555 let promise = clipboard.write_text(text);
556
557 let cache = std::rc::Rc::clone(&self.cache);
558 let text = text.to_string();
559 let fut = wasm_bindgen_futures::JsFuture::from(promise);
560 wasm_bindgen_futures::spawn_local(async move {
561 match fut.await {
562 Ok(_) => {
563 *cache.borrow_mut() = Some(text);
564 }
565 Err(err) => {
566 web_sys::console::warn_1(&err);
567 }
568 }
569 });
570 Ok(())
571 }
572
573 fn set_clipboard_text_cache(&mut self, text: String) {
574 *self.cache.borrow_mut() = Some(text);
575 }
576
577 fn supports_primary_selection(&self) -> bool {
578 false
579 }
580}