| 1 | //! Wayland clipboard access via wl-clipboard-rs |
| 2 | //! |
| 3 | //! Uses the wlr-data-control or ext-data-control protocol for clipboard access |
| 4 | //! without needing a Wayland surface (perfect for daemon use). |
| 5 | |
| 6 | use std::io::Read; |
| 7 | |
| 8 | use wl_clipboard_rs::copy::{MimeType as CopyMimeType, Options as CopyOptions, Source}; |
| 9 | use wl_clipboard_rs::paste::{ |
| 10 | get_contents, get_mime_types, ClipboardType, MimeType as PasteMimeType, Seat, |
| 11 | }; |
| 12 | |
| 13 | use super::ClipboardError; |
| 14 | |
| 15 | /// Read clipboard content for a specific MIME type |
| 16 | /// |
| 17 | /// If `mime_type` is None, reads with any available MIME type. |
| 18 | /// Returns the data and the actual MIME type used. |
| 19 | pub fn read_clipboard(mime_type: Option<&str>) -> Result<(Vec<u8>, String), ClipboardError> { |
| 20 | let mime = match mime_type { |
| 21 | Some(mt) => PasteMimeType::Specific(mt), |
| 22 | None => PasteMimeType::Any, |
| 23 | }; |
| 24 | |
| 25 | let (mut pipe, actual_mime) = get_contents(ClipboardType::Regular, Seat::Unspecified, mime) |
| 26 | .map_err(|e| ClipboardError::Wayland(format!("Failed to get clipboard contents: {}", e)))?; |
| 27 | |
| 28 | let mut data = Vec::new(); |
| 29 | pipe.read_to_end(&mut data) |
| 30 | .map_err(|e| ClipboardError::Io(e))?; |
| 31 | |
| 32 | if data.is_empty() { |
| 33 | return Err(ClipboardError::Empty); |
| 34 | } |
| 35 | |
| 36 | Ok((data, actual_mime.to_string())) |
| 37 | } |
| 38 | |
| 39 | /// Get available MIME types from clipboard |
| 40 | pub fn get_available_mime_types() -> Result<Vec<String>, ClipboardError> { |
| 41 | let mime_types = get_mime_types(ClipboardType::Regular, Seat::Unspecified) |
| 42 | .map_err(|e| ClipboardError::Wayland(format!("Failed to get MIME types: {}", e)))?; |
| 43 | |
| 44 | Ok(mime_types.into_iter().map(|m| m.to_string()).collect()) |
| 45 | } |
| 46 | |
| 47 | /// Set clipboard content with a specific MIME type |
| 48 | pub fn set_clipboard(data: &[u8], mime_type: &str) -> Result<(), ClipboardError> { |
| 49 | let mut opts = CopyOptions::new(); |
| 50 | |
| 51 | // Fork to background so the clipboard persists after we return |
| 52 | opts.foreground(false); |
| 53 | opts.copy( |
| 54 | Source::Bytes(data.to_vec().into_boxed_slice()), |
| 55 | CopyMimeType::Specific(mime_type.to_string()), |
| 56 | ) |
| 57 | .map_err(|e| ClipboardError::Wayland(format!("Failed to set clipboard: {}", e)))?; |
| 58 | |
| 59 | Ok(()) |
| 60 | } |
| 61 | |
| 62 | /// MIME type priority lists for selection |
| 63 | pub const IMAGE_MIME_PRIORITY: &[&str] = &["image/png", "image/jpeg", "image/webp", "image/gif"]; |
| 64 | |
| 65 | pub const TEXT_MIME_PRIORITY: &[&str] = &[ |
| 66 | "text/plain;charset=utf-8", |
| 67 | "text/plain", |
| 68 | "UTF8_STRING", |
| 69 | "STRING", |
| 70 | "TEXT", |
| 71 | ]; |
| 72 | |
| 73 | /// Select the best MIME type from offered types based on priority |
| 74 | pub fn select_mime_type(offered: &[String], sync_images: bool, sync_text: bool) -> Option<String> { |
| 75 | // Try images first if enabled (higher priority for screenshots) |
| 76 | if sync_images { |
| 77 | for pref in IMAGE_MIME_PRIORITY { |
| 78 | if offered.iter().any(|m| m == *pref) { |
| 79 | return Some(pref.to_string()); |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | // Then text if enabled |
| 85 | if sync_text { |
| 86 | for pref in TEXT_MIME_PRIORITY { |
| 87 | if offered.iter().any(|m| m == *pref) { |
| 88 | return Some(pref.to_string()); |
| 89 | } |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | None |
| 94 | } |
| 95 | |
| 96 | /// Check if a MIME type is an image type |
| 97 | pub fn is_image_mime(mime: &str) -> bool { |
| 98 | mime.starts_with("image/") |
| 99 | } |
| 100 | |
| 101 | /// Check if a MIME type is a text type |
| 102 | pub fn is_text_mime(mime: &str) -> bool { |
| 103 | mime.starts_with("text/") || mime == "UTF8_STRING" || mime == "STRING" || mime == "TEXT" |
| 104 | } |
| 105 |