clean_dev_dirs/project/projects.rs
1//! Collection management and operations for development projects.
2//!
3//! This module provides the `Projects` struct which wraps a collection of
4//! development projects and provides various operations on them, including
5//! interactive selection, summary reporting, and parallel iteration support.
6
7use anyhow::Result;
8use colored::Colorize;
9use humansize::{DECIMAL, format_size};
10use inquire::MultiSelect;
11use rayon::prelude::*;
12
13use crate::project::ProjectType;
14
15use super::Project;
16
17/// A collection of development projects with associated operations.
18///
19/// The `Projects` struct wraps a vector of `Project` instances and provides
20/// higher-level operations such as interactive selection, summary reporting,
21/// and parallel processing support. It serves as the main data structure
22/// for managing collections of projects throughout the application.
23pub struct Projects(Vec<Project>);
24
25impl From<Vec<Project>> for Projects {
26    /// Create a `Projects` collection from a vector of projects.
27    ///
28    /// This conversion allows easy creation of a `Projects` instance from
29    /// any vector of `Project` objects, typically used when the scanner
30    /// returns a collection of detected projects.
31    ///
32    /// # Arguments
33    ///
34    /// * `projects` - A vector of `Project` instances
35    ///
36    /// # Returns
37    ///
38    /// A new `Projects` collection containing the provided projects.
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// # use crate::{Projects, Project};
44    /// let project_vec = vec![/* project instances */];
45    /// let projects: Projects = project_vec.into();
46    /// ```
47    fn from(projects: Vec<Project>) -> Self {
48        Self(projects)
49    }
50}
51
52impl IntoParallelIterator for Projects {
53    type Iter = rayon::vec::IntoIter<Project>;
54    type Item = Project;
55
56    /// Enable parallel iteration with ownership transfer.
57    ///
58    /// This implementation allows the collection to be consumed and processed
59    /// in parallel, transferring ownership of each project to the parallel
60    /// processing context.
61    ///
62    /// # Returns
63    ///
64    /// A parallel iterator that takes ownership of the projects in the collection.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// # use rayon::prelude::*;
70    /// # use crate::Projects;
71    /// let results: Vec<_> = projects.into_par_iter().map(|project| {
72    ///     // Transform each project in parallel
73    ///     process_project(project)
74    /// }).collect();
75    /// ```
76    fn into_par_iter(self) -> Self::Iter {
77        self.0.into_par_iter()
78    }
79}
80
81impl<'a> IntoParallelIterator for &'a Projects {
82    type Iter = rayon::slice::Iter<'a, Project>;
83    type Item = &'a Project;
84
85    /// Enable parallel iteration over project references.
86    ///
87    /// This implementation allows the collection to be processed in parallel
88    /// using Rayon's parallel iterators, which can significantly improve
89    /// performance for operations that can be parallelized.
90    ///
91    /// # Returns
92    ///
93    /// A parallel iterator over references to the projects in the collection.
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// # use rayon::prelude::*;
99    /// # use crate::Projects;
100    /// projects.into_par_iter().for_each(|project| {
101    ///     // Process each project in parallel
102    ///     println!("Processing: {}", project.root_path.display());
103    /// });
104    /// ```
105    fn into_par_iter(self) -> Self::Iter {
106        self.0.par_iter()
107    }
108}
109
110impl Projects {
111    /// Calculate the total size of all build directories in the collection.
112    ///
113    /// This method sums up the sizes of all build directories (target/ or
114    /// `node_modules`/) across all projects in the collection to provide a
115    /// total estimate of reclaimable disk space.
116    ///
117    /// # Returns
118    ///
119    /// The total size in bytes of all build directories combined.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// # use crate::Projects;
125    /// let total_bytes = projects.get_total_size();
126    /// println!("Total reclaimable space: {} bytes", total_bytes);
127    /// ```
128    #[must_use]
129    pub fn get_total_size(&self) -> u64 {
130        self.0.iter().map(|p| p.build_arts.size).sum()
131    }
132
133    /// Present an interactive selection interface for choosing projects to clean.
134    ///
135    /// This method displays a multi-select dialog that allows users to choose
136    /// which projects they want to clean. Each project is shown with its type
137    /// icon, path, and reclaimable space. All projects are selected by default.
138    ///
139    /// # Returns
140    ///
141    /// - `Ok(Vec<Project>)` - The projects selected by the user
142    /// - `Err(anyhow::Error)` - If the interactive dialog fails or is canceled
143    ///
144    /// # Interface Details
145    ///
146    /// - Uses a colorful theme for better visual appeal
147    /// - Shows project type icons (🦀 for Rust, 📦 for Node.js, 🐍 for Python, 🐹 for Go)
148    /// - Displays project paths and sizes in human-readable format
149    /// - Allows toggling selections with space bar
150    /// - Confirms selection with the Enter key
151    ///
152    /// # Examples
153    ///
154    /// ```
155    /// # use crate::Projects;
156    /// # use anyhow::Result;
157    /// let selected_projects = projects.interactive_selection()?;
158    /// println!("User selected {} projects", selected_projects.len());
159    /// ```
160    ///
161    /// # Errors
162    ///
163    /// This method can fail if:
164    /// - The terminal doesn't support interactive input
165    /// - The user cancels the dialog (Ctrl+C)
166    /// - There are I/O errors with the terminal
167    pub fn interactive_selection(&self) -> Result<Vec<Project>> {
168        let items: Vec<String> = self
169            .0
170            .iter()
171            .map(|p| {
172                let icon = match p.kind {
173                    ProjectType::Rust => "🦀",
174                    ProjectType::Node => "📦",
175                    ProjectType::Python => "🐍",
176                    ProjectType::Go => "🐹",
177                };
178                format!(
179                    "{icon} {} ({})",
180                    p.root_path.display(),
181                    format_size(p.build_arts.size, DECIMAL)
182                )
183            })
184            .collect();
185
186        let defaults: Vec<usize> = (0..self.0.len()).collect();
187
188        let selections = MultiSelect::new("Select projects to clean:", items)
189            .with_default(&defaults)
190            .prompt()?;
191
192        // Find indices of selected items
193        let selected_indices: Vec<usize> = selections
194            .iter()
195            .filter_map(|selected_item| {
196                self.0
197                    .iter()
198                    .enumerate()
199                    .find(|(_, p)| {
200                        let icon = match p.kind {
201                            ProjectType::Rust => "🦀",
202                            ProjectType::Node => "📦",
203                            ProjectType::Python => "🐍",
204                            ProjectType::Go => "🐹",
205                        };
206                        let expected = format!(
207                            "{icon} {} ({})",
208                            p.root_path.display(),
209                            format_size(p.build_arts.size, DECIMAL)
210                        );
211                        &expected == selected_item
212                    })
213                    .map(|(i, _)| i)
214            })
215            .collect();
216
217        Ok(selected_indices
218            .into_iter()
219            .map(|i| self.0[i].clone())
220            .collect())
221    }
222
223    /// Get the number of projects in the collection.
224    ///
225    /// # Returns
226    ///
227    /// The number of projects contained in this collection.
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// # use crate::Projects;
233    /// println!("Found {} projects", projects.len());
234    /// ```
235    #[must_use]
236    pub fn len(&self) -> usize {
237        self.0.len()
238    }
239
240    /// Check if the collection is empty.
241    ///
242    /// # Returns
243    ///
244    /// `true` if the collection contains no projects, `false` otherwise.
245    ///
246    /// # Examples
247    ///
248    /// ```
249    /// # use crate::Projects;
250    /// if projects.is_empty() {
251    ///     println!("No projects found");
252    /// }
253    /// ```
254    #[must_use]
255    pub fn is_empty(&self) -> bool {
256        self.0.is_empty()
257    }
258
259    /// Print a detailed summary of the projects and their reclaimable space.
260    ///
261    /// This method analyzes the collection and prints statistics including:
262    /// - Number and total size of Rust projects
263    /// - Number and total size of Node.js projects
264    /// - Number and total size of Python projects
265    /// - Number and total size of Go projects
266    /// - Total reclaimable space across all projects
267    ///
268    /// The output is formatted with colors and emoji icons for better readability.
269    ///
270    /// # Arguments
271    ///
272    /// * `total_size` - The total size in bytes (usually from `get_total_size()`)
273    ///
274    /// # Examples
275    ///
276    /// ```
277    /// # use crate::Projects;
278    /// let total_size = projects.get_total_size();
279    /// projects.print_summary(total_size);
280    /// ```
281    ///
282    /// # Output Format
283    ///
284    /// ```text
285    ///   🦀 5 Rust projects (2.3 GB)
286    ///   📦 3 Node.js projects (1.7 GB)
287    ///   🐍 2 Python projects (1.2 GB)
288    ///   🐹 1 Go project (0.5 GB)
289    ///   💾 Total reclaimable space: 4.0 GB
290    /// ```
291    pub fn print_summary(&self, total_size: u64) {
292        let mut rust_count = 0;
293        let mut node_count = 0;
294        let mut python_count = 0;
295        let mut go_count = 0;
296        let mut rust_size = 0u64;
297        let mut node_size = 0u64;
298        let mut python_size = 0u64;
299        let mut go_size = 0u64;
300
301        for project in &self.0 {
302            match project.kind {
303                ProjectType::Rust => {
304                    rust_count += 1;
305                    rust_size += project.build_arts.size;
306                }
307                ProjectType::Node => {
308                    node_count += 1;
309                    node_size += project.build_arts.size;
310                }
311                ProjectType::Python => {
312                    python_count += 1;
313                    python_size += project.build_arts.size;
314                }
315                ProjectType::Go => {
316                    go_count += 1;
317                    go_size += project.build_arts.size;
318                }
319            }
320        }
321
322        if rust_count > 0 {
323            println!(
324                "  🦀 {} Rust projects ({})",
325                rust_count.to_string().bright_white(),
326                format_size(rust_size, DECIMAL).bright_white()
327            );
328        }
329
330        if node_count > 0 {
331            println!(
332                "  📦 {} Node.js projects ({})",
333                node_count.to_string().bright_white(),
334                format_size(node_size, DECIMAL).bright_white()
335            );
336        }
337
338        if python_count > 0 {
339            println!(
340                "  🐍 {} Python projects ({})",
341                python_count.to_string().bright_white(),
342                format_size(python_size, DECIMAL).bright_white()
343            );
344        }
345
346        if go_count > 0 {
347            println!(
348                "  🐹 {} Go projects ({})",
349                go_count.to_string().bright_white(),
350                format_size(go_size, DECIMAL).bright_white()
351            );
352        }
353
354        println!(
355            "  💾 Total reclaimable space: {}",
356            format_size(total_size, DECIMAL).bright_green().bold()
357        );
358    }
359}