cargo_e/
e_command_builder.rs

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