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 assert_eq!(data.formats().count(), 2);
626 }
627
628 #[test]
630 fn test_clipboard_result_is_success() {
631 assert!(ClipboardResult::Success.is_success());
632 assert!(!ClipboardResult::Unavailable.is_success());
633 assert!(!ClipboardResult::PermissionDenied.is_success());
634 }
635
636 #[test]
637 fn test_clipboard_result_is_error() {
638 assert!(!ClipboardResult::Success.is_error());
639 assert!(ClipboardResult::Unavailable.is_error());
640 assert!(ClipboardResult::Error("test".to_string()).is_error());
641 }
642
643 #[test]
645 fn test_clipboard_event_new() {
646 let event = ClipboardEvent::new(ClipboardOperation::Copy, vec![ClipboardFormat::Text], 42);
647 assert_eq!(event.operation, ClipboardOperation::Copy);
648 assert_eq!(event.formats.len(), 1);
649 assert_eq!(event.timestamp, 42);
650 }
651
652 #[test]
654 fn test_clipboard_new() {
655 let clipboard = Clipboard::new();
656 assert!(clipboard.is_available());
657 assert_eq!(clipboard.listener_count(), 0);
658 }
659
660 #[test]
661 fn test_clipboard_unavailable() {
662 let mut clipboard = Clipboard::unavailable();
663 assert!(!clipboard.is_available());
664
665 let result = clipboard.write_text("test");
666 assert_eq!(result, ClipboardResult::Unavailable);
667
668 let result = clipboard.read();
669 assert!(result.is_err());
670 }
671
672 #[test]
673 fn test_clipboard_write_text() {
674 let mut clipboard = Clipboard::new();
675 let result = clipboard.write_text("Hello");
676
677 assert!(result.is_success());
678 assert!(clipboard.has_format(&ClipboardFormat::Text));
679 }
680
681 #[test]
682 fn test_clipboard_read_text() {
683 let mut clipboard = Clipboard::new();
684 clipboard.write_text("Hello");
685
686 let text = clipboard.read_text().unwrap();
687 assert_eq!(text, Some("Hello".to_string()));
688 }
689
690 #[test]
691 fn test_clipboard_write_html() {
692 let mut clipboard = Clipboard::new();
693 let result = clipboard.write_html("<b>Bold</b>");
694
695 assert!(result.is_success());
696 assert!(clipboard.has_format(&ClipboardFormat::Html));
697 }
698
699 #[test]
700 fn test_clipboard_read_html() {
701 let mut clipboard = Clipboard::new();
702 clipboard.write_html("<p>Test</p>");
703
704 let html = clipboard.read_html().unwrap();
705 assert_eq!(html, Some("<p>Test</p>".to_string()));
706 }
707
708 #[test]
709 fn test_clipboard_read() {
710 let mut clipboard = Clipboard::new();
711 clipboard.write_text("test");
712
713 let data = clipboard.read().unwrap();
714 assert_eq!(data.get_text(), Some("test".to_string()));
715 }
716
717 #[test]
718 fn test_clipboard_clear() {
719 let mut clipboard = Clipboard::new();
720 clipboard.write_text("test");
721 assert!(clipboard.has_format(&ClipboardFormat::Text));
722
723 let result = clipboard.clear();
724 assert!(result.is_success());
725 assert!(!clipboard.has_format(&ClipboardFormat::Text));
726 }
727
728 #[test]
729 fn test_clipboard_available_formats() {
730 let mut clipboard = Clipboard::new();
731
732 let mut data = ClipboardData::new();
733 data.set_text("text");
734 data.set_html("html");
735 clipboard.write(data);
736
737 let formats = clipboard.available_formats();
738 assert_eq!(formats.len(), 2);
739 }
740
741 #[test]
742 fn test_clipboard_on_change() {
743 let counter = Arc::new(AtomicUsize::new(0));
744 let counter_clone = counter.clone();
745
746 let mut clipboard = Clipboard::new();
747 clipboard.on_change(Box::new(move |_event| {
748 counter_clone.fetch_add(1, Ordering::SeqCst);
749 }));
750
751 clipboard.write_text("test");
752 assert_eq!(counter.load(Ordering::SeqCst), 1);
753
754 clipboard.write_text("test2");
755 assert_eq!(counter.load(Ordering::SeqCst), 2);
756 }
757
758 #[test]
759 fn test_clipboard_cut() {
760 let counter = Arc::new(AtomicUsize::new(0));
761 let counter_clone = counter.clone();
762
763 let mut clipboard = Clipboard::new();
764 clipboard.on_change(Box::new(move |event| {
765 if event.operation == ClipboardOperation::Cut {
766 counter_clone.fetch_add(1, Ordering::SeqCst);
767 }
768 }));
769
770 clipboard.cut(ClipboardData::text("cut text"));
771 assert_eq!(counter.load(Ordering::SeqCst), 1);
772 assert_eq!(clipboard.read_text().unwrap(), Some("cut text".to_string()));
773 }
774
775 #[test]
776 fn test_clipboard_signal_paste() {
777 let counter = Arc::new(AtomicUsize::new(0));
778 let counter_clone = counter.clone();
779
780 let mut clipboard = Clipboard::new();
781 clipboard.write_text("test");
782
783 clipboard.on_change(Box::new(move |event| {
784 if event.operation == ClipboardOperation::Paste {
785 counter_clone.fetch_add(1, Ordering::SeqCst);
786 }
787 }));
788
789 clipboard.signal_paste();
790 assert_eq!(counter.load(Ordering::SeqCst), 1);
791 }
792
793 #[test]
795 fn test_history_new() {
796 let history = ClipboardHistory::new(10);
797 assert!(history.is_empty());
798 assert_eq!(history.len(), 0);
799 }
800
801 #[test]
802 fn test_history_push() {
803 let mut history = ClipboardHistory::new(10);
804 history.push(ClipboardData::text("first"));
805 history.push(ClipboardData::text("second"));
806
807 assert_eq!(history.len(), 2);
808 }
809
810 #[test]
811 fn test_history_current() {
812 let mut history = ClipboardHistory::new(10);
813 assert!(history.current().is_none());
814
815 history.push(ClipboardData::text("first"));
816 assert_eq!(
817 history.current().unwrap().get_text(),
818 Some("first".to_string())
819 );
820
821 history.push(ClipboardData::text("second"));
822 assert_eq!(
823 history.current().unwrap().get_text(),
824 Some("second".to_string())
825 );
826 }
827
828 #[test]
829 fn test_history_previous_next() {
830 let mut history = ClipboardHistory::new(10);
831 history.push(ClipboardData::text("first"));
832 history.push(ClipboardData::text("second"));
833 history.push(ClipboardData::text("third"));
834
835 assert_eq!(
837 history.current().unwrap().get_text(),
838 Some("third".to_string())
839 );
840
841 let prev = history.previous();
843 assert_eq!(prev.unwrap().get_text(), Some("second".to_string()));
844
845 let prev = history.previous();
847 assert_eq!(prev.unwrap().get_text(), Some("first".to_string()));
848
849 assert!(history.previous().is_none());
851
852 let next = history.next();
854 assert_eq!(next.unwrap().get_text(), Some("second".to_string()));
855
856 let next = history.next();
858 assert_eq!(next.unwrap().get_text(), Some("third".to_string()));
859
860 assert!(history.next().is_none());
862 }
863
864 #[test]
865 fn test_history_max_size() {
866 let mut history = ClipboardHistory::new(3);
867
868 history.push(ClipboardData::text("1"));
869 history.push(ClipboardData::text("2"));
870 history.push(ClipboardData::text("3"));
871 history.push(ClipboardData::text("4"));
872
873 assert_eq!(history.len(), 3);
874 assert_eq!(history.get(0).unwrap().get_text(), Some("2".to_string()));
875 }
876
877 #[test]
878 fn test_history_clear() {
879 let mut history = ClipboardHistory::new(10);
880 history.push(ClipboardData::text("test"));
881
882 history.clear();
883 assert!(history.is_empty());
884 assert_eq!(history.current_index(), 0);
885 }
886
887 #[test]
888 fn test_history_can_navigate() {
889 let mut history = ClipboardHistory::new(10);
890 assert!(!history.can_go_back());
891 assert!(!history.can_go_forward());
892
893 history.push(ClipboardData::text("first"));
894 assert!(!history.can_go_back());
895 assert!(!history.can_go_forward());
896
897 history.push(ClipboardData::text("second"));
898 assert!(history.can_go_back());
899 assert!(!history.can_go_forward());
900
901 history.previous();
902 assert!(!history.can_go_back());
903 assert!(history.can_go_forward());
904 }
905
906 #[test]
907 fn test_history_get() {
908 let mut history = ClipboardHistory::new(10);
909 history.push(ClipboardData::text("first"));
910 history.push(ClipboardData::text("second"));
911
912 assert_eq!(
913 history.get(0).unwrap().get_text(),
914 Some("first".to_string())
915 );
916 assert_eq!(
917 history.get(1).unwrap().get_text(),
918 Some("second".to_string())
919 );
920 assert!(history.get(2).is_none());
921 }
922
923 #[test]
924 fn test_history_truncate_on_push() {
925 let mut history = ClipboardHistory::new(10);
926 history.push(ClipboardData::text("first"));
927 history.push(ClipboardData::text("second"));
928 history.push(ClipboardData::text("third"));
929
930 history.previous();
932 history.previous();
933 assert_eq!(history.current_index(), 1);
934
935 history.push(ClipboardData::text("new"));
937 assert_eq!(history.len(), 2);
938 assert_eq!(
939 history.get(0).unwrap().get_text(),
940 Some("first".to_string())
941 );
942 assert_eq!(history.get(1).unwrap().get_text(), Some("new".to_string()));
943 }
944}