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