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}