Skip to main content

mermaid_cli/render/widgets/
approval.rs

1//! Bottom-zone modal prompt: a title, a body (the command / path / question),
2//! and numbered options. Used for inline tool-approval prompts (`ask` mode +
3//! Auto-mode escalations) and the `/clear`-style confirmation.
4
5use ratatui::buffer::Buffer;
6use ratatui::layout::Rect;
7use ratatui::style::{Color, Modifier, Style};
8use ratatui::text::{Line, Span};
9use ratatui::widgets::{Block, Borders, Paragraph, Widget};
10
11use crate::render::theme::Theme;
12
13pub struct ApprovalModalWidget<'a> {
14    pub theme: &'a Theme,
15    pub title: String,
16    /// Body text — may be multi-line (the command, plus any classifier reason).
17    pub body: &'a str,
18    /// The numbered option lines, e.g. `"1. Yes"`.
19    pub options: Vec<String>,
20    /// Highlighted option for arrow-key navigation, if the modal supports it
21    /// (`None` for plain confirmations that only take y/n).
22    pub selected_index: Option<usize>,
23    /// Border + title accent.
24    pub accent: Color,
25}
26
27impl<'a> Widget for ApprovalModalWidget<'a> {
28    fn render(self, area: Rect, buf: &mut Buffer) {
29        let block = Block::default()
30            .borders(Borders::ALL)
31            .title(self.title)
32            .border_style(Style::default().fg(self.accent));
33
34        let inner_width = area.width.saturating_sub(4).max(8) as usize;
35        let mut lines: Vec<Line<'_>> = Vec::new();
36        for raw in self.body.lines() {
37            lines.push(Line::from(Span::styled(
38                truncate(raw, inner_width),
39                Style::default().fg(Color::White),
40            )));
41        }
42        lines.push(Line::from(""));
43        for (idx, opt) in self.options.iter().enumerate() {
44            let style = if self.selected_index == Some(idx) {
45                // Highlighted row — mirrors the slash palette's selection style.
46                Style::default()
47                    .fg(self.theme.colors.text_highlight.to_color())
48                    .add_modifier(Modifier::BOLD | Modifier::REVERSED)
49            } else {
50                Style::default()
51                    .fg(Color::White)
52                    .add_modifier(Modifier::BOLD)
53            };
54            lines.push(Line::from(Span::styled(format!("  {}", opt), style)));
55        }
56        Paragraph::new(lines).block(block).render(area, buf);
57    }
58}
59
60fn truncate(s: &str, max: usize) -> String {
61    if s.chars().count() <= max {
62        s.to_string()
63    } else {
64        let cut = s.floor_char_boundary(max);
65        format!("{}…", &s[..cut])
66    }
67}