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}