cargo_e/
e_command_builder.rs

1use regex::Regex;
2use std::collections::{HashMap, HashSet};
3use std::env;
4use std::io::Read;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::mpsc::{channel, Sender};
9use std::time::SystemTime;
10use which::which;
11
12use crate::e_cargocommand_ext::CargoProcessResult;
13use crate::e_cargocommand_ext::{CargoCommandExt, CargoDiagnostic, CargoProcessHandle};
14use crate::e_eventdispatcher::{
15    CallbackResponse, CallbackType, CargoDiagnosticLevel, EventDispatcher, ThreadLocalContext,
16};
17use crate::e_runner::GLOBAL_CHILDREN;
18use crate::e_target::{CargoTarget, TargetKind, TargetOrigin};
19use std::sync::{Arc, Mutex};
20
21#[derive(Debug, Clone, PartialEq, Copy)]
22pub enum TerminalError {
23    NotConnected,
24    NoTerminal,
25    NoError,
26}
27
28impl Default for TerminalError {
29    fn default() -> Self {
30        TerminalError::NoError
31    }
32}
33
34/// A builder that constructs a Cargo command for a given target.
35#[derive(Clone, Debug)]
36pub struct CargoCommandBuilder {
37    pub target_name: String,
38    pub manifest_path: PathBuf,
39    pub args: Vec<String>,
40    pub subcommand: String,
41    pub pid: Option<u32>,
42    pub alternate_cmd: Option<String>,
43    pub execution_dir: Option<PathBuf>,
44    pub suppressed_flags: HashSet<String>,
45    pub stdout_dispatcher: Option<Arc<EventDispatcher>>,
46    pub stderr_dispatcher: Option<Arc<EventDispatcher>>,
47    pub progress_dispatcher: Option<Arc<EventDispatcher>>,
48    pub stage_dispatcher: Option<Arc<EventDispatcher>>,
49    pub terminal_error_flag: Arc<Mutex<bool>>,
50    pub sender: Option<Arc<Mutex<Sender<TerminalError>>>>,
51    pub diagnostics: Arc<Mutex<Vec<CargoDiagnostic>>>,
52    pub is_filter: bool,
53    pub use_cache: bool,
54}
55
56impl std::fmt::Display for CargoCommandBuilder {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(
59            f,
60            "CargoCommandBuilder {{\n  target_name: {:?},\n  manifest_path: {:?},\n  args: {:?},\n  subcommand: {:?},\n  pid: {:?},\n  alternate_cmd: {:?},\n  execution_dir: {:?},\n  suppressed_flags: {:?},\n  is_filter: {:?}\n,\n  use_cache: {:?}\n}}",
61            self.target_name,
62            self.manifest_path,
63            self.args,
64            self.subcommand,
65            self.pid,
66            self.alternate_cmd,
67            self.execution_dir,
68            self.suppressed_flags,
69            self.is_filter,
70            self.use_cache,
71        )
72    }
73}
74impl Default for CargoCommandBuilder {
75    fn default() -> Self {
76        Self::new(
77            &String::new(),
78            &PathBuf::from("Cargo.toml"),
79            "run".into(),
80            false,
81            false,
82        )
83    }
84}
85impl CargoCommandBuilder {
86    /// Creates a new, empty builder.
87    pub fn new(target_name: &str, manifest: &PathBuf, subcommand: &str, is_filter: bool, use_cache: bool) -> Self {
88        ThreadLocalContext::set_context(target_name, manifest.to_str().unwrap_or_default());
89        let (sender, _receiver) = channel::<TerminalError>();
90        let sender = Arc::new(Mutex::new(sender));
91        let mut builder = CargoCommandBuilder {
92            target_name: target_name.to_owned(),
93            manifest_path: manifest.clone(),
94            args: Vec::new(),
95            subcommand: subcommand.to_string(),
96            pid: None,
97            alternate_cmd: None,
98            execution_dir: None,
99            suppressed_flags: HashSet::new(),
100            stdout_dispatcher: None,
101            stderr_dispatcher: None,
102            progress_dispatcher: None,
103            stage_dispatcher: None,
104            terminal_error_flag: Arc::new(Mutex::new(false)),
105            sender: Some(sender),
106            diagnostics: Arc::new(Mutex::new(Vec::<CargoDiagnostic>::new())),
107            is_filter,
108            use_cache,
109        };
110        builder.set_default_dispatchers();
111        builder
112    }
113
114    // Switch to passthrough mode when the terminal error is detected
115    fn switch_to_passthrough_mode<F>(self: Arc<Self>, on_spawn: F) -> anyhow::Result<u32>
116    where
117        F: FnOnce(u32, CargoProcessHandle),
118    {
119        let mut command = self.build_command();
120
121        // Now, spawn the cargo process in passthrough mode
122        let cargo_process_handle = command.spawn_cargo_passthrough(Arc::clone(&self));
123        let pid = cargo_process_handle.pid;
124        // Notify observer
125        on_spawn(pid, cargo_process_handle);
126
127        Ok(pid)
128    }
129
130    // Set up the default dispatchers, which includes error detection
131    fn set_default_dispatchers(&mut self) {
132        if !self.is_filter {
133            // If this is a filter, we don't need to set up dispatchers
134            return;
135        }
136        let sender = self.sender.clone().unwrap();
137
138        let mut stdout_dispatcher = EventDispatcher::new();
139        stdout_dispatcher.add_callback(
140            r"listening on",
141            Box::new(|line, _captures, _state, stats, _prior_response| {
142                println!("(STDOUT) Dispatcher caught: {}", line);
143                // Use a regex to capture a URL from the line.
144                if let Ok(url_regex) = Regex::new(r"(http://[^\s]+)") {
145                    if let Some(url_caps) = url_regex.captures(line) {
146                        if let Some(url_match) = url_caps.get(1) {
147                            let url = url_match.as_str();
148                            // Call open::that on the captured URL.
149                            if let Err(e) = open::that_detached(url) {
150                                eprintln!("Failed to open URL: {}. Error: {}", url, e);
151                            } else {
152                                println!("Opened URL: {}", url);
153                            }
154                        }
155                    }
156                } else {
157                    eprintln!("Failed to create URL regex");
158                }
159                let mut stats = stats.lock().unwrap();
160                if stats.build_finished_time.is_none() {
161                    let now = SystemTime::now();
162                    stats.build_finished_time = Some(now);
163                }
164                None
165            }),
166        );
167
168        stdout_dispatcher.add_callback(
169            r"BuildFinished",
170            Box::new(|line, _captures, _state, stats, _prior_response| {
171                println!("******* {}", line);
172                let mut stats = stats.lock().unwrap();
173                if stats.build_finished_time.is_none() {
174                    let now = SystemTime::now();
175                    stats.build_finished_time = Some(now);
176                }
177                None
178            }),
179        );
180        stdout_dispatcher.add_callback(
181            r"server listening at:",
182            Box::new(|line, _captures, state, stats, _prior_response| {
183                // If we're not already in multiline mode, this is the initial match.
184                if !state.load(Ordering::Relaxed) {
185                    println!("Matched 'server listening at:' in: {}", line);
186                    state.store(true, Ordering::Relaxed);
187                    Some(CallbackResponse {
188                        callback_type: CallbackType::Note, // Choose as appropriate
189                        message: Some(format!("Started multiline mode after: {}", line)),
190                        file: None,
191                        line: None,
192                        column: None,
193                        suggestion: None,
194                        terminal_status: None,
195                    })
196                } else {
197                    // We are in multiline mode; process subsequent lines.
198                    println!("Multiline callback received: {}", line);
199                    // Use a regex to capture a URL from the line.
200                    let url_regex = match Regex::new(r"(http://[^\s]+)") {
201                        Ok(regex) => regex,
202                        Err(e) => {
203                            eprintln!("Failed to create URL regex: {}", e);
204                            return None;
205                        }
206                    };
207                    if let Some(url_caps) = url_regex.captures(line) {
208                        let url = url_caps.get(1).unwrap().as_str();
209                        // Call open::that on the captured URL.
210                        match open::that_detached(url) {
211                            Ok(_) => println!("Opened URL: {}", url),
212                            Err(e) => eprintln!("Failed to open URL: {}. Error: {}", url, e),
213                        }
214                        let mut stats = stats.lock().unwrap();
215                        if stats.build_finished_time.is_none() {
216                            let now = SystemTime::now();
217                            stats.build_finished_time = Some(now);
218                        }
219                        // End multiline mode.
220                        state.store(false, Ordering::Relaxed);
221                        Some(CallbackResponse {
222                            callback_type: CallbackType::Note, // Choose as appropriate
223                            message: Some(format!("Captured and opened URL: {}", url)),
224                            file: None,
225                            line: None,
226                            column: None,
227                            suggestion: None,
228                            terminal_status: None,
229                        })
230                    } else {
231                        None
232                    }
233                }
234            }),
235        );
236
237        let mut stderr_dispatcher = EventDispatcher::new();
238
239        let suggestion_mode = Arc::new(AtomicBool::new(false));
240        let suggestion_regex = Regex::new(r"^\s*(\d+)\s*\|\s*(.*)$").unwrap();
241        let warning_location: Arc<Mutex<Option<CallbackResponse>>> = Arc::new(Mutex::new(None));
242        let pending_diag: Arc<Mutex<Option<CargoDiagnostic>>> = Arc::new(Mutex::new(None));
243        let diagnostic_counts: Arc<Mutex<HashMap<CargoDiagnosticLevel, usize>>> =
244            Arc::new(Mutex::new(HashMap::new()));
245
246        let pending_d = Arc::clone(&pending_diag);
247        let counts = Arc::clone(&diagnostic_counts);
248
249        let diagnostics_arc = Arc::clone(&self.diagnostics);
250        // Callback for Rust panic messages (e.g., "thread 'main' panicked at ...")
251        stderr_dispatcher.add_callback(
252            r"^thread '([^']+)' panicked at (.+):(\d+):(\d+):$",
253            Box::new(|line, captures, multiline_flag, stats, prior_response| {
254                multiline_flag.store(false, Ordering::Relaxed);
255
256                if let Some(caps) = captures {
257                    multiline_flag.store(true, Ordering::Relaxed); // the next line is the panic message
258                    let thread = caps.get(1).map(|m| m.as_str()).unwrap_or("unknown");
259                    let message = caps.get(2).map(|m| m.as_str()).unwrap_or("unknown panic");
260                    let file = caps.get(3).map(|m| m.as_str()).unwrap_or("unknown file");
261                    let line_num = caps
262                        .get(4)
263                        .map(|m| m.as_str())
264                        .unwrap_or("0")
265                        .parse()
266                        .unwrap_or(0);
267                    let col_num = caps
268                        .get(5)
269                        .map(|m| m.as_str())
270                        .unwrap_or("0")
271                        .parse()
272                        .unwrap_or(0);
273                    println!("\n\n\n");
274                    println!("{}", line);
275                    // Use a global TTS instance via OnceCell for program lifetime
276
277                    #[cfg(feature = "uses_tts")]
278                    {
279                        let mut say_something = true;
280                        if let Some(cli) = crate::GLOBAL_CLI.get() {
281                            if cli.no_tts {
282                                say_something = false;
283                            }
284                        }
285                        if say_something {
286                            let tts_mutex = crate::GLOBAL_TTS.get_or_init(|| {
287                                std::sync::Mutex::new(
288                                    tts::Tts::default().expect("TTS engine failure"),
289                                )
290                            });
291                            // Extract the filename without extension
292                            let filename = Path::new(message)
293                                .file_stem()
294                                .and_then(|s| s.to_str())
295                                .unwrap_or("unknown file");
296                            let speech =
297                                format!("thread {} panic, {} line {}", thread, filename, line_num);
298                            println!("TTS: {}", speech);
299                            crate::e_runner::wait_for_tts_to_finish(15000);
300                            let mut tts = tts_mutex.lock().expect("Failed to lock TTS mutex");
301                            let _ = tts.speak(&speech, false);
302                            drop(tts);
303                        }
304                    }
305
306                    println!(
307                        "Panic detected: thread='{}', message='{}', file='{}:{}:{}'",
308                        thread, message, file, line_num, col_num
309                    );
310                    println!("\n\n\n");
311                    Some(CallbackResponse {
312                        callback_type: CallbackType::Error,
313                        message: Some(format!(
314                            "thread '{}' panicked at {} ({}:{}:{})",
315                            thread, message, file, line_num, col_num
316                        )),
317                        file: Some(message.to_string()),
318                        line: Some(file.parse::<usize>().unwrap_or(0)),
319                        column: Some(line_num),
320                        suggestion: None,
321                        terminal_status: None,
322                    })
323                } else {
324                    let context = ThreadLocalContext::get_context();
325                    let mut show_window = true;
326                    let mut say_something = true;
327                    if let Some(cli) = crate::GLOBAL_CLI.get() {
328                        if cli.no_window {
329                            show_window = false;
330                        }
331                        if cli.no_tts {
332                            say_something = false;
333                        }
334                    }
335                    if show_window {
336                        show_graphical_panic(
337                            line,
338                            prior_response,
339                            &PathBuf::from(&context.manifest_path),
340                        );
341                    }
342                    #[cfg(feature = "uses_tts")]
343                    {
344                        if say_something {
345                            let tts_mutex = crate::GLOBAL_TTS.get_or_init(|| {
346                                std::sync::Mutex::new(
347                                    tts::Tts::default().expect("TTS engine failure"),
348                                )
349                            });
350
351                            let speech = format!("panic says {}", line);
352                            println!("TTS: {}", speech);
353                            crate::e_runner::wait_for_tts_to_finish(15000);
354                            let mut tts = tts_mutex.lock().expect("Failed to lock TTS mutex");
355                            let _ = tts.speak(&speech, true);
356                        }
357                    }
358
359                    None
360                }
361            }),
362        );
363
364        // Add a callback to detect "could not compile" errors
365        stderr_dispatcher.add_callback(
366            r"error: could not compile `(?P<crate_name>.+)` \((?P<due_to>.+)\) due to (?P<error_count>\d+) previous errors; (?P<warning_count>\d+) warnings emitted",
367            Box::new(|line, captures, _state, stats, _prior_response| {
368                println!("{}", line);
369            if let Some(caps) = captures {
370                // Extract dynamic fields from the error message
371                let crate_name = caps.name("crate_name").map(|m| m.as_str()).unwrap_or("unknown");
372                let due_to = caps.name("due_to").map(|m| m.as_str()).unwrap_or("unknown");
373                let error_count: usize = caps
374                .name("error_count")
375                .map(|m| m.as_str().parse().unwrap_or(0))
376                .unwrap_or(0);
377                let warning_count: usize = caps
378                .name("warning_count")
379                .map(|m| m.as_str().parse().unwrap_or(0))
380                .unwrap_or(0);
381
382                // Log the captured information (optional)
383                println!(
384                "Detected compilation failure: crate=`{}`, due_to=`{}`, errors={}, warnings={}",
385                crate_name, due_to, error_count, warning_count
386                );
387
388                // Set `is_could_not_compile` to true in the stats
389                let mut stats = stats.lock().unwrap();
390                stats.is_could_not_compile = true;
391            }
392            None // No callback response needed
393            }),
394        );
395
396        // Clone diagnostics_arc for this closure to avoid move
397        let diagnostics_arc_for_diag = Arc::clone(&diagnostics_arc);
398        stderr_dispatcher.add_callback(
399            r"^(?P<level>\w+)(\[(?P<error_code>E\d+)\])?:\s+(?P<msg>.+)$", // Regex for diagnostic line
400            Box::new(
401                move |_line, caps, _multiline_flag, _stats, _prior_response| {
402                    if let Some(caps) = caps {
403                        let mut counts = counts.lock().unwrap();
404                        // Create a PendingDiag and save the message
405                        let mut pending_diag = pending_d.lock().unwrap();
406                        let mut last_lineref = String::new();
407                        if let Some(existing_diag) = pending_diag.take() {
408                            let mut diags = diagnostics_arc_for_diag.lock().unwrap();
409                            last_lineref = existing_diag.lineref.clone();
410                            diags.push(existing_diag.clone());
411                        }
412                        log::trace!("Diagnostic line: {}", _line);
413                        let level = caps["level"].to_string(); // e.g., "warning", "error"
414                        let message = caps["msg"].to_string();
415                        // If the message contains "generated" followed by one or more digits,
416                        // then ignore this diagnostic by returning None.
417                        let re_generated = regex::Regex::new(r"generated\s+\d+").unwrap();
418                        if re_generated.is_match(&message) {
419                            log::trace!("Skipping generated diagnostic: {}", _line);
420                            return None;
421                        }
422
423                        let error_code = caps.name("error_code").map(|m| m.as_str().to_string());
424                        let diag_level = match level.as_str() {
425                            "error" => CargoDiagnosticLevel::Error,
426                            "warning" => CargoDiagnosticLevel::Warning,
427                            "help" => CargoDiagnosticLevel::Help,
428                            "note" => CargoDiagnosticLevel::Note,
429                            _ => {
430                                println!("Unknown diagnostic level: {}", level);
431                                return None; // Ignore unknown levels
432                            }
433                        };
434                        // Increment the count for this level
435                        *counts.entry(diag_level).or_insert(0) += 1;
436
437                        let current_count = counts.get(&diag_level).unwrap_or(&0);
438                        let diag = CargoDiagnostic {
439                            error_code: error_code.clone(),
440                            lineref: last_lineref.clone(),
441                            level: level.clone(),
442                            message,
443                            suggestion: None,
444                            help: None,
445                            note: None,
446                            uses_color: true,
447                            diag_num_padding: Some(2),
448                            diag_number: Some(*current_count),
449                        };
450
451                        // Save the new diagnostic
452                        *pending_diag = Some(diag);
453
454                        // Track the count of diagnostics for each level
455                        return Some(CallbackResponse {
456                            callback_type: CallbackType::LevelMessage, // Treat subsequent lines as warnings
457                            message: None,
458                            file: None,
459                            line: None,
460                            column: None,
461                            suggestion: None, // This is the suggestion part
462                            terminal_status: None,
463                        });
464                    } else {
465                        println!("No captures found in line: {}", _line);
466                        None
467                    }
468                },
469            ),
470        );
471        // Look-behind buffer for last 6 lines before backtrace
472        let look_behind = Arc::new(Mutex::new(Vec::<String>::new()));
473        {
474            let look_behind = Arc::clone(&look_behind);
475            // This callback runs for every stderr line to update the look-behind buffer
476            stderr_dispatcher.add_callback(
477                r"^(?P<msg>.*)$",
478                Box::new(move |line, _captures, _state, _stats, _prior_response| {
479                    let mut buf = look_behind.lock().unwrap();
480                    if line.trim().is_empty() {
481                        return None;
482                    }
483                    buf.push(line.to_string());
484                    if buf.len() > 6 {
485                        buf.remove(0);
486                    }
487                    None
488                }),
489            );
490        }
491
492        // --- Patch: Use look_behind before backtrace_lines in the note ---
493        {
494            let pending_diag = Arc::clone(&pending_diag);
495            let diagnostics_arc = Arc::clone(&diagnostics_arc);
496            let backtrace_mode = Arc::new(AtomicBool::new(false));
497            let backtrace_lines = Arc::new(Mutex::new(Vec::<String>::new()));
498            let look_behind = Arc::clone(&look_behind);
499            let stored_lines_behind = Arc::new(Mutex::new(Vec::<String>::new()));
500
501            // Enable backtrace mode when we see "stack backtrace:"
502            {
503                let backtrace_mode = Arc::clone(&backtrace_mode);
504                let backtrace_lines = Arc::clone(&backtrace_lines);
505                let stored_lines_behind = Arc::clone(&stored_lines_behind);
506                let look_behind = Arc::clone(&look_behind);
507                stderr_dispatcher.add_callback(
508                    r"stack backtrace:",
509                    Box::new(move |_line, _captures, _state, _stats, _prior_response| {
510                        backtrace_mode.store(true, Ordering::Relaxed);
511                        backtrace_lines.lock().unwrap().clear();
512                        // Save the current look_behind buffer into a shared stored_lines_behind for later use
513                        {
514                            let look_behind_buf = look_behind.lock().unwrap();
515                            let mut stored = stored_lines_behind.lock().unwrap();
516                            *stored = look_behind_buf.clone();
517                        }
518                        None
519                    }),
520                );
521            }
522
523            // Process backtrace lines, filter and summarize
524            {
525                let backtrace_mode = Arc::clone(&backtrace_mode);
526                let backtrace_lines = Arc::clone(&backtrace_lines);
527                let pending_diag = Arc::clone(&pending_diag);
528                let diagnostics_arc = Arc::clone(&diagnostics_arc);
529                let look_behind = Arc::clone(&look_behind);
530
531                // Regex for numbered backtrace line: "  0: type::path"
532                let re_number_type = Regex::new(r"^\s*(\d+):\s+(.*)$").unwrap();
533                // Regex for "at path:line"
534                let re_at_path = Regex::new(r"^\s*at\s+([^\s:]+):(\d+)").unwrap();
535
536                stderr_dispatcher.add_callback(
537                    r"^(?P<msg>.*)$",
538                    Box::new(
539                        move |mut line, _captures, _state, _stats, _prior_response| {
540                            if backtrace_mode.load(Ordering::Relaxed) {
541                                line = line.trim();
542                                // End of backtrace if empty line or new diagnostic/note
543                                if line.trim().is_empty()
544                                    || line.starts_with("note:")
545                                    || line.starts_with("error:")
546                                {
547                                    let mut bt_lines = Vec::new();
548                                    let mut skip_next = false;
549                                    let mut last_number_type: Option<(String, String)> = None;
550                                    for l in backtrace_lines.lock().unwrap().iter() {
551                                        if let Some(caps) = re_number_type.captures(l) {
552                                            // Save the (number, type) line, but don't push yet
553                                            last_number_type =
554                                                Some((caps[1].to_string(), caps[2].to_string()));
555                                            skip_next = true;
556                                        } else if skip_next && re_at_path.is_match(l) {
557                                            let path_caps = re_at_path.captures(l).unwrap();
558                                            let path = path_caps.get(1).unwrap().as_str();
559                                            let line_num = path_caps.get(2).unwrap().as_str();
560                                            if path.starts_with("/rustc")
561                                                || path.contains(".cargo")
562                                                || path.contains(".rustup")
563                                            {
564                                                // Skip both the number: type and the at line
565                                                // (do not push either)
566                                            } else {
567                                                // Push both the number: type and the at line, on the same line
568                                                if let Some((num, typ)) = last_number_type.take() {
569                                                    // Canonicalize the path if possible for better readability
570                                                    let path = match std::fs::canonicalize(path) {
571                                                        Ok(canon) => canon.display().to_string(),
572                                                        Err(_) => path.to_string(),
573                                                    };
574
575                                                    bt_lines.push(format!(
576                                                        "{}: {} @ {}:{}",
577                                                        num, typ, path, line_num
578                                                    ));
579                                                }
580                                            }
581                                            skip_next = false;
582                                        } else if let Some((num, typ)) = last_number_type.take() {
583                                            // If the previous number: type was not followed by an at line, push it
584                                            bt_lines.push(format!("{}: {}", num, typ));
585                                            if !l.trim().is_empty() {
586                                                bt_lines.push(l.clone());
587                                            }
588                                            skip_next = false;
589                                        } else if !l.trim().is_empty() {
590                                            bt_lines.push(l.clone());
591                                            skip_next = false;
592                                        }
593                                    }
594                                    if !bt_lines.is_empty() {
595                                        let mut pending_diag = pending_diag.lock().unwrap();
596                                        if let Some(ref mut diag) = *pending_diag {
597                                            // --- Insert stored_lines_behind lines before backtrace_lines ---
598                                            let stored_lines = {
599                                                let buf = stored_lines_behind.lock().unwrap();
600                                                buf.clone()
601                                            };
602                                            let note = diag.note.get_or_insert_with(String::new);
603                                            if !stored_lines.is_empty() {
604                                                note.push_str(&stored_lines.join("\n"));
605                                                note.push('\n');
606                                            }
607                                            note.push_str(&bt_lines.join("\n"));
608                                            let mut diags = diagnostics_arc.lock().unwrap();
609                                            diags.push(diag.clone());
610                                        }
611                                    }
612                                    backtrace_mode.store(false, Ordering::Relaxed);
613                                    backtrace_lines.lock().unwrap().clear();
614                                    return None;
615                                }
616
617                                // Only keep lines that are part of the backtrace
618                                if re_number_type.is_match(line) || re_at_path.is_match(line) {
619                                    backtrace_lines.lock().unwrap().push(line.to_string());
620                                }
621                                // Ignore further lines
622                                return None;
623                            }
624                            None
625                        },
626                    ),
627                );
628            }
629        }
630
631        // suggestion callback
632        {
633            let location_lock_clone = Arc::clone(&warning_location);
634            let suggestion_m = Arc::clone(&suggestion_mode);
635
636            // Suggestion callback that adds subsequent lines as suggestions
637            stderr_dispatcher.add_callback(
638                r"^(?P<msg>.*)$", // Capture all lines following the location
639                Box::new(
640                    move |line, _captures, _multiline_flag, _stats, _prior_response| {
641                        if suggestion_m.load(Ordering::Relaxed) {
642                            // Only process lines that match the suggestion format
643                            if let Some(caps) = suggestion_regex.captures(line.trim()) {
644                                // Capture the line number and code from the suggestion line
645                                // let line_num = caps[1].parse::<usize>().unwrap_or(0);
646                                let code = caps[2].to_string();
647
648                                // Lock the pending_diag to add the suggestion
649                                if let Ok(mut lock) = location_lock_clone.lock() {
650                                    if let Some(mut loc) = lock.take() {
651                                        // let file = loc.file.clone().unwrap_or_default();
652                                        // let col = loc.column.unwrap_or(0);
653
654                                        // Concatenate the suggestion line to the message
655                                        let mut msg = loc.message.unwrap_or_default();
656                                        msg.push_str(&format!("\n{}", code));
657
658                                        // Print the concatenated suggestion for debugging
659                                        // println!("daveSuggestion for {}:{}:{} - {}", file, line_num, col, msg);
660
661                                        // Update the location with the new concatenated message
662                                        loc.message = Some(msg.clone());
663                                        // println!("Updating location lock with new suggestion: {}", msg);
664                                        // Save the updated location back to shared state
665                                        // if let Ok(mut lock) = location_lock_clone.lock() {
666                                        // println!("Updating location lock with new suggestion: {}", msg);
667                                        lock.replace(loc);
668                                        // } else {
669                                        //     eprintln!("Failed to acquire lock for location_lock_clone");
670                                        // }
671                                    }
672                                    // return Some(CallbackResponse {
673                                    //     callback_type: CallbackType::Warning, // Treat subsequent lines as warnings
674                                    //     message: Some(msg.clone()),
675                                    //     file: Some(file),
676                                    //     line: Some(line_num),
677                                    //     column: Some(col),
678                                    //     suggestion: Some(msg),  // This is the suggestion part
679                                    //     terminal_status: None,
680                                    // });
681                                }
682                            }
683                        } else {
684                            // println!("Suggestion mode is not active. Ignoring line: {}", line);
685                        }
686
687                        None
688                    },
689                ),
690            );
691        }
692        {
693            let suggestion_m = Arc::clone(&suggestion_mode);
694            let pending_diag_clone = Arc::clone(&pending_diag);
695            let diagnostics_arc = Arc::clone(&self.diagnostics);
696            // Callback for handling when an empty line or new diagnostic is received
697            stderr_dispatcher.add_callback(
698                r"^\s*$", // Regex to capture empty line
699                Box::new(
700                    move |_line, _captures, _multiline_flag, _stats, _prior_response| {
701                        // println!("Empty line detected: {}", line);
702                        suggestion_m.store(false, Ordering::Relaxed);
703                        // End of current diagnostic: take and process it.
704                        if let Some(pending_diag) = pending_diag_clone.lock().unwrap().take() {
705                            //println!("{:?}", pending_diag);
706                            // Use diagnostics_arc instead of self.diagnostices
707                            let mut diags = diagnostics_arc.lock().unwrap();
708                            diags.push(pending_diag.clone());
709                        } else {
710                            // println!("No pending diagnostic to process.");
711                        }
712                        // Handle empty line scenario to end the current diagnostic processing
713                        // if let Some(pending_diag) = pending_diag_clone.lock().unwrap().take() {
714                        //     println!("{:?}", pending_diag);
715                        //     let mut diags = self.diagnostics.lock().unwrap();
716                        //     diags.push(pending_diag.clone());
717                        //                             // let diag = crate::e_eventdispatcher::convert_message_to_diagnostic(msg, &msg_str);
718                        //                             // diags.push(diag.clone());
719                        //                             // if let Some(ref sd) = stage_disp_clone {
720                        //                             //     sd.dispatch(&format!("Stage: Diagnostic occurred at {:?}", now));
721                        //                             // }
722                        //     // Handle the saved PendingDiag and its CallbackResponse
723                        //     // if let Some(callback_response) = pending_diag.callback_response {
724                        //     //     println!("End of Diagnostic: {:?}", callback_response);
725                        //     // }
726                        // } else {
727                        //     println!("No pending diagnostic to process.");
728                        // }
729
730                        None
731                    },
732                ),
733            );
734        }
735
736        // {
737        //     let pending_diag = Arc::clone(&pending_diag);
738        //     let location_lock = Arc::clone(&warning_location);
739        //     let suggestion_m = Arc::clone(&suggestion_mode);
740
741        // let suggestion_regex = Regex::new(r"^\s*(\d+)\s*\|\s*(.*)$").unwrap();
742
743        //     stderr_dispatcher.add_callback(
744        //     r"^\s*(\d+)\s*\|\s*(.*)$",  // Match suggestion line format
745        //     Box::new(move |line, _captures, _multiline_flag| {
746        //         if suggestion_m.load(Ordering::Relaxed) {
747        //             // Only process lines that match the suggestion format
748        //             if let Some(caps) = suggestion_regex.captures(line.trim()) {
749        //                 // Capture the line number and code from the suggestion line
750        //                 let line_num = caps[1].parse::<usize>().unwrap_or(0);
751        //                 let code = caps[2].to_string();
752
753        //                 // Lock the pending_diag to add the suggestion
754        //                 if let Some(mut loc) = location_lock.lock().unwrap().take() {
755        //                     println!("Suggestion line: {}", line);
756        //                     let file = loc.file.clone().unwrap_or_default();
757        //                     let col = loc.column.unwrap_or(0);
758
759        //                     // Concatenate the suggestion line to the message
760        //                     let mut msg = loc.message.unwrap_or_default();
761        //                     msg.push_str(&format!("\n{} | {}", line_num, code));  // Append the suggestion properly
762
763        //                     // Print the concatenated suggestion for debugging
764        //                     println!("Suggestion for {}:{}:{} - {}", file, line_num, col, msg);
765
766        //                     // Update the location with the new concatenated message
767        //                     loc.message = Some(msg.clone());
768
769        //                     // Save the updated location back to shared state
770        //                     location_lock.lock().unwrap().replace(loc);
771
772        //                     // return Some(CallbackResponse {
773        //                     //     callback_type: CallbackType::Warning, // Treat subsequent lines as warnings
774        //                     //     message: Some(msg.clone()),
775        //                     //     file: Some(file),
776        //                     //     line: Some(line_num),
777        //                     //     column: Some(col),
778        //                     //     suggestion: Some(msg),  // This is the suggestion part
779        //                     //     terminal_status: None,
780        //                     // });
781        //                 } else {
782        //                     println!("No location information available for suggestion line: {}", line);
783        //                 }
784        //             } else {
785        //                 println!("Suggestion line does not match expected format: {}", line);
786        //             }
787        //         } else {
788        //             println!("Suggestion mode is not active. Ignoring line: {}", line);
789        //         }
790
791        //         None
792        //     }),
793        // );
794
795        // }
796
797        {
798            let location_lock = Arc::clone(&warning_location);
799            let pending_diag = Arc::clone(&pending_diag);
800            let suggestion_mode = Arc::clone(&suggestion_mode);
801            stderr_dispatcher.add_callback(
802                r"^(?P<msg>.*)$", // Capture all lines following the location
803                Box::new(
804                    move |line, _captures, _multiline_flag, _stats, _prior_response| {
805                        // Lock the location to fetch the original diagnostic info
806                        if let Ok(location_guard) = location_lock.lock() {
807                            if let Some(loc) = location_guard.as_ref() {
808                                let file = loc.file.clone().unwrap_or_default();
809                                let line_num = loc.line.unwrap_or(0);
810                                let col = loc.column.unwrap_or(0);
811                                // println!("SUGGESTION: Suggestion for {}:{}:{} {}", file, line_num, col, line);
812
813                                // Only treat lines starting with | or numbers as suggestion lines
814                                if line.trim().starts_with('|')
815                                    || line.trim().starts_with(char::is_numeric)
816                                {
817                                    // Get the existing suggestion and append the new line
818                                    let suggestion = line.trim();
819
820                                    // Print the suggestion for debugging
821                                    // println!("Suggestion for {}:{}:{} - {}", file, line_num, col, suggestion);
822
823                                    // Lock the pending_diag and update its callback_response field
824                                    let mut pending_diag = match pending_diag.lock() {
825                                        Ok(lock) => lock,
826                                        Err(e) => {
827                                            eprintln!("Failed to acquire lock: {}", e);
828                                            return None; // Handle the error appropriately
829                                        }
830                                    };
831                                    if let Some(diag) = pending_diag.take() {
832                                        // If a PendingDiag already exists, update the existing callback response with the new suggestion
833                                        let mut diag = diag;
834
835                                        // Append the new suggestion to the existing one
836                                        if let Some(ref mut existing) = diag.suggestion {
837                                            diag.suggestion =
838                                                Some(format!("{}\n{}", existing, suggestion));
839                                        } else {
840                                            diag.suggestion = Some(suggestion.to_string());
841                                        }
842
843                                        // Update the shared state with the new PendingDiag
844                                        *pending_diag = Some(diag.clone());
845                                        return Some(CallbackResponse {
846                                            callback_type: CallbackType::Suggestion, // Treat subsequent lines as warnings
847                                            message: Some(
848                                                diag.clone().suggestion.clone().unwrap().clone(),
849                                            ),
850                                            file: Some(file),
851                                            line: Some(line_num),
852                                            column: Some(col),
853                                            suggestion: diag.clone().suggestion.clone(), // This is the suggestion part
854                                            terminal_status: None,
855                                        });
856                                    } else {
857                                        // println!("No pending diagnostic to process for suggestion line: {}", line);
858                                    }
859                                } else {
860                                    // If the line doesn't match the suggestion format, just return it as is
861                                    if line.trim().is_empty() {
862                                        // Ignore empty lines
863                                        suggestion_mode.store(false, Ordering::Relaxed);
864                                        return None;
865                                    }
866                                }
867                            } else {
868                                // println!("No location information available for suggestion line: {}", line);
869                            }
870                        }
871                        None
872                    },
873                ),
874            );
875        }
876
877        // 2) Location callback stores its response into that shared state
878        {
879            let pending_diag = Arc::clone(&pending_diag);
880            let warning_location = Arc::clone(&warning_location);
881            let location_lock = Arc::clone(&warning_location);
882            let suggestion_mode = Arc::clone(&suggestion_mode);
883            let manifest_path = self.manifest_path.clone();
884            stderr_dispatcher.add_callback(
885                // r"^\s*-->\s+(?P<file>[^:]+):(?P<line>\d+):(?P<col>\d+)$",
886                r"^\s*-->\s+(?P<file>.+?)(?::(?P<line>\d+))?(?::(?P<col>\d+))?\s*$",
887                Box::new(
888                    move |_line, caps, _multiline_flag, _stats, _prior_response| {
889                        log::trace!("Location line: {}", _line);
890                        // if multiline_flag.load(Ordering::Relaxed) {
891                        if let Some(caps) = caps {
892                            let file = caps["file"].to_string();
893                            let resolved_path = resolve_file_path(&manifest_path, &file);
894                            let file = resolved_path.to_str().unwrap_or_default().to_string();
895                            let line = caps["line"].parse::<usize>().unwrap_or(0);
896                            let column = caps["col"].parse::<usize>().unwrap_or(0);
897                            let resp = CallbackResponse {
898                                callback_type: CallbackType::Location,
899                                message: format!("{}:{}:{}", file, line, column).into(),
900                                file: Some(file.clone()),
901                                line: Some(line),
902                                column: Some(column),
903                                suggestion: None,
904                                terminal_status: None,
905                            };
906                            // Lock the pending_diag and update its callback_response field
907                            let mut pending_diag = pending_diag.lock().unwrap();
908                            if let Some(diag) = pending_diag.take() {
909                                // If a PendingDiag already exists, save the new callback response in the existing PendingDiag
910                                let mut diag = diag;
911                                diag.lineref = format!("{}:{}:{}", file, line, column); // Update the lineref
912                                                                                        // diag.save_callback_response(resp.clone()); // Save the callback response
913                                                                                        // Update the shared state with the new PendingDiag
914                                *pending_diag = Some(diag);
915                            }
916                            // Save it for the generic callback to see
917                            *warning_location.lock().unwrap() = Some(resp.clone());
918                            *location_lock.lock().unwrap() = Some(resp.clone());
919                            // Set suggestion mode to true as we've encountered a location line
920                            suggestion_mode.store(true, Ordering::Relaxed);
921                            return Some(resp.clone());
922                        } else {
923                            println!("No captures found in line: {}", _line);
924                        }
925                        // }
926                        None
927                    },
928                ),
929            );
930        }
931
932        // // 3) Note callback — attach note to pending_diag
933        {
934            let pending_diag = Arc::clone(&pending_diag);
935            stderr_dispatcher.add_callback(
936                r"^\s*=\s*note:\s*(?P<msg>.+)$",
937                Box::new(move |_line, caps, _state, _stats, _prior_response| {
938                    if let Some(caps) = caps {
939                        let mut pending_diag = pending_diag.lock().unwrap();
940                        if let Some(ref mut resp) = *pending_diag {
941                            // Prepare the new note with the blue prefix
942                            let new_note = format!("note: {}", caps["msg"].to_string());
943
944                            // Append or set the note
945                            if let Some(existing_note) = &resp.note {
946                                // If there's already a note, append with newline and the new note
947                                resp.note = Some(format!("{}\n{}", existing_note, new_note));
948                            } else {
949                                // If no existing note, just set the new note
950                                resp.note = Some(new_note);
951                            }
952                        }
953                    }
954                    None
955                }),
956            );
957        }
958
959        // 4) Help callback — attach help to pending_diag
960        {
961            let pending_diag = Arc::clone(&pending_diag);
962            stderr_dispatcher.add_callback(
963                r"^\s*(?:\=|\|)\s*help:\s*(?P<msg>.+)$", // Regex to match both '=' and '|' before help:
964                Box::new(move |_line, caps, _state, _stats, _prior_response| {
965                    if let Some(caps) = caps {
966                        let mut pending_diag = pending_diag.lock().unwrap();
967                        if let Some(ref mut resp) = *pending_diag {
968                            // Create the new help message with the orange "h:" prefix
969                            let new_help =
970                                format!("\x1b[38;5;214mhelp: {}\x1b[0m", caps["msg"].to_string());
971
972                            // Append or set the help message
973                            if let Some(existing_help) = &resp.help {
974                                // If there's already a help message, append with newline
975                                resp.help = Some(format!("{}\n{}", existing_help, new_help));
976                            } else {
977                                // If no existing help message, just set the new one
978                                resp.help = Some(new_help);
979                            }
980                        }
981                    }
982                    None
983                }),
984            );
985        }
986
987        stderr_dispatcher.add_callback(
988    r"(?:\x1b\[[0-9;]*[A-Za-z])*\s*Serving(?:\x1b\[[0-9;]*[A-Za-z])*\s+at\s+(http://[^\s]+)",
989    Box::new(|line, captures, _state, stats , _prior_response| {
990        if let Some(caps) = captures {
991            let url = caps.get(1).unwrap().as_str();
992            let url = url.replace("0.0.0.0", "127.0.0.1");
993            println!("(STDERR) Captured URL: {}", url);
994            match open::that_detached(&url) {
995                Ok(_) => println!("(STDERR) Opened URL: {}",&url),
996                Err(e) => eprintln!("(STDERR) Failed to open URL: {}. Error: {:?}", url, e),
997            }
998             let mut stats = stats.lock().unwrap();
999             if stats.build_finished_time.is_none() {
1000              let now = SystemTime::now();
1001             stats.build_finished_time = Some(now);
1002             }
1003            Some(CallbackResponse {
1004                callback_type: CallbackType::OpenedUrl, // Choose as appropriate
1005                message: Some(format!("Captured and opened URL: {}", url)),
1006                file: None,
1007                line: None,
1008                column: None,
1009                suggestion: None,
1010                terminal_status: None,
1011            })
1012        } else {
1013            println!("(STDERR) No URL captured in line: {}", line);
1014            None
1015        }
1016    }),
1017);
1018
1019        let finished_flag = Arc::new(AtomicBool::new(false));
1020
1021        // 0) Finished‐profile summary callback
1022        {
1023            let finished_flag = Arc::clone(&finished_flag);
1024            stderr_dispatcher.add_callback(
1025        r"^Finished\s+`(?P<profile>[^`]+)`\s+profile\s+\[(?P<opts>[^\]]+)\]\s+target\(s\)\s+in\s+(?P<dur>[0-9.]+s)$",
1026        Box::new(move |_line, caps, _multiline_flag, stats, _prior_response | {
1027            if let Some(caps) = caps {
1028                finished_flag.store(true, Ordering::Relaxed);
1029                let profile = &caps["profile"];
1030                let opts    = &caps["opts"];
1031                let dur     = &caps["dur"];
1032                             let mut stats = stats.lock().unwrap();
1033             if stats.build_finished_time.is_none() {
1034              let now = SystemTime::now();
1035             stats.build_finished_time = Some(now);
1036             }
1037                Some(CallbackResponse {
1038                    callback_type: CallbackType::Note,
1039                    message: Some(format!("Finished `{}` [{}] in {}", profile, opts, dur)),
1040                    file: None, line: None, column: None, suggestion: None, terminal_status: None,
1041                })
1042            } else {
1043                None
1044            }
1045        }),
1046    );
1047        }
1048
1049        let summary_flag = Arc::new(AtomicBool::new(false));
1050        {
1051            let summary_flag = Arc::clone(&summary_flag);
1052            stderr_dispatcher.add_callback(
1053    r"^(?P<level>warning|error):\s+`(?P<name>[^`]+)`\s+\((?P<otype>lib|bin)\)\s+generated\s+(?P<count>\d+)\s+(?P<kind>warnings|errors).*run\s+`(?P<cmd>[^`]+)`\s+to apply\s+(?P<fixes>\d+)\s+suggestions",
1054    Box::new(move |_line, caps, multiline_flag, _stats, _prior_response | {
1055        let summary_flag = Arc::clone(&summary_flag);
1056        if let Some(caps) = caps {
1057            summary_flag.store(true, Ordering::Relaxed);
1058            // Always start fresh
1059            multiline_flag.store(false, Ordering::Relaxed);
1060
1061            let level    = &caps["level"];
1062            let name     = &caps["name"];
1063            let otype    = &caps["otype"];
1064            let count: usize = caps["count"].parse().unwrap_or(0);
1065            let kind     = &caps["kind"];   // "warnings" or "errors"
1066            let cmd      = caps["cmd"].to_string();
1067            let fixes: usize = caps["fixes"].parse().unwrap_or(0);
1068
1069            println!("SUMMARIZATION CALLBACK {}",
1070                    &format!("{}: `{}` ({}) generated {} {}; run `{}` to apply {} fixes",
1071                    level, name, otype, count, kind, cmd, fixes));
1072            Some(CallbackResponse {
1073                callback_type: CallbackType::Note,  // treat as informational
1074                message: Some(format!(
1075                    "{}: `{}` ({}) generated {} {}; run `{}` to apply {} fixes",
1076                    level, name, otype, count, kind, cmd, fixes
1077                )),
1078                file: None,
1079                line: None,
1080                column: None,
1081                suggestion: Some(cmd),
1082                terminal_status: None,
1083            })
1084        } else {
1085            None
1086        }
1087    }),
1088    );
1089        }
1090
1091        // {
1092        //     let summary_flag = Arc::clone(&summary_flag);
1093        //     let finished_flag = Arc::clone(&finished_flag);
1094        //     let warning_location = Arc::clone(&warning_location);
1095        //     // Warning callback for stdout.
1096        //     stderr_dispatcher.add_callback(
1097        //         r"^warning:\s+(?P<msg>.+)$",
1098        //         Box::new(
1099        //             move |line: &str, captures: Option<regex::Captures>, multiline_flag: Arc<AtomicBool>| {
1100        //                             // If summary or finished just matched, skip
1101        //             if summary_flag.swap(false, Ordering::Relaxed)
1102        //                 || finished_flag.swap(false, Ordering::Relaxed)
1103        //             {
1104        //                 return None;
1105        //             }
1106
1107        //         // 2) If this line *matches* the warning regex, handle as a new warning
1108        //         if let Some(caps) = captures {
1109        //             let msg = caps.name("msg").unwrap().as_str().to_string();
1110        //                    // 1) If a location was saved, print file:line:col – msg
1111        //             // println!("*WARNING detected: {:?}", msg);
1112        //                 multiline_flag.store(true, Ordering::Relaxed);
1113        //         if let Some(loc) = warning_location.lock().unwrap().take() {
1114        //                 let file = loc.file.unwrap_or_default();
1115        //                 let line_num = loc.line.unwrap_or(0);
1116        //                 let col  = loc.column.unwrap_or(0);
1117        //                 println!("{}:{}:{} - {}", file, line_num, col, msg);
1118        //                 return Some(CallbackResponse {
1119        //                     callback_type: CallbackType::Warning,
1120        //                     message: Some(msg.to_string()),
1121        //                     file: None, line: None, column: None, suggestion: None, terminal_status: None,
1122        //                 });
1123        //         }
1124        //             return Some(CallbackResponse {
1125        //                 callback_type: CallbackType::Warning,
1126        //                 message: Some(msg),
1127        //                 file: None,
1128        //                 line: None,
1129        //                 column: None,
1130        //                 suggestion: None,
1131        //                 terminal_status: None,
1132        //             });
1133        //         }
1134
1135        //                 // 3) Otherwise, if we’re in multiline mode, treat as continuation
1136        //         if multiline_flag.load(Ordering::Relaxed) {
1137        //             let text = line.trim();
1138        //             if text.is_empty() {
1139        //                 multiline_flag.store(false, Ordering::Relaxed);
1140        //                 return None;
1141        //             }
1142        //             // println!("   - {:?}", text);
1143        //             return Some(CallbackResponse {
1144        //                 callback_type: CallbackType::Warning,
1145        //                 message: Some(text.to_string()),
1146        //                 file: None,
1147        //                 line: None,
1148        //                 column: None,
1149        //                 suggestion: None,
1150        //                 terminal_status: None,
1151        //             });
1152        //         }
1153        //                     None
1154        //             },
1155        //         ),
1156        //     );
1157        // }
1158
1159        stderr_dispatcher.add_callback(
1160            r"IO\(Custom \{ kind: NotConnected",
1161            Box::new(move |line, _captures, _state, _stats, _prior_response| {
1162                println!("(STDERR) Terminal error detected: {:?}", &line);
1163                let result = if line.contains("NotConnected") {
1164                    TerminalError::NoTerminal
1165                } else {
1166                    TerminalError::NoError
1167                };
1168                let sender = sender.lock().unwrap();
1169                sender.send(result).ok();
1170                Some(CallbackResponse {
1171                    callback_type: CallbackType::Warning, // Choose as appropriate
1172                    message: Some(format!("Terminal Error: {}", line)),
1173                    file: None,
1174                    line: None,
1175                    column: None,
1176                    suggestion: None,
1177                    terminal_status: None,
1178                })
1179            }),
1180        );
1181        stderr_dispatcher.add_callback(
1182            r".*",
1183            Box::new(|line, _captures, _state, _stats, _prior_response| {
1184                log::trace!("stdraw[{:?}]", line);
1185                None // We're just printing, so no callback response is needed.
1186            }),
1187        );
1188        // need to implement autosense/filtering for tool installers; TBD
1189        // stderr_dispatcher.add_callback(
1190        //     r"Command 'perl' not found\. Is perl installed\?",
1191        //     Box::new(|line, _captures, _state, stats| {
1192        //     println!("cargo e sees a perl issue; maybe a prompt in the future or auto-resolution.");
1193        //     crate::e_autosense::auto_sense_perl();
1194        //     None
1195        //     }),
1196        // );
1197        // need to implement autosense/filtering for tool installers; TBD
1198        // stderr_dispatcher.add_callback(
1199        //     r"Error configuring OpenSSL build:\s+Command 'perl' not found\. Is perl installed\?",
1200        //     Box::new(|line, _captures, _state, stats| {
1201        //     println!("Detected OpenSSL build error due to missing 'perl'. Attempting auto-resolution.");
1202        //     crate::e_autosense::auto_sense_perl();
1203        //     None
1204        //     }),
1205        // );
1206        self.stderr_dispatcher = Some(Arc::new(stderr_dispatcher));
1207
1208        // let mut progress_dispatcher = EventDispatcher::new();
1209        // progress_dispatcher.add_callback(r"Progress", Box::new(|line, _captures,_state| {
1210        //     println!("(Progress) {}", line);
1211        //     None
1212        // }));
1213        // self.progress_dispatcher = Some(Arc::new(progress_dispatcher));
1214
1215        // let mut stage_dispatcher = EventDispatcher::new();
1216        // stage_dispatcher.add_callback(r"Stage:", Box::new(|line, _captures, _state| {
1217        //     println!("(Stage) {}", line);
1218        //     None
1219        // }));
1220        // self.stage_dispatcher = Some(Arc::new(stage_dispatcher));
1221    }
1222
1223    pub fn run<F>(self: Arc<Self>, on_spawn: F) -> anyhow::Result<u32>
1224    where
1225        F: FnOnce(u32, CargoProcessHandle),
1226    {
1227        if !self.is_filter {
1228            return self.switch_to_passthrough_mode(on_spawn);
1229        }
1230        let mut command = self.build_command();
1231
1232        let mut cargo_process_handle = command.spawn_cargo_capture(
1233            self.clone(),
1234            self.stdout_dispatcher.clone(),
1235            self.stderr_dispatcher.clone(),
1236            self.progress_dispatcher.clone(),
1237            self.stage_dispatcher.clone(),
1238            None,
1239        );
1240        cargo_process_handle.diagnostics = Arc::clone(&self.diagnostics);
1241        let pid = cargo_process_handle.pid;
1242
1243        // Notify observer
1244        on_spawn(pid, cargo_process_handle);
1245
1246        Ok(pid)
1247    }
1248
1249    // pub fn run(self: Arc<Self>) -> anyhow::Result<u32> {
1250    //     // Build the command using the builder's configuration
1251    //     let mut command = self.build_command();
1252
1253    //     // Spawn the cargo process handle
1254    //     let cargo_process_handle = command.spawn_cargo_capture(
1255    //         self.stdout_dispatcher.clone(),
1256    //         self.stderr_dispatcher.clone(),
1257    //         self.progress_dispatcher.clone(),
1258    //         self.stage_dispatcher.clone(),
1259    //         None,
1260    //     );
1261    // let pid = cargo_process_handle.pid;
1262    // let mut global = GLOBAL_CHILDREN.lock().unwrap();
1263    // global.insert(pid, Arc::new(Mutex::new(cargo_process_handle)));
1264    //     Ok(pid)
1265    // }
1266
1267    pub fn wait(self: Arc<Self>, pid: Option<u32>) -> anyhow::Result<CargoProcessResult> {
1268        let mut global = GLOBAL_CHILDREN.lock().unwrap();
1269        if let Some(pid) = pid {
1270            // Lock the global list of processes and attempt to find the cargo process handle directly by pid
1271            if let Some(cargo_process_handle) = global.get_mut(&pid) {
1272                let mut cargo_process_handle = cargo_process_handle.lock().unwrap();
1273
1274                // Wait for the process to finish and retrieve the result
1275                // println!("Waiting for process with PID: {}", pid);
1276                // let result = cargo_process_handle.wait();
1277                // println!("Process with PID {} finished", pid);
1278                loop {
1279                    println!("Waiting for process with PID: {}", pid);
1280
1281                    // Attempt to wait for the process, but don't block indefinitely
1282                    let status = cargo_process_handle.child.try_wait()?;
1283
1284                    // If the status is `Some(status)`, the process has finished
1285                    if let Some(status) = status {
1286                        if status.code() == Some(101) {
1287                            println!("Process with PID {} finished with cargo error", pid);
1288                        }
1289
1290                        // Check the terminal error flag and update the result if there is an error
1291                        if *cargo_process_handle.terminal_error_flag.lock().unwrap()
1292                            != TerminalError::NoError
1293                        {
1294                            let terminal_error =
1295                                *cargo_process_handle.terminal_error_flag.lock().unwrap();
1296                            cargo_process_handle.result.terminal_error = Some(terminal_error);
1297                        }
1298
1299                        let final_diagnostics = {
1300                            let diag_lock = self.diagnostics.lock().unwrap();
1301                            diag_lock.clone()
1302                        };
1303                        cargo_process_handle.result.diagnostics = final_diagnostics.clone();
1304                        cargo_process_handle.result.exit_status = Some(status);
1305                        cargo_process_handle.result.end_time = Some(SystemTime::now());
1306                        cargo_process_handle.result.elapsed_time = Some(
1307                            cargo_process_handle
1308                                .result
1309                                .end_time
1310                                .unwrap()
1311                                .duration_since(cargo_process_handle.result.start_time.unwrap())
1312                                .unwrap(),
1313                        );
1314                        println!(
1315                            "Process with PID {} finished {:?} {}",
1316                            pid,
1317                            status,
1318                            final_diagnostics.len()
1319                        );
1320                        return Ok(cargo_process_handle.result.clone());
1321                        // return Ok(CargoProcessResult { exit_status: status, ..Default::default() });
1322                    }
1323
1324                    // Sleep briefly to yield control back to the system and avoid blocking
1325                    std::thread::sleep(std::time::Duration::from_secs(1));
1326                }
1327
1328                // Return the result
1329                // match result {
1330                //     Ok(res) => Ok(res),
1331                //     Err(e) => Err(anyhow::anyhow!("Failed to wait for cargo process: {}", e).into()),
1332                // }
1333            } else {
1334                Err(anyhow::anyhow!(
1335                    "Process handle with PID {} not found in GLOBAL_CHILDREN",
1336                    pid
1337                )
1338                .into())
1339            }
1340        } else {
1341            Err(anyhow::anyhow!("No PID provided for waiting on cargo process").into())
1342        }
1343    }
1344
1345    // pub fn run_wait(self: Arc<Self>) -> anyhow::Result<CargoProcessResult> {
1346    //     // Run the cargo command and get the process handle (non-blocking)
1347    //     let pid = self.clone().run()?; // adds to global list of processes
1348    //     let result = self.wait(Some(pid)); // Wait for the process to finish
1349    //     // Remove the completed process from GLOBAL_CHILDREN
1350    //     let mut global = GLOBAL_CHILDREN.lock().unwrap();
1351    //     global.remove(&pid);
1352
1353    //     result
1354    // }
1355
1356    // Runs the cargo command using the builder's configuration.
1357    // pub fn run(&self) -> anyhow::Result<CargoProcessResult> {
1358    //     // Build the command using the builder's configuration
1359    //     let mut command = self.build_command();
1360
1361    //     // Now use the `spawn_cargo_capture` extension to run the command
1362    //     let mut cargo_process_handle = command.spawn_cargo_capture(
1363    //         self.stdout_dispatcher.clone(),
1364    //         self.stderr_dispatcher.clone(),
1365    //         self.progress_dispatcher.clone(),
1366    //         self.stage_dispatcher.clone(),
1367    //         None,
1368    //     );
1369
1370    //     // Wait for the process to finish and retrieve the results
1371    //     cargo_process_handle.wait().context("Failed to execute cargo process")
1372    // }
1373
1374    /// Configure the command based on the target kind.
1375    pub fn with_target(mut self, target: &CargoTarget) -> Self {
1376        if let Some(origin) = target.origin.clone() {
1377            println!("Target origin: {:?}", origin);
1378        } else {
1379            println!("Target origin is not set");
1380        }
1381        match target.kind {
1382            TargetKind::Unknown | TargetKind::Plugin => {
1383                return self;
1384            }
1385            TargetKind::Bench => {
1386                // // To run benchmarks, use the "bench" command.
1387                //  let exe_path = match which("bench") {
1388                //     Ok(path) => path,
1389                //     Err(err) => {
1390                //         eprintln!("Error: 'trunk' not found in PATH: {}", err);
1391                //         return self;
1392                //     }
1393                // };
1394                // self.alternate_cmd = Some("bench".to_string())
1395                self.args.push("bench".into());
1396                self.args.push(target.name.clone());
1397            }
1398            TargetKind::Test => {
1399                self.args.push("test".into());
1400                // Pass the target's name as a filter to run specific tests.
1401                self.args.push(target.name.clone());
1402            }
1403            TargetKind::UnknownExample
1404            | TargetKind::UnknownExtendedExample
1405            | TargetKind::Example
1406            | TargetKind::ExtendedExample => {
1407                self.args.push(self.subcommand.clone());
1408                //self.args.push("--message-format=json".into());
1409                self.args.push("--example".into());
1410                self.args.push(target.name.clone());
1411                self.args.push("--manifest-path".into());
1412                self.args.push(
1413                    target
1414                        .manifest_path
1415                        .clone()
1416                        .to_str()
1417                        .unwrap_or_default()
1418                        .to_owned(),
1419                );
1420                self = self.with_required_features(&target.manifest_path, target);
1421            }
1422            TargetKind::UnknownBinary
1423            | TargetKind::UnknownExtendedBinary
1424            | TargetKind::Binary
1425            | TargetKind::ExtendedBinary => {
1426                self.args.push(self.subcommand.clone());
1427                self.args.push("--bin".into());
1428                self.args.push(target.name.clone());
1429                self.args.push("--manifest-path".into());
1430                self.args.push(
1431                    target
1432                        .manifest_path
1433                        .clone()
1434                        .to_str()
1435                        .unwrap_or_default()
1436                        .to_owned(),
1437                );
1438                self = self.with_required_features(&target.manifest_path, target);
1439            }
1440            TargetKind::Manifest => {
1441                self.suppressed_flags.insert("quiet".to_string());
1442                self.args.push(self.subcommand.clone());
1443                self.args.push("--manifest-path".into());
1444                self.args.push(
1445                    target
1446                        .manifest_path
1447                        .clone()
1448                        .to_str()
1449                        .unwrap_or_default()
1450                        .to_owned(),
1451                );
1452            }
1453            TargetKind::ManifestTauriExample => {
1454                self.suppressed_flags.insert("quiet".to_string());
1455                self.args.push(self.subcommand.clone());
1456                self.args.push("--example".into());
1457                self.args.push(target.name.clone());
1458                self.args.push("--manifest-path".into());
1459                self.args.push(
1460                    target
1461                        .manifest_path
1462                        .clone()
1463                        .to_str()
1464                        .unwrap_or_default()
1465                        .to_owned(),
1466                );
1467                self = self.with_required_features(&target.manifest_path, target);
1468            }
1469            TargetKind::ScriptScriptisto => {
1470                let exe_path = match which("scriptisto") {
1471                    Ok(path) => path,
1472                    Err(err) => {
1473                        eprintln!("Error: 'scriptisto' not found in PATH: {}", err);
1474                        return self;
1475                    }
1476                };
1477                self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1478                let candidate_opt = match &target.origin {
1479                    Some(TargetOrigin::SingleFile(path))
1480                    | Some(TargetOrigin::DefaultBinary(path)) => Some(path),
1481                    _ => None,
1482                };
1483                if let Some(candidate) = candidate_opt {
1484                    self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1485                    self.args.push(candidate.to_string_lossy().to_string());
1486                } else {
1487                    println!("No scriptisto origin found for: {:?}", target);
1488                }
1489            }
1490            TargetKind::ScriptRustScript => {
1491                let exe_path = match crate::e_installer::ensure_rust_script() {
1492                    Ok(p) => p,
1493                    Err(e) => {
1494                        eprintln!("{}", e);
1495                        return self;
1496                    }
1497                };
1498                let candidate_opt = match &target.origin {
1499                    Some(TargetOrigin::SingleFile(path))
1500                    | Some(TargetOrigin::DefaultBinary(path)) => Some(path),
1501                    _ => None,
1502                };
1503                if let Some(candidate) = candidate_opt {
1504                    self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1505                    if self.is_filter {
1506                        self.args.push("-c".into()); // ask for cargo output
1507                    }
1508                    self.args.push(candidate.to_string_lossy().to_string());
1509                } else {
1510                    println!("No rust-script origin found for: {:?}", target);
1511                }
1512            }
1513            TargetKind::ManifestTauri => {
1514                // First, locate the Cargo.toml using the existing function
1515                let manifest_path = crate::locate_manifest(true).unwrap_or_else(|_| {
1516                    eprintln!("Error: Unable to locate Cargo.toml file.");
1517                    std::process::exit(1);
1518                });
1519
1520                // Now, get the workspace parent from the manifest directory
1521                let manifest_dir = Path::new(&manifest_path)
1522                    .parent()
1523                    .unwrap_or(Path::new(".."));
1524
1525                // Ensure npm dependencies are handled at the workspace parent level
1526                let pnpm =
1527                    crate::e_installer::check_pnpm_and_install(manifest_dir).unwrap_or_else(|_| {
1528                        eprintln!("Error: Unable to check pnpm dependencies.");
1529                        PathBuf::new()
1530                    });
1531                if pnpm == PathBuf::new() {
1532                    crate::e_installer::check_npm_and_install(manifest_dir).unwrap_or_else(|_| {
1533                        eprintln!("Error: Unable to check npm dependencies.");
1534                    });
1535                }
1536
1537                self.suppressed_flags.insert("quiet".to_string());
1538                // Helper closure to check for tauri.conf.json in a directory.
1539                let has_tauri_conf = |dir: &Path| -> bool { dir.join("tauri.conf.json").exists() };
1540
1541                // Helper closure to check for tauri.conf.json and package.json in a directory.
1542                let has_file = |dir: &Path, filename: &str| -> bool { dir.join(filename).exists() };
1543                // Try candidate's parent (if origin is SingleFile or DefaultBinary).
1544                let candidate_dir_opt = match &target.origin {
1545                    Some(TargetOrigin::SingleFile(path))
1546                    | Some(TargetOrigin::DefaultBinary(path)) => path.parent(),
1547                    _ => None,
1548                };
1549
1550                if let Some(candidate_dir) = candidate_dir_opt {
1551                    if has_tauri_conf(candidate_dir) {
1552                        println!("Using candidate directory: {}", candidate_dir.display());
1553                        self.execution_dir = Some(candidate_dir.to_path_buf());
1554                    } else if let Some(manifest_parent) = target.manifest_path.parent() {
1555                        if has_tauri_conf(manifest_parent) {
1556                            println!("Using manifest parent: {}", manifest_parent.display());
1557                            self.execution_dir = Some(manifest_parent.to_path_buf());
1558                        } else if let Some(grandparent) = manifest_parent.parent() {
1559                            if has_tauri_conf(grandparent) {
1560                                println!("Using manifest grandparent: {}", grandparent.display());
1561                                self.execution_dir = Some(grandparent.to_path_buf());
1562                            } else {
1563                                println!("No tauri.conf.json found in candidate, manifest parent, or grandparent; defaulting to manifest parent: {}", manifest_parent.display());
1564                                self.execution_dir = Some(manifest_parent.to_path_buf());
1565                            }
1566                        } else {
1567                            println!("No grandparent for manifest; defaulting to candidate directory: {}", candidate_dir.display());
1568                            self.execution_dir = Some(candidate_dir.to_path_buf());
1569                        }
1570                    } else {
1571                        println!(
1572                            "No manifest parent found for: {}",
1573                            target.manifest_path.display()
1574                        );
1575                    }
1576                    // Check for package.json and run npm ls if found.
1577                    println!("Checking for package.json in: {}", candidate_dir.display());
1578                    if has_file(candidate_dir, "package.json") {
1579                        crate::e_installer::check_npm_and_install(candidate_dir).ok();
1580                    }
1581                } else if let Some(manifest_parent) = target.manifest_path.parent() {
1582                    if has_tauri_conf(manifest_parent) {
1583                        println!("Using manifest parent: {}", manifest_parent.display());
1584                        self.execution_dir = Some(manifest_parent.to_path_buf());
1585                    } else if let Some(grandparent) = manifest_parent.parent() {
1586                        if has_tauri_conf(grandparent) {
1587                            println!("Using manifest grandparent: {}", grandparent.display());
1588                            self.execution_dir = Some(grandparent.to_path_buf());
1589                        } else {
1590                            println!(
1591                                "No tauri.conf.json found; defaulting to manifest parent: {}",
1592                                manifest_parent.display()
1593                            );
1594                            self.execution_dir = Some(manifest_parent.to_path_buf());
1595                        }
1596                    }
1597                    // Check for package.json and run npm ls if found.
1598                    println!(
1599                        "Checking for package.json in: {}",
1600                        manifest_parent.display()
1601                    );
1602                    if has_file(manifest_parent, "package.json") {
1603                        crate::e_installer::check_npm_and_install(manifest_parent).ok();
1604                    }
1605                    if has_file(Path::new("."), "package.json") {
1606                        crate::e_installer::check_npm_and_install(manifest_parent).ok();
1607                    }
1608                } else {
1609                    println!(
1610                        "No manifest parent found for: {}",
1611                        target.manifest_path.display()
1612                    );
1613                }
1614                self.args.push("tauri".into());
1615                self.args.push("dev".into());
1616            }
1617            TargetKind::ManifestLeptos => {
1618                let readme_path = target
1619                    .manifest_path
1620                    .parent()
1621                    .map(|p| p.join("README.md"))
1622                    .filter(|p| p.exists())
1623                    .or_else(|| {
1624                        target
1625                            .manifest_path
1626                            .parent()
1627                            .map(|p| p.join("readme.md"))
1628                            .filter(|p| p.exists())
1629                    });
1630
1631                if let Some(readme) = readme_path {
1632                    if let Ok(mut file) = std::fs::File::open(&readme) {
1633                        let mut contents = String::new();
1634                        if file.read_to_string(&mut contents).is_ok()
1635                            && contents.contains("cargo leptos watch")
1636                        {
1637                            // Use cargo leptos watch
1638                            println!("Detected 'cargo leptos watch' in {}", readme.display());
1639                            crate::e_installer::ensure_leptos().unwrap_or_else(|_| {
1640                                eprintln!("Error: Unable to ensure leptos installation.");
1641                                PathBuf::new() // Return an empty PathBuf as a fallback
1642                            });
1643                            self.execution_dir =
1644                                target.manifest_path.parent().map(|p| p.to_path_buf());
1645
1646                            self.alternate_cmd = Some("cargo".to_string());
1647                            self.args.push("leptos".into());
1648                            self.args.push("watch".into());
1649                            self = self.with_required_features(&target.manifest_path, target);
1650                            if let Some(exec_dir) = &self.execution_dir {
1651                                if exec_dir.join("package.json").exists() {
1652                                    println!(
1653                                        "Found package.json in execution directory: {}",
1654                                        exec_dir.display()
1655                                    );
1656                                    crate::e_installer::check_npm_and_install(exec_dir).ok();
1657                                }
1658                            }
1659                            return self;
1660                        }
1661                    }
1662                }
1663
1664                // fallback to trunk
1665                let exe_path = match crate::e_installer::ensure_trunk() {
1666                    Ok(p) => p,
1667                    Err(e) => {
1668                        eprintln!("{}", e);
1669                        return self;
1670                    }
1671                };
1672
1673                if let Some(manifest_parent) = target.manifest_path.parent() {
1674                    println!("Manifest path: {}", target.manifest_path.display());
1675                    println!(
1676                        "Execution directory (same as manifest folder): {}",
1677                        manifest_parent.display()
1678                    );
1679                    self.execution_dir = Some(manifest_parent.to_path_buf());
1680                } else {
1681                    println!(
1682                        "No manifest parent found for: {}",
1683                        target.manifest_path.display()
1684                    );
1685                }
1686                if let Some(exec_dir) = &self.execution_dir {
1687                    if exec_dir.join("package.json").exists() {
1688                        println!(
1689                            "Found package.json in execution directory: {}",
1690                            exec_dir.display()
1691                        );
1692                        crate::e_installer::check_npm_and_install(exec_dir).ok();
1693                    }
1694                }
1695                self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1696                self.args.push("serve".into());
1697                self.args.push("--open".into());
1698                self.args.push("--color".into());
1699                self.args.push("always".into());
1700                self = self.with_required_features(&target.manifest_path, target);
1701            }
1702            TargetKind::ManifestDioxus => {
1703                // For Dioxus targets, print the manifest path and set the execution directory
1704                let exe_path = match crate::e_installer::ensure_dx() {
1705                    Ok(path) => path,
1706                    Err(e) => {
1707                        eprintln!("Error locating `dx`: {}", e);
1708                        return self;
1709                    }
1710                };
1711                // to be the same directory as the manifest.
1712                if let Some(manifest_parent) = target.manifest_path.parent() {
1713                    println!("Manifest path: {}", target.manifest_path.display());
1714                    println!(
1715                        "Execution directory (same as manifest folder): {}",
1716                        manifest_parent.display()
1717                    );
1718                    self.execution_dir = Some(manifest_parent.to_path_buf());
1719                } else {
1720                    println!(
1721                        "No manifest parent found for: {}",
1722                        target.manifest_path.display()
1723                    );
1724                }
1725                self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1726                self.args.push("serve".into());
1727                self = self.with_required_features(&target.manifest_path, target);
1728            }
1729            TargetKind::ManifestDioxusExample => {
1730                let exe_path = match crate::e_installer::ensure_dx() {
1731                    Ok(path) => path,
1732                    Err(e) => {
1733                        eprintln!("Error locating `dx`: {}", e);
1734                        return self;
1735                    }
1736                };
1737                // For Dioxus targets, print the manifest path and set the execution directory
1738                // to be the same directory as the manifest.
1739                if let Some(manifest_parent) = target.manifest_path.parent() {
1740                    println!("Manifest path: {}", target.manifest_path.display());
1741                    println!(
1742                        "Execution directory (same as manifest folder): {}",
1743                        manifest_parent.display()
1744                    );
1745                    self.execution_dir = Some(manifest_parent.to_path_buf());
1746                } else {
1747                    println!(
1748                        "No manifest parent found for: {}",
1749                        target.manifest_path.display()
1750                    );
1751                }
1752                self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1753                self.args.push("serve".into());
1754                self.args.push("--example".into());
1755                self.args.push(target.name.clone());
1756                self = self.with_required_features(&target.manifest_path, target);
1757            }
1758        }
1759        self
1760    }
1761
1762    /// Configure the command using CLI options.
1763    pub fn with_cli(mut self, cli: &crate::Cli) -> Self {
1764        if cli.quiet && !self.suppressed_flags.contains("quiet") {
1765            // Insert --quiet right after "run" if present.
1766            if let Some(pos) = self.args.iter().position(|arg| arg == &self.subcommand) {
1767                self.args.insert(pos + 1, "--quiet".into());
1768            } else {
1769                self.args.push("--quiet".into());
1770            }
1771        }
1772        if cli.release {
1773            // Insert --release right after the initial "run" command if applicable.
1774            // For example, if the command already contains "run", insert "--release" after it.
1775            if let Some(pos) = self.args.iter().position(|arg| arg == &self.subcommand) {
1776                self.args.insert(pos + 1, "--release".into());
1777            } else {
1778                // If not running a "run" command (like in the Tauri case), simply push it.
1779                self.args.push("--release".into());
1780            }
1781        }
1782        // Append extra arguments (if any) after a "--" separator.
1783        if !cli.extra.is_empty() {
1784            self.args.push("--".into());
1785            self.args.extend(cli.extra.iter().cloned());
1786        }
1787        self
1788    }
1789    /// Append required features based on the manifest, target kind, and name.
1790    /// This method queries your manifest helper function and, if features are found,
1791    /// appends "--features" and the feature list.
1792    pub fn with_required_features(mut self, manifest: &PathBuf, target: &CargoTarget) -> Self {
1793        if !self.args.contains(&"--features".to_string()) {
1794            if let Some(features) = crate::e_manifest::get_required_features_from_manifest(
1795                manifest,
1796                &target.kind,
1797                &target.name,
1798            ) {
1799                self.args.push("--features".to_string());
1800                self.args.push(features);
1801            }
1802        }
1803        self
1804    }
1805
1806    /// Appends extra arguments to the command.
1807    pub fn with_extra_args(mut self, extra: &[String]) -> Self {
1808        if !extra.is_empty() {
1809            // Use "--" to separate Cargo arguments from target-specific arguments.
1810            self.args.push("--".into());
1811            self.args.extend(extra.iter().cloned());
1812        }
1813        self
1814    }
1815
1816    /// Builds the final vector of command-line arguments.
1817    pub fn build(self) -> Vec<String> {
1818        self.args
1819    }
1820
1821    pub fn is_compiler_target(&self) -> bool {
1822        let supported_subcommands = ["run", "build", "check", "leptos", "tauri"];
1823        if let Some(alternate) = &self.alternate_cmd {
1824            if alternate == "trunk" {
1825                return true;
1826            }
1827            if alternate != "cargo" {
1828                return false;
1829            }
1830        }
1831        if let Some(_) = self
1832            .args
1833            .iter()
1834            .position(|arg| supported_subcommands.contains(&arg.as_str()))
1835        {
1836            return true;
1837        }
1838        false
1839    }
1840
1841    pub fn injected_args(&self) -> (String, Vec<String>) {
1842        let mut new_args = self.args.clone();
1843        let supported_subcommands = [
1844            "run", "build", "test", "bench", "clean", "doc", "publish", "update",
1845        ];
1846
1847        if self.is_filter {
1848            if let Some(pos) = new_args
1849                .iter()
1850                .position(|arg| supported_subcommands.contains(&arg.as_str()))
1851            {
1852                // If the command is a supported subcommand like "cargo run", insert the JSON output format and color options.
1853                new_args.insert(pos + 1, "--message-format=json".into());
1854                new_args.insert(pos + 2, "--color".into());
1855                new_args.insert(pos + 3, "always".into());
1856            }
1857        }
1858
1859        let mut program = self.alternate_cmd.as_deref().unwrap_or("cargo").to_string();
1860
1861        if self.use_cache {
1862            #[cfg(target_os = "windows")]
1863            {
1864                // On Windows, we use the `cargo-e` executable.
1865                program = format!("{}.exe",self.target_name.clone());
1866            }
1867            #[cfg(not(target_os = "windows"))]
1868            {
1869                program = self.target_name.clone();
1870            }
1871            let debug_path = Path::new("target").join("debug").join(program.clone());
1872            let release_path = Path::new("target").join("release").join(program.clone());
1873            let release_examples_path = Path::new("target").join("release").join("examples").join(program.clone());
1874            let debug_examples_path = Path::new("target").join("debug").join("examples").join(program.clone());
1875            if release_path.exists() {
1876                program = release_path.to_string_lossy().to_string();
1877            } else if release_examples_path.exists() {
1878                program = release_examples_path.to_string_lossy().to_string();
1879            } else if debug_path.exists() {
1880                program = debug_path.to_string_lossy().to_string();
1881            } else if debug_examples_path.exists() {
1882                program = debug_examples_path.to_string_lossy().to_string();
1883            } else if Path::new(&program).exists() {
1884                // If the program exists in the current directory, use it.
1885                program = Path::new(&program).to_string_lossy().to_string();
1886            } else {
1887                program = self.alternate_cmd.as_deref().unwrap_or("cargo").to_string();
1888            }            
1889            new_args= vec![]
1890        }
1891        
1892        (program, new_args)
1893    }
1894
1895    pub fn print_command(&self) {
1896        let (program, new_args) = self.injected_args();
1897        println!("{} {}", program, new_args.join(" "));
1898    }
1899
1900    /// builds a std::process::Command.
1901    pub fn build_command(&self) -> Command {
1902        let (program, new_args) = self.injected_args();
1903
1904        let mut cmd = Command::new(program);
1905        cmd.args(new_args);
1906
1907        if let Some(dir) = &self.execution_dir {
1908            cmd.current_dir(dir);
1909        }
1910
1911        cmd
1912    }
1913    /// Runs the command and returns everything it printed (stdout + stderr),
1914    /// regardless of exit status.
1915    pub fn capture_output(&self) -> anyhow::Result<String> {
1916        // Build and run
1917        let mut cmd = self.build_command();
1918        let output = cmd
1919            .output()
1920            .map_err(|e| anyhow::anyhow!("Failed to spawn cargo process: {}", e))?;
1921
1922        // Decode both stdout and stderr lossily
1923        let mut all = String::new();
1924        all.push_str(&String::from_utf8_lossy(&output.stdout));
1925        all.push_str(&String::from_utf8_lossy(&output.stderr));
1926
1927        // Return the combined string, even if exit was !success
1928        Ok(all)
1929    }
1930}
1931
1932fn show_graphical_panic(
1933    line: &str,
1934    prior_response: Option<CallbackResponse>,
1935    manifest_path: &PathBuf,
1936) {
1937    if let Ok(e_window_path) = which("e_window") {
1938        // Compose a nice message for e_window's stdin
1939        // let stats = stats.lock().unwrap();
1940        // Compose a table with cargo-e and its version, plus panic info
1941        let cargo_e_version = env!("CARGO_PKG_VERSION");
1942
1943        let anchor: String = {
1944            // If there's no prior response, return an empty string.
1945            if prior_response.is_none() {
1946                String::new()
1947            } else {
1948                // Try to parse the line as "file:line:col"
1949                let prior = prior_response.as_ref().unwrap();
1950                let file = prior.file.as_deref().unwrap_or("");
1951                let line_num = prior.line.map(|n| n.to_string()).unwrap_or_default();
1952                let col_num = prior.column.map(|n| n.to_string()).unwrap_or_default();
1953
1954                let full_path = std::fs::canonicalize(file).unwrap_or_else(|_| {
1955                    // Remove the top folder from the file path if possible
1956                    let stripped_file = Path::new(file).components().skip(1).collect::<PathBuf>();
1957                    let fallback_path = stripped_file.clone();
1958                    std::fs::canonicalize(&fallback_path).unwrap_or_else(|_| {
1959                        let manifest_dir = manifest_path.parent().unwrap_or_else(|| {
1960                            eprintln!(
1961                                "Failed to determine parent directory for manifest: {:?}",
1962                                manifest_path
1963                            );
1964                            Path::new(".")
1965                        });
1966                        let parent_fallback_path = manifest_dir.join(file);
1967                        std::fs::canonicalize(&parent_fallback_path).unwrap_or_else(|_| {
1968                            eprintln!("Failed to resolve full path for: {} using ../", file);
1969                            let parent_fallback_path = manifest_dir.join(&stripped_file);
1970                            if parent_fallback_path.exists() {
1971                                parent_fallback_path
1972                            } else {
1973                                PathBuf::from(file)
1974                            }
1975                        })
1976                    })
1977                });
1978                let stripped_file = full_path.to_string_lossy().replace("\\\\?\\", "");
1979                let code_path = which("code").unwrap_or_else(|_| "code".to_string().into());
1980                String::from(format!(
1981                    "\nanchor: code {} {} {}|\"{}\" --goto \"{}:{}:{}\"\n",
1982                    stripped_file,
1983                    prior.line.unwrap_or(0),
1984                    prior.column.unwrap_or(0),
1985                    code_path.display(),
1986                    stripped_file,
1987                    prior.line.unwrap_or(0),
1988                    prior.column.unwrap_or(0)
1989                ))
1990            }
1991        };
1992        let context = ThreadLocalContext::get_context();
1993        let mut card = format!(
1994            "--title \"panic: {target}\" --width 400 --height 300\n\
1995                        target | {target} | string\n\
1996                        cargo-e | {version} | string\n\
1997                        \n\
1998                        panic: {target}\n{line}",
1999            target = context.target_name,
2000            version = cargo_e_version,
2001            line = line
2002        );
2003        if let Some(prior) = prior_response {
2004            if let Some(msg) = &prior.message {
2005                card = format!("{}\n{}", card, msg);
2006            }
2007        }
2008        if !anchor.is_empty() {
2009            card = format!("{}{}", card, anchor);
2010        }
2011        let child = std::process::Command::new(e_window_path)
2012            .stdin(std::process::Stdio::piped())
2013            .spawn();
2014        if let Ok(mut child) = child {
2015            if let Some(stdin) = child.stdin.as_mut() {
2016                use std::io::Write;
2017                let _ = stdin.write_all(card.as_bytes());
2018            }
2019        }
2020    }
2021}
2022/// Resolves a file path by:
2023///   1. If the path is relative, try to resolve it relative to the current working directory.
2024///   2. If that file does not exist, try to resolve it relative to the parent directory of the manifest path.
2025///   3. Otherwise, return the original relative path.
2026pub(crate) fn resolve_file_path(manifest_path: &PathBuf, file_str: &str) -> PathBuf {
2027    let file_path = Path::new(file_str);
2028    if file_path.is_relative() {
2029        // 1. Try resolving relative to the current working directory.
2030        if let Ok(cwd) = env::current_dir() {
2031            let cwd_path = cwd.join(file_path);
2032            if cwd_path.exists() {
2033                return cwd_path;
2034            }
2035        }
2036        // 2. Try resolving relative to the parent of the manifest path.
2037        if let Some(manifest_parent) = manifest_path.parent() {
2038            let parent_path = manifest_parent.join(file_path);
2039            if parent_path.exists() {
2040                return parent_path;
2041            }
2042        }
2043        // 3. Neither existed; return the relative path as-is.
2044        return file_path.to_path_buf();
2045    }
2046    file_path.to_path_buf()
2047}
2048
2049// --- Example usage ---
2050#[cfg(test)]
2051mod tests {
2052    use crate::e_target::TargetOrigin;
2053
2054    use super::*;
2055
2056    #[test]
2057    fn test_command_builder_example() {
2058        let target_name = "my_example".to_string();
2059        let target = CargoTarget {
2060            name: "my_example".to_string(),
2061            display_name: "My Example".to_string(),
2062            manifest_path: "Cargo.toml".into(),
2063            kind: TargetKind::Example,
2064            extended: true,
2065            toml_specified: false,
2066            origin: Some(TargetOrigin::SingleFile(PathBuf::from(
2067                "examples/my_example.rs",
2068            ))),
2069        };
2070
2071        let extra_args = vec!["--flag".to_string(), "value".to_string()];
2072
2073        let manifest_path = PathBuf::from("Cargo.toml");
2074        let args = CargoCommandBuilder::new(&target_name, &manifest_path, "run", false, false)
2075            .with_target(&target)
2076            .with_extra_args(&extra_args)
2077            .build();
2078
2079        // For an example target, we expect something like:
2080        // cargo run --example my_example --manifest-path Cargo.toml -- --flag value
2081        assert!(args.contains(&"--example".to_string()));
2082        assert!(args.contains(&"my_example".to_string()));
2083        assert!(args.contains(&"--manifest-path".to_string()));
2084        assert!(args.contains(&"Cargo.toml".to_string()));
2085        assert!(args.contains(&"--".to_string()));
2086        assert!(args.contains(&"--flag".to_string()));
2087        assert!(args.contains(&"value".to_string()));
2088    }
2089}