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.
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(Project::total_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 (🦀 Rust, 📦 Node.js, 🐍 Python, 🐹 Go, ☕ Java, ⚙️ C/C++, 🐦 Swift, 🔷 .NET)
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 = icon_for_project_type(&p.kind);
173                format!(
174                    "{icon} {} ({})",
175                    p.root_path.display(),
176                    format_size(p.total_size(), DECIMAL)
177                )
178            })
179            .collect();
180
181        let defaults: Vec<usize> = (0..self.0.len()).collect();
182
183        let selections = MultiSelect::new("Select projects to clean:", items)
184            .with_default(&defaults)
185            .prompt()?;
186
187        Ok(selections
188            .iter()
189            .filter_map(|selected_item| {
190                self.0
191                    .iter()
192                    .enumerate()
193                    .find(|(_, p)| {
194                        let icon = icon_for_project_type(&p.kind);
195                        let expected = format!(
196                            "{icon} {} ({})",
197                            p.root_path.display(),
198                            format_size(p.total_size(), DECIMAL)
199                        );
200                        &expected == selected_item
201                    })
202                    .map(|(i, _)| i)
203            })
204            .map(|i| self.0[i].clone())
205            .collect())
206    }
207
208    /// Get the number of projects in the collection.
209    ///
210    /// # Returns
211    ///
212    /// The number of projects contained in this collection.
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// # use crate::Projects;
218    /// println!("Found {} projects", projects.len());
219    /// ```
220    #[must_use]
221    pub const fn len(&self) -> usize {
222        self.0.len()
223    }
224
225    /// Check if the collection is empty.
226    ///
227    /// # Returns
228    ///
229    /// `true` if the collection contains no projects, `false` otherwise.
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// # use crate::Projects;
235    /// if projects.is_empty() {
236    ///     println!("No projects found");
237    /// }
238    /// ```
239    #[must_use]
240    pub const fn is_empty(&self) -> bool {
241        self.0.is_empty()
242    }
243
244    /// Return a slice of the underlying project collection.
245    ///
246    /// Useful for inspecting projects without consuming the collection,
247    /// for example to build JSON output before cleanup.
248    #[must_use]
249    pub fn as_slice(&self) -> &[Project] {
250        &self.0
251    }
252
253    /// Print a detailed summary of the projects and their reclaimable space.
254    ///
255    /// This method analyzes the collection and prints statistics including:
256    /// - Number and total size of Rust projects
257    /// - Number and total size of Node.js projects
258    /// - Number and total size of Python projects
259    /// - Number and total size of Go projects
260    /// - Total reclaimable space across all projects
261    ///
262    /// The output is formatted with colors and emoji icons for better readability.
263    ///
264    /// # Arguments
265    ///
266    /// * `total_size` - The total size in bytes (usually from `get_total_size()`)
267    ///
268    /// # Examples
269    ///
270    /// ```
271    /// # use crate::Projects;
272    /// let total_size = projects.get_total_size();
273    /// projects.print_summary(total_size);
274    /// ```
275    ///
276    /// # Output Format
277    ///
278    /// ```text
279    ///   🦀 5 Rust projects (2.3 GB)
280    ///   📦 3 Node.js projects (1.7 GB)
281    ///   🐍 2 Python projects (1.2 GB)
282    ///   🐹 1 Go project (0.5 GB)
283    ///   ☕ 2 Java/Kotlin projects (0.8 GB)
284    ///   ⚙️ 1 C/C++ project (0.3 GB)
285    ///   🐦 1 Swift project (0.2 GB)
286    ///   🔷 1 .NET/C# project (0.1 GB)
287    ///   💾 Total reclaimable space: 4.0 GB
288    /// ```
289    pub fn print_summary(&self, total_size: u64) {
290        let type_entries: &[(ProjectType, &str, &str)] = &[
291            (ProjectType::Rust, "🦀", "Rust"),
292            (ProjectType::Node, "📦", "Node.js"),
293            (ProjectType::Python, "🐍", "Python"),
294            (ProjectType::Go, "🐹", "Go"),
295            (ProjectType::Java, "☕", "Java/Kotlin"),
296            (ProjectType::Cpp, "⚙️", "C/C++"),
297            (ProjectType::Swift, "🐦", "Swift"),
298            (ProjectType::DotNet, "🔷", ".NET/C#"),
299        ];
300
301        for (kind, icon, label) in type_entries {
302            let (count, size) = self.0.iter().fold((0usize, 0u64), |(c, s), p| {
303                if &p.kind == kind {
304                    (c + 1, s + p.total_size())
305                } else {
306                    (c, s)
307                }
308            });
309
310            if count > 0 {
311                println!(
312                    "  {icon} {} {label} projects ({})",
313                    count.to_string().bright_white(),
314                    format_size(size, DECIMAL).bright_white()
315                );
316            }
317        }
318
319        println!(
320            "  💾 Total reclaimable space: {}",
321            format_size(total_size, DECIMAL).bright_green().bold()
322        );
323    }
324}
325
326/// Return the icon for a given project type.
327const fn icon_for_project_type(kind: &ProjectType) -> &'static str {
328    match kind {
329        ProjectType::Rust => "🦀",
330        ProjectType::Node => "📦",
331        ProjectType::Python => "🐍",
332        ProjectType::Go => "🐹",
333        ProjectType::Java => "☕",
334        ProjectType::Cpp => "⚙️",
335        ProjectType::Swift => "🐦",
336        ProjectType::DotNet => "🔷",
337        ProjectType::Ruby => "💎",
338        ProjectType::Elixir => "💧",
339        ProjectType::Deno => "🦕",
340        ProjectType::Php => "🐘",
341        ProjectType::Haskell => "λ",
342        ProjectType::Dart => "🎯",
343        ProjectType::Zig => "⚡",
344        ProjectType::Scala => "🔴",
345    }
346}