ricecoder_tui/
image_integration.rs

1//! Image integration for ricecoder-tui
2//!
3//! This module provides integration between ricecoder-tui and ricecoder-images,
4//! handling drag-and-drop events and image display in the terminal UI.
5//!
6//! # Requirements
7//!
8//! - Req 1.1: Detect drag-and-drop events and pass to ricecoder-images handler
9//! - Req 5.1: Display images in terminal using ricecoder-images ImageDisplay
10//! - Req 1.4: Add images to prompt context
11
12use std::path::PathBuf;
13
14/// Image integration manager for ricecoder-tui
15///
16/// Handles:
17/// - Drag-and-drop event detection and forwarding to ricecoder-images
18/// - Image display coordination with ricecoder-images ImageDisplay
19/// - Image context management for prompts
20///
21/// # Requirements
22///
23/// - Req 1.1: Create interface for receiving drag-and-drop events from ricecoder-tui
24/// - Req 1.1: Implement file path extraction from events
25/// - Req 1.1: Handle multiple files in single drag-and-drop
26pub struct ImageIntegration {
27    /// Whether image integration is enabled
28    pub enabled: bool,
29    /// Maximum number of images per prompt
30    pub max_images_per_prompt: usize,
31    /// Current images in the prompt context
32    pub current_images: Vec<PathBuf>,
33}
34
35impl ImageIntegration {
36    /// Create a new image integration manager
37    pub fn new() -> Self {
38        Self {
39            enabled: true,
40            max_images_per_prompt: 10,
41            current_images: Vec::new(),
42        }
43    }
44
45    /// Handle a drag-and-drop event
46    ///
47    /// # Arguments
48    ///
49    /// * `paths` - File paths from the drag-and-drop event
50    ///
51    /// # Returns
52    ///
53    /// Vector of successfully added image paths and any errors
54    ///
55    /// # Requirements
56    ///
57    /// - Req 1.1: Handle multiple files in single drag-and-drop
58    /// - Req 1.1: Implement file existence and permission checks
59    pub fn handle_drag_drop_event(&mut self, paths: Vec<PathBuf>) -> (Vec<PathBuf>, Vec<String>) {
60        let mut added = Vec::new();
61        let mut errors = Vec::new();
62
63        for path in paths {
64            // Check if we've reached the maximum number of images
65            if self.current_images.len() >= self.max_images_per_prompt {
66                errors.push(format!(
67                    "Maximum number of images ({}) reached",
68                    self.max_images_per_prompt
69                ));
70                break;
71            }
72
73            // Check if image is already in the context
74            if self.current_images.contains(&path) {
75                errors.push(format!("Image already in context: {}", path.display()));
76                continue;
77            }
78
79            // Check if file exists and is readable
80            if !path.exists() {
81                errors.push(format!("File does not exist: {}", path.display()));
82                continue;
83            }
84
85            if !path.is_file() {
86                errors.push(format!("Path is not a file: {}", path.display()));
87                continue;
88            }
89
90            // Add to current images
91            self.current_images.push(path.clone());
92            added.push(path);
93        }
94
95        (added, errors)
96    }
97
98    /// Remove an image from the prompt context
99    ///
100    /// # Arguments
101    ///
102    /// * `path` - Path to the image to remove
103    ///
104    /// # Returns
105    ///
106    /// True if image was removed, false if not found
107    pub fn remove_image(&mut self, path: &PathBuf) -> bool {
108        if let Some(pos) = self.current_images.iter().position(|p| p == path) {
109            self.current_images.remove(pos);
110            true
111        } else {
112            false
113        }
114    }
115
116    /// Clear all images from the prompt context
117    pub fn clear_images(&mut self) {
118        self.current_images.clear();
119    }
120
121    /// Get the current images in the prompt context
122    pub fn get_images(&self) -> &[PathBuf] {
123        &self.current_images
124    }
125
126    /// Check if there are any images in the prompt context
127    pub fn has_images(&self) -> bool {
128        !self.current_images.is_empty()
129    }
130
131    /// Get the number of images in the prompt context
132    pub fn image_count(&self) -> usize {
133        self.current_images.len()
134    }
135
136    /// Enable image integration
137    pub fn enable(&mut self) {
138        self.enabled = true;
139    }
140
141    /// Disable image integration
142    pub fn disable(&mut self) {
143        self.enabled = false;
144    }
145
146    /// Set the maximum number of images per prompt
147    pub fn set_max_images(&mut self, max: usize) {
148        self.max_images_per_prompt = max;
149    }
150}
151
152impl Default for ImageIntegration {
153    fn default() -> Self {
154        Self::new()
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use tempfile::NamedTempFile;
162
163    #[test]
164    fn test_image_integration_creation() {
165        let integration = ImageIntegration::new();
166        assert!(integration.enabled);
167        assert_eq!(integration.max_images_per_prompt, 10);
168        assert_eq!(integration.current_images.len(), 0);
169    }
170
171    #[test]
172    fn test_handle_drag_drop_event_single_file() {
173        let mut integration = ImageIntegration::new();
174        let temp_file = NamedTempFile::new().unwrap();
175        let path = temp_file.path().to_path_buf();
176
177        let (added, errors) = integration.handle_drag_drop_event(vec![path.clone()]);
178
179        assert_eq!(added.len(), 1);
180        assert_eq!(errors.len(), 0);
181        assert_eq!(integration.current_images.len(), 1);
182        assert_eq!(integration.current_images[0], path);
183    }
184
185    #[test]
186    fn test_handle_drag_drop_event_multiple_files() {
187        let mut integration = ImageIntegration::new();
188        let temp_file1 = NamedTempFile::new().unwrap();
189        let temp_file2 = NamedTempFile::new().unwrap();
190        let path1 = temp_file1.path().to_path_buf();
191        let path2 = temp_file2.path().to_path_buf();
192
193        let (added, errors) = integration.handle_drag_drop_event(vec![path1.clone(), path2.clone()]);
194
195        assert_eq!(added.len(), 2);
196        assert_eq!(errors.len(), 0);
197        assert_eq!(integration.current_images.len(), 2);
198    }
199
200    #[test]
201    fn test_handle_drag_drop_event_nonexistent_file() {
202        let mut integration = ImageIntegration::new();
203        let path = PathBuf::from("/nonexistent/image.png");
204
205        let (added, errors) = integration.handle_drag_drop_event(vec![path]);
206
207        assert_eq!(added.len(), 0);
208        assert_eq!(errors.len(), 1);
209        assert!(errors[0].contains("does not exist"));
210    }
211
212    #[test]
213    fn test_handle_drag_drop_event_directory() {
214        let mut integration = ImageIntegration::new();
215        let temp_dir = tempfile::tempdir().unwrap();
216        let path = temp_dir.path().to_path_buf();
217
218        let (added, errors) = integration.handle_drag_drop_event(vec![path]);
219
220        assert_eq!(added.len(), 0);
221        assert_eq!(errors.len(), 1);
222        assert!(errors[0].contains("not a file"));
223    }
224
225    #[test]
226    fn test_handle_drag_drop_event_duplicate() {
227        let mut integration = ImageIntegration::new();
228        let temp_file = NamedTempFile::new().unwrap();
229        let path = temp_file.path().to_path_buf();
230
231        // Add first time
232        let (added1, errors1) = integration.handle_drag_drop_event(vec![path.clone()]);
233        assert_eq!(added1.len(), 1);
234        assert_eq!(errors1.len(), 0);
235
236        // Try to add again
237        let (added2, errors2) = integration.handle_drag_drop_event(vec![path]);
238        assert_eq!(added2.len(), 0);
239        assert_eq!(errors2.len(), 1);
240        assert!(errors2[0].contains("already in context"));
241    }
242
243    #[test]
244    fn test_handle_drag_drop_event_max_images() {
245        let mut integration = ImageIntegration::new();
246        integration.set_max_images(2);
247
248        let temp_file1 = NamedTempFile::new().unwrap();
249        let temp_file2 = NamedTempFile::new().unwrap();
250        let temp_file3 = NamedTempFile::new().unwrap();
251
252        let path1 = temp_file1.path().to_path_buf();
253        let path2 = temp_file2.path().to_path_buf();
254        let path3 = temp_file3.path().to_path_buf();
255
256        // Add first two
257        let (added1, errors1) = integration.handle_drag_drop_event(vec![path1, path2]);
258        assert_eq!(added1.len(), 2);
259        assert_eq!(errors1.len(), 0);
260
261        // Try to add third
262        let (added2, errors2) = integration.handle_drag_drop_event(vec![path3]);
263        assert_eq!(added2.len(), 0);
264        assert_eq!(errors2.len(), 1);
265        assert!(errors2[0].contains("Maximum number of images"));
266    }
267
268    #[test]
269    fn test_remove_image() {
270        let mut integration = ImageIntegration::new();
271        let temp_file = NamedTempFile::new().unwrap();
272        let path = temp_file.path().to_path_buf();
273
274        integration.handle_drag_drop_event(vec![path.clone()]);
275        assert_eq!(integration.current_images.len(), 1);
276
277        let removed = integration.remove_image(&path);
278        assert!(removed);
279        assert_eq!(integration.current_images.len(), 0);
280    }
281
282    #[test]
283    fn test_remove_image_not_found() {
284        let mut integration = ImageIntegration::new();
285        let path = PathBuf::from("/nonexistent/image.png");
286
287        let removed = integration.remove_image(&path);
288        assert!(!removed);
289    }
290
291    #[test]
292    fn test_clear_images() {
293        let mut integration = ImageIntegration::new();
294        let temp_file1 = NamedTempFile::new().unwrap();
295        let temp_file2 = NamedTempFile::new().unwrap();
296
297        integration.handle_drag_drop_event(vec![
298            temp_file1.path().to_path_buf(),
299            temp_file2.path().to_path_buf(),
300        ]);
301        assert_eq!(integration.current_images.len(), 2);
302
303        integration.clear_images();
304        assert_eq!(integration.current_images.len(), 0);
305    }
306
307    #[test]
308    fn test_get_images() {
309        let mut integration = ImageIntegration::new();
310        let temp_file = NamedTempFile::new().unwrap();
311        let path = temp_file.path().to_path_buf();
312
313        integration.handle_drag_drop_event(vec![path.clone()]);
314
315        let images = integration.get_images();
316        assert_eq!(images.len(), 1);
317        assert_eq!(images[0], path);
318    }
319
320    #[test]
321    fn test_has_images() {
322        let mut integration = ImageIntegration::new();
323        assert!(!integration.has_images());
324
325        let temp_file = NamedTempFile::new().unwrap();
326        integration.handle_drag_drop_event(vec![temp_file.path().to_path_buf()]);
327        assert!(integration.has_images());
328    }
329
330    #[test]
331    fn test_image_count() {
332        let mut integration = ImageIntegration::new();
333        assert_eq!(integration.image_count(), 0);
334
335        let temp_file1 = NamedTempFile::new().unwrap();
336        let temp_file2 = NamedTempFile::new().unwrap();
337
338        integration.handle_drag_drop_event(vec![
339            temp_file1.path().to_path_buf(),
340            temp_file2.path().to_path_buf(),
341        ]);
342        assert_eq!(integration.image_count(), 2);
343    }
344
345    #[test]
346    fn test_enable_disable() {
347        let mut integration = ImageIntegration::new();
348        assert!(integration.enabled);
349
350        integration.disable();
351        assert!(!integration.enabled);
352
353        integration.enable();
354        assert!(integration.enabled);
355    }
356
357    #[test]
358    fn test_set_max_images() {
359        let mut integration = ImageIntegration::new();
360        assert_eq!(integration.max_images_per_prompt, 10);
361
362        integration.set_max_images(5);
363        assert_eq!(integration.max_images_per_prompt, 5);
364    }
365}