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