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
8const COMMON_PATH_VARIABLES: &[&str] = &["PATH", "PYTHONPATH", "CLASSPATH", "LD_LIBRARY_PATH", "LIBRARY_PATH"];
9
10fn get_path_variable_error_message(var: &str, manager: &EnvVarManager) -> String {
11 let mut suggestions = Vec::new();
12
13 if cfg!(windows) {
15 for env_var in manager.list() {
17 if env_var.name.to_lowercase() == var.to_lowercase() && env_var.name != var {
18 suggestions.push(env_var.name.clone());
19 }
20 }
21 }
22
23 for &common_var in COMMON_PATH_VARIABLES {
25 if common_var.to_lowercase() == var.to_lowercase()
26 && common_var != var
27 && !suggestions.contains(&common_var.to_string())
28 {
29 suggestions.push(common_var.to_string());
30 }
31 }
32
33 let os_info = if cfg!(windows) {
34 "On Windows, PATH variable names are case-insensitive."
35 } else {
36 "On Unix/Linux systems, PATH variable names are case-sensitive."
37 };
38
39 if suggestions.is_empty() {
40 format!(
41 "Environment variable '{}' not found.\n\
42 {}\n\
43 This variable might not be set in your environment.\n\
44 Common path variables: {}",
45 var,
46 os_info,
47 COMMON_PATH_VARIABLES.join(", ")
48 )
49 } else {
50 format!(
51 "Environment variable '{}' not found.\n\
52 {}\n\
53 Did you mean: {}?\n\
54 Common path variables: {}",
55 var,
56 os_info,
57 suggestions.join(", "),
58 COMMON_PATH_VARIABLES.join(", ")
59 )
60 }
61}
62
63#[allow(clippy::too_many_lines)]
83pub fn handle_path_command(action: Option<PathAction>, check: bool, var: &str, permanent: bool) -> Result<()> {
84 let mut manager = EnvVarManager::new();
85 manager.load_all()?;
86
87 let path_var = manager
89 .get(var)
90 .ok_or_else(|| eyre!("{}", get_path_variable_error_message(var, &manager)))?;
91
92 let mut path_mgr = PathManager::new(&path_var.value);
93
94 if action.is_none() {
96 if check {
97 handle_path_check(&path_mgr, true);
98 }
99 handle_path_list(&path_mgr, false, false);
100 return Ok(());
101 }
102
103 let command = action.expect("We should not reach here if PathAction is None");
104 match command {
105 PathAction::Add {
106 directory,
107 first,
108 create,
109 } => {
110 let path = Path::new(&directory);
111
112 if !path.exists() {
114 if create {
115 std::fs::create_dir_all(path)?;
116 println!("Created directory: {directory}");
117 } else if !path.exists() {
118 eprintln!("Warning: Directory does not exist: {directory}");
119 print!("Add anyway? [y/N]: ");
120 std::io::stdout().flush()?;
121
122 let mut input = String::new();
123 std::io::stdin().read_line(&mut input)?;
124
125 if !input.trim().eq_ignore_ascii_case("y") {
126 return Ok(());
127 }
128 }
129 }
130
131 if path_mgr.contains(&directory) {
133 println!("Directory already in {var}: {directory}");
134 return Ok(());
135 }
136
137 if first {
139 path_mgr.add_first(directory.clone());
140 println!("Added to beginning of {var}: {directory}");
141 } else {
142 path_mgr.add_last(directory.clone());
143 println!("Added to end of {var}: {directory}");
144 }
145
146 let new_value = path_mgr.to_string();
148 manager.set(var, &new_value, permanent)?;
149 }
150
151 PathAction::Remove { directory, all } => {
152 let removed = if all {
153 path_mgr.remove_all(&directory)
154 } else {
155 path_mgr.remove_first(&directory)
156 };
157
158 if removed > 0 {
159 println!("Removed {removed} occurrence(s) of: {directory}");
160 let new_value = path_mgr.to_string();
161 manager.set(var, &new_value, permanent)?;
162 } else {
163 println!("Directory not found in {var}: {directory}");
164 }
165 }
166
167 PathAction::Clean { dedupe, dry_run } => {
168 let invalid = path_mgr.get_invalid();
169 let duplicates = if dedupe { path_mgr.get_duplicates() } else { vec![] };
170
171 if invalid.is_empty() && duplicates.is_empty() {
172 println!("No invalid or duplicate entries found in {var}");
173 return Ok(());
174 }
175
176 if !invalid.is_empty() {
177 println!("Invalid/non-existent paths to remove:");
178 for path in &invalid {
179 println!(" - {path}");
180 }
181 }
182
183 if !duplicates.is_empty() {
184 println!("Duplicate paths to remove:");
185 for path in &duplicates {
186 println!(" - {path}");
187 }
188 }
189
190 if dry_run {
191 println!("\n(Dry run - no changes made)");
192 } else {
193 let removed_invalid = path_mgr.remove_invalid();
194 let removed_dupes = if dedupe {
195 path_mgr.deduplicate(false) } else {
197 0
198 };
199
200 println!("Removed {removed_invalid} invalid and {removed_dupes} duplicate entries");
201 let new_value = path_mgr.to_string();
202 manager.set(var, &new_value, permanent)?;
203 }
204 }
205
206 PathAction::Dedupe { keep_first, dry_run } => {
207 let duplicates = path_mgr.get_duplicates();
208
209 if duplicates.is_empty() {
210 println!("No duplicate entries found in {var}");
211 return Ok(());
212 }
213
214 println!("Duplicate paths to remove:");
215 for path in &duplicates {
216 println!(" - {path}");
217 }
218 println!(
219 "Strategy: keep {} occurrence",
220 if keep_first { "first" } else { "last" }
221 );
222
223 if dry_run {
224 println!("\n(Dry run - no changes made)");
225 } else {
226 let removed = path_mgr.deduplicate(keep_first);
227 println!("Removed {removed} duplicate entries");
228 let new_value = path_mgr.to_string();
229 manager.set(var, &new_value, permanent)?;
230 }
231 }
232
233 PathAction::Check { verbose } => {
234 handle_path_check(&path_mgr, verbose);
235 }
236
237 PathAction::List { numbered, check } => {
238 handle_path_list(&path_mgr, numbered, check);
239 }
240
241 PathAction::Move { from, to } => {
242 let from_idx = if let Ok(idx) = from.parse::<usize>() {
244 idx
245 } else {
246 path_mgr
247 .find_index(&from)
248 .ok_or_else(|| eyre!("Path not found: {}", from))?
249 };
250
251 let to_idx = match to.as_str() {
253 "first" => 0,
254 "last" => path_mgr.len() - 1,
255 _ => to.parse::<usize>().map_err(|_| eyre!("Invalid position: {}", to))?,
256 };
257
258 path_mgr.move_entry(from_idx, to_idx)?;
259 println!("Moved entry from position {from_idx} to {to_idx}");
260
261 let new_value = path_mgr.to_string();
262 manager.set(var, &new_value, permanent)?;
263 }
264 }
265
266 Ok(())
267}
268
269fn handle_path_check(path_mgr: &PathManager, verbose: bool) {
270 let entries = path_mgr.entries();
271 let mut issues = Vec::new();
272 let mut valid_count = 0;
273
274 for (idx, entry) in entries.iter().enumerate() {
275 let path = Path::new(entry);
276 let exists = path.exists();
277 let is_dir = path.is_dir();
278
279 if verbose || !exists {
280 let status = if !exists {
281 issues.push(format!("Not found: {entry}"));
282 "❌ NOT FOUND"
283 } else if !is_dir {
284 issues.push(format!("Not a directory: {entry}"));
285 "⚠️ NOT DIR"
286 } else {
287 valid_count += 1;
288 "✓ OK"
289 };
290
291 if verbose {
292 println!("[{idx:3}] {status} - {entry}");
293 }
294 } else if exists && is_dir {
295 valid_count += 1;
296 }
297 }
298
299 println!("\nPATH Analysis:");
301 println!(" Total entries: {}", entries.len());
302 println!(" Valid entries: {valid_count}");
303
304 let duplicates = path_mgr.get_duplicates();
305 if !duplicates.is_empty() {
306 println!(" Duplicates: {} entries", duplicates.len());
307 if verbose {
308 for dup in &duplicates {
309 println!(" - {dup}");
310 }
311 }
312 }
313
314 let invalid = path_mgr.get_invalid();
315 if !invalid.is_empty() {
316 println!(" Invalid entries: {}", invalid.len());
317 if verbose {
318 for inv in &invalid {
319 println!(" - {inv}");
320 }
321 }
322 }
323
324 if issues.is_empty() {
325 println!("\n✅ No issues found!");
326 } else {
327 println!("\n⚠️ {} issue(s) found", issues.len());
328 if !verbose {
329 println!("Run with --verbose for details");
330 }
331 }
332}
333
334fn handle_path_list(path_mgr: &PathManager, numbered: bool, check: bool) {
335 let entries = path_mgr.entries();
336
337 if entries.is_empty() {
338 println!("PATH is empty");
339 }
340
341 for (idx, entry) in entries.iter().enumerate() {
342 let prefix = if numbered { format!("[{idx:3}] ") } else { String::new() };
343
344 let suffix = if check {
345 let path = Path::new(entry);
346 if !path.exists() {
347 " [NOT FOUND]"
348 } else if !path.is_dir() {
349 " [NOT A DIRECTORY]"
350 } else {
351 ""
352 }
353 } else {
354 ""
355 };
356
357 println!("{prefix}{entry}{suffix}");
358 }
359}