1use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
5use std::borrow::Cow;
6use std::error::Error;
7use std::path::{Path, PathBuf};
8use std::str;
9use url::Url;
10
11#[derive(Clone, Copy, Debug)]
12pub enum ClipboardKind {
13 Copy,
14 Cut { is_dnd: bool },
15}
16
17#[derive(Clone, Debug)]
18pub struct ClipboardCopy {
19 pub available: Cow<'static, [String]>,
20 pub text_plain: Cow<'static, [u8]>,
21 pub text_uri_list: Cow<'static, [u8]>,
22 pub x_special_gnome_copied_files: Cow<'static, [u8]>,
23}
24
25impl ClipboardCopy {
26 pub fn new<P: AsRef<Path>>(kind: ClipboardKind, paths: impl IntoIterator<Item = P>) -> Self {
27 let available = vec![
28 "text/plain".to_string(),
29 "text/plain;charset=utf-8".to_string(),
30 "UTF8_STRING".to_string(),
31 "text/uri-list".to_string(),
32 "x-special/gnome-copied-files".to_string(),
33 ];
34 let mut text_plain = String::new();
35 let mut text_uri_list = String::new();
36 let mut x_special_gnome_copied_files = match kind {
37 ClipboardKind::Copy => "copy",
38 ClipboardKind::Cut { .. } => "cut",
39 }
40 .to_string();
41 let cr_nl = "\r\n";
43 for path in paths {
44 let path = path.as_ref();
45
46 match path.to_str() {
47 Some(path_str) => {
48 if !text_plain.is_empty() {
49 text_plain.push_str(cr_nl);
50 }
51 text_plain.push_str(path_str);
53 }
54 None => {
55 log::warn!(
57 "{} is not valid UTF-8, not adding to text/plain clipboard",
58 path.display()
59 );
60 }
61 }
62
63 match Url::from_file_path(path) {
64 Ok(url) => {
65 let url_str = url.as_ref();
66
67 text_uri_list.push_str(url_str);
68 text_uri_list.push_str(cr_nl);
69
70 x_special_gnome_copied_files.push('\n');
71 x_special_gnome_copied_files.push_str(url_str);
72 }
73 Err(()) => {
74 log::warn!(
75 "{} cannot be turned into a URL, not adding to text/uri-list clipboard",
76 path.display()
77 );
78 }
79 }
80 }
81 Self {
82 available: Cow::from(available),
83 text_plain: Cow::from(text_plain.into_bytes()),
84 text_uri_list: Cow::from(text_uri_list.into_bytes()),
85 x_special_gnome_copied_files: Cow::from(x_special_gnome_copied_files.into_bytes()),
86 }
87 }
88}
89
90impl AsMimeTypes for ClipboardCopy {
91 fn available(&self) -> Cow<'static, [String]> {
92 self.available.clone()
93 }
94
95 fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> {
96 match mime_type {
97 "text/plain" | "text/plain;charset=utf-8" | "UTF8_STRING" => {
98 Some(self.text_plain.clone())
99 }
100 "text/uri-list" => Some(self.text_uri_list.clone()),
101 "x-special/gnome-copied-files" => Some(self.x_special_gnome_copied_files.clone()),
102 _ => None,
103 }
104 }
105}
106
107#[derive(Clone, Debug)]
108pub struct ClipboardPaste {
109 pub kind: ClipboardKind,
110 pub paths: Vec<PathBuf>,
111}
112
113impl AllowedMimeTypes for ClipboardPaste {
114 fn allowed() -> Cow<'static, [String]> {
115 Cow::from(vec![
116 "x-special/gnome-copied-files".to_string(),
117 "text/uri-list".to_string(),
118 ])
119 }
120}
121
122impl TryFrom<(Vec<u8>, String)> for ClipboardPaste {
123 type Error = Box<dyn Error>;
124 fn try_from(value: (Vec<u8>, String)) -> Result<Self, Self::Error> {
125 let (data, mime) = value;
126 let mut kind = ClipboardKind::Copy;
128 let mut paths = Vec::new();
129
130 match mime.as_str() {
131 "text/uri-list" => {
132 let text = str::from_utf8(&data)?;
133 let _lines = text.lines();
134
135 for line in text.lines() {
136 let url = Url::parse(line)?;
137 match url.to_file_path() {
138 Ok(path) => paths.push(path),
139 Err(()) => Err(format!("invalid file URL {url:?}"))?,
140 }
141 }
142 }
143 "x-special/gnome-copied-files" => {
144 let text = str::from_utf8(&data)?;
145 for (i, line) in text.lines().enumerate() {
146 if i == 0 {
147 kind = match line {
148 "copy" => ClipboardKind::Copy,
149 "cut" => ClipboardKind::Cut { is_dnd: false },
150 _ => Err(format!("unsupported clipboard operation {line:?}"))?,
151 };
152 } else {
153 let url = Url::parse(line)?;
154 match url.to_file_path() {
155 Ok(path) => paths.push(path),
156 Err(()) => Err(format!("invalid file URL {url:?}"))?,
157 }
158 }
159 }
160 }
161 _ => Err(format!("unsupported mime type {mime:?}"))?,
162 }
163 Ok(Self { kind, paths })
164 }
165}
166
167#[derive(Clone, Debug)]
169pub struct ClipboardPasteImage {
170 pub data: Vec<u8>,
171 pub mime_type: String,
172}
173
174impl AllowedMimeTypes for ClipboardPasteImage {
175 fn allowed() -> Cow<'static, [String]> {
176 Cow::from(vec![
177 "image/png".to_string(),
178 "image/jpeg".to_string(),
179 "image/gif".to_string(),
180 "image/bmp".to_string(),
181 "image/webp".to_string(),
182 "image/tiff".to_string(),
183 "image/x-tiff".to_string(),
184 "image/svg+xml".to_string(),
185 "image/x-icon".to_string(),
186 "image/vnd.microsoft.icon".to_string(),
187 "image/x-bmp".to_string(),
188 "image/x-ms-bmp".to_string(),
189 "image/pjpeg".to_string(),
190 "image/x-png".to_string(),
191 "image/avif".to_string(),
192 "image/heic".to_string(),
193 "image/heif".to_string(),
194 "image/jxl".to_string(),
195 ])
196 }
197}
198
199impl TryFrom<(Vec<u8>, String)> for ClipboardPasteImage {
200 type Error = Box<dyn Error>;
201 fn try_from(value: (Vec<u8>, String)) -> Result<Self, Self::Error> {
202 let (data, mime) = value;
203 if data.is_empty() {
204 return Err("Empty image data".into());
205 }
206 Ok(Self {
207 data,
208 mime_type: mime,
209 })
210 }
211}
212
213impl ClipboardPasteImage {
214 pub fn extension(&self) -> Option<&'static str> {
217 match self.mime_type.as_str() {
218 "image/png" | "image/x-png" => Some("png"),
219 "image/jpeg" | "image/pjpeg" => Some("jpg"),
220 "image/gif" => Some("gif"),
221 "image/bmp" | "image/x-bmp" | "image/x-ms-bmp" => Some("bmp"),
222 "image/webp" => Some("webp"),
223 "image/tiff" | "image/x-tiff" => Some("tiff"),
224 "image/svg+xml" => Some("svg"),
225 "image/x-icon" | "image/vnd.microsoft.icon" => Some("ico"),
226 "image/avif" => Some("avif"),
227 "image/heic" => Some("heic"),
228 "image/heif" => Some("heif"),
229 "image/jxl" => Some("jxl"),
230 _ => None,
231 }
232 }
233}
234
235#[derive(Clone, Debug)]
237pub struct ClipboardPasteVideo {
238 pub data: Vec<u8>,
239 pub mime_type: String,
240}
241
242impl AllowedMimeTypes for ClipboardPasteVideo {
243 fn allowed() -> Cow<'static, [String]> {
244 Cow::from(vec![
245 "video/mp4".to_string(),
246 "video/webm".to_string(),
247 "video/ogg".to_string(),
248 "video/mpeg".to_string(),
249 "video/quicktime".to_string(),
250 "video/x-msvideo".to_string(),
251 "video/x-matroska".to_string(),
252 "video/x-flv".to_string(),
253 "video/3gpp".to_string(),
254 "video/3gpp2".to_string(),
255 "video/x-ms-wmv".to_string(),
256 "video/avi".to_string(),
257 ])
258 }
259}
260
261impl TryFrom<(Vec<u8>, String)> for ClipboardPasteVideo {
262 type Error = Box<dyn Error>;
263 fn try_from(value: (Vec<u8>, String)) -> Result<Self, Self::Error> {
264 let (data, mime) = value;
265 if data.is_empty() {
266 return Err("Empty video data".into());
267 }
268 Ok(Self {
269 data,
270 mime_type: mime,
271 })
272 }
273}
274
275impl ClipboardPasteVideo {
276 pub fn extension(&self) -> Option<&'static str> {
279 match self.mime_type.as_str() {
280 "video/mp4" => Some("mp4"),
281 "video/webm" => Some("webm"),
282 "video/ogg" => Some("ogv"),
283 "video/mpeg" => Some("mpeg"),
284 "video/quicktime" => Some("mov"),
285 "video/x-msvideo" | "video/avi" => Some("avi"),
286 "video/x-matroska" => Some("mkv"),
287 "video/x-flv" => Some("flv"),
288 "video/3gpp" => Some("3gp"),
289 "video/3gpp2" => Some("3g2"),
290 "video/x-ms-wmv" => Some("wmv"),
291 _ => None,
292 }
293 }
294}
295
296#[derive(Clone, Debug)]
298pub struct ClipboardPasteText {
299 pub data: String,
300}
301
302impl AllowedMimeTypes for ClipboardPasteText {
303 fn allowed() -> Cow<'static, [String]> {
304 Cow::from(vec![
305 "text/plain".to_string(),
306 "text/plain;charset=utf-8".to_string(),
307 "UTF8_STRING".to_string(),
308 "STRING".to_string(),
309 "TEXT".to_string(),
310 ])
311 }
312}
313
314impl TryFrom<(Vec<u8>, String)> for ClipboardPasteText {
315 type Error = Box<dyn Error>;
316 fn try_from(value: (Vec<u8>, String)) -> Result<Self, Self::Error> {
317 let (data, _mime) = value;
318 if data.is_empty() {
319 return Err("Empty text data".into());
320 }
321 let text = String::from_utf8_lossy(&data);
324 Ok(Self {
325 data: text.into_owned(),
326 })
327 }
328}
329
330#[derive(Clone, Debug)]
333pub enum ClipboardCache {
334 Files(ClipboardPaste),
335 Image(ClipboardPasteImage),
336 Video(ClipboardPasteVideo),
337 Text(ClipboardPasteText),
338 Empty,
339}