ricecoder_tui/
clipboard.rs

1//! Clipboard operations for copying content
2
3use thiserror::Error;
4
5/// Clipboard error types
6#[derive(Debug, Error)]
7pub enum ClipboardError {
8    /// Failed to access clipboard
9    #[error("Failed to access clipboard: {0}")]
10    AccessError(String),
11
12    /// Failed to copy content
13    #[error("Failed to copy content: {0}")]
14    CopyError(String),
15
16    /// Failed to read from clipboard
17    #[error("Failed to read from clipboard: {0}")]
18    ReadError(String),
19
20    /// Content is too large
21    #[error("Content is too large to copy: {0} bytes")]
22    ContentTooLarge(usize),
23}
24
25/// Maximum clipboard content size (100 MB)
26const MAX_CLIPBOARD_SIZE: usize = 100 * 1024 * 1024;
27
28/// Clipboard manager for copy operations
29pub struct ClipboardManager;
30
31impl ClipboardManager {
32    /// Copy text to clipboard
33    pub fn copy_text(text: &str) -> Result<(), ClipboardError> {
34        // Check size limit
35        if text.len() > MAX_CLIPBOARD_SIZE {
36            return Err(ClipboardError::ContentTooLarge(text.len()));
37        }
38
39        match arboard::Clipboard::new() {
40            Ok(mut clipboard) => {
41                clipboard
42                    .set_text(text)
43                    .map_err(|e| ClipboardError::CopyError(e.to_string()))?;
44                Ok(())
45            }
46            Err(e) => Err(ClipboardError::AccessError(e.to_string())),
47        }
48    }
49
50    /// Read text from clipboard
51    pub fn read_text() -> Result<String, ClipboardError> {
52        match arboard::Clipboard::new() {
53            Ok(mut clipboard) => clipboard
54                .get_text()
55                .map_err(|e| ClipboardError::ReadError(e.to_string())),
56            Err(e) => Err(ClipboardError::AccessError(e.to_string())),
57        }
58    }
59
60    /// Copy command block content
61    pub fn copy_command_block(
62        command: &str,
63        output: &str,
64        status: &str,
65    ) -> Result<(), ClipboardError> {
66        let content = format!(
67            "Command: {}\nStatus: {}\nOutput:\n{}",
68            command, status, output
69        );
70        Self::copy_text(&content)
71    }
72
73    /// Copy command output only
74    pub fn copy_command_output(output: &str) -> Result<(), ClipboardError> {
75        Self::copy_text(output)
76    }
77
78    /// Copy command text only
79    pub fn copy_command_text(command: &str) -> Result<(), ClipboardError> {
80        Self::copy_text(command)
81    }
82}
83
84/// Copy action feedback
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum CopyFeedback {
87    /// Copy was successful
88    Success,
89    /// Copy failed
90    Failed,
91    /// Copy was cancelled
92    Cancelled,
93}
94
95impl CopyFeedback {
96    /// Get display text for the feedback
97    pub fn display_text(&self) -> &'static str {
98        match self {
99            CopyFeedback::Success => "✓ Copied to clipboard",
100            CopyFeedback::Failed => "✗ Failed to copy",
101            CopyFeedback::Cancelled => "⊘ Copy cancelled",
102        }
103    }
104}
105
106/// Copy operation state
107#[derive(Debug, Clone)]
108pub struct CopyOperation {
109    /// Content being copied
110    pub content: String,
111    /// Copy feedback
112    pub feedback: Option<CopyFeedback>,
113    /// Feedback display duration in frames
114    pub feedback_duration: u32,
115    /// Current feedback frame counter
116    pub feedback_frame: u32,
117}
118
119impl CopyOperation {
120    /// Create a new copy operation
121    pub fn new(content: impl Into<String>) -> Self {
122        Self {
123            content: content.into(),
124            feedback: None,
125            feedback_duration: 60, // ~1 second at 60 FPS
126            feedback_frame: 0,
127        }
128    }
129
130    /// Execute the copy operation
131    pub fn execute(&mut self) -> Result<(), ClipboardError> {
132        match ClipboardManager::copy_text(&self.content) {
133            Ok(()) => {
134                self.feedback = Some(CopyFeedback::Success);
135                self.feedback_frame = 0;
136                Ok(())
137            }
138            Err(e) => {
139                self.feedback = Some(CopyFeedback::Failed);
140                self.feedback_frame = 0;
141                Err(e)
142            }
143        }
144    }
145
146    /// Update feedback animation
147    pub fn update_feedback(&mut self) {
148        if self.feedback.is_some() {
149            self.feedback_frame += 1;
150            if self.feedback_frame >= self.feedback_duration {
151                self.feedback = None;
152                self.feedback_frame = 0;
153            }
154        }
155    }
156
157    /// Check if feedback is visible
158    pub fn is_feedback_visible(&self) -> bool {
159        self.feedback.is_some()
160    }
161
162    /// Get current feedback
163    pub fn get_feedback(&self) -> Option<CopyFeedback> {
164        self.feedback
165    }
166
167    /// Get feedback progress (0.0 to 1.0)
168    pub fn feedback_progress(&self) -> f32 {
169        if self.feedback_duration == 0 {
170            1.0
171        } else {
172            self.feedback_frame as f32 / self.feedback_duration as f32
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_copy_operation_creation() {
183        let op = CopyOperation::new("test content");
184        assert_eq!(op.content, "test content");
185        assert_eq!(op.feedback, None);
186        assert_eq!(op.feedback_frame, 0);
187    }
188
189    #[test]
190    fn test_copy_operation_feedback_progress() {
191        let op = CopyOperation::new("test");
192        assert_eq!(op.feedback_progress(), 0.0);
193
194        let mut op = CopyOperation::new("test");
195        op.feedback = Some(CopyFeedback::Success);
196        op.feedback_frame = 30;
197        op.feedback_duration = 60;
198        assert_eq!(op.feedback_progress(), 0.5);
199
200        op.feedback_frame = 60;
201        assert_eq!(op.feedback_progress(), 1.0);
202    }
203
204    #[test]
205    fn test_copy_operation_feedback_animation() {
206        let mut op = CopyOperation::new("test");
207        op.feedback = Some(CopyFeedback::Success);
208        op.feedback_duration = 3;
209
210        assert!(op.is_feedback_visible());
211        assert_eq!(op.get_feedback(), Some(CopyFeedback::Success));
212
213        op.update_feedback();
214        assert!(op.is_feedback_visible());
215
216        op.update_feedback();
217        assert!(op.is_feedback_visible());
218
219        op.update_feedback();
220        assert!(!op.is_feedback_visible());
221    }
222
223    #[test]
224    fn test_copy_feedback_display() {
225        assert_eq!(
226            CopyFeedback::Success.display_text(),
227            "✓ Copied to clipboard"
228        );
229        assert_eq!(CopyFeedback::Failed.display_text(), "✗ Failed to copy");
230        assert_eq!(CopyFeedback::Cancelled.display_text(), "⊘ Copy cancelled");
231    }
232
233    #[test]
234    fn test_clipboard_manager_copy_text() {
235        // This test will only work if clipboard is available
236        let result = ClipboardManager::copy_text("test content");
237        // We don't assert here because clipboard might not be available in test environment
238        // Just verify the function doesn't panic
239        let _ = result;
240    }
241
242    #[test]
243    fn test_clipboard_manager_copy_command_block() {
244        let result = ClipboardManager::copy_command_block(
245            "cargo build",
246            "Compiling...\nFinished",
247            "success",
248        );
249        // Just verify the function doesn't panic
250        let _ = result;
251    }
252
253    #[test]
254    fn test_clipboard_manager_copy_command_output() {
255        let result = ClipboardManager::copy_command_output("output text");
256        let _ = result;
257    }
258
259    #[test]
260    fn test_clipboard_manager_copy_command_text() {
261        let result = ClipboardManager::copy_command_text("cargo test");
262        let _ = result;
263    }
264
265    #[test]
266    fn test_copy_operation_size_limit() {
267        // Create content larger than limit
268        let large_content = "x".repeat(MAX_CLIPBOARD_SIZE + 1);
269        let result = ClipboardManager::copy_text(&large_content);
270        assert!(result.is_err());
271        match result {
272            Err(ClipboardError::ContentTooLarge(size)) => {
273                assert_eq!(size, MAX_CLIPBOARD_SIZE + 1);
274            }
275            _ => panic!("Expected ContentTooLarge error"),
276        }
277    }
278}