presentar_core/
clipboard.rs

1#![allow(clippy::unwrap_used, clippy::disallowed_methods)]
2//! Clipboard API for copy, cut, and paste operations.
3//!
4//! This module provides:
5//! - Async clipboard read/write
6//! - Multiple data format support (text, HTML, image, custom)
7//! - Clipboard change detection
8//! - Cross-platform abstraction
9
10use std::collections::HashMap;
11use std::sync::{Arc, RwLock};
12
13/// Clipboard data format.
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub enum ClipboardFormat {
16    /// Plain text.
17    Text,
18    /// HTML content.
19    Html,
20    /// Rich text format.
21    Rtf,
22    /// Image data (PNG format).
23    ImagePng,
24    /// Image data (JPEG format).
25    ImageJpeg,
26    /// File list (paths).
27    Files,
28    /// Custom format with MIME type.
29    Custom(String),
30}
31
32impl ClipboardFormat {
33    /// Get the MIME type for this format.
34    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    /// Create from MIME type.
47    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    /// Check if this is a text format.
60    pub fn is_text(&self) -> bool {
61        matches!(self, Self::Text | Self::Html | Self::Rtf)
62    }
63
64    /// Check if this is an image format.
65    pub fn is_image(&self) -> bool {
66        matches!(self, Self::ImagePng | Self::ImageJpeg)
67    }
68}
69
70/// Data stored in the clipboard.
71#[derive(Debug, Clone, Default)]
72pub struct ClipboardData {
73    /// Data in various formats.
74    formats: HashMap<ClipboardFormat, Vec<u8>>,
75}
76
77impl ClipboardData {
78    /// Create empty clipboard data.
79    pub fn new() -> Self {
80        Self::default()
81    }
82
83    /// Create clipboard data with plain text.
84    pub fn text(content: &str) -> Self {
85        let mut data = Self::new();
86        data.set_text(content);
87        data
88    }
89
90    /// Create clipboard data with HTML.
91    pub fn html(content: &str) -> Self {
92        let mut data = Self::new();
93        data.set_html(content);
94        data
95    }
96
97    /// Set text content.
98    pub fn set_text(&mut self, content: &str) {
99        self.formats
100            .insert(ClipboardFormat::Text, content.as_bytes().to_vec());
101    }
102
103    /// Get text content.
104    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    /// Set HTML content.
111    pub fn set_html(&mut self, content: &str) {
112        self.formats
113            .insert(ClipboardFormat::Html, content.as_bytes().to_vec());
114    }
115
116    /// Get HTML content.
117    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    /// Set data for a specific format.
124    pub fn set(&mut self, format: ClipboardFormat, data: Vec<u8>) {
125        self.formats.insert(format, data);
126    }
127
128    /// Get data for a specific format.
129    pub fn get(&self, format: &ClipboardFormat) -> Option<&[u8]> {
130        self.formats.get(format).map(std::vec::Vec::as_slice)
131    }
132
133    /// Check if a format is available.
134    pub fn has_format(&self, format: &ClipboardFormat) -> bool {
135        self.formats.contains_key(format)
136    }
137
138    /// Get all available formats.
139    pub fn formats(&self) -> impl Iterator<Item = &ClipboardFormat> {
140        self.formats.keys()
141    }
142
143    /// Check if clipboard data is empty.
144    pub fn is_empty(&self) -> bool {
145        self.formats.is_empty()
146    }
147
148    /// Clear all data.
149    pub fn clear(&mut self) {
150        self.formats.clear();
151    }
152
153    /// Get the number of formats.
154    pub fn format_count(&self) -> usize {
155        self.formats.len()
156    }
157}
158
159/// Result of a clipboard operation.
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub enum ClipboardResult {
162    /// Operation succeeded.
163    Success,
164    /// Clipboard is not available.
165    Unavailable,
166    /// Permission denied.
167    PermissionDenied,
168    /// Format not supported.
169    UnsupportedFormat,
170    /// Other error.
171    Error(String),
172}
173
174impl ClipboardResult {
175    /// Check if operation was successful.
176    pub fn is_success(&self) -> bool {
177        matches!(self, Self::Success)
178    }
179
180    /// Check if operation failed.
181    pub fn is_error(&self) -> bool {
182        !self.is_success()
183    }
184}
185
186/// Clipboard operation type.
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum ClipboardOperation {
189    /// Copy operation.
190    Copy,
191    /// Cut operation.
192    Cut,
193    /// Paste operation.
194    Paste,
195}
196
197/// Event triggered by clipboard changes.
198#[derive(Debug, Clone)]
199pub struct ClipboardEvent {
200    /// Operation that occurred.
201    pub operation: ClipboardOperation,
202    /// Available formats.
203    pub formats: Vec<ClipboardFormat>,
204    /// Timestamp (monotonic counter).
205    pub timestamp: u64,
206}
207
208impl ClipboardEvent {
209    /// Create a new clipboard event.
210    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
223/// Callback for clipboard changes.
224pub type ClipboardCallback = Box<dyn Fn(&ClipboardEvent) + Send + Sync>;
225
226/// Clipboard manager for handling copy/cut/paste operations.
227pub struct Clipboard {
228    /// Current clipboard content.
229    data: Arc<RwLock<ClipboardData>>,
230    /// Change listeners.
231    listeners: Vec<ClipboardCallback>,
232    /// Event counter.
233    counter: u64,
234    /// Whether clipboard is available.
235    available: bool,
236}
237
238impl Clipboard {
239    /// Create a new clipboard.
240    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    /// Create an unavailable clipboard (for testing).
250    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    /// Check if clipboard is available.
260    pub fn is_available(&self) -> bool {
261        self.available
262    }
263
264    /// Write data to the clipboard.
265    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    /// Write text to the clipboard.
285    pub fn write_text(&mut self, text: &str) -> ClipboardResult {
286        self.write(ClipboardData::text(text))
287    }
288
289    /// Write HTML to the clipboard.
290    pub fn write_html(&mut self, html: &str) -> ClipboardResult {
291        self.write(ClipboardData::html(html))
292    }
293
294    /// Read all data from the clipboard.
295    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    /// Read text from the clipboard.
307    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    /// Read HTML from the clipboard.
319    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    /// Check if clipboard has a specific format.
331    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    /// Get available formats.
339    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    /// Clear the clipboard.
347    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    /// Add a listener for clipboard changes.
361    pub fn on_change(&mut self, callback: ClipboardCallback) {
362        self.listeners.push(callback);
363    }
364
365    /// Get listener count.
366    pub fn listener_count(&self) -> usize {
367        self.listeners.len()
368    }
369
370    /// Notify listeners of a change.
371    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    /// Simulate a cut operation (copies and signals cut).
379    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    /// Signal that a paste occurred.
399    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/// Clipboard history for undo support.
423#[derive(Debug, Default)]
424pub struct ClipboardHistory {
425    /// History entries.
426    entries: Vec<ClipboardData>,
427    /// Maximum history size.
428    max_size: usize,
429    /// Current index in history.
430    current: usize,
431}
432
433impl ClipboardHistory {
434    /// Create a new clipboard history.
435    pub fn new(max_size: usize) -> Self {
436        Self {
437            entries: Vec::new(),
438            max_size,
439            current: 0,
440        }
441    }
442
443    /// Add an entry to the history.
444    pub fn push(&mut self, data: ClipboardData) {
445        // Trim history if we're not at the end
446        if self.current < self.entries.len() {
447            self.entries.truncate(self.current);
448        }
449
450        self.entries.push(data);
451
452        // Trim to max size
453        while self.entries.len() > self.max_size {
454            self.entries.remove(0);
455        }
456
457        self.current = self.entries.len();
458    }
459
460    /// Get the current entry.
461    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    /// Go to previous entry.
470    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    /// Go to next entry.
480    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    /// Get entry at index.
490    pub fn get(&self, index: usize) -> Option<&ClipboardData> {
491        self.entries.get(index)
492    }
493
494    /// Get history length.
495    pub fn len(&self) -> usize {
496        self.entries.len()
497    }
498
499    /// Check if history is empty.
500    pub fn is_empty(&self) -> bool {
501        self.entries.is_empty()
502    }
503
504    /// Clear history.
505    pub fn clear(&mut self) {
506        self.entries.clear();
507        self.current = 0;
508    }
509
510    /// Get current index (1-based).
511    pub fn current_index(&self) -> usize {
512        self.current
513    }
514
515    /// Check if can go back.
516    pub fn can_go_back(&self) -> bool {
517        self.current > 1
518    }
519
520    /// Check if can go forward.
521    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    // ClipboardFormat tests
532    #[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    // ClipboardData tests
579    #[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    // ClipboardResult tests
630    #[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    // ClipboardEvent tests
645    #[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    // Clipboard tests
654    #[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    // ClipboardHistory tests
795    #[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        // At "third"
837        assert_eq!(
838            history.current().unwrap().get_text(),
839            Some("third".to_string())
840        );
841
842        // Go back to "second"
843        let prev = history.previous();
844        assert_eq!(prev.unwrap().get_text(), Some("second".to_string()));
845
846        // Go back to "first"
847        let prev = history.previous();
848        assert_eq!(prev.unwrap().get_text(), Some("first".to_string()));
849
850        // Can't go back further
851        assert!(history.previous().is_none());
852
853        // Go forward to "second"
854        let next = history.next();
855        assert_eq!(next.unwrap().get_text(), Some("second".to_string()));
856
857        // Go forward to "third"
858        let next = history.next();
859        assert_eq!(next.unwrap().get_text(), Some("third".to_string()));
860
861        // Can't go forward further
862        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        // Go back two steps
932        history.previous();
933        history.previous();
934        assert_eq!(history.current_index(), 1);
935
936        // Push new entry - should truncate "second" and "third"
937        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}