cargo_e/e_command_builder.rs
1use regex::Regex;
2use std::collections::{HashMap, HashSet};
3use std::env;
4use std::io::Read;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::mpsc::{channel, Sender};
9use std::time::SystemTime;
10use which::which;
11
12use crate::e_cargocommand_ext::CargoProcessResult;
13use crate::e_cargocommand_ext::{CargoCommandExt, CargoDiagnostic, CargoProcessHandle};
14use crate::e_eventdispatcher::{
15 CallbackResponse, CallbackType, CargoDiagnosticLevel, EventDispatcher,
16};
17use crate::e_runner::GLOBAL_CHILDREN;
18use crate::e_target::{CargoTarget, TargetKind, TargetOrigin};
19use std::sync::{Arc, Mutex};
20
21#[derive(Debug, Clone, PartialEq, Copy)]
22pub enum TerminalError {
23 NotConnected,
24 NoTerminal,
25 NoError,
26}
27
28impl Default for TerminalError {
29 fn default() -> Self {
30 TerminalError::NoError
31 }
32}
33
34/// A builder that constructs a Cargo command for a given target.
35#[derive(Clone)]
36pub struct CargoCommandBuilder {
37 pub manifest_path: PathBuf,
38 pub args: Vec<String>,
39 pub subcommand: String,
40 pub pid: Option<u32>,
41 pub alternate_cmd: Option<String>,
42 pub execution_dir: Option<PathBuf>,
43 pub suppressed_flags: HashSet<String>,
44 pub stdout_dispatcher: Option<Arc<EventDispatcher>>,
45 pub stderr_dispatcher: Option<Arc<EventDispatcher>>,
46 pub progress_dispatcher: Option<Arc<EventDispatcher>>,
47 pub stage_dispatcher: Option<Arc<EventDispatcher>>,
48 pub terminal_error_flag: Arc<Mutex<bool>>,
49 pub sender: Option<Arc<Mutex<Sender<TerminalError>>>>,
50 pub diagnostics: Arc<Mutex<Vec<CargoDiagnostic>>>,
51 pub is_filter: bool,
52}
53impl Default for CargoCommandBuilder {
54 fn default() -> Self {
55 Self::new(&PathBuf::from("Cargo.toml"), "run".into(), false)
56 }
57}
58impl CargoCommandBuilder {
59 /// Creates a new, empty builder.
60 pub fn new(manifest: &PathBuf, subcommand: &str, is_filter: bool) -> Self {
61 let (sender, _receiver) = channel::<TerminalError>();
62 let sender = Arc::new(Mutex::new(sender));
63 let mut builder = CargoCommandBuilder {
64 manifest_path: manifest.clone(),
65 args: Vec::new(),
66 subcommand: subcommand.to_string(),
67 pid: None,
68 alternate_cmd: None,
69 execution_dir: None,
70 suppressed_flags: HashSet::new(),
71 stdout_dispatcher: None,
72 stderr_dispatcher: None,
73 progress_dispatcher: None,
74 stage_dispatcher: None,
75 terminal_error_flag: Arc::new(Mutex::new(false)),
76 sender: Some(sender),
77 diagnostics: Arc::new(Mutex::new(Vec::<CargoDiagnostic>::new())),
78 is_filter,
79 };
80 builder.set_default_dispatchers();
81
82 builder
83 }
84
85 // Switch to passthrough mode when the terminal error is detected
86 fn switch_to_passthrough_mode<F>(self: Arc<Self>, on_spawn: F) -> anyhow::Result<u32>
87 where
88 F: FnOnce(u32, CargoProcessHandle),
89 {
90 let mut command = self.build_command();
91
92 // Now, spawn the cargo process in passthrough mode
93 let cargo_process_handle = command.spawn_cargo_passthrough(Arc::clone(&self));
94 let pid = cargo_process_handle.pid;
95 // Notify observer
96 on_spawn(pid, cargo_process_handle);
97
98 Ok(pid)
99 }
100
101 // Set up the default dispatchers, which includes error detection
102 fn set_default_dispatchers(&mut self) {
103 if !self.is_filter {
104 // If this is a filter, we don't need to set up dispatchers
105 return;
106 }
107 let sender = self.sender.clone().unwrap();
108
109 let mut stdout_dispatcher = EventDispatcher::new();
110 stdout_dispatcher.add_callback(
111 r"listening on",
112 Box::new(|line, _captures, _state| {
113 println!("(STDOUT) Dispatcher caught: {}", line);
114 // Use a regex to capture a URL from the line.
115 if let Ok(url_regex) = Regex::new(r"(http://[^\s]+)") {
116 if let Some(url_caps) = url_regex.captures(line) {
117 if let Some(url_match) = url_caps.get(1) {
118 let url = url_match.as_str();
119 // Call open::that on the captured URL.
120 if let Err(e) = open::that(url) {
121 eprintln!("Failed to open URL: {}. Error: {}", url, e);
122 } else {
123 println!("Opened URL: {}", url);
124 }
125 }
126 }
127 } else {
128 eprintln!("Failed to create URL regex");
129 }
130 None
131 }),
132 );
133 stdout_dispatcher.add_callback(
134 r"BuildFinished",
135 Box::new(|line, _captures, _state| {
136 println!("******* {}", line);
137 None
138 }),
139 );
140 stdout_dispatcher.add_callback(
141 r"server listening at:",
142 Box::new(|line, _captures, state| {
143 // If we're not already in multiline mode, this is the initial match.
144 if !state.load(Ordering::Relaxed) {
145 println!("Matched 'server listening at:' in: {}", line);
146 state.store(true, Ordering::Relaxed);
147 Some(CallbackResponse {
148 callback_type: CallbackType::Note, // Choose as appropriate
149 message: Some(format!("Started multiline mode after: {}", line)),
150 file: None,
151 line: None,
152 column: None,
153 suggestion: None,
154 terminal_status: None,
155 })
156 } else {
157 // We are in multiline mode; process subsequent lines.
158 println!("Multiline callback received: {}", line);
159 // Use a regex to capture a URL from the line.
160 let url_regex = match Regex::new(r"(http://[^\s]+)") {
161 Ok(regex) => regex,
162 Err(e) => {
163 eprintln!("Failed to create URL regex: {}", e);
164 return None;
165 }
166 };
167 if let Some(url_caps) = url_regex.captures(line) {
168 let url = url_caps.get(1).unwrap().as_str();
169 // Call open::that on the captured URL.
170 match open::that(url) {
171 Ok(_) => println!("Opened URL: {}", url),
172 Err(e) => eprintln!("Failed to open URL: {}. Error: {}", url, e),
173 }
174 // End multiline mode.
175 state.store(false, Ordering::Relaxed);
176 Some(CallbackResponse {
177 callback_type: CallbackType::Note, // Choose as appropriate
178 message: Some(format!("Captured and opened URL: {}", url)),
179 file: None,
180 line: None,
181 column: None,
182 suggestion: None,
183 terminal_status: None,
184 })
185 } else {
186 None
187 }
188 }
189 }),
190 );
191
192 let mut stderr_dispatcher = EventDispatcher::new();
193
194 let suggestion_mode = Arc::new(AtomicBool::new(false));
195 let suggestion_regex = Regex::new(r"^\s*(\d+)\s*\|\s*(.*)$").unwrap();
196 let warning_location: Arc<Mutex<Option<CallbackResponse>>> = Arc::new(Mutex::new(None));
197 let pending_diag: Arc<Mutex<Option<CargoDiagnostic>>> = Arc::new(Mutex::new(None));
198 let diagnostic_counts: Arc<Mutex<HashMap<CargoDiagnosticLevel, usize>>> =
199 Arc::new(Mutex::new(HashMap::new()));
200
201 let pending_d = Arc::clone(&pending_diag);
202 let counts = Arc::clone(&diagnostic_counts);
203
204 let diagnostics_arc = Arc::clone(&self.diagnostics);
205 stderr_dispatcher.add_callback(
206 r"^(?P<level>\w+)(\[(?P<error_code>E\d+)\])?:\s+(?P<msg>.+)$", // Regex for diagnostic line
207 Box::new(move |_line, caps, _multiline_flag| {
208 if let Some(caps) = caps {
209 let mut counts = counts.lock().unwrap();
210 // Create a PendingDiag and save the message
211 let mut pending_diag = pending_d.lock().unwrap();
212 let mut last_lineref = String::new();
213 if let Some(existing_diag) = pending_diag.take() {
214 let mut diags = diagnostics_arc.lock().unwrap();
215 last_lineref = existing_diag.lineref.clone();
216 // let diag_level = match existing_diag.level.as_str() {
217 // "error" => CargoDiagnosticLevel::Error,
218 // "warning" => CargoDiagnosticLevel::Warning,
219 // "help" => CargoDiagnosticLevel::Help,
220 // "note" => CargoDiagnosticLevel::Note,
221 // _ => {
222 // println!("Unknown diagnostic level: {}", existing_diag.level);
223 // return None; // Ignore unknown levels
224 // }
225 // };
226 // *counts.entry(diag_level).or_insert(0) += 1;
227 // let current_count = counts.get(&diag_level).unwrap_or(&0);
228 // existing_diag.diag_number = Some(*current_count);
229 //println!("{:?}", existing_diag);
230 diags.push(existing_diag.clone());
231 }
232 log::trace!("Diagnostic line: {}", _line);
233 let level = caps["level"].to_string(); // e.g., "warning", "error"
234 // If the diagnostic level is "note" or "help", skip processing.
235 // if level == "note" || level == "help" {
236 // println!("Skipping note/help diagnostic: {}", _line);
237 // return None;
238 // }
239 let message = caps["msg"].to_string();
240 // If the message contains "generated" followed by one or more digits,
241 // then ignore this diagnostic by returning None.
242 //
243 // Using a regex to check for "generated\s+\d+"
244 let re_generated = regex::Regex::new(r"generated\s+\d+").unwrap();
245 if re_generated.is_match(&message) {
246 log::trace!("Skipping generated diagnostic: {}", _line);
247 return None;
248 }
249
250 let error_code = caps.name("error_code").map(|m| m.as_str().to_string());
251 let diag_level = match level.as_str() {
252 "error" => CargoDiagnosticLevel::Error,
253 "warning" => CargoDiagnosticLevel::Warning,
254 "help" => CargoDiagnosticLevel::Help,
255 "note" => CargoDiagnosticLevel::Note,
256 _ => {
257 println!("Unknown diagnostic level: {}", level);
258 return None; // Ignore unknown levels
259 }
260 };
261 // Increment the count for this level
262 *counts.entry(diag_level).or_insert(0) += 1;
263 // // Check if the previous diagnostic
264
265 let current_count = counts.get(&diag_level).unwrap_or(&0);
266 let diag = CargoDiagnostic {
267 error_code: error_code.clone(),
268 lineref: last_lineref.clone(),
269 level: level.clone(),
270 message,
271 suggestion: None,
272 help: None,
273 note: None,
274 uses_color: true,
275 diag_num_padding: Some(2),
276 diag_number: Some(*current_count),
277 };
278
279 // // Check if the previous diagnostic has a callback response
280 // if let Some(existing_diag) = pending_diag.take() {
281 // // Save the current diagnostic's callback response
282 // if let Some(response) = existing_diag.callback_response {
283 // // Save it into the new diagnostic
284 // diag.save_callback_response(response);
285 // }
286 // }
287
288 // Save the new diagnostic
289 *pending_diag = Some(diag);
290
291 // Track the count of diagnostics for each level
292 return Some(CallbackResponse {
293 callback_type: CallbackType::LevelMessage, // Treat subsequent lines as warnings
294 message: None,
295 file: None,
296 line: None,
297 column: None,
298 suggestion: None, // This is the suggestion part
299 terminal_status: None,
300 });
301 } else {
302 println!("No captures found in line: {}", _line);
303 None
304 }
305 }),
306 );
307
308 {
309 let location_lock_clone = Arc::clone(&warning_location);
310 let suggestion_m = Arc::clone(&suggestion_mode);
311
312 // Suggestion callback that adds subsequent lines as suggestions
313 stderr_dispatcher.add_callback(
314 r"^(?P<msg>.*)$", // Capture all lines following the location
315 Box::new(move |line, _captures, _multiline_flag| {
316 if suggestion_m.load(Ordering::Relaxed) {
317 // Only process lines that match the suggestion format
318 if let Some(caps) = suggestion_regex.captures(line.trim()) {
319 // Capture the line number and code from the suggestion line
320 // let line_num = caps[1].parse::<usize>().unwrap_or(0);
321 let code = caps[2].to_string();
322
323 // Lock the pending_diag to add the suggestion
324 if let Ok(mut lock) = location_lock_clone.lock() {
325 if let Some(mut loc) = lock.take() {
326 // let file = loc.file.clone().unwrap_or_default();
327 // let col = loc.column.unwrap_or(0);
328
329 // Concatenate the suggestion line to the message
330 let mut msg = loc.message.unwrap_or_default();
331 msg.push_str(&format!("\n{}", code));
332
333 // Print the concatenated suggestion for debugging
334 // println!("daveSuggestion for {}:{}:{} - {}", file, line_num, col, msg);
335
336 // Update the location with the new concatenated message
337 loc.message = Some(msg.clone());
338 // println!("Updating location lock with new suggestion: {}", msg);
339 // Save the updated location back to shared state
340 // if let Ok(mut lock) = location_lock_clone.lock() {
341 // println!("Updating location lock with new suggestion: {}", msg);
342 lock.replace(loc);
343 // } else {
344 // eprintln!("Failed to acquire lock for location_lock_clone");
345 // }
346 }
347 // return Some(CallbackResponse {
348 // callback_type: CallbackType::Warning, // Treat subsequent lines as warnings
349 // message: Some(msg.clone()),
350 // file: Some(file),
351 // line: Some(line_num),
352 // column: Some(col),
353 // suggestion: Some(msg), // This is the suggestion part
354 // terminal_status: None,
355 // });
356 }
357 }
358 } else {
359 // println!("Suggestion mode is not active. Ignoring line: {}", line);
360 }
361
362 None
363 }),
364 );
365 }
366 {
367 let suggestion_m = Arc::clone(&suggestion_mode);
368 let pending_diag_clone = Arc::clone(&pending_diag);
369 let diagnostics_arc = Arc::clone(&self.diagnostics);
370 // Callback for handling when an empty line or new diagnostic is received
371 stderr_dispatcher.add_callback(
372 r"^\s*$", // Regex to capture empty line
373 Box::new(move |_line, _captures, _multiline_flag| {
374 // println!("Empty line detected: {}", line);
375 suggestion_m.store(false, Ordering::Relaxed);
376 // End of current diagnostic: take and process it.
377 if let Some(pending_diag) = pending_diag_clone.lock().unwrap().take() {
378 //println!("{:?}", pending_diag);
379 // Use diagnostics_arc instead of self.diagnostices
380 let mut diags = diagnostics_arc.lock().unwrap();
381 diags.push(pending_diag.clone());
382 } else {
383 // println!("No pending diagnostic to process.");
384 }
385 // Handle empty line scenario to end the current diagnostic processing
386 // if let Some(pending_diag) = pending_diag_clone.lock().unwrap().take() {
387 // println!("{:?}", pending_diag);
388 // let mut diags = self.diagnostics.lock().unwrap();
389 // diags.push(pending_diag.clone());
390 // // let diag = crate::e_eventdispatcher::convert_message_to_diagnostic(msg, &msg_str);
391 // // diags.push(diag.clone());
392 // // if let Some(ref sd) = stage_disp_clone {
393 // // sd.dispatch(&format!("Stage: Diagnostic occurred at {:?}", now));
394 // // }
395 // // Handle the saved PendingDiag and its CallbackResponse
396 // // if let Some(callback_response) = pending_diag.callback_response {
397 // // println!("End of Diagnostic: {:?}", callback_response);
398 // // }
399 // } else {
400 // println!("No pending diagnostic to process.");
401 // }
402
403 None
404 }),
405 );
406 }
407
408 // {
409 // let pending_diag = Arc::clone(&pending_diag);
410 // let location_lock = Arc::clone(&warning_location);
411 // let suggestion_m = Arc::clone(&suggestion_mode);
412
413 // let suggestion_regex = Regex::new(r"^\s*(\d+)\s*\|\s*(.*)$").unwrap();
414
415 // stderr_dispatcher.add_callback(
416 // r"^\s*(\d+)\s*\|\s*(.*)$", // Match suggestion line format
417 // Box::new(move |line, _captures, _multiline_flag| {
418 // if suggestion_m.load(Ordering::Relaxed) {
419 // // Only process lines that match the suggestion format
420 // if let Some(caps) = suggestion_regex.captures(line.trim()) {
421 // // Capture the line number and code from the suggestion line
422 // let line_num = caps[1].parse::<usize>().unwrap_or(0);
423 // let code = caps[2].to_string();
424
425 // // Lock the pending_diag to add the suggestion
426 // if let Some(mut loc) = location_lock.lock().unwrap().take() {
427 // println!("Suggestion line: {}", line);
428 // let file = loc.file.clone().unwrap_or_default();
429 // let col = loc.column.unwrap_or(0);
430
431 // // Concatenate the suggestion line to the message
432 // let mut msg = loc.message.unwrap_or_default();
433 // msg.push_str(&format!("\n{} | {}", line_num, code)); // Append the suggestion properly
434
435 // // Print the concatenated suggestion for debugging
436 // println!("Suggestion for {}:{}:{} - {}", file, line_num, col, msg);
437
438 // // Update the location with the new concatenated message
439 // loc.message = Some(msg.clone());
440
441 // // Save the updated location back to shared state
442 // location_lock.lock().unwrap().replace(loc);
443
444 // // return Some(CallbackResponse {
445 // // callback_type: CallbackType::Warning, // Treat subsequent lines as warnings
446 // // message: Some(msg.clone()),
447 // // file: Some(file),
448 // // line: Some(line_num),
449 // // column: Some(col),
450 // // suggestion: Some(msg), // This is the suggestion part
451 // // terminal_status: None,
452 // // });
453 // } else {
454 // println!("No location information available for suggestion line: {}", line);
455 // }
456 // } else {
457 // println!("Suggestion line does not match expected format: {}", line);
458 // }
459 // } else {
460 // println!("Suggestion mode is not active. Ignoring line: {}", line);
461 // }
462
463 // None
464 // }),
465 // );
466
467 // }
468
469 {
470 let location_lock = Arc::clone(&warning_location);
471 let pending_diag = Arc::clone(&pending_diag);
472 let suggestion_mode = Arc::clone(&suggestion_mode);
473 stderr_dispatcher.add_callback(
474 r"^(?P<msg>.*)$", // Capture all lines following the location
475 Box::new(move |line, _captures, _multiline_flag| {
476 // Lock the location to fetch the original diagnostic info
477 if let Ok(location_guard) = location_lock.lock() {
478 if let Some(loc) = location_guard.as_ref() {
479 let file = loc.file.clone().unwrap_or_default();
480 let line_num = loc.line.unwrap_or(0);
481 let col = loc.column.unwrap_or(0);
482 // println!("SUGGESTION: Suggestion for {}:{}:{} {}", file, line_num, col, line);
483
484 // Only treat lines starting with | or numbers as suggestion lines
485 if line.trim().starts_with('|')
486 || line.trim().starts_with(char::is_numeric)
487 {
488 // Get the existing suggestion and append the new line
489 let suggestion = line.trim();
490
491 // Print the suggestion for debugging
492 // println!("Suggestion for {}:{}:{} - {}", file, line_num, col, suggestion);
493
494 // Lock the pending_diag and update its callback_response field
495 let mut pending_diag = match pending_diag.lock() {
496 Ok(lock) => lock,
497 Err(e) => {
498 eprintln!("Failed to acquire lock: {}", e);
499 return None; // Handle the error appropriately
500 }
501 };
502 if let Some(diag) = pending_diag.take() {
503 // If a PendingDiag already exists, update the existing callback response with the new suggestion
504 let mut diag = diag;
505
506 // Append the new suggestion to the existing one
507 if let Some(ref mut existing) = diag.suggestion {
508 diag.suggestion =
509 Some(format!("{}\n{}", existing, suggestion));
510 } else {
511 diag.suggestion = Some(suggestion.to_string());
512 }
513
514 // Update the shared state with the new PendingDiag
515 *pending_diag = Some(diag.clone());
516 return Some(CallbackResponse {
517 callback_type: CallbackType::Suggestion, // Treat subsequent lines as warnings
518 message: Some(
519 diag.clone().suggestion.clone().unwrap().clone(),
520 ),
521 file: Some(file),
522 line: Some(line_num),
523 column: Some(col),
524 suggestion: diag.clone().suggestion.clone(), // This is the suggestion part
525 terminal_status: None,
526 });
527 } else {
528 // println!("No pending diagnostic to process for suggestion line: {}", line);
529 }
530 } else {
531 // If the line doesn't match the suggestion format, just return it as is
532 if line.trim().is_empty() {
533 // Ignore empty lines
534 suggestion_mode.store(false, Ordering::Relaxed);
535 return None;
536 }
537 }
538 } else {
539 // println!("No location information available for suggestion line: {}", line);
540 }
541 }
542 None
543 }),
544 );
545 }
546
547 // 2) Location callback stores its response into that shared state
548 {
549 let pending_diag = Arc::clone(&pending_diag);
550 let warning_location = Arc::clone(&warning_location);
551 let location_lock = Arc::clone(&warning_location);
552 let suggestion_mode = Arc::clone(&suggestion_mode);
553 let manifest_path = self.manifest_path.clone();
554 stderr_dispatcher.add_callback(
555 // r"^\s*-->\s+(?P<file>[^:]+):(?P<line>\d+):(?P<col>\d+)$",
556 r"^\s*-->\s+(?P<file>.+?)(?::(?P<line>\d+))?(?::(?P<col>\d+))?\s*$",
557 Box::new(move |_line, caps, _multiline_flag| {
558 log::trace!("Location line: {}", _line);
559 // if multiline_flag.load(Ordering::Relaxed) {
560 if let Some(caps) = caps {
561 let file = caps["file"].to_string();
562 let resolved_path = resolve_file_path(&manifest_path, &file);
563 let file = resolved_path.to_str().unwrap_or_default().to_string();
564 let line = caps["line"].parse::<usize>().unwrap_or(0);
565 let column = caps["col"].parse::<usize>().unwrap_or(0);
566 let resp = CallbackResponse {
567 callback_type: CallbackType::Location,
568 message: format!("{}:{}:{}", file, line, column).into(),
569 file: Some(file.clone()),
570 line: Some(line),
571 column: Some(column),
572 suggestion: None,
573 terminal_status: None,
574 };
575 // Lock the pending_diag and update its callback_response field
576 let mut pending_diag = pending_diag.lock().unwrap();
577 if let Some(diag) = pending_diag.take() {
578 // If a PendingDiag already exists, save the new callback response in the existing PendingDiag
579 let mut diag = diag;
580 diag.lineref = format!("{}:{}:{}", file, line, column); // Update the lineref
581 // diag.save_callback_response(resp.clone()); // Save the callback response
582 // Update the shared state with the new PendingDiag
583 *pending_diag = Some(diag);
584 }
585 // Save it for the generic callback to see
586 *warning_location.lock().unwrap() = Some(resp.clone());
587 *location_lock.lock().unwrap() = Some(resp.clone());
588 // Set suggestion mode to true as we've encountered a location line
589 suggestion_mode.store(true, Ordering::Relaxed);
590 return Some(resp.clone());
591 } else {
592 println!("No captures found in line: {}", _line);
593 }
594 // }
595 None
596 }),
597 );
598 }
599
600 // // 3) Note callback — attach note to pending_diag
601 {
602 let pending_diag = Arc::clone(&pending_diag);
603 stderr_dispatcher.add_callback(
604 r"^\s*=\s*note:\s*(?P<msg>.+)$",
605 Box::new(move |_line, caps, _state| {
606 if let Some(caps) = caps {
607 let mut pending_diag = pending_diag.lock().unwrap();
608 if let Some(ref mut resp) = *pending_diag {
609 // Prepare the new note with the blue prefix
610 let new_note =
611 format!("\x1b[34m{}\x1b[0m: {}", "note", caps["msg"].to_string());
612
613 // Append or set the note
614 if let Some(existing_note) = &resp.note {
615 // If there's already a note, append with newline and the new note
616 resp.note = Some(format!("{}\n{}", existing_note, new_note));
617 } else {
618 // If no existing note, just set the new note
619 resp.note = Some(new_note);
620 }
621 }
622 }
623 None
624 }),
625 );
626 }
627
628 // 4) Help callback — attach help to pending_diag
629 {
630 let pending_diag = Arc::clone(&pending_diag);
631 stderr_dispatcher.add_callback(
632 r"^\s*(?:\=|\|)\s*help:\s*(?P<msg>.+)$", // Regex to match both '=' and '|' before help:
633 Box::new(move |_line, caps, _state| {
634 if let Some(caps) = caps {
635 let mut pending_diag = pending_diag.lock().unwrap();
636 if let Some(ref mut resp) = *pending_diag {
637 // Create the new help message with the orange "h:" prefix
638 let new_help =
639 format!("\x1b[38;5;214mhelp: {}\x1b[0m", caps["msg"].to_string());
640
641 // Append or set the help message
642 if let Some(existing_help) = &resp.help {
643 // If there's already a help message, append with newline
644 resp.help = Some(format!("{}\n{}", existing_help, new_help));
645 } else {
646 // If no existing help message, just set the new one
647 resp.help = Some(new_help);
648 }
649 }
650 }
651 None
652 }),
653 );
654 }
655
656 stderr_dispatcher.add_callback(
657 r"(?:\x1b\[[0-9;]*[A-Za-z])*\s*Serving(?:\x1b\[[0-9;]*[A-Za-z])*\s+at\s+(http://[^\s]+)",
658 Box::new(|line, captures, _state| {
659 if let Some(caps) = captures {
660 let url = caps.get(1).unwrap().as_str();
661 println!("(STDERR) Captured URL: {}", url);
662 match open::that(url) {
663 Ok(_) => println!("(STDERR) Opened URL: {}", url),
664 Err(e) => eprintln!("(STDERR) Failed to open URL: {}. Error: {:?}", url, e),
665 }
666 Some(CallbackResponse {
667 callback_type: CallbackType::OpenedUrl, // Choose as appropriate
668 message: Some(format!("Captured and opened URL: {}", url)),
669 file: None,
670 line: None,
671 column: None,
672 suggestion: None,
673 terminal_status: None,
674 })
675 } else {
676 println!("(STDERR) No URL captured in line: {}", line);
677 None
678 }
679 }),
680);
681
682 let finished_flag = Arc::new(AtomicBool::new(false));
683
684 // 0) Finished‐profile summary callback
685 {
686 let finished_flag = Arc::clone(&finished_flag);
687 stderr_dispatcher.add_callback(
688 r"^Finished\s+`(?P<profile>[^`]+)`\s+profile\s+\[(?P<opts>[^\]]+)\]\s+target\(s\)\s+in\s+(?P<dur>[0-9.]+s)$",
689 Box::new(move |_line, caps, _multiline_flag| {
690 if let Some(caps) = caps {
691 finished_flag.store(true, Ordering::Relaxed);
692 let profile = &caps["profile"];
693 let opts = &caps["opts"];
694 let dur = &caps["dur"];
695 Some(CallbackResponse {
696 callback_type: CallbackType::Note,
697 message: Some(format!("Finished `{}` [{}] in {}", profile, opts, dur)),
698 file: None, line: None, column: None, suggestion: None, terminal_status: None,
699 })
700 } else {
701 None
702 }
703 }),
704 );
705 }
706
707 let summary_flag = Arc::new(AtomicBool::new(false));
708 {
709 let summary_flag = Arc::clone(&summary_flag);
710 stderr_dispatcher.add_callback(
711 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",
712 Box::new(move |_line, caps, multiline_flag| {
713 let summary_flag = Arc::clone(&summary_flag);
714 if let Some(caps) = caps {
715 summary_flag.store(true, Ordering::Relaxed);
716 // Always start fresh
717 multiline_flag.store(false, Ordering::Relaxed);
718
719 let level = &caps["level"];
720 let name = &caps["name"];
721 let otype = &caps["otype"];
722 let count: usize = caps["count"].parse().unwrap_or(0);
723 let kind = &caps["kind"]; // "warnings" or "errors"
724 let cmd = caps["cmd"].to_string();
725 let fixes: usize = caps["fixes"].parse().unwrap_or(0);
726
727 println!("SUMMARIZATION CALLBACK {}",
728 &format!("{}: `{}` ({}) generated {} {}; run `{}` to apply {} fixes",
729 level, name, otype, count, kind, cmd, fixes));
730 Some(CallbackResponse {
731 callback_type: CallbackType::Note, // treat as informational
732 message: Some(format!(
733 "{}: `{}` ({}) generated {} {}; run `{}` to apply {} fixes",
734 level, name, otype, count, kind, cmd, fixes
735 )),
736 file: None,
737 line: None,
738 column: None,
739 suggestion: Some(cmd),
740 terminal_status: None,
741 })
742 } else {
743 None
744 }
745 }),
746 );
747 }
748
749 // {
750 // let summary_flag = Arc::clone(&summary_flag);
751 // let finished_flag = Arc::clone(&finished_flag);
752 // let warning_location = Arc::clone(&warning_location);
753 // // Warning callback for stdout.
754 // stderr_dispatcher.add_callback(
755 // r"^warning:\s+(?P<msg>.+)$",
756 // Box::new(
757 // move |line: &str, captures: Option<regex::Captures>, multiline_flag: Arc<AtomicBool>| {
758 // // If summary or finished just matched, skip
759 // if summary_flag.swap(false, Ordering::Relaxed)
760 // || finished_flag.swap(false, Ordering::Relaxed)
761 // {
762 // return None;
763 // }
764
765 // // 2) If this line *matches* the warning regex, handle as a new warning
766 // if let Some(caps) = captures {
767 // let msg = caps.name("msg").unwrap().as_str().to_string();
768 // // 1) If a location was saved, print file:line:col – msg
769 // // println!("*WARNING detected: {:?}", msg);
770 // multiline_flag.store(true, Ordering::Relaxed);
771 // if let Some(loc) = warning_location.lock().unwrap().take() {
772 // let file = loc.file.unwrap_or_default();
773 // let line_num = loc.line.unwrap_or(0);
774 // let col = loc.column.unwrap_or(0);
775 // println!("{}:{}:{} - {}", file, line_num, col, msg);
776 // return Some(CallbackResponse {
777 // callback_type: CallbackType::Warning,
778 // message: Some(msg.to_string()),
779 // file: None, line: None, column: None, suggestion: None, terminal_status: None,
780 // });
781 // }
782 // return Some(CallbackResponse {
783 // callback_type: CallbackType::Warning,
784 // message: Some(msg),
785 // file: None,
786 // line: None,
787 // column: None,
788 // suggestion: None,
789 // terminal_status: None,
790 // });
791 // }
792
793 // // 3) Otherwise, if we’re in multiline mode, treat as continuation
794 // if multiline_flag.load(Ordering::Relaxed) {
795 // let text = line.trim();
796 // if text.is_empty() {
797 // multiline_flag.store(false, Ordering::Relaxed);
798 // return None;
799 // }
800 // // println!(" - {:?}", text);
801 // return Some(CallbackResponse {
802 // callback_type: CallbackType::Warning,
803 // message: Some(text.to_string()),
804 // file: None,
805 // line: None,
806 // column: None,
807 // suggestion: None,
808 // terminal_status: None,
809 // });
810 // }
811 // None
812 // },
813 // ),
814 // );
815 // }
816
817 stderr_dispatcher.add_callback(
818 r"IO\(Custom \{ kind: NotConnected",
819 Box::new(move |line, _captures, _state| {
820 println!("(STDERR) Terminal error detected: {:?}", &line);
821 let result = if line.contains("NotConnected") {
822 TerminalError::NoTerminal
823 } else {
824 TerminalError::NoError
825 };
826 let sender = sender.lock().unwrap();
827 sender.send(result).ok();
828 Some(CallbackResponse {
829 callback_type: CallbackType::Warning, // Choose as appropriate
830 message: Some(format!("Terminal Error: {}", line)),
831 file: None,
832 line: None,
833 column: None,
834 suggestion: None,
835 terminal_status: None,
836 })
837 }),
838 );
839 stderr_dispatcher.add_callback(
840 r".*",
841 Box::new(|line, _captures, _state| {
842 log::trace!("stdraw[{:?}]", line);
843 None // We're just printing, so no callback response is needed.
844 }),
845 );
846 self.stderr_dispatcher = Some(Arc::new(stderr_dispatcher));
847
848 // let mut progress_dispatcher = EventDispatcher::new();
849 // progress_dispatcher.add_callback(r"Progress", Box::new(|line, _captures,_state| {
850 // println!("(Progress) {}", line);
851 // None
852 // }));
853 // self.progress_dispatcher = Some(Arc::new(progress_dispatcher));
854
855 // let mut stage_dispatcher = EventDispatcher::new();
856 // stage_dispatcher.add_callback(r"Stage:", Box::new(|line, _captures, _state| {
857 // println!("(Stage) {}", line);
858 // None
859 // }));
860 // self.stage_dispatcher = Some(Arc::new(stage_dispatcher));
861 }
862
863 pub fn run<F>(self: Arc<Self>, on_spawn: F) -> anyhow::Result<u32>
864 where
865 F: FnOnce(u32, CargoProcessHandle),
866 {
867 if !self.is_filter {
868 return self.switch_to_passthrough_mode(on_spawn);
869 }
870 let mut command = self.build_command();
871
872 let mut cargo_process_handle = command.spawn_cargo_capture(
873 self.clone(),
874 self.stdout_dispatcher.clone(),
875 self.stderr_dispatcher.clone(),
876 self.progress_dispatcher.clone(),
877 self.stage_dispatcher.clone(),
878 None,
879 );
880 cargo_process_handle.diagnostics = Arc::clone(&self.diagnostics);
881 let pid = cargo_process_handle.pid;
882
883 // Notify observer
884 on_spawn(pid, cargo_process_handle);
885
886 Ok(pid)
887 }
888
889 // pub fn run(self: Arc<Self>) -> anyhow::Result<u32> {
890 // // Build the command using the builder's configuration
891 // let mut command = self.build_command();
892
893 // // Spawn the cargo process handle
894 // let cargo_process_handle = command.spawn_cargo_capture(
895 // self.stdout_dispatcher.clone(),
896 // self.stderr_dispatcher.clone(),
897 // self.progress_dispatcher.clone(),
898 // self.stage_dispatcher.clone(),
899 // None,
900 // );
901 // let pid = cargo_process_handle.pid;
902 // let mut global = GLOBAL_CHILDREN.lock().unwrap();
903 // global.insert(pid, Arc::new(Mutex::new(cargo_process_handle)));
904 // Ok(pid)
905 // }
906
907 pub fn wait(self: Arc<Self>, pid: Option<u32>) -> anyhow::Result<CargoProcessResult> {
908 let mut global = GLOBAL_CHILDREN.lock().unwrap();
909 if let Some(pid) = pid {
910 // Lock the global list of processes and attempt to find the cargo process handle directly by pid
911 if let Some(cargo_process_handle) = global.get_mut(&pid) {
912 let mut cargo_process_handle = cargo_process_handle.lock().unwrap();
913
914 // Wait for the process to finish and retrieve the result
915 // println!("Waiting for process with PID: {}", pid);
916 // let result = cargo_process_handle.wait();
917 // println!("Process with PID {} finished", pid);
918 loop {
919 println!("Waiting for process with PID: {}", pid);
920
921 // Attempt to wait for the process, but don't block indefinitely
922 let status = cargo_process_handle.child.try_wait()?;
923
924 // If the status is `Some(status)`, the process has finished
925 if let Some(status) = status {
926 if status.code() == Some(101) {
927 println!("Process with PID {} finished with cargo error", pid);
928 }
929
930 // Check the terminal error flag and update the result if there is an error
931 if *cargo_process_handle.terminal_error_flag.lock().unwrap()
932 != TerminalError::NoError
933 {
934 let terminal_error =
935 *cargo_process_handle.terminal_error_flag.lock().unwrap();
936 cargo_process_handle.result.terminal_error = Some(terminal_error);
937 }
938
939 let final_diagnostics = {
940 let diag_lock = self.diagnostics.lock().unwrap();
941 diag_lock.clone()
942 };
943 cargo_process_handle.result.diagnostics = final_diagnostics.clone();
944 cargo_process_handle.result.exit_status = Some(status);
945 cargo_process_handle.result.end_time = Some(SystemTime::now());
946 cargo_process_handle.result.elapsed_time = Some(
947 cargo_process_handle
948 .result
949 .end_time
950 .unwrap()
951 .duration_since(cargo_process_handle.result.start_time.unwrap())
952 .unwrap(),
953 );
954 println!(
955 "Process with PID {} finished {:?} {}",
956 pid,
957 status,
958 final_diagnostics.len()
959 );
960 return Ok(cargo_process_handle.result.clone());
961 // return Ok(CargoProcessResult { exit_status: status, ..Default::default() });
962 }
963
964 // Sleep briefly to yield control back to the system and avoid blocking
965 std::thread::sleep(std::time::Duration::from_secs(1));
966 }
967
968 // Return the result
969 // match result {
970 // Ok(res) => Ok(res),
971 // Err(e) => Err(anyhow::anyhow!("Failed to wait for cargo process: {}", e).into()),
972 // }
973 } else {
974 Err(anyhow::anyhow!(
975 "Process handle with PID {} not found in GLOBAL_CHILDREN",
976 pid
977 )
978 .into())
979 }
980 } else {
981 Err(anyhow::anyhow!("No PID provided for waiting on cargo process").into())
982 }
983 }
984
985 // pub fn run_wait(self: Arc<Self>) -> anyhow::Result<CargoProcessResult> {
986 // // Run the cargo command and get the process handle (non-blocking)
987 // let pid = self.clone().run()?; // adds to global list of processes
988 // let result = self.wait(Some(pid)); // Wait for the process to finish
989 // // Remove the completed process from GLOBAL_CHILDREN
990 // let mut global = GLOBAL_CHILDREN.lock().unwrap();
991 // global.remove(&pid);
992
993 // result
994 // }
995
996 // Runs the cargo command using the builder's configuration.
997 // pub fn run(&self) -> anyhow::Result<CargoProcessResult> {
998 // // Build the command using the builder's configuration
999 // let mut command = self.build_command();
1000
1001 // // Now use the `spawn_cargo_capture` extension to run the command
1002 // let mut cargo_process_handle = command.spawn_cargo_capture(
1003 // self.stdout_dispatcher.clone(),
1004 // self.stderr_dispatcher.clone(),
1005 // self.progress_dispatcher.clone(),
1006 // self.stage_dispatcher.clone(),
1007 // None,
1008 // );
1009
1010 // // Wait for the process to finish and retrieve the results
1011 // cargo_process_handle.wait().context("Failed to execute cargo process")
1012 // }
1013
1014 /// Configure the command based on the target kind.
1015 pub fn with_target(mut self, target: &CargoTarget) -> Self {
1016 if let Some(origin) = target.origin.clone() {
1017 println!("Target origin: {:?}", origin);
1018 } else {
1019 println!("Target origin is not set");
1020 }
1021 match target.kind {
1022 TargetKind::Unknown | TargetKind::Plugin => {
1023 return self;
1024 }
1025 TargetKind::Bench => {
1026 // // To run benchmarks, use the "bench" command.
1027 // let exe_path = match which("bench") {
1028 // Ok(path) => path,
1029 // Err(err) => {
1030 // eprintln!("Error: 'trunk' not found in PATH: {}", err);
1031 // return self;
1032 // }
1033 // };
1034 // self.alternate_cmd = Some("bench".to_string())
1035 self.args.push("bench".into());
1036 self.args.push(target.name.clone());
1037 }
1038 TargetKind::Test => {
1039 self.args.push("test".into());
1040 // Pass the target's name as a filter to run specific tests.
1041 self.args.push(target.name.clone());
1042 }
1043 TargetKind::UnknownExample
1044 | TargetKind::UnknownExtendedExample
1045 | TargetKind::Example
1046 | TargetKind::ExtendedExample => {
1047 self.args.push(self.subcommand.clone());
1048 //self.args.push("--message-format=json".into());
1049 self.args.push("--example".into());
1050 self.args.push(target.name.clone());
1051 self.args.push("--manifest-path".into());
1052 self.args.push(
1053 target
1054 .manifest_path
1055 .clone()
1056 .to_str()
1057 .unwrap_or_default()
1058 .to_owned(),
1059 );
1060 self = self.with_required_features(&target.manifest_path, target);
1061 }
1062 TargetKind::UnknownBinary
1063 | TargetKind::UnknownExtendedBinary
1064 | TargetKind::Binary
1065 | TargetKind::ExtendedBinary => {
1066 self.args.push(self.subcommand.clone());
1067 self.args.push("--bin".into());
1068 self.args.push(target.name.clone());
1069 self.args.push("--manifest-path".into());
1070 self.args.push(
1071 target
1072 .manifest_path
1073 .clone()
1074 .to_str()
1075 .unwrap_or_default()
1076 .to_owned(),
1077 );
1078 self = self.with_required_features(&target.manifest_path, target);
1079 }
1080 TargetKind::Manifest => {
1081 self.suppressed_flags.insert("quiet".to_string());
1082 self.args.push(self.subcommand.clone());
1083 self.args.push("--manifest-path".into());
1084 self.args.push(
1085 target
1086 .manifest_path
1087 .clone()
1088 .to_str()
1089 .unwrap_or_default()
1090 .to_owned(),
1091 );
1092 }
1093 TargetKind::ManifestTauriExample => {
1094 self.suppressed_flags.insert("quiet".to_string());
1095 self.args.push(self.subcommand.clone());
1096 self.args.push("--example".into());
1097 self.args.push(target.name.clone());
1098 self.args.push("--manifest-path".into());
1099 self.args.push(
1100 target
1101 .manifest_path
1102 .clone()
1103 .to_str()
1104 .unwrap_or_default()
1105 .to_owned(),
1106 );
1107 self = self.with_required_features(&target.manifest_path, target);
1108 }
1109 TargetKind::ScriptScriptisto => {
1110 let exe_path = match which("scriptisto") {
1111 Ok(path) => path,
1112 Err(err) => {
1113 eprintln!("Error: 'scriptisto' not found in PATH: {}", err);
1114 return self;
1115 }
1116 };
1117 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1118 let candidate_opt = match &target.origin {
1119 Some(TargetOrigin::SingleFile(path))
1120 | Some(TargetOrigin::DefaultBinary(path)) => Some(path),
1121 _ => None,
1122 };
1123 if let Some(candidate) = candidate_opt {
1124 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1125 self.args.push(candidate.to_string_lossy().to_string());
1126 } else {
1127 println!("No scriptisto origin found for: {:?}", target);
1128 }
1129 }
1130 TargetKind::ScriptRustScript => {
1131 let exe_path = match crate::e_installer::ensure_rust_script() {
1132 Ok(p) => p,
1133 Err(e) => {
1134 eprintln!("{}", e);
1135 return self;
1136 }
1137 };
1138 let candidate_opt = match &target.origin {
1139 Some(TargetOrigin::SingleFile(path))
1140 | Some(TargetOrigin::DefaultBinary(path)) => Some(path),
1141 _ => None,
1142 };
1143 if let Some(candidate) = candidate_opt {
1144 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1145 if self.is_filter {
1146 self.args.push("-c".into()); // ask for cargo output
1147 }
1148 self.args.push(candidate.to_string_lossy().to_string());
1149 } else {
1150 println!("No rust-script origin found for: {:?}", target);
1151 }
1152 }
1153 TargetKind::ManifestTauri => {
1154 // First, locate the Cargo.toml using the existing function
1155 let manifest_path = crate::locate_manifest(true).unwrap_or_else(|_| {
1156 eprintln!("Error: Unable to locate Cargo.toml file.");
1157 std::process::exit(1);
1158 });
1159
1160 // Now, get the workspace parent from the manifest directory
1161 let manifest_dir = Path::new(&manifest_path)
1162 .parent()
1163 .unwrap_or(Path::new(".."));
1164
1165 let pnpm =
1166 crate::e_installer::check_pnpm_and_install(manifest_dir).unwrap_or_else(|_| {
1167 eprintln!("Error: Unable to check pnpm dependencies.");
1168 std::process::exit(1);
1169 });
1170 // Ensure npm dependencies are handled at the workspace parent level
1171 if pnpm == PathBuf::new() {
1172 crate::e_installer::check_npm_and_install(manifest_dir).unwrap_or_else(|_| {
1173 eprintln!("Error: Unable to check npm dependencies.");
1174 std::process::exit(1);
1175 });
1176 }
1177
1178 self.suppressed_flags.insert("quiet".to_string());
1179 // Helper closure to check for tauri.conf.json in a directory.
1180 let has_tauri_conf = |dir: &Path| -> bool { dir.join("tauri.conf.json").exists() };
1181
1182 // Helper closure to check for tauri.conf.json and package.json in a directory.
1183 let has_file = |dir: &Path, filename: &str| -> bool { dir.join(filename).exists() };
1184 // Try candidate's parent (if origin is SingleFile or DefaultBinary).
1185 let candidate_dir_opt = match &target.origin {
1186 Some(TargetOrigin::SingleFile(path))
1187 | Some(TargetOrigin::DefaultBinary(path)) => path.parent(),
1188 _ => None,
1189 };
1190
1191 if let Some(candidate_dir) = candidate_dir_opt {
1192 if has_tauri_conf(candidate_dir) {
1193 println!("Using candidate directory: {}", candidate_dir.display());
1194 self.execution_dir = Some(candidate_dir.to_path_buf());
1195 } else if let Some(manifest_parent) = target.manifest_path.parent() {
1196 if has_tauri_conf(manifest_parent) {
1197 println!("Using manifest parent: {}", manifest_parent.display());
1198 self.execution_dir = Some(manifest_parent.to_path_buf());
1199 } else if let Some(grandparent) = manifest_parent.parent() {
1200 if has_tauri_conf(grandparent) {
1201 println!("Using manifest grandparent: {}", grandparent.display());
1202 self.execution_dir = Some(grandparent.to_path_buf());
1203 } else {
1204 println!("No tauri.conf.json found in candidate, manifest parent, or grandparent; defaulting to manifest parent: {}", manifest_parent.display());
1205 self.execution_dir = Some(manifest_parent.to_path_buf());
1206 }
1207 } else {
1208 println!("No grandparent for manifest; defaulting to candidate directory: {}", candidate_dir.display());
1209 self.execution_dir = Some(candidate_dir.to_path_buf());
1210 }
1211 } else {
1212 println!(
1213 "No manifest parent found for: {}",
1214 target.manifest_path.display()
1215 );
1216 }
1217 // Check for package.json and run npm ls if found.
1218 println!("Checking for package.json in: {}", candidate_dir.display());
1219 if has_file(candidate_dir, "package.json") {
1220 crate::e_installer::check_npm_and_install(candidate_dir).ok();
1221 }
1222 } else if let Some(manifest_parent) = target.manifest_path.parent() {
1223 if has_tauri_conf(manifest_parent) {
1224 println!("Using manifest parent: {}", manifest_parent.display());
1225 self.execution_dir = Some(manifest_parent.to_path_buf());
1226 } else if let Some(grandparent) = manifest_parent.parent() {
1227 if has_tauri_conf(grandparent) {
1228 println!("Using manifest grandparent: {}", grandparent.display());
1229 self.execution_dir = Some(grandparent.to_path_buf());
1230 } else {
1231 println!(
1232 "No tauri.conf.json found; defaulting to manifest parent: {}",
1233 manifest_parent.display()
1234 );
1235 self.execution_dir = Some(manifest_parent.to_path_buf());
1236 }
1237 }
1238 // Check for package.json and run npm ls if found.
1239 println!(
1240 "Checking for package.json in: {}",
1241 manifest_parent.display()
1242 );
1243 if has_file(manifest_parent, "package.json") {
1244 crate::e_installer::check_npm_and_install(manifest_parent).ok();
1245 }
1246 if has_file(Path::new("."), "package.json") {
1247 crate::e_installer::check_npm_and_install(manifest_parent).ok();
1248 }
1249 } else {
1250 println!(
1251 "No manifest parent found for: {}",
1252 target.manifest_path.display()
1253 );
1254 }
1255 self.args.push("tauri".into());
1256 self.args.push("dev".into());
1257 }
1258 TargetKind::ManifestLeptos => {
1259 let readme_path = target
1260 .manifest_path
1261 .parent()
1262 .map(|p| p.join("README.md"))
1263 .filter(|p| p.exists())
1264 .or_else(|| {
1265 target
1266 .manifest_path
1267 .parent()
1268 .map(|p| p.join("readme.md"))
1269 .filter(|p| p.exists())
1270 });
1271
1272 if let Some(readme) = readme_path {
1273 if let Ok(mut file) = std::fs::File::open(&readme) {
1274 let mut contents = String::new();
1275 if file.read_to_string(&mut contents).is_ok()
1276 && contents.contains("cargo leptos watch")
1277 {
1278 // Use cargo leptos watch
1279 println!("Detected 'cargo leptos watch' in {}", readme.display());
1280 self.execution_dir =
1281 target.manifest_path.parent().map(|p| p.to_path_buf());
1282 self.execution_dir =
1283 Some(target.manifest_path.parent().unwrap().to_path_buf());
1284 self.alternate_cmd = Some("cargo".to_string());
1285 self.args.push("leptos".into());
1286 self.args.push("watch".into());
1287 self = self.with_required_features(&target.manifest_path, target);
1288 return self;
1289 }
1290 }
1291 }
1292
1293 // fallback to trunk
1294 let exe_path = match crate::e_installer::ensure_trunk() {
1295 Ok(p) => p,
1296 Err(e) => {
1297 eprintln!("{}", e);
1298 return self;
1299 }
1300 };
1301
1302 if let Some(manifest_parent) = target.manifest_path.parent() {
1303 println!("Manifest path: {}", target.manifest_path.display());
1304 println!(
1305 "Execution directory (same as manifest folder): {}",
1306 manifest_parent.display()
1307 );
1308 self.execution_dir = Some(manifest_parent.to_path_buf());
1309 } else {
1310 println!(
1311 "No manifest parent found for: {}",
1312 target.manifest_path.display()
1313 );
1314 }
1315
1316 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1317 self.args.push("serve".into());
1318 self.args.push("--open".into());
1319 self.args.push("--color".into());
1320 self.args.push("always".into());
1321 self = self.with_required_features(&target.manifest_path, target);
1322 }
1323 TargetKind::ManifestDioxus => {
1324 // For Dioxus targets, print the manifest path and set the execution directory
1325 let exe_path = match crate::e_installer::ensure_dx() {
1326 Ok(path) => path,
1327 Err(e) => {
1328 eprintln!("Error locating `dx`: {}", e);
1329 return self;
1330 }
1331 };
1332 // to be the same directory as the manifest.
1333 if let Some(manifest_parent) = target.manifest_path.parent() {
1334 println!("Manifest path: {}", target.manifest_path.display());
1335 println!(
1336 "Execution directory (same as manifest folder): {}",
1337 manifest_parent.display()
1338 );
1339 self.execution_dir = Some(manifest_parent.to_path_buf());
1340 } else {
1341 println!(
1342 "No manifest parent found for: {}",
1343 target.manifest_path.display()
1344 );
1345 }
1346 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1347 self.args.push("serve".into());
1348 self = self.with_required_features(&target.manifest_path, target);
1349 }
1350 TargetKind::ManifestDioxusExample => {
1351 let exe_path = match crate::e_installer::ensure_dx() {
1352 Ok(path) => path,
1353 Err(e) => {
1354 eprintln!("Error locating `dx`: {}", e);
1355 return self;
1356 }
1357 };
1358 // For Dioxus targets, print the manifest path and set the execution directory
1359 // to be the same directory as the manifest.
1360 if let Some(manifest_parent) = target.manifest_path.parent() {
1361 println!("Manifest path: {}", target.manifest_path.display());
1362 println!(
1363 "Execution directory (same as manifest folder): {}",
1364 manifest_parent.display()
1365 );
1366 self.execution_dir = Some(manifest_parent.to_path_buf());
1367 } else {
1368 println!(
1369 "No manifest parent found for: {}",
1370 target.manifest_path.display()
1371 );
1372 }
1373 self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
1374 self.args.push("serve".into());
1375 self.args.push("--example".into());
1376 self.args.push(target.name.clone());
1377 self = self.with_required_features(&target.manifest_path, target);
1378 }
1379 }
1380 self
1381 }
1382
1383 /// Configure the command using CLI options.
1384 pub fn with_cli(mut self, cli: &crate::Cli) -> Self {
1385 if cli.quiet && !self.suppressed_flags.contains("quiet") {
1386 // Insert --quiet right after "run" if present.
1387 if let Some(pos) = self.args.iter().position(|arg| arg == &self.subcommand) {
1388 self.args.insert(pos + 1, "--quiet".into());
1389 } else {
1390 self.args.push("--quiet".into());
1391 }
1392 }
1393 if cli.release {
1394 // Insert --release right after the initial "run" command if applicable.
1395 // For example, if the command already contains "run", insert "--release" after it.
1396 if let Some(pos) = self.args.iter().position(|arg| arg == &self.subcommand) {
1397 self.args.insert(pos + 1, "--release".into());
1398 } else {
1399 // If not running a "run" command (like in the Tauri case), simply push it.
1400 self.args.push("--release".into());
1401 }
1402 }
1403 // Append extra arguments (if any) after a "--" separator.
1404 if !cli.extra.is_empty() {
1405 self.args.push("--".into());
1406 self.args.extend(cli.extra.iter().cloned());
1407 }
1408 self
1409 }
1410 /// Append required features based on the manifest, target kind, and name.
1411 /// This method queries your manifest helper function and, if features are found,
1412 /// appends "--features" and the feature list.
1413 pub fn with_required_features(mut self, manifest: &PathBuf, target: &CargoTarget) -> Self {
1414 if let Some(features) = crate::e_manifest::get_required_features_from_manifest(
1415 manifest,
1416 &target.kind,
1417 &target.name,
1418 ) {
1419 self.args.push("--features".to_string());
1420 self.args.push(features);
1421 }
1422 self
1423 }
1424
1425 /// Appends extra arguments to the command.
1426 pub fn with_extra_args(mut self, extra: &[String]) -> Self {
1427 if !extra.is_empty() {
1428 // Use "--" to separate Cargo arguments from target-specific arguments.
1429 self.args.push("--".into());
1430 self.args.extend(extra.iter().cloned());
1431 }
1432 self
1433 }
1434
1435 /// Builds the final vector of command-line arguments.
1436 pub fn build(self) -> Vec<String> {
1437 self.args
1438 }
1439
1440 pub fn is_compiler_target(&self) -> bool {
1441 let supported_subcommands = ["run", "build", "check", "leptos", "tauri"];
1442 if let Some(alternate) = &self.alternate_cmd {
1443 if alternate != "cargo" {
1444 return false;
1445 }
1446 }
1447 if let Some(_) = self
1448 .args
1449 .iter()
1450 .position(|arg| supported_subcommands.contains(&arg.as_str()))
1451 {
1452 return true;
1453 }
1454 false
1455 }
1456
1457 pub fn injected_args(&self) -> (String, Vec<String>) {
1458 let mut new_args = self.args.clone();
1459 let supported_subcommands = [
1460 "run", "build", "test", "bench", "clean", "doc", "publish", "update",
1461 ];
1462
1463 if self.is_filter {
1464 if let Some(pos) = new_args
1465 .iter()
1466 .position(|arg| supported_subcommands.contains(&arg.as_str()))
1467 {
1468 // If the command is a supported subcommand like "cargo run", insert the JSON output format and color options.
1469 new_args.insert(pos + 1, "--message-format=json".into());
1470 new_args.insert(pos + 2, "--color".into());
1471 new_args.insert(pos + 3, "always".into());
1472 }
1473 }
1474
1475 let program = self.alternate_cmd.as_deref().unwrap_or("cargo").to_string();
1476 (program, new_args)
1477 }
1478
1479 pub fn print_command(&self) {
1480 let (program, new_args) = self.injected_args();
1481 println!("{} {}", program, new_args.join(" "));
1482 }
1483
1484 /// builds a std::process::Command.
1485 pub fn build_command(&self) -> Command {
1486 let (program, new_args) = self.injected_args();
1487
1488 let mut cmd = Command::new(program);
1489 cmd.args(new_args);
1490
1491 if let Some(dir) = &self.execution_dir {
1492 cmd.current_dir(dir);
1493 }
1494
1495 cmd
1496 }
1497 /// Runs the command and returns everything it printed (stdout + stderr),
1498 /// regardless of exit status.
1499 pub fn capture_output(&self) -> anyhow::Result<String> {
1500 // Build and run
1501 let mut cmd = self.build_command();
1502 let output = cmd
1503 .output()
1504 .map_err(|e| anyhow::anyhow!("Failed to spawn cargo process: {}", e))?;
1505
1506 // Decode both stdout and stderr lossily
1507 let mut all = String::new();
1508 all.push_str(&String::from_utf8_lossy(&output.stdout));
1509 all.push_str(&String::from_utf8_lossy(&output.stderr));
1510
1511 // Return the combined string, even if exit was !success
1512 Ok(all)
1513 }
1514}
1515/// Resolves a file path by:
1516/// 1. If the path is relative, try to resolve it relative to the current working directory.
1517/// 2. If that file does not exist, try to resolve it relative to the parent directory of the manifest path.
1518/// 3. Otherwise, return the original relative path.
1519fn resolve_file_path(manifest_path: &PathBuf, file_str: &str) -> PathBuf {
1520 let file_path = Path::new(file_str);
1521 if file_path.is_relative() {
1522 // 1. Try resolving relative to the current working directory.
1523 if let Ok(cwd) = env::current_dir() {
1524 let cwd_path = cwd.join(file_path);
1525 if cwd_path.exists() {
1526 return cwd_path;
1527 }
1528 }
1529 // 2. Try resolving relative to the parent of the manifest path.
1530 if let Some(manifest_parent) = manifest_path.parent() {
1531 let parent_path = manifest_parent.join(file_path);
1532 if parent_path.exists() {
1533 return parent_path;
1534 }
1535 }
1536 // 3. Neither existed; return the relative path as-is.
1537 return file_path.to_path_buf();
1538 }
1539 file_path.to_path_buf()
1540}
1541
1542// --- Example usage ---
1543#[cfg(test)]
1544mod tests {
1545 use crate::e_target::TargetOrigin;
1546
1547 use super::*;
1548
1549 #[test]
1550 fn test_command_builder_example() {
1551 let target = CargoTarget {
1552 name: "my_example".to_string(),
1553 display_name: "My Example".to_string(),
1554 manifest_path: "Cargo.toml".into(),
1555 kind: TargetKind::Example,
1556 extended: true,
1557 toml_specified: false,
1558 origin: Some(TargetOrigin::SingleFile(PathBuf::from(
1559 "examples/my_example.rs",
1560 ))),
1561 };
1562
1563 let extra_args = vec!["--flag".to_string(), "value".to_string()];
1564
1565 let manifest_path = PathBuf::from("Cargo.toml");
1566 let args = CargoCommandBuilder::new(&manifest_path, "run", false)
1567 .with_target(&target)
1568 .with_extra_args(&extra_args)
1569 .build();
1570
1571 // For an example target, we expect something like:
1572 // cargo run --example my_example --manifest-path Cargo.toml -- --flag value
1573 assert!(args.contains(&"--example".to_string()));
1574 assert!(args.contains(&"my_example".to_string()));
1575 assert!(args.contains(&"--manifest-path".to_string()));
1576 assert!(args.contains(&"Cargo.toml".to_string()));
1577 assert!(args.contains(&"--".to_string()));
1578 assert!(args.contains(&"--flag".to_string()));
1579 assert!(args.contains(&"value".to_string()));
1580 }
1581}