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