1use collections::FxBuildHasher;
4use seahash::SeaHasher;
5use serde::{Deserialize, Serialize};
6use smallvec::SmallVec;
7use std::{
8 hash::{BuildHasher, Hash, Hasher},
9 path::PathBuf,
10};
11use strum::EnumIter;
12
13#[derive(Debug, Clone, Default, Eq, PartialEq)]
15pub struct ExternalPaths(pub SmallVec<[PathBuf; 2]>);
16
17impl ExternalPaths {
18 pub fn paths(&self) -> &[PathBuf] {
20 &self.0
21 }
22}
23
24#[derive(Debug, Clone, Eq, PartialEq)]
27pub enum ExternalDragPayload {
28 Files(FileDragPaths),
30}
31
32#[derive(Debug, Clone, Default, Eq, PartialEq)]
35pub struct FileDragPaths(SmallVec<[(PathBuf, bool); 2]>);
36
37impl FileDragPaths {
38 pub fn new(entries: impl IntoIterator<Item = (PathBuf, bool)>) -> Self {
40 Self(entries.into_iter().collect())
41 }
42
43 pub fn entries(&self) -> &[(PathBuf, bool)] {
45 &self.0
46 }
47}
48
49pub fn hash<T: Hash>(data: &T) -> u64 {
51 FxBuildHasher.hash_one(data)
52}
53
54#[derive(Clone, Debug, Eq, PartialEq)]
56pub struct ClipboardItem {
57 pub entries: Vec<ClipboardEntry>,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
66pub enum ClipboardReadError {
67 Unavailable,
71 Denied(String),
74 UnsupportedContent,
77}
78
79impl std::fmt::Display for ClipboardReadError {
80 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 match self {
82 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
83 Self::Denied(message) => {
84 write!(formatter, "clipboard access was denied: {message}")
85 }
86 Self::UnsupportedContent => {
87 formatter.write_str("the clipboard contents are unsupported")
88 }
89 }
90 }
91}
92
93impl std::error::Error for ClipboardReadError {}
94
95#[derive(Clone, Debug, Eq, PartialEq)]
97pub enum ClipboardEntry {
98 String(ClipboardString),
100 Image(Image),
102 ExternalPaths(ExternalPaths),
104}
105
106impl ClipboardItem {
107 pub fn new_string(text: String) -> Self {
109 Self {
110 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
111 }
112 }
113
114 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
116 Self {
117 entries: vec![ClipboardEntry::String(ClipboardString {
118 text,
119 metadata: Some(metadata),
120 })],
121 }
122 }
123
124 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
126 Self {
127 entries: vec![ClipboardEntry::String(
128 ClipboardString::new(text).with_json_metadata(metadata),
129 )],
130 }
131 }
132
133 pub fn new_image(image: &Image) -> Self {
135 Self {
136 entries: vec![ClipboardEntry::Image(image.clone())],
137 }
138 }
139
140 pub fn text(&self) -> Option<String> {
143 let mut answer = String::new();
144
145 for entry in self.entries.iter() {
146 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
147 answer.push_str(text);
148 }
149 }
150
151 if answer.is_empty() {
152 for entry in self.entries.iter() {
153 if let ClipboardEntry::ExternalPaths(paths) = entry {
154 for path in &paths.0 {
155 use std::fmt::Write as _;
156 _ = write!(answer, "{}", path.display());
157 }
158 }
159 }
160 }
161
162 if !answer.is_empty() {
163 Some(answer)
164 } else {
165 None
166 }
167 }
168
169 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
171 pub fn metadata(&self) -> Option<&String> {
172 match self.entries().first() {
173 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
174 clipboard_string.metadata.as_ref()
175 }
176 _ => None,
177 }
178 }
179
180 pub fn entries(&self) -> &[ClipboardEntry] {
182 &self.entries
183 }
184
185 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
187 self.entries.into_iter()
188 }
189}
190
191impl From<ClipboardString> for ClipboardEntry {
192 fn from(value: ClipboardString) -> Self {
193 Self::String(value)
194 }
195}
196
197impl From<String> for ClipboardEntry {
198 fn from(value: String) -> Self {
199 Self::from(ClipboardString::from(value))
200 }
201}
202
203impl From<Image> for ClipboardEntry {
204 fn from(value: Image) -> Self {
205 Self::Image(value)
206 }
207}
208
209impl From<ClipboardEntry> for ClipboardItem {
210 fn from(value: ClipboardEntry) -> Self {
211 Self {
212 entries: vec![value],
213 }
214 }
215}
216
217impl From<String> for ClipboardItem {
218 fn from(value: String) -> Self {
219 Self::from(ClipboardEntry::from(value))
220 }
221}
222
223impl From<Image> for ClipboardItem {
224 fn from(value: Image) -> Self {
225 Self::from(ClipboardEntry::from(value))
226 }
227}
228
229#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
231pub enum ImageFormat {
232 Png,
237 Jpeg,
239 Webp,
241 Gif,
243 Svg,
245 Bmp,
247 Tiff,
249 Ico,
251 Pnm,
253}
254
255impl ImageFormat {
256 pub const fn mime_type(self) -> &'static str {
258 match self {
259 ImageFormat::Png => "image/png",
260 ImageFormat::Jpeg => "image/jpeg",
261 ImageFormat::Webp => "image/webp",
262 ImageFormat::Gif => "image/gif",
263 ImageFormat::Svg => "image/svg+xml",
264 ImageFormat::Bmp => "image/bmp",
265 ImageFormat::Tiff => "image/tiff",
266 ImageFormat::Ico => "image/ico",
267 ImageFormat::Pnm => "image/x-portable-anymap",
268 }
269 }
270
271 pub const fn extension(self) -> &'static str {
273 match self {
274 ImageFormat::Png => "png",
275 ImageFormat::Jpeg => "jpg",
276 ImageFormat::Webp => "webp",
277 ImageFormat::Gif => "gif",
278 ImageFormat::Svg => "svg",
279 ImageFormat::Bmp => "bmp",
280 ImageFormat::Tiff => "tiff",
281 ImageFormat::Ico => "ico",
282 ImageFormat::Pnm => "pnm",
283 }
284 }
285
286 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
288 use strum::IntoEnumIterator;
289 Self::iter()
290 .find(|format| format.mime_type() == mime_type)
291 .or_else(|| Self::from_mime_type_alias(mime_type))
292 }
293
294 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
298 match mime_type {
299 "image/jpg" => Some(Self::Jpeg),
300 "image/tif" => Some(Self::Tiff),
301 _ => None,
302 }
303 }
304}
305
306#[derive(Clone, Debug, PartialEq, Eq)]
308pub struct Image {
309 pub format: ImageFormat,
311 pub bytes: Vec<u8>,
313 pub id: u64,
315}
316
317impl Hash for Image {
318 fn hash<H: Hasher>(&self, state: &mut H) {
319 state.write_u64(self.id);
320 }
321}
322
323impl Image {
324 pub fn empty() -> Self {
326 Self::from_bytes(ImageFormat::Png, Vec::new())
327 }
328
329 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
331 Self {
332 id: hash(&bytes),
333 format,
334 bytes,
335 }
336 }
337
338 pub fn id(&self) -> u64 {
340 self.id
341 }
342
343 pub fn format(&self) -> ImageFormat {
345 self.format
346 }
347
348 pub fn bytes(&self) -> &[u8] {
350 self.bytes.as_slice()
351 }
352}
353
354#[derive(Clone, Debug, Eq, PartialEq)]
356pub struct ClipboardString {
357 pub text: String,
359 pub metadata: Option<String>,
361}
362
363impl ClipboardString {
364 pub fn new(text: String) -> Self {
366 Self {
367 text,
368 metadata: None,
369 }
370 }
371
372 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
375 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
376 self
377 }
378
379 pub fn text(&self) -> &String {
381 &self.text
382 }
383
384 pub fn into_text(self) -> String {
386 self.text
387 }
388
389 pub fn metadata_json<T>(&self) -> Option<T>
391 where
392 T: for<'a> Deserialize<'a>,
393 {
394 self.metadata
395 .as_ref()
396 .and_then(|m| serde_json::from_str(m).ok())
397 }
398
399 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
400 pub fn text_hash(text: &str) -> u64 {
402 let mut hasher = SeaHasher::new();
403 text.hash(&mut hasher);
404 hasher.finish()
405 }
406}
407
408impl From<String> for ClipboardString {
409 fn from(value: String) -> Self {
410 Self {
411 text: value,
412 metadata: None,
413 }
414 }
415}