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}