envx_cli/
path.rs

1use std::{io::Write, path::Path};
2
3use crate::PathAction;
4use color_eyre::Result;
5use color_eyre::eyre::eyre;
6use envx_core::{EnvVarManager, PathManager};
7
8/// Handles PATH command operations including add, remove, clean, dedupe, check, list, and move.
9///
10/// # Arguments
11/// * `action` - The specific PATH action to perform, or None to list entries
12/// * `check` - Whether to check for invalid entries when listing
13/// * `var` - The environment variable name (typically "PATH")
14/// * `permanent` - Whether to make changes permanent to the system
15///
16/// # Errors
17/// Returns an error if:
18/// - The specified environment variable is not found
19/// - File system operations fail (creating directories, reading/writing)
20/// - Invalid input is provided for move operations
21/// - Environment variable operations fail
22///
23/// # Panics
24/// Panics if `action` is `None` but the function logic expects it to be `Some`.
25/// This should not happen in normal usage as the logic handles the `None` case before
26/// calling `expect()`.
27#[allow(clippy::too_many_lines)]
28pub fn handle_path_command(action: Option<PathAction>, check: bool, var: &str, permanent: bool) -> Result<()> {
29    let mut manager = EnvVarManager::new();
30    manager.load_all()?;
31
32    // Get the PATH variable
33    let path_var = manager.get(var).ok_or_else(|| eyre!("Variable '{}' not found", var))?;
34
35    let mut path_mgr = PathManager::new(&path_var.value);
36
37    // If no action specified, list PATH entries
38    if action.is_none() {
39        if check {
40            handle_path_check(&path_mgr, true);
41        }
42        handle_path_list(&path_mgr, false, false);
43    }
44
45    let command = action.expect("Action should be Some if we reach here");
46    match command {
47        PathAction::Add {
48            directory,
49            first,
50            create,
51        } => {
52            let path = Path::new(&directory);
53
54            // Check if directory exists
55            if !path.exists() {
56                if create {
57                    std::fs::create_dir_all(path)?;
58                    println!("Created directory: {directory}");
59                } else if !path.exists() {
60                    eprintln!("Warning: Directory does not exist: {directory}");
61                    print!("Add anyway? [y/N]: ");
62                    std::io::stdout().flush()?;
63
64                    let mut input = String::new();
65                    std::io::stdin().read_line(&mut input)?;
66
67                    if !input.trim().eq_ignore_ascii_case("y") {
68                        return Ok(());
69                    }
70                }
71            }
72
73            // Check if already in PATH
74            if path_mgr.contains(&directory) {
75                println!("Directory already in {var}: {directory}");
76                return Ok(());
77            }
78
79            // Add to PATH
80            if first {
81                path_mgr.add_first(directory.clone());
82                println!("Added to beginning of {var}: {directory}");
83            } else {
84                path_mgr.add_last(directory.clone());
85                println!("Added to end of {var}: {directory}");
86            }
87
88            // Save changes
89            let new_value = path_mgr.to_string();
90            manager.set(var, &new_value, permanent)?;
91        }
92
93        PathAction::Remove { directory, all } => {
94            let removed = if all {
95                path_mgr.remove_all(&directory)
96            } else {
97                path_mgr.remove_first(&directory)
98            };
99
100            if removed > 0 {
101                println!("Removed {removed} occurrence(s) of: {directory}");
102                let new_value = path_mgr.to_string();
103                manager.set(var, &new_value, permanent)?;
104            } else {
105                println!("Directory not found in {var}: {directory}");
106            }
107        }
108
109        PathAction::Clean { dedupe, dry_run } => {
110            let invalid = path_mgr.get_invalid();
111            let duplicates = if dedupe { path_mgr.get_duplicates() } else { vec![] };
112
113            if invalid.is_empty() && duplicates.is_empty() {
114                println!("No invalid or duplicate entries found in {var}");
115                return Ok(());
116            }
117
118            if !invalid.is_empty() {
119                println!("Invalid/non-existent paths to remove:");
120                for path in &invalid {
121                    println!("  - {path}");
122                }
123            }
124
125            if !duplicates.is_empty() {
126                println!("Duplicate paths to remove:");
127                for path in &duplicates {
128                    println!("  - {path}");
129                }
130            }
131
132            if dry_run {
133                println!("\n(Dry run - no changes made)");
134            } else {
135                let removed_invalid = path_mgr.remove_invalid();
136                let removed_dupes = if dedupe {
137                    path_mgr.deduplicate(false) // Keep last by default
138                } else {
139                    0
140                };
141
142                println!("Removed {removed_invalid} invalid and {removed_dupes} duplicate entries");
143                let new_value = path_mgr.to_string();
144                manager.set(var, &new_value, permanent)?;
145            }
146        }
147
148        PathAction::Dedupe { keep_first, dry_run } => {
149            let duplicates = path_mgr.get_duplicates();
150
151            if duplicates.is_empty() {
152                println!("No duplicate entries found in {var}");
153                return Ok(());
154            }
155
156            println!("Duplicate paths to remove:");
157            for path in &duplicates {
158                println!("  - {path}");
159            }
160            println!(
161                "Strategy: keep {} occurrence",
162                if keep_first { "first" } else { "last" }
163            );
164
165            if dry_run {
166                println!("\n(Dry run - no changes made)");
167            } else {
168                let removed = path_mgr.deduplicate(keep_first);
169                println!("Removed {removed} duplicate entries");
170                let new_value = path_mgr.to_string();
171                manager.set(var, &new_value, permanent)?;
172            }
173        }
174
175        PathAction::Check { verbose } => {
176            handle_path_check(&path_mgr, verbose);
177        }
178
179        PathAction::List { numbered, check } => {
180            handle_path_list(&path_mgr, numbered, check);
181        }
182
183        PathAction::Move { from, to } => {
184            // Parse from (can be index or path)
185            let from_idx = if let Ok(idx) = from.parse::<usize>() {
186                idx
187            } else {
188                path_mgr
189                    .find_index(&from)
190                    .ok_or_else(|| eyre!("Path not found: {}", from))?
191            };
192
193            // Parse to (can be "first", "last", or index)
194            let to_idx = match to.as_str() {
195                "first" => 0,
196                "last" => path_mgr.len() - 1,
197                _ => to.parse::<usize>().map_err(|_| eyre!("Invalid position: {}", to))?,
198            };
199
200            path_mgr.move_entry(from_idx, to_idx)?;
201            println!("Moved entry from position {from_idx} to {to_idx}");
202
203            let new_value = path_mgr.to_string();
204            manager.set(var, &new_value, permanent)?;
205        }
206    }
207
208    Ok(())
209}
210
211fn handle_path_check(path_mgr: &PathManager, verbose: bool) {
212    let entries = path_mgr.entries();
213    let mut issues = Vec::new();
214    let mut valid_count = 0;
215
216    for (idx, entry) in entries.iter().enumerate() {
217        let path = Path::new(entry);
218        let exists = path.exists();
219        let is_dir = path.is_dir();
220
221        if verbose || !exists {
222            let status = if !exists {
223                issues.push(format!("Not found: {entry}"));
224                "❌ NOT FOUND"
225            } else if !is_dir {
226                issues.push(format!("Not a directory: {entry}"));
227                "⚠️  NOT DIR"
228            } else {
229                valid_count += 1;
230                "✓ OK"
231            };
232
233            if verbose {
234                println!("[{idx:3}] {status} - {entry}");
235            }
236        } else if exists && is_dir {
237            valid_count += 1;
238        }
239    }
240
241    // Summary
242    println!("\nPATH Analysis:");
243    println!("  Total entries: {}", entries.len());
244    println!("  Valid entries: {valid_count}");
245
246    let duplicates = path_mgr.get_duplicates();
247    if !duplicates.is_empty() {
248        println!("  Duplicates: {} entries", duplicates.len());
249        if verbose {
250            for dup in &duplicates {
251                println!("    - {dup}");
252            }
253        }
254    }
255
256    let invalid = path_mgr.get_invalid();
257    if !invalid.is_empty() {
258        println!("  Invalid entries: {}", invalid.len());
259        if verbose {
260            for inv in &invalid {
261                println!("    - {inv}");
262            }
263        }
264    }
265
266    if issues.is_empty() {
267        println!("\n✅ No issues found!");
268    } else {
269        println!("\n⚠️  {} issue(s) found", issues.len());
270        if !verbose {
271            println!("Run with --verbose for details");
272        }
273    }
274}
275
276fn handle_path_list(path_mgr: &PathManager, numbered: bool, check: bool) {
277    let entries = path_mgr.entries();
278
279    if entries.is_empty() {
280        println!("PATH is empty");
281    }
282
283    for (idx, entry) in entries.iter().enumerate() {
284        let prefix = if numbered { format!("[{idx:3}] ") } else { String::new() };
285
286        let suffix = if check {
287            let path = Path::new(entry);
288            if !path.exists() {
289                " [NOT FOUND]"
290            } else if !path.is_dir() {
291                " [NOT A DIRECTORY]"
292            } else {
293                ""
294            }
295        } else {
296            ""
297        };
298
299        println!("{prefix}{entry}{suffix}");
300    }
301}