cargo_e/
e_tui.rs

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