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 dialoguer::{MultiSelect, theme::ColorfulTheme};
10use humansize::{DECIMAL, format_size};
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(|p| p.build_arts.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 (🦀 for Rust, 📦 for Node.js, 🐍 for Python, 🐹 for Go)
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 = match p.kind {
173 ProjectType::Rust => "🦀",
174 ProjectType::Node => "📦",
175 ProjectType::Python => "🐍",
176 ProjectType::Go => "🐹",
177 };
178 format!(
179 "{icon} {} ({})",
180 p.root_path.display(),
181 format_size(p.build_arts.size, DECIMAL)
182 )
183 })
184 .collect();
185
186 let defaults = vec![true; self.0.len()];
187
188 let selections = MultiSelect::with_theme(&ColorfulTheme::default())
189 .with_prompt("Select projects to clean:")
190 .items(&items)
191 .defaults(&defaults)
192 .interact()?;
193
194 Ok(selections.into_iter().map(|i| self.0[i].clone()).collect())
195 }
196
197 /// Get the number of projects in the collection.
198 ///
199 /// # Returns
200 ///
201 /// The number of projects contained in this collection.
202 ///
203 /// # Examples
204 ///
205 /// ```
206 /// # use crate::Projects;
207 /// println!("Found {} projects", projects.len());
208 /// ```
209 #[must_use]
210 pub fn len(&self) -> usize {
211 self.0.len()
212 }
213
214 /// Check if the collection is empty.
215 ///
216 /// # Returns
217 ///
218 /// `true` if the collection contains no projects, `false` otherwise.
219 ///
220 /// # Examples
221 ///
222 /// ```
223 /// # use crate::Projects;
224 /// if projects.is_empty() {
225 /// println!("No projects found");
226 /// }
227 /// ```
228 #[must_use]
229 pub fn is_empty(&self) -> bool {
230 self.0.is_empty()
231 }
232
233 /// Print a detailed summary of the projects and their reclaimable space.
234 ///
235 /// This method analyzes the collection and prints statistics including:
236 /// - Number and total size of Rust projects
237 /// - Number and total size of Node.js projects
238 /// - Number and total size of Python projects
239 /// - Number and total size of Go projects
240 /// - Total reclaimable space across all projects
241 ///
242 /// The output is formatted with colors and emoji icons for better readability.
243 ///
244 /// # Arguments
245 ///
246 /// * `total_size` - The total size in bytes (usually from `get_total_size()`)
247 ///
248 /// # Examples
249 ///
250 /// ```
251 /// # use crate::Projects;
252 /// let total_size = projects.get_total_size();
253 /// projects.print_summary(total_size);
254 /// ```
255 ///
256 /// # Output Format
257 ///
258 /// ```text
259 /// 🦀 5 Rust projects (2.3 GB)
260 /// 📦 3 Node.js projects (1.7 GB)
261 /// 🐍 2 Python projects (1.2 GB)
262 /// 🐹 1 Go project (0.5 GB)
263 /// 💾 Total reclaimable space: 4.0 GB
264 /// ```
265 pub fn print_summary(&self, total_size: u64) {
266 let mut rust_count = 0;
267 let mut node_count = 0;
268 let mut python_count = 0;
269 let mut go_count = 0;
270 let mut rust_size = 0u64;
271 let mut node_size = 0u64;
272 let mut python_size = 0u64;
273 let mut go_size = 0u64;
274
275 for project in &self.0 {
276 match project.kind {
277 ProjectType::Rust => {
278 rust_count += 1;
279 rust_size += project.build_arts.size;
280 }
281 ProjectType::Node => {
282 node_count += 1;
283 node_size += project.build_arts.size;
284 }
285 ProjectType::Python => {
286 python_count += 1;
287 python_size += project.build_arts.size;
288 }
289 ProjectType::Go => {
290 go_count += 1;
291 go_size += project.build_arts.size;
292 }
293 }
294 }
295
296 if rust_count > 0 {
297 println!(
298 " 🦀 {} Rust projects ({})",
299 rust_count.to_string().bright_white(),
300 format_size(rust_size, DECIMAL).bright_white()
301 );
302 }
303
304 if node_count > 0 {
305 println!(
306 " 📦 {} Node.js projects ({})",
307 node_count.to_string().bright_white(),
308 format_size(node_size, DECIMAL).bright_white()
309 );
310 }
311
312 if python_count > 0 {
313 println!(
314 " 🐍 {} Python projects ({})",
315 python_count.to_string().bright_white(),
316 format_size(python_size, DECIMAL).bright_white()
317 );
318 }
319
320 if go_count > 0 {
321 println!(
322 " 🐹 {} Go projects ({})",
323 go_count.to_string().bright_white(),
324 format_size(go_size, DECIMAL).bright_white()
325 );
326 }
327
328 println!(
329 " 💾 Total reclaimable space: {}",
330 format_size(total_size, DECIMAL).bright_green().bold()
331 );
332 }
333}