Skip to main content

stynx_code_tui/widgets/
permission_dialog.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::{Constraint, Layout, Rect},
4    style::{Modifier, Style},
5    text::{Line, Span},
6    widgets::{Clear, Paragraph, Widget, Wrap},
7};
8
9use crate::state::PermissionChoice;
10use crate::theme;
11
12pub struct PermissionDialog<'a> {
13    pub tool_name: &'a str,
14    pub description: &'a str,
15    pub choice: PermissionChoice,
16}
17
18impl<'a> PermissionDialog<'a> {
19    pub fn new(tool_name: &'a str, description: &'a str, choice: PermissionChoice) -> Self {
20        Self { tool_name, description, choice }
21    }
22
23    fn rect(area: Rect) -> Rect {
24        let h = 15.min(area.height.saturating_sub(2));
25        let y = area.y + area.height.saturating_sub(h);
26        Rect { x: area.x, y, width: area.width, height: h }
27    }
28}
29
30impl<'a> Widget for PermissionDialog<'a> {
31    fn render(self, area: Rect, buf: &mut Buffer) {
32        let dialog = Self::rect(area);
33        Clear.render(dialog, buf);
34
35        for y in dialog.y..dialog.y + dialog.height {
36            for x in dialog.x..dialog.x + dialog.width {
37                buf[(x, y)].set_style(Style::default().bg(theme::BACKGROUND_PANEL()));
38            }
39            buf[(dialog.x, y)].set_symbol("▏");
40            buf[(dialog.x, y)].set_style(
41                Style::default().fg(theme::WARNING()).bg(theme::BACKGROUND_PANEL()),
42            );
43        }
44
45        let content = Rect {
46            x: dialog.x + 2,
47            y: dialog.y + 1,
48            width: dialog.width.saturating_sub(4),
49            height: dialog.height.saturating_sub(2),
50        };
51
52        let chunks = Layout::vertical([
53            Constraint::Length(1),
54            Constraint::Length(1),
55            Constraint::Min(1),
56            Constraint::Length(2),
57        ])
58        .split(content);
59
60        Paragraph::new(Line::from(vec![
61            Span::styled("△ ", Style::default().fg(theme::WARNING()).bg(theme::BACKGROUND_PANEL())),
62            Span::styled(
63                "Permission required",
64                Style::default()
65                    .fg(theme::TEXT())
66                    .bg(theme::BACKGROUND_PANEL())
67                    .add_modifier(Modifier::BOLD),
68            ),
69        ]))
70        .style(Style::default().bg(theme::BACKGROUND_PANEL()))
71        .render(chunks[0], buf);
72
73        Paragraph::new(Line::from(vec![
74            Span::styled(
75                "Tool: ",
76                Style::default().fg(theme::TEXT_MUTED()).bg(theme::BACKGROUND_PANEL()),
77            ),
78            Span::styled(
79                self.tool_name.to_string(),
80                Style::default()
81                    .fg(theme::ACCENT())
82                    .bg(theme::BACKGROUND_PANEL())
83                    .add_modifier(Modifier::BOLD),
84            ),
85        ]))
86        .style(Style::default().bg(theme::BACKGROUND_PANEL()))
87        .render(chunks[1], buf);
88
89        Paragraph::new(self.description)
90            .style(Style::default().fg(theme::TEXT()).bg(theme::BACKGROUND_PANEL()))
91            .wrap(Wrap { trim: false })
92            .render(chunks[2], buf);
93
94        let bar_area = chunks[3];
95        for y in bar_area.y..bar_area.y + bar_area.height {
96            for x in bar_area.x..bar_area.x + bar_area.width {
97                buf[(x, y)].set_style(Style::default().bg(theme::BACKGROUND_ELEMENT()));
98            }
99        }
100
101        let opts = [
102            (PermissionChoice::Once, "Allow once"),
103            (PermissionChoice::Always, "Allow always"),
104            (PermissionChoice::Reject, "Reject"),
105        ];
106        let mut spans: Vec<Span<'static>> = vec![Span::styled(
107            " ",
108            Style::default().bg(theme::BACKGROUND_ELEMENT()),
109        )];
110        for (i, (val, label)) in opts.iter().enumerate() {
111            let selected = *val == self.choice;
112            let (bg, fg) = if selected {
113                (theme::WARNING(), theme::BACKGROUND())
114            } else {
115                (theme::BACKGROUND_MENU(), theme::TEXT_MUTED())
116            };
117            spans.push(Span::styled(
118                format!(" {label} "),
119                Style::default().fg(fg).bg(bg).add_modifier(if selected {
120                    Modifier::BOLD
121                } else {
122                    Modifier::empty()
123                }),
124            ));
125            if i < opts.len() - 1 {
126                spans.push(Span::styled(
127                    " ",
128                    Style::default().bg(theme::BACKGROUND_ELEMENT()),
129                ));
130            }
131        }
132
133        let hint = Line::from(vec![
134            Span::styled(
135                "←→ ",
136                Style::default().fg(theme::TEXT()).bg(theme::BACKGROUND_ELEMENT()),
137            ),
138            Span::styled(
139                "select  ",
140                Style::default().fg(theme::TEXT_MUTED()).bg(theme::BACKGROUND_ELEMENT()),
141            ),
142            Span::styled(
143                "↵ ",
144                Style::default().fg(theme::TEXT()).bg(theme::BACKGROUND_ELEMENT()),
145            ),
146            Span::styled(
147                "confirm ",
148                Style::default().fg(theme::TEXT_MUTED()).bg(theme::BACKGROUND_ELEMENT()),
149            ),
150        ]);
151
152        let buttons_line = Line::from(spans);
153        let buttons_w = buttons_line.width() as u16;
154        let hint_w = hint.width() as u16;
155
156        Paragraph::new(buttons_line)
157            .style(Style::default().bg(theme::BACKGROUND_ELEMENT()))
158            .render(
159                Rect {
160                    x: bar_area.x,
161                    y: bar_area.y + 1,
162                    width: buttons_w.min(bar_area.width),
163                    height: 1,
164                },
165                buf,
166            );
167        if bar_area.width > hint_w {
168            Paragraph::new(hint)
169                .style(Style::default().bg(theme::BACKGROUND_ELEMENT()))
170                .render(
171                    Rect {
172                        x: bar_area.x + bar_area.width - hint_w,
173                        y: bar_area.y + 1,
174                        width: hint_w,
175                        height: 1,
176                    },
177                    buf,
178                );
179        }
180    }
181}