Skip to main content

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