Skip to main content

vtcode_tui/ui/interactive_list/
mod.rs

1use 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;