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 dialoguer::{MultiSelect, theme::ColorfulTheme};
10use humansize::{DECIMAL, format_size};
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![true; self.0.len()];
187
188        let selections = MultiSelect::with_theme(&ColorfulTheme::default())
189            .with_prompt("Select projects to clean:")
190            .items(&items)
191            .defaults(&defaults)
192            .interact()?;
193
194        Ok(selections.into_iter().map(|i| self.0[i].clone()).collect())
195    }
196
197    /// Get the number of projects in the collection.
198    ///
199    /// # Returns
200    ///
201    /// The number of projects contained in this collection.
202    ///
203    /// # Examples
204    ///
205    /// ```
206    /// # use crate::Projects;
207    /// println!("Found {} projects", projects.len());
208    /// ```
209    #[must_use]
210    pub fn len(&self) -> usize {
211        self.0.len()
212    }
213
214    /// Check if the collection is empty.
215    ///
216    /// # Returns
217    ///
218    /// `true` if the collection contains no projects, `false` otherwise.
219    ///
220    /// # Examples
221    ///
222    /// ```
223    /// # use crate::Projects;
224    /// if projects.is_empty() {
225    ///     println!("No projects found");
226    /// }
227    /// ```
228    #[must_use]
229    pub fn is_empty(&self) -> bool {
230        self.0.is_empty()
231    }
232
233    /// Print a detailed summary of the projects and their reclaimable space.
234    ///
235    /// This method analyzes the collection and prints statistics including:
236    /// - Number and total size of Rust projects
237    /// - Number and total size of Node.js projects
238    /// - Number and total size of Python projects
239    /// - Number and total size of Go projects
240    /// - Total reclaimable space across all projects
241    ///
242    /// The output is formatted with colors and emoji icons for better readability.
243    ///
244    /// # Arguments
245    ///
246    /// * `total_size` - The total size in bytes (usually from `get_total_size()`)
247    ///
248    /// # Examples
249    ///
250    /// ```
251    /// # use crate::Projects;
252    /// let total_size = projects.get_total_size();
253    /// projects.print_summary(total_size);
254    /// ```
255    ///
256    /// # Output Format
257    ///
258    /// ```text
259    ///   🦀 5 Rust projects (2.3 GB)
260    ///   📦 3 Node.js projects (1.7 GB)
261    ///   🐍 2 Python projects (1.2 GB)
262    ///   🐹 1 Go project (0.5 GB)
263    ///   💾 Total reclaimable space: 4.0 GB
264    /// ```
265    pub fn print_summary(&self, total_size: u64) {
266        let mut rust_count = 0;
267        let mut node_count = 0;
268        let mut python_count = 0;
269        let mut go_count = 0;
270        let mut rust_size = 0u64;
271        let mut node_size = 0u64;
272        let mut python_size = 0u64;
273        let mut go_size = 0u64;
274
275        for project in &self.0 {
276            match project.kind {
277                ProjectType::Rust => {
278                    rust_count += 1;
279                    rust_size += project.build_arts.size;
280                }
281                ProjectType::Node => {
282                    node_count += 1;
283                    node_size += project.build_arts.size;
284                }
285                ProjectType::Python => {
286                    python_count += 1;
287                    python_size += project.build_arts.size;
288                }
289                ProjectType::Go => {
290                    go_count += 1;
291                    go_size += project.build_arts.size;
292                }
293            }
294        }
295
296        if rust_count > 0 {
297            println!(
298                "  🦀 {} Rust projects ({})",
299                rust_count.to_string().bright_white(),
300                format_size(rust_size, DECIMAL).bright_white()
301            );
302        }
303
304        if node_count > 0 {
305            println!(
306                "  📦 {} Node.js projects ({})",
307                node_count.to_string().bright_white(),
308                format_size(node_size, DECIMAL).bright_white()
309            );
310        }
311
312        if python_count > 0 {
313            println!(
314                "  🐍 {} Python projects ({})",
315                python_count.to_string().bright_white(),
316                format_size(python_size, DECIMAL).bright_white()
317            );
318        }
319
320        if go_count > 0 {
321            println!(
322                "  🐹 {} Go projects ({})",
323                go_count.to_string().bright_white(),
324                format_size(go_size, DECIMAL).bright_white()
325            );
326        }
327
328        println!(
329            "  💾 Total reclaimable space: {}",
330            format_size(total_size, DECIMAL).bright_green().bold()
331        );
332    }
333}