1#![allow(clippy::unwrap_used, clippy::disallowed_methods)]
2use std::collections::HashMap;
11use std::sync::{Arc, RwLock};
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub enum ClipboardFormat {
16 Text,
18 Html,
20 Rtf,
22 ImagePng,
24 ImageJpeg,
26 Files,
28 Custom(String),
30}
31
32impl ClipboardFormat {
33 pub fn mime_type(&self) -> &str {
35 match self {
36 Self::Text => "text/plain",
37 Self::Html => "text/html",
38 Self::Rtf => "text/rtf",
39 Self::ImagePng => "image/png",
40 Self::ImageJpeg => "image/jpeg",
41 Self::Files => "application/x-file-list",
42 Self::Custom(mime) => mime,
43 }
44 }
45
46 pub fn from_mime(mime: &str) -> Self {
48 match mime {
49 "text/plain" => Self::Text,
50 "text/html" => Self::Html,
51 "text/rtf" => Self::Rtf,
52 "image/png" => Self::ImagePng,
53 "image/jpeg" => Self::ImageJpeg,
54 "application/x-file-list" => Self::Files,
55 other => Self::Custom(other.to_string()),
56 }
57 }
58
59 pub fn is_text(&self) -> bool {
61 matches!(self, Self::Text | Self::Html | Self::Rtf)
62 }
63
64 pub fn is_image(&self) -> bool {
66 matches!(self, Self::ImagePng | Self::ImageJpeg)
67 }
68}
69
70#[derive(Debug, Clone, Default)]
72pub struct ClipboardData {
73 formats: HashMap<ClipboardFormat, Vec<u8>>,
75}
76
77impl ClipboardData {
78 pub fn new() -> Self {
80 Self::default()
81 }
82
83 pub fn text(content: &str) -> Self {
85 let mut data = Self::new();
86 data.set_text(content);
87 data
88 }
89
90 pub fn html(content: &str) -> Self {
92 let mut data = Self::new();
93 data.set_html(content);
94 data
95 }
96
97 pub fn set_text(&mut self, content: &str) {
99 self.formats
100 .insert(ClipboardFormat::Text, content.as_bytes().to_vec());
101 }
102
103 pub fn get_text(&self) -> Option<String> {
105 self.formats
106 .get(&ClipboardFormat::Text)
107 .and_then(|bytes| String::from_utf8(bytes.clone()).ok())
108 }
109
110 pub fn set_html(&mut self, content: &str) {
112 self.formats
113 .insert(ClipboardFormat::Html, content.as_bytes().to_vec());
114 }
115
116 pub fn get_html(&self) -> Option<String> {
118 self.formats
119 .get(&ClipboardFormat::Html)
120 .and_then(|bytes| String::from_utf8(bytes.clone()).ok())
121 }
122
123 pub fn set(&mut self, format: ClipboardFormat, data: Vec<u8>) {
125 self.formats.insert(format, data);
126 }
127
128 pub fn get(&self, format: &ClipboardFormat) -> Option<&[u8]> {
130 self.formats.get(format).map(std::vec::Vec::as_slice)
131 }
132
133 pub fn has_format(&self, format: &ClipboardFormat) -> bool {
135 self.formats.contains_key(format)
136 }
137
138 pub fn formats(&self) -> impl Iterator<Item = &ClipboardFormat> {
140 self.formats.keys()
141 }
142
143 pub fn is_empty(&self) -> bool {
145 self.formats.is_empty()
146 }
147
148 pub fn clear(&mut self) {
150 self.formats.clear();
151 }
152
153 pub fn format_count(&self) -> usize {
155 self.formats.len()
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
161pub enum ClipboardResult {
162 Success,
164 Unavailable,
166 PermissionDenied,
168 UnsupportedFormat,
170 Error(String),
172}
173
174impl ClipboardResult {
175 pub fn is_success(&self) -> bool {
177 matches!(self, Self::Success)
178 }
179
180 pub fn is_error(&self) -> bool {
182 !self.is_success()
183 }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum ClipboardOperation {
189 Copy,
191 Cut,
193 Paste,
195}
196
197#[derive(Debug, Clone)]
199pub struct ClipboardEvent {
200 pub operation: ClipboardOperation,
202 pub formats: Vec<ClipboardFormat>,
204 pub timestamp: u64,
206}
207
208impl ClipboardEvent {
209 pub fn new(
211 operation: ClipboardOperation,
212 formats: Vec<ClipboardFormat>,
213 timestamp: u64,
214 ) -> Self {
215 Self {
216 operation,
217 formats,
218 timestamp,
219 }
220 }
221}
222
223pub type ClipboardCallback = Box<dyn Fn(&ClipboardEvent) + Send + Sync>;
225
226pub struct Clipboard {
228 data: Arc<RwLock<ClipboardData>>,
230 listeners: Vec<ClipboardCallback>,
232 counter: u64,
234 available: bool,
236}
237
238impl Clipboard {
239 pub fn new() -> Self {
241 Self {
242 data: Arc::new(RwLock::new(ClipboardData::new())),
243 listeners: Vec::new(),
244 counter: 0,
245 available: true,
246 }
247 }
248
249 pub fn unavailable() -> Self {
251 Self {
252 data: Arc::new(RwLock::new(ClipboardData::new())),
253 listeners: Vec::new(),
254 counter: 0,
255 available: false,
256 }
257 }
258
259 pub fn is_available(&self) -> bool {
261 self.available
262 }
263
264 pub fn write(&mut self, data: ClipboardData) -> ClipboardResult {
266 if !self.available {
267 return ClipboardResult::Unavailable;
268 }
269
270 let formats: Vec<ClipboardFormat> = data.formats().cloned().collect();
271
272 if let Ok(mut clipboard) = self.data.write() {
273 *clipboard = data;
274 } else {
275 return ClipboardResult::Error("Lock error".to_string());
276 }
277
278 self.counter += 1;
279 self.notify(ClipboardOperation::Copy, formats);
280
281 ClipboardResult::Success
282 }
283
284 pub fn write_text(&mut self, text: &str) -> ClipboardResult {
286 self.write(ClipboardData::text(text))
287 }
288
289 pub fn write_html(&mut self, html: &str) -> ClipboardResult {
291 self.write(ClipboardData::html(html))
292 }
293
294 pub fn read(&self) -> Result<ClipboardData, ClipboardResult> {
296 if !self.available {
297 return Err(ClipboardResult::Unavailable);
298 }
299
300 self.data
301 .read()
302 .map(|data| data.clone())
303 .map_err(|_| ClipboardResult::Error("Lock error".to_string()))
304 }
305
306 pub fn read_text(&self) -> Result<Option<String>, ClipboardResult> {
308 if !self.available {
309 return Err(ClipboardResult::Unavailable);
310 }
311
312 self.data
313 .read()
314 .map(|data| data.get_text())
315 .map_err(|_| ClipboardResult::Error("Lock error".to_string()))
316 }
317
318 pub fn read_html(&self) -> Result<Option<String>, ClipboardResult> {
320 if !self.available {
321 return Err(ClipboardResult::Unavailable);
322 }
323
324 self.data
325 .read()
326 .map(|data| data.get_html())
327 .map_err(|_| ClipboardResult::Error("Lock error".to_string()))
328 }
329
330 pub fn has_format(&self, format: &ClipboardFormat) -> bool {
332 self.data
333 .read()
334 .map(|data| data.has_format(format))
335 .unwrap_or(false)
336 }
337
338 pub fn available_formats(&self) -> Vec<ClipboardFormat> {
340 self.data
341 .read()
342 .map(|data| data.formats().cloned().collect())
343 .unwrap_or_default()
344 }
345
346 pub fn clear(&mut self) -> ClipboardResult {
348 if !self.available {
349 return ClipboardResult::Unavailable;
350 }
351
352 if let Ok(mut data) = self.data.write() {
353 data.clear();
354 ClipboardResult::Success
355 } else {
356 ClipboardResult::Error("Lock error".to_string())
357 }
358 }
359
360 pub fn on_change(&mut self, callback: ClipboardCallback) {
362 self.listeners.push(callback);
363 }
364
365 pub fn listener_count(&self) -> usize {
367 self.listeners.len()
368 }
369
370 fn notify(&self, operation: ClipboardOperation, formats: Vec<ClipboardFormat>) {
372 let event = ClipboardEvent::new(operation, formats, self.counter);
373 for listener in &self.listeners {
374 listener(&event);
375 }
376 }
377
378 pub fn cut(&mut self, data: ClipboardData) -> ClipboardResult {
380 if !self.available {
381 return ClipboardResult::Unavailable;
382 }
383
384 let formats: Vec<ClipboardFormat> = data.formats().cloned().collect();
385
386 if let Ok(mut clipboard) = self.data.write() {
387 *clipboard = data;
388 } else {
389 return ClipboardResult::Error("Lock error".to_string());
390 }
391
392 self.counter += 1;
393 self.notify(ClipboardOperation::Cut, formats);
394
395 ClipboardResult::Success
396 }
397
398 pub fn signal_paste(&mut self) {
400 let formats = self.available_formats();
401 self.counter += 1;
402 self.notify(ClipboardOperation::Paste, formats);
403 }
404}
405
406impl Default for Clipboard {
407 fn default() -> Self {
408 Self::new()
409 }
410}
411
412impl std::fmt::Debug for Clipboard {
413 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414 f.debug_struct("Clipboard")
415 .field("available", &self.available)
416 .field("counter", &self.counter)
417 .field("listener_count", &self.listeners.len())
418 .finish()
419 }
420}
421
422#[derive(Debug, Default)]
424pub struct ClipboardHistory {
425 entries: Vec<ClipboardData>,
427 max_size: usize,
429 current: usize,
431}
432
433impl ClipboardHistory {
434 pub fn new(max_size: usize) -> Self {
436 Self {
437 entries: Vec::new(),
438 max_size,
439 current: 0,
440 }
441 }
442
443 pub fn push(&mut self, data: ClipboardData) {
445 if self.current < self.entries.len() {
447 self.entries.truncate(self.current);
448 }
449
450 self.entries.push(data);
451
452 while self.entries.len() > self.max_size {
454 self.entries.remove(0);
455 }
456
457 self.current = self.entries.len();
458 }
459
460 pub fn current(&self) -> Option<&ClipboardData> {
462 if self.current > 0 && self.current <= self.entries.len() {
463 self.entries.get(self.current - 1)
464 } else {
465 None
466 }
467 }
468
469 pub fn previous(&mut self) -> Option<&ClipboardData> {
471 if self.current > 1 {
472 self.current -= 1;
473 self.entries.get(self.current - 1)
474 } else {
475 None
476 }
477 }
478
479 pub fn next(&mut self) -> Option<&ClipboardData> {
481 if self.current < self.entries.len() {
482 self.current += 1;
483 self.entries.get(self.current - 1)
484 } else {
485 None
486 }
487 }
488
489 pub fn get(&self, index: usize) -> Option<&ClipboardData> {
491 self.entries.get(index)
492 }
493
494 pub fn len(&self) -> usize {
496 self.entries.len()
497 }
498
499 pub fn is_empty(&self) -> bool {
501 self.entries.is_empty()
502 }
503
504 pub fn clear(&mut self) {
506 self.entries.clear();
507 self.current = 0;
508 }
509
510 pub fn current_index(&self) -> usize {
512 self.current
513 }
514
515 pub fn can_go_back(&self) -> bool {
517 self.current > 1
518 }
519
520 pub fn can_go_forward(&self) -> bool {
522 self.current < self.entries.len()
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529 use std::sync::atomic::{AtomicUsize, Ordering};
530
531 #[test]
533 fn test_clipboard_format_mime_type() {
534 assert_eq!(ClipboardFormat::Text.mime_type(), "text/plain");
535 assert_eq!(ClipboardFormat::Html.mime_type(), "text/html");
536 assert_eq!(ClipboardFormat::ImagePng.mime_type(), "image/png");
537 assert_eq!(
538 ClipboardFormat::Custom("application/json".to_string()).mime_type(),
539 "application/json"
540 );
541 }
542
543 #[test]
544 fn test_clipboard_format_from_mime() {
545 assert_eq!(
546 ClipboardFormat::from_mime("text/plain"),
547 ClipboardFormat::Text
548 );
549 assert_eq!(
550 ClipboardFormat::from_mime("text/html"),
551 ClipboardFormat::Html
552 );
553 assert_eq!(
554 ClipboardFormat::from_mime("image/png"),
555 ClipboardFormat::ImagePng
556 );
557 assert_eq!(
558 ClipboardFormat::from_mime("application/json"),
559 ClipboardFormat::Custom("application/json".to_string())
560 );
561 }
562
563 #[test]
564 fn test_clipboard_format_is_text() {
565 assert!(ClipboardFormat::Text.is_text());
566 assert!(ClipboardFormat::Html.is_text());
567 assert!(ClipboardFormat::Rtf.is_text());
568 assert!(!ClipboardFormat::ImagePng.is_text());
569 }
570
571 #[test]
572 fn test_clipboard_format_is_image() {
573 assert!(ClipboardFormat::ImagePng.is_image());
574 assert!(ClipboardFormat::ImageJpeg.is_image());
575 assert!(!ClipboardFormat::Text.is_image());
576 }
577
578 #[test]
580 fn test_clipboard_data_new() {
581 let data = ClipboardData::new();
582 assert!(data.is_empty());
583 assert_eq!(data.format_count(), 0);
584 }
585
586 #[test]
587 fn test_clipboard_data_text() {
588 let data = ClipboardData::text("Hello");
589 assert!(!data.is_empty());
590 assert!(data.has_format(&ClipboardFormat::Text));
591 assert_eq!(data.get_text(), Some("Hello".to_string()));
592 }
593
594 #[test]
595 fn test_clipboard_data_html() {
596 let data = ClipboardData::html("<b>Bold</b>");
597 assert!(data.has_format(&ClipboardFormat::Html));
598 assert_eq!(data.get_html(), Some("<b>Bold</b>".to_string()));
599 }
600
601 #[test]
602 fn test_clipboard_data_set_get() {
603 let mut data = ClipboardData::new();
604 data.set(ClipboardFormat::Text, b"test".to_vec());
605
606 assert!(data.has_format(&ClipboardFormat::Text));
607 assert_eq!(data.get(&ClipboardFormat::Text), Some(b"test".as_slice()));
608 }
609
610 #[test]
611 fn test_clipboard_data_clear() {
612 let mut data = ClipboardData::text("test");
613 assert!(!data.is_empty());
614
615 data.clear();
616 assert!(data.is_empty());
617 }
618
619 #[test]
620 fn test_clipboard_data_formats() {
621 let mut data = ClipboardData::new();
622 data.set_text("text");
623 data.set_html("<p>html</p>");
624
625 let formats: Vec<_> = data.formats().collect();
626 assert_eq!(formats.len(), 2);
627 }
628
629 #[test]
631 fn test_clipboard_result_is_success() {
632 assert!(ClipboardResult::Success.is_success());
633 assert!(!ClipboardResult::Unavailable.is_success());
634 assert!(!ClipboardResult::PermissionDenied.is_success());
635 }
636
637 #[test]
638 fn test_clipboard_result_is_error() {
639 assert!(!ClipboardResult::Success.is_error());
640 assert!(ClipboardResult::Unavailable.is_error());
641 assert!(ClipboardResult::Error("test".to_string()).is_error());
642 }
643
644 #[test]
646 fn test_clipboard_event_new() {
647 let event = ClipboardEvent::new(ClipboardOperation::Copy, vec![ClipboardFormat::Text], 42);
648 assert_eq!(event.operation, ClipboardOperation::Copy);
649 assert_eq!(event.formats.len(), 1);
650 assert_eq!(event.timestamp, 42);
651 }
652
653 #[test]
655 fn test_clipboard_new() {
656 let clipboard = Clipboard::new();
657 assert!(clipboard.is_available());
658 assert_eq!(clipboard.listener_count(), 0);
659 }
660
661 #[test]
662 fn test_clipboard_unavailable() {
663 let mut clipboard = Clipboard::unavailable();
664 assert!(!clipboard.is_available());
665
666 let result = clipboard.write_text("test");
667 assert_eq!(result, ClipboardResult::Unavailable);
668
669 let result = clipboard.read();
670 assert!(result.is_err());
671 }
672
673 #[test]
674 fn test_clipboard_write_text() {
675 let mut clipboard = Clipboard::new();
676 let result = clipboard.write_text("Hello");
677
678 assert!(result.is_success());
679 assert!(clipboard.has_format(&ClipboardFormat::Text));
680 }
681
682 #[test]
683 fn test_clipboard_read_text() {
684 let mut clipboard = Clipboard::new();
685 clipboard.write_text("Hello");
686
687 let text = clipboard.read_text().unwrap();
688 assert_eq!(text, Some("Hello".to_string()));
689 }
690
691 #[test]
692 fn test_clipboard_write_html() {
693 let mut clipboard = Clipboard::new();
694 let result = clipboard.write_html("<b>Bold</b>");
695
696 assert!(result.is_success());
697 assert!(clipboard.has_format(&ClipboardFormat::Html));
698 }
699
700 #[test]
701 fn test_clipboard_read_html() {
702 let mut clipboard = Clipboard::new();
703 clipboard.write_html("<p>Test</p>");
704
705 let html = clipboard.read_html().unwrap();
706 assert_eq!(html, Some("<p>Test</p>".to_string()));
707 }
708
709 #[test]
710 fn test_clipboard_read() {
711 let mut clipboard = Clipboard::new();
712 clipboard.write_text("test");
713
714 let data = clipboard.read().unwrap();
715 assert_eq!(data.get_text(), Some("test".to_string()));
716 }
717
718 #[test]
719 fn test_clipboard_clear() {
720 let mut clipboard = Clipboard::new();
721 clipboard.write_text("test");
722 assert!(clipboard.has_format(&ClipboardFormat::Text));
723
724 let result = clipboard.clear();
725 assert!(result.is_success());
726 assert!(!clipboard.has_format(&ClipboardFormat::Text));
727 }
728
729 #[test]
730 fn test_clipboard_available_formats() {
731 let mut clipboard = Clipboard::new();
732
733 let mut data = ClipboardData::new();
734 data.set_text("text");
735 data.set_html("html");
736 clipboard.write(data);
737
738 let formats = clipboard.available_formats();
739 assert_eq!(formats.len(), 2);
740 }
741
742 #[test]
743 fn test_clipboard_on_change() {
744 let counter = Arc::new(AtomicUsize::new(0));
745 let counter_clone = counter.clone();
746
747 let mut clipboard = Clipboard::new();
748 clipboard.on_change(Box::new(move |_event| {
749 counter_clone.fetch_add(1, Ordering::SeqCst);
750 }));
751
752 clipboard.write_text("test");
753 assert_eq!(counter.load(Ordering::SeqCst), 1);
754
755 clipboard.write_text("test2");
756 assert_eq!(counter.load(Ordering::SeqCst), 2);
757 }
758
759 #[test]
760 fn test_clipboard_cut() {
761 let counter = Arc::new(AtomicUsize::new(0));
762 let counter_clone = counter.clone();
763
764 let mut clipboard = Clipboard::new();
765 clipboard.on_change(Box::new(move |event| {
766 if event.operation == ClipboardOperation::Cut {
767 counter_clone.fetch_add(1, Ordering::SeqCst);
768 }
769 }));
770
771 clipboard.cut(ClipboardData::text("cut text"));
772 assert_eq!(counter.load(Ordering::SeqCst), 1);
773 assert_eq!(clipboard.read_text().unwrap(), Some("cut text".to_string()));
774 }
775
776 #[test]
777 fn test_clipboard_signal_paste() {
778 let counter = Arc::new(AtomicUsize::new(0));
779 let counter_clone = counter.clone();
780
781 let mut clipboard = Clipboard::new();
782 clipboard.write_text("test");
783
784 clipboard.on_change(Box::new(move |event| {
785 if event.operation == ClipboardOperation::Paste {
786 counter_clone.fetch_add(1, Ordering::SeqCst);
787 }
788 }));
789
790 clipboard.signal_paste();
791 assert_eq!(counter.load(Ordering::SeqCst), 1);
792 }
793
794 #[test]
796 fn test_history_new() {
797 let history = ClipboardHistory::new(10);
798 assert!(history.is_empty());
799 assert_eq!(history.len(), 0);
800 }
801
802 #[test]
803 fn test_history_push() {
804 let mut history = ClipboardHistory::new(10);
805 history.push(ClipboardData::text("first"));
806 history.push(ClipboardData::text("second"));
807
808 assert_eq!(history.len(), 2);
809 }
810
811 #[test]
812 fn test_history_current() {
813 let mut history = ClipboardHistory::new(10);
814 assert!(history.current().is_none());
815
816 history.push(ClipboardData::text("first"));
817 assert_eq!(
818 history.current().unwrap().get_text(),
819 Some("first".to_string())
820 );
821
822 history.push(ClipboardData::text("second"));
823 assert_eq!(
824 history.current().unwrap().get_text(),
825 Some("second".to_string())
826 );
827 }
828
829 #[test]
830 fn test_history_previous_next() {
831 let mut history = ClipboardHistory::new(10);
832 history.push(ClipboardData::text("first"));
833 history.push(ClipboardData::text("second"));
834 history.push(ClipboardData::text("third"));
835
836 assert_eq!(
838 history.current().unwrap().get_text(),
839 Some("third".to_string())
840 );
841
842 let prev = history.previous();
844 assert_eq!(prev.unwrap().get_text(), Some("second".to_string()));
845
846 let prev = history.previous();
848 assert_eq!(prev.unwrap().get_text(), Some("first".to_string()));
849
850 assert!(history.previous().is_none());
852
853 let next = history.next();
855 assert_eq!(next.unwrap().get_text(), Some("second".to_string()));
856
857 let next = history.next();
859 assert_eq!(next.unwrap().get_text(), Some("third".to_string()));
860
861 assert!(history.next().is_none());
863 }
864
865 #[test]
866 fn test_history_max_size() {
867 let mut history = ClipboardHistory::new(3);
868
869 history.push(ClipboardData::text("1"));
870 history.push(ClipboardData::text("2"));
871 history.push(ClipboardData::text("3"));
872 history.push(ClipboardData::text("4"));
873
874 assert_eq!(history.len(), 3);
875 assert_eq!(history.get(0).unwrap().get_text(), Some("2".to_string()));
876 }
877
878 #[test]
879 fn test_history_clear() {
880 let mut history = ClipboardHistory::new(10);
881 history.push(ClipboardData::text("test"));
882
883 history.clear();
884 assert!(history.is_empty());
885 assert_eq!(history.current_index(), 0);
886 }
887
888 #[test]
889 fn test_history_can_navigate() {
890 let mut history = ClipboardHistory::new(10);
891 assert!(!history.can_go_back());
892 assert!(!history.can_go_forward());
893
894 history.push(ClipboardData::text("first"));
895 assert!(!history.can_go_back());
896 assert!(!history.can_go_forward());
897
898 history.push(ClipboardData::text("second"));
899 assert!(history.can_go_back());
900 assert!(!history.can_go_forward());
901
902 history.previous();
903 assert!(!history.can_go_back());
904 assert!(history.can_go_forward());
905 }
906
907 #[test]
908 fn test_history_get() {
909 let mut history = ClipboardHistory::new(10);
910 history.push(ClipboardData::text("first"));
911 history.push(ClipboardData::text("second"));
912
913 assert_eq!(
914 history.get(0).unwrap().get_text(),
915 Some("first".to_string())
916 );
917 assert_eq!(
918 history.get(1).unwrap().get_text(),
919 Some("second".to_string())
920 );
921 assert!(history.get(2).is_none());
922 }
923
924 #[test]
925 fn test_history_truncate_on_push() {
926 let mut history = ClipboardHistory::new(10);
927 history.push(ClipboardData::text("first"));
928 history.push(ClipboardData::text("second"));
929 history.push(ClipboardData::text("third"));
930
931 history.previous();
933 history.previous();
934 assert_eq!(history.current_index(), 1);
935
936 history.push(ClipboardData::text("new"));
938 assert_eq!(history.len(), 2);
939 assert_eq!(
940 history.get(0).unwrap().get_text(),
941 Some("first".to_string())
942 );
943 assert_eq!(history.get(1).unwrap().get_text(), Some("new".to_string()));
944 }
945}