vtcode_tui/ui/interactive_list/
mod.rs1use std::io;
2
3use crate::utils::tty::TtyExt;
4use anyhow::{Context, Result, anyhow};
5use ratatui::Terminal;
6use ratatui::backend::CrosstermBackend;
7use ratatui::crossterm::event;
8use ratatui::widgets::ListState;
9
10mod input;
11mod render;
12mod terminal;
13
14#[derive(Debug, Clone)]
15pub struct SelectionEntry {
16 pub title: String,
17 pub description: Option<String>,
18}
19
20impl SelectionEntry {
21 pub fn new(title: impl Into<String>, description: Option<String>) -> Self {
22 Self {
23 title: title.into(),
24 description,
25 }
26 }
27}
28
29#[derive(Debug, thiserror::Error)]
30#[error("selection interrupted by Ctrl+C")]
31pub struct SelectionInterrupted;
32
33pub fn run_interactive_selection(
34 title: &str,
35 instructions: &str,
36 entries: &[SelectionEntry],
37 default_index: usize,
38) -> Result<Option<usize>> {
39 if entries.is_empty() {
40 return Err(anyhow!("No options available for selection"));
41 }
42
43 if !io::stderr().is_tty_ext() {
44 return Err(anyhow!("Terminal UI is unavailable"));
45 }
46
47 let mut stderr = io::stderr();
48 let mut terminal_guard = TerminalModeGuard::new(title);
49 terminal_guard.save_cursor_position(&mut stderr);
50 terminal_guard.enable_raw_mode()?;
51 terminal_guard.enter_alternate_screen(&mut stderr)?;
52
53 let backend = CrosstermBackend::new(stderr);
54 let mut terminal = Terminal::new(backend)
55 .with_context(|| format!("Failed to initialize Ratatui terminal for {title} selector"))?;
56 terminal_guard.hide_cursor(&mut terminal)?;
57
58 let selection_result = (|| -> Result<Option<usize>> {
59 let total = entries.len();
60 let mut selected_index = default_index.min(total.saturating_sub(1));
61 let mut number_buffer = String::new();
62 let mut list_state = ListState::default();
63
64 loop {
65 render::draw_selection_ui(
66 &mut terminal,
67 title,
68 instructions,
69 entries,
70 selected_index,
71 &mut list_state,
72 )?;
73
74 let event = event::read()
75 .with_context(|| format!("Failed to read terminal input for {title} selector"))?;
76 match input::handle_event(event, total, &mut selected_index, &mut number_buffer)? {
77 input::SelectionAction::Continue => {}
78 input::SelectionAction::Select => return Ok(Some(selected_index)),
79 input::SelectionAction::Cancel => return Ok(None),
80 }
81 }
82 })();
83
84 let cleanup_result = terminal_guard.restore_with_terminal(&mut terminal);
85 cleanup_result?;
86 selection_result
87}
88
89use terminal::TerminalModeGuard;