cargo_e/
e_tui.rs

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