ricecoder_tui/
clipboard.rs1use thiserror::Error;
4
5#[derive(Debug, Error)]
7pub enum ClipboardError {
8 #[error("Failed to access clipboard: {0}")]
10 AccessError(String),
11
12 #[error("Failed to copy content: {0}")]
14 CopyError(String),
15
16 #[error("Failed to read from clipboard: {0}")]
18 ReadError(String),
19
20 #[error("Content is too large to copy: {0} bytes")]
22 ContentTooLarge(usize),
23}
24
25const MAX_CLIPBOARD_SIZE: usize = 100 * 1024 * 1024;
27
28pub struct ClipboardManager;
30
31impl ClipboardManager {
32 pub fn copy_text(text: &str) -> Result<(), ClipboardError> {
34 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 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 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 pub fn copy_command_output(output: &str) -> Result<(), ClipboardError> {
75 Self::copy_text(output)
76 }
77
78 pub fn copy_command_text(command: &str) -> Result<(), ClipboardError> {
80 Self::copy_text(command)
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum CopyFeedback {
87 Success,
89 Failed,
91 Cancelled,
93}
94
95impl CopyFeedback {
96 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#[derive(Debug, Clone)]
108pub struct CopyOperation {
109 pub content: String,
111 pub feedback: Option<CopyFeedback>,
113 pub feedback_duration: u32,
115 pub feedback_frame: u32,
117}
118
119impl CopyOperation {
120 pub fn new(content: impl Into<String>) -> Self {
122 Self {
123 content: content.into(),
124 feedback: None,
125 feedback_duration: 60, feedback_frame: 0,
127 }
128 }
129
130 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 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 pub fn is_feedback_visible(&self) -> bool {
159 self.feedback.is_some()
160 }
161
162 pub fn get_feedback(&self) -> Option<CopyFeedback> {
164 self.feedback
165 }
166
167 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 let result = ClipboardManager::copy_text("test content");
237 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 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 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}