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