cargo_e/
e_tui.rs

1#[cfg(feature = "tui")]
2pub mod tui_interactive {
3    use crate::e_command_builder::CargoCommandBuilder;
4    use crate::e_manifest::maybe_patch_manifest_for_run;
5    use crate::e_target::{CargoTarget, TargetKind};
6    use crate::prelude::*;
7    use crate::{e_bacon, e_findmain, Cli};
8    use crossterm::event::KeyEventKind;
9    use crossterm::event::{poll, read};
10    use crossterm::{
11        event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseEventKind},
12        execute,
13        terminal::{
14            disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
15            LeaveAlternateScreen,
16        },
17    };
18    use ratatui::{
19        backend::CrosstermBackend,
20        layout::{Constraint, Direction, Layout, Rect},
21        style::{Color, Style},
22        text::{Line, Span},
23        widgets::{Block, Borders, List, ListItem, ListState},
24        Terminal,
25    };
26    use std::{collections::HashSet, thread, time::Duration};
27
28    /// Flushes the input event queue, ignoring any stray Enter key events.
29    pub fn flush_input() -> Result<(), Box<dyn std::error::Error>> {
30        while poll(Duration::from_millis(0))? {
31            if let Event::Key(key_event) = read()? {
32                // Optionally, log or ignore specific keys.
33                if key_event.code == KeyCode::Enter {
34                    // Filtering out stray Return keys.
35                    continue;
36                }
37                // You can also choose to ignore all events:
38                // continue;
39            }
40        }
41        Ok(())
42    }
43
44    /// Launches an interactive terminal UI for selecting an example.
45    pub fn launch_tui(
46        cli: &Cli,
47        examples: &[CargoTarget],
48    ) -> Result<(), Box<dyn std::error::Error>> {
49        flush_input()?; // Clear any buffered input (like stray Return keys)
50        let mut exs = examples.to_vec();
51        if exs.is_empty() {
52            println!("No examples found!");
53            return Ok(());
54        }
55        exs.sort_by(|a, b| a.display_name.cmp(&b.display_name));
56        // Determine the directory containing the Cargo.toml at runtime.
57        let manifest_dir = crate::e_manifest::find_manifest_dir()?;
58        let history_path = manifest_dir.join("run_history.txt");
59        let mut run_history: HashSet<String> = HashSet::new();
60        if let Ok(contents) = fs::read_to_string(&history_path) {
61            for line in contents.lines() {
62                if !line.trim().is_empty() {
63                    run_history.insert(line.trim().to_string());
64                }
65            }
66        }
67
68        enable_raw_mode()?;
69        let mut stdout = io::stdout();
70        execute!(
71            stdout,
72            EnterAlternateScreen,
73            EnableMouseCapture,
74            Clear(ClearType::All)
75        )?;
76        let backend = CrosstermBackend::new(stdout);
77        let mut terminal = Terminal::new(backend)?;
78
79        let mut list_state = ListState::default();
80        list_state.select(Some(0));
81        let mut exit_hover = false;
82
83        'main_loop: loop {
84            terminal.draw(|f| {
85                let size = f.area();
86                let area = Rect::new(0, 0, size.width, size.height);
87                let chunks = Layout::default()
88                    .direction(Direction::Vertical)
89                    .margin(2)
90                    .constraints([Constraint::Min(0)].as_ref())
91                    .split(area);
92                let list_area = chunks[0];
93
94                let left_text = format!("Select example ({} examples found)", exs.len());
95                let separator = " ┃ ";
96                let right_text = "Esc or q to EXIT";
97                let title_line = if exit_hover {
98                    Line::from(vec![
99                        Span::raw(left_text),
100                        Span::raw(separator),
101                        Span::styled(right_text, Style::default().fg(Color::Yellow)),
102                    ])
103                } else {
104                    Line::from(vec![
105                        Span::raw(left_text),
106                        Span::raw(separator),
107                        Span::styled("Esc or q to ", Style::default().fg(Color::White)),
108                        Span::styled("EXIT", Style::default().fg(Color::Red)),
109                    ])
110                };
111
112                let block = Block::default().borders(Borders::ALL).title(title_line);
113                // let items: Vec<ListItem> = exs.iter().map(|e| {
114                //     let mut item = ListItem::new(e.as_str());
115                //     if run_history.contains(e) {
116                //         item = item.style(Style::default().fg(Color::Blue));
117                //     }
118                //     item
119                // }).collect();
120                let items: Vec<ListItem> = exs
121                    .iter()
122                    .map(|ex| {
123                        let display_text = ex.display_name.clone();
124
125                        let mut item = ListItem::new(display_text);
126                        if run_history.contains(&ex.name) {
127                            item = item.style(Style::default().fg(Color::Blue));
128                        }
129                        item
130                    })
131                    .collect();
132                let list = List::new(items)
133                    .block(block)
134                    .highlight_style(Style::default().fg(Color::Yellow))
135                    .highlight_symbol(">> ");
136                f.render_stateful_widget(list, list_area, &mut list_state);
137            })?;
138
139            if event::poll(Duration::from_millis(200))? {
140                match event::read()? {
141                    Event::Key(key) => {
142                        // Only process key-press events.
143                        if key.kind != KeyEventKind::Press {
144                            continue;
145                        }
146                        match key.code {
147                            KeyCode::Char('q') | KeyCode::Esc => break 'main_loop,
148                            KeyCode::Down => {
149                                let i = match list_state.selected() {
150                                    Some(i) if i >= exs.len() - 1 => i,
151                                    Some(i) => i + 1,
152                                    None => 0,
153                                };
154                                list_state.select(Some(i));
155                                // Debounce: wait a short while to avoid duplicate processing.
156                                thread::sleep(Duration::from_millis(50));
157                            }
158                            KeyCode::Up => {
159                                let i = match list_state.selected() {
160                                    Some(0) | None => 0,
161                                    Some(i) => i - 1,
162                                };
163                                list_state.select(Some(i));
164                                // Debounce: wait a short while to avoid duplicate processing.
165                                thread::sleep(Duration::from_millis(50));
166                            }
167                            KeyCode::PageDown => {
168                                // Compute page size based on the terminal's current height.
169                                let page = terminal
170                                    .size()
171                                    .map(|r| r.height.saturating_sub(4)) // subtract borders/margins; adjust as needed
172                                    .unwrap_or(5)
173                                    as usize;
174                                let current = list_state.selected().unwrap_or(0);
175                                let new = std::cmp::min(current + page, exs.len() - 1);
176                                list_state.select(Some(new));
177                            }
178                            KeyCode::PageUp => {
179                                let page = terminal
180                                    .size()
181                                    .map(|r| r.height.saturating_sub(4))
182                                    .unwrap_or(5)
183                                    as usize;
184                                let current = list_state.selected().unwrap_or(0);
185                                let new = current.saturating_sub(page);
186                                list_state.select(Some(new));
187                            }
188                            KeyCode::Char('b') => {
189                                if let Some(selected) = list_state.selected() {
190                                    let sample = &examples[selected];
191                                    // Run bacon in detached mode. Extra arguments can be added if needed.
192                                    if let Err(e) = e_bacon::run_bacon(sample, &Vec::new()) {
193                                        eprintln!("Error running bacon: {}", e);
194                                    } else {
195                                        println!("Bacon launched for sample: {}", sample.name);
196                                    }
197                                    reinit_terminal(&mut terminal)?;
198                                }
199                            }
200                            KeyCode::Char('e') => {
201                                if let Some(selected) = list_state.selected() {
202                                    // Disable raw mode for debug printing.
203                                    crossterm::terminal::disable_raw_mode()?;
204                                    crossterm::execute!(
205                                        std::io::stdout(),
206                                        crossterm::terminal::LeaveAlternateScreen
207                                    )?;
208                                    // When 'e' is pressed, attempt to open the sample in VSCode.
209                                    let sample = &exs[selected];
210                                    println!(
211                                        "Opening VSCode for path: {}",
212                                        sample
213                                            .manifest_path
214                                            .to_str()
215                                            .unwrap_or_default()
216                                            .to_owned()
217                                    );
218                                    // Here we block on the asynchronous open_vscode call.
219                                    // futures::executor::block_on(open_vscode(Path::new(&sample.manifest_path)));
220                                    futures::executor::block_on(
221                                        e_findmain::open_vscode_for_sample(sample),
222                                    );
223                                    std::thread::sleep(std::time::Duration::from_secs(5));
224                                    reinit_terminal(&mut terminal)?;
225                                }
226                            }
227                            KeyCode::Char('i') => {
228                                if let Some(selected) = list_state.selected() {
229                                    // Disable raw mode for debug printing.
230                                    crossterm::terminal::disable_raw_mode()?;
231                                    crossterm::execute!(
232                                        std::io::stdout(),
233                                        crossterm::terminal::LeaveAlternateScreen
234                                    )?;
235                                    let sample = &exs[selected];
236                                    println!("Target: {:?}", sample);
237                                    std::thread::sleep(std::time::Duration::from_secs(5));
238                                    reinit_terminal(&mut terminal)?;
239                                }
240                            }
241                            // KeyCode::Char('v') => {
242                            //     if let Some(selected) = list_state.selected() {
243                            //         // Disable raw mode for debug printing.
244                            //         crossterm::terminal::disable_raw_mode()?;
245                            //         crossterm::execute!(
246                            //             std::io::stdout(),
247                            //             crossterm::terminal::LeaveAlternateScreen
248                            //         )?;
249                            //         // When 'e' is pressed, attempt to open the sample in VSCode.
250                            //         let sample = &examples[selected];
251                            //         println!("Opening VIM for path: {}", sample.manifest_path);
252                            //         // Here we block on the asynchronous open_vscode call.
253                            //         // futures::executor::block_on(open_vscode(Path::new(&sample.manifest_path)));
254                            //         e_findmain::open_vim_for_sample(sample);
255                            //         std::thread::sleep(std::time::Duration::from_secs(5));
256                            //         reinit_terminal(&mut terminal)?;
257                            //     }
258                            // }
259                            KeyCode::Enter => {
260                                if let Some(selected) = list_state.selected() {
261                                    run_piece(
262                                        &exs,
263                                        selected,
264                                        &history_path,
265                                        &mut run_history,
266                                        &mut terminal,
267                                        cli,
268                                    )?;
269                                }
270                            }
271                            _ => {}
272                        }
273                    }
274                    Event::Mouse(mouse_event) => {
275                        let size = terminal.size()?;
276                        let area = Rect::new(0, 0, size.width, size.height);
277                        let chunks = Layout::default()
278                            .direction(Direction::Vertical)
279                            .margin(2)
280                            .constraints([Constraint::Min(0)].as_ref())
281                            .split(area);
282                        let list_area = chunks[0];
283                        let title_row = list_area.y;
284                        let title_start = list_area.x + 2;
285                        let left_text = format!("Select example ({} examples found)", exs.len());
286                        let separator = " ┃ ";
287                        let right_text = "Esc or q to EXIT";
288                        let offset = (left_text.len() + separator.len()) as u16;
289                        let right_region_start = title_start + offset;
290                        let right_region_end = right_region_start + (right_text.len() as u16);
291
292                        match mouse_event.kind {
293                            MouseEventKind::ScrollDown => {
294                                let current = list_state.selected().unwrap_or(0);
295                                let new = std::cmp::min(current + 1, exs.len() - 1);
296                                list_state.select(Some(new));
297                            }
298                            MouseEventKind::ScrollUp => {
299                                let current = list_state.selected().unwrap_or(0);
300                                let new = if current == 0 { 0 } else { current - 1 };
301                                list_state.select(Some(new));
302                            }
303
304                            MouseEventKind::Moved => {
305                                if mouse_event.row == title_row {
306                                    exit_hover = mouse_event.column >= right_region_start
307                                        && mouse_event.column < right_region_end;
308                                } else {
309                                    exit_hover = false;
310                                    let inner_y = list_area.y + 1;
311                                    let inner_height = list_area.height.saturating_sub(2);
312                                    if mouse_event.column > list_area.x + 1
313                                        && mouse_event.column < list_area.x + list_area.width - 1
314                                        && mouse_event.row >= inner_y
315                                        && mouse_event.row < inner_y + inner_height
316                                    {
317                                        let index = (mouse_event.row - inner_y) as usize;
318                                        if index < exs.len() {
319                                            list_state.select(Some(index));
320                                        }
321                                    }
322                                }
323                            }
324                            MouseEventKind::Down(_) => {
325                                if mouse_event.row == title_row
326                                    && mouse_event.column >= right_region_start
327                                    && mouse_event.column < right_region_end
328                                {
329                                    break 'main_loop;
330                                }
331                                let inner_y = list_area.y + 1;
332                                let inner_height = list_area.height.saturating_sub(2);
333                                if mouse_event.column > list_area.x + 1
334                                    && mouse_event.column < list_area.x + list_area.width - 1
335                                    && mouse_event.row >= inner_y
336                                    && mouse_event.row < inner_y + inner_height
337                                {
338                                    let index = (mouse_event.row - inner_y) as usize;
339                                    if index < exs.len() {
340                                        list_state.select(Some(index));
341                                        run_piece(
342                                            &exs.clone(),
343                                            index,
344                                            &history_path,
345                                            &mut run_history,
346                                            &mut terminal,
347                                            cli,
348                                        )?;
349                                    }
350                                }
351                            }
352                            _ => {}
353                        }
354                    }
355                    _ => {}
356                }
357            }
358        }
359
360        disable_raw_mode()?;
361        let mut stdout = io::stdout();
362        execute!(
363            stdout,
364            LeaveAlternateScreen,
365            DisableMouseCapture,
366            Clear(ClearType::All)
367        )?;
368        terminal.show_cursor()?;
369        Ok(())
370    }
371
372    /// Reinitializes the terminal: enables raw mode, enters the alternate screen,
373    /// enables mouse capture, clears the screen, and creates a new Terminal instance.
374    /// This function updates the provided terminal reference.
375    pub fn reinit_terminal(
376        terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
377    ) -> Result<(), Box<dyn Error>> {
378        enable_raw_mode()?;
379        let mut stdout = io::stdout();
380        execute!(
381            stdout,
382            EnterAlternateScreen,
383            EnableMouseCapture,
384            Clear(ClearType::All)
385        )?;
386        *terminal = Terminal::new(CrosstermBackend::new(stdout))?;
387        Ok(())
388    }
389
390    /// Runs the given example (or binary) target. It leaves TUI mode, spawns a cargo process,
391    /// installs a Ctrl+C handler to kill the process, waits for it to finish, updates history,
392    /// flushes stray input, and then reinitializes the terminal.
393    pub fn run_piece(
394        examples: &[CargoTarget],
395        index: usize,
396        history_path: &Path,
397        run_history: &mut HashSet<String>,
398        terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
399        cli: &Cli,
400    ) -> Result<(), Box<dyn Error>> {
401        let target = &examples[index];
402        // Leave TUI mode before running the target.
403        disable_raw_mode()?;
404        execute!(
405            terminal.backend_mut(),
406            LeaveAlternateScreen,
407            crossterm::event::DisableMouseCapture
408        )?;
409        terminal.show_cursor()?;
410
411        //let manifest_path = target.manifest_path.clone();
412        let manifest_path = PathBuf::from(target.manifest_path.clone());
413
414        // let mut args: Vec<String> = if target.kind == TargetKind::Example {
415        //     if target.extended {
416        //         if cli.print_program_name {
417        //             println!("Running extended example with manifest: {}", manifest_path);
418        //         }
419        //         // For workspace extended examples, assume the current directory is set correctly.
420        //         vec![
421        //             "run".to_string(),
422        //             "--manifest-path".to_string(),
423        //             manifest_path.to_owned(),
424        //         ]
425        //     } else {
426        //         if cli.print_program_name {
427        //             println!(
428        //                 "Running example: cargo run --release --example {}",
429        //                 target.name
430        //             );
431        //         }
432        //         vec![
433        //             "run".to_string(),
434        //             "--manifest-path".to_string(),
435        //             manifest_path.to_owned(),
436        //             "--release".to_string(),
437        //             "--example".to_string(),
438        //             format!("{}", target.name),
439        //         ]
440        //     }
441        // } else {
442        //     if cli.print_program_name {
443        //         println!("Running binary: cargo run --release --bin {}", target.name);
444        //     }
445        //     vec![
446        //         "run".to_string(),
447        //         "--manifest-path".to_string(),
448        //         manifest_path.to_owned(),
449        //         "--release".to_string(),
450        //         "--bin".to_string(),
451        //         format!("{}", target.name),
452        //     ]
453        // };
454
455        // // Determine required features based on the target kind and name.
456        // if let Some(features) = crate::e_manifest::get_required_features_from_manifest(
457        //     Path::new(&manifest_path),
458        //     &target.kind,
459        //     &target.name,
460        // ) {
461        //     args.push("--features".to_string());
462        //     args.push(features);
463        // }
464        let builder = CargoCommandBuilder::new()
465            .with_target(target)
466            .with_required_features(&manifest_path, target)
467            .with_cli(cli);
468        let mut cmd = builder.build_command();
469
470        // Set current directory appropriately.
471        if target.kind == TargetKind::ManifestTauri {
472            let manifest_dir = manifest_path.parent().expect("Expected parent directory");
473            cmd.current_dir(manifest_dir);
474        } else if target.extended {
475            if let Some(dir) = manifest_path.parent() {
476                cmd.current_dir(dir);
477            }
478        }
479
480        println!("Running command: {:?}", cmd);
481        // If the target is extended, we want to run it from its directory.
482        if target.extended {
483            Path::new(&manifest_path).parent().map(|p| p.to_owned())
484        } else {
485            None
486        };
487
488        // Before spawning, patch the manifest if needed.
489        let manifest_path_obj = Path::new(&manifest_path);
490        let backup = maybe_patch_manifest_for_run(manifest_path_obj)?;
491
492        // // Build the command.
493        // let mut cmd = Command::new("cargo");
494        // cmd.args(&args);
495        // if let Some(ref dir) = current_dir {
496        //     cmd.current_dir(dir);
497        // }
498        // Convert command args into &str slices for spawn_cargo_process.
499        // (Assuming spawn_cargo_process accepts a slice of &str.)
500        let owned_args: Vec<String> = cmd
501            .get_args()
502            .map(|arg| arg.to_string_lossy().to_string())
503            .collect();
504        // Now create a vector of &str references valid as long as `owned_args` is in scope:
505        let args_ref: Vec<&str> = owned_args.iter().map(|s| s.as_str()).collect();
506
507        // let args_ref: Vec<&str> = args.iter().map(|s| &**s).collect();
508        let mut child = crate::e_runner::spawn_cargo_process(&args_ref)?;
509        if cli.print_instruction {
510            println!("Process started. Press Ctrl+C to terminate or 'd' to detach...");
511        }
512        let mut update_history = true;
513        let status_code: i32;
514        let mut detached = false;
515        // Now we enter an event loop, periodically checking if the child has exited
516        // and polling for keyboard input.
517        loop {
518            // Check if the child process has finished.
519            if let Some(status) = child.try_wait()? {
520                status_code = status.code().unwrap_or(1);
521
522                //println!("Process exited with status: {}", status_code);
523                break;
524            }
525            // Poll for input events with a 100ms timeout.
526            if event::poll(Duration::from_millis(100))? {
527                if let Event::Key(key_event) = event::read()? {
528                    if key_event.code == KeyCode::Char('c')
529                        && key_event.modifiers.contains(event::KeyModifiers::CONTROL)
530                    {
531                        if cli.print_instruction {
532                            println!("Ctrl+C detected in event loop, killing process...");
533                        }
534                        child.kill()?;
535                        update_history = false; // do not update history if cancelled
536                                                // Optionally, you can also wait for the child after killing.
537                        let status = child.wait()?;
538                        status_code = status.code().unwrap_or(1);
539                        break;
540                    } else if key_event.code == KeyCode::Char('d') && key_event.modifiers.is_empty()
541                    {
542                        if cli.print_instruction {
543                            println!(
544                                "'d' pressed; detaching process. Process will continue running."
545                            );
546                        }
547                        detached = true;
548                        update_history = false;
549                        // Do not kill or wait on the child.
550                        // Break out of the loop immediately.
551                        // We can optionally leave the process running.
552                        status_code = 0;
553                        break;
554                    }
555                }
556            }
557        }
558        // Restore the manifest if it was patched.
559        if let Some(original) = backup {
560            fs::write(manifest_path_obj, original)?;
561        }
562        // Wrap the child process so that we can share it with our Ctrl+C handler.
563        // let child_arc = Arc::new(Mutex::new(child));
564        // let child_for_handler = Arc::clone(&child_arc);
565
566        // Set up a Ctrl+C handler to kill the spawned process.
567        // ctrlc::set_handler(move || {
568        // eprintln!("Ctrl+C pressed, terminating process...");
569        // if let Ok(mut child) = child_for_handler.lock() {
570        // let _ = child.kill();
571        // }
572        // })?;
573
574        // Wait for the process to finish.
575        // let status = child_arc.lock().unwrap().wait()?;
576        // println!("Process exited with status: {:?}", status.code());
577
578        if !detached {
579            // Only update run history if update_history is true and exit code is zero.
580            if update_history && status_code == 0 && run_history.insert(target.name.clone()) {
581                let history_data = run_history.iter().cloned().collect::<Vec<_>>().join("\n");
582                fs::write(history_path, history_data)?;
583            }
584            let message = if cli.print_exit_code {
585                format!("Exitcode {:?}. Press any key to continue...", status_code)
586            } else {
587                "".to_string()
588            };
589            let _ = crate::e_prompts::prompt(&message, cli.wait)?;
590        }
591
592        // Flush stray input events.
593        while event::poll(std::time::Duration::from_millis(0))? {
594            let _ = event::read()?;
595        }
596        std::thread::sleep(std::time::Duration::from_millis(50));
597
598        // Reinitialize the terminal.
599        enable_raw_mode()?;
600        let mut stdout = io::stdout();
601        execute!(
602            stdout,
603            EnterAlternateScreen,
604            crossterm::event::EnableMouseCapture,
605            Clear(ClearType::All)
606        )?;
607        *terminal = Terminal::new(CrosstermBackend::new(stdout))?;
608        Ok(())
609    }
610}