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}