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}