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}