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