mockforge_tui/widgets/
confirm.rs1use crossterm::event::{KeyCode, KeyEvent};
4use ratatui::{
5 layout::{Constraint, Flex, Layout, Rect},
6 text::{Line, Span},
7 widgets::{Block, Borders, Clear, Paragraph},
8 Frame,
9};
10
11use crate::theme::Theme;
12
13#[derive(Debug)]
15pub struct ConfirmDialog {
16 pub visible: bool,
17 pub title: String,
18 pub message: String,
19 pub selected_yes: bool,
20}
21
22impl Default for ConfirmDialog {
23 fn default() -> Self {
24 Self {
25 visible: false,
26 title: String::new(),
27 message: String::new(),
28 selected_yes: false,
29 }
30 }
31}
32
33impl ConfirmDialog {
34 pub fn new() -> Self {
35 Self::default()
36 }
37
38 pub fn show(&mut self, title: impl Into<String>, message: impl Into<String>) {
40 self.title = title.into();
41 self.message = message.into();
42 self.selected_yes = false;
43 self.visible = true;
44 }
45
46 pub fn hide(&mut self) {
48 self.visible = false;
49 }
50
51 pub fn handle_key(&mut self, key: KeyEvent) -> Option<bool> {
54 if !self.visible {
55 return None;
56 }
57 match key.code {
58 KeyCode::Left | KeyCode::Char('h') => {
59 self.selected_yes = true;
60 None
61 }
62 KeyCode::Right | KeyCode::Char('l') => {
63 self.selected_yes = false;
64 None
65 }
66 KeyCode::Char('y') => {
67 self.hide();
68 Some(true)
69 }
70 KeyCode::Char('n') | KeyCode::Esc => {
71 self.hide();
72 Some(false)
73 }
74 KeyCode::Enter => {
75 let result = self.selected_yes;
76 self.hide();
77 Some(result)
78 }
79 _ => None,
80 }
81 }
82
83 pub fn render(&self, frame: &mut Frame) {
85 if !self.visible {
86 return;
87 }
88
89 let area = centered_rect(40, 20, frame.area());
90 frame.render_widget(Clear, area);
91
92 let yes_style = if self.selected_yes {
93 Theme::highlight()
94 } else {
95 Theme::dim()
96 };
97 let no_style = if self.selected_yes {
98 Theme::dim()
99 } else {
100 Theme::highlight()
101 };
102
103 let lines = vec![
104 Line::from(""),
105 Line::from(Span::styled(&self.message, Theme::base())),
106 Line::from(""),
107 Line::from(vec![
108 Span::styled(" [ Yes ] ", yes_style),
109 Span::raw(" "),
110 Span::styled(" [ No ] ", no_style),
111 ]),
112 ];
113
114 let block = Block::default()
115 .title(format!(" {} ", self.title))
116 .title_style(Theme::title())
117 .borders(Borders::ALL)
118 .border_style(Theme::dim())
119 .style(Theme::surface());
120
121 let paragraph =
122 Paragraph::new(lines).block(block).alignment(ratatui::layout::Alignment::Center);
123 frame.render_widget(paragraph, area);
124 }
125}
126
127fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
128 let vertical = Layout::vertical([Constraint::Percentage(percent_y)])
129 .flex(Flex::Center)
130 .split(area);
131 Layout::horizontal([Constraint::Percentage(percent_x)])
132 .flex(Flex::Center)
133 .split(vertical[0])[0]
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
140
141 fn key(code: KeyCode) -> KeyEvent {
142 KeyEvent {
143 code,
144 modifiers: KeyModifiers::NONE,
145 kind: KeyEventKind::Press,
146 state: KeyEventState::NONE,
147 }
148 }
149
150 #[test]
151 fn new_creates_hidden_dialog() {
152 let d = ConfirmDialog::new();
153 assert!(!d.visible);
154 assert!(d.title.is_empty());
155 assert!(d.message.is_empty());
156 assert!(!d.selected_yes);
157 }
158
159 #[test]
160 fn show_makes_dialog_visible() {
161 let mut d = ConfirmDialog::new();
162 d.show("Delete?", "Are you sure you want to delete this?");
163 assert!(d.visible);
164 assert_eq!(d.title, "Delete?");
165 assert_eq!(d.message, "Are you sure you want to delete this?");
166 assert!(!d.selected_yes);
168 }
169
170 #[test]
171 fn hide_makes_dialog_invisible() {
172 let mut d = ConfirmDialog::new();
173 d.show("Title", "Message");
174 d.hide();
175 assert!(!d.visible);
176 }
177
178 #[test]
179 fn hidden_dialog_does_not_consume_keys() {
180 let mut d = ConfirmDialog::new();
181 let result = d.handle_key(key(KeyCode::Char('y')));
182 assert_eq!(result, None);
183 }
184
185 #[test]
186 fn y_key_confirms_and_hides() {
187 let mut d = ConfirmDialog::new();
188 d.show("Title", "Msg");
189
190 let result = d.handle_key(key(KeyCode::Char('y')));
191 assert_eq!(result, Some(true));
192 assert!(!d.visible);
193 }
194
195 #[test]
196 fn n_key_rejects_and_hides() {
197 let mut d = ConfirmDialog::new();
198 d.show("Title", "Msg");
199
200 let result = d.handle_key(key(KeyCode::Char('n')));
201 assert_eq!(result, Some(false));
202 assert!(!d.visible);
203 }
204
205 #[test]
206 fn esc_key_rejects_and_hides() {
207 let mut d = ConfirmDialog::new();
208 d.show("Title", "Msg");
209
210 let result = d.handle_key(key(KeyCode::Esc));
211 assert_eq!(result, Some(false));
212 assert!(!d.visible);
213 }
214
215 #[test]
216 fn left_right_toggle_selection() {
217 let mut d = ConfirmDialog::new();
218 d.show("Title", "Msg");
219 assert!(!d.selected_yes);
221
222 let result = d.handle_key(key(KeyCode::Left));
224 assert_eq!(result, None); assert!(d.selected_yes);
226
227 let result = d.handle_key(key(KeyCode::Right));
229 assert_eq!(result, None);
230 assert!(!d.selected_yes);
231 }
232
233 #[test]
234 fn h_l_toggle_selection() {
235 let mut d = ConfirmDialog::new();
236 d.show("Title", "Msg");
237
238 let result = d.handle_key(key(KeyCode::Char('h')));
240 assert_eq!(result, None);
241 assert!(d.selected_yes);
242
243 let result = d.handle_key(key(KeyCode::Char('l')));
245 assert_eq!(result, None);
246 assert!(!d.selected_yes);
247 }
248
249 #[test]
250 fn enter_confirms_current_selection_yes() {
251 let mut d = ConfirmDialog::new();
252 d.show("Title", "Msg");
253
254 d.handle_key(key(KeyCode::Left));
256 assert!(d.selected_yes);
257
258 let result = d.handle_key(key(KeyCode::Enter));
259 assert_eq!(result, Some(true));
260 assert!(!d.visible);
261 }
262
263 #[test]
264 fn enter_confirms_current_selection_no() {
265 let mut d = ConfirmDialog::new();
266 d.show("Title", "Msg");
267 assert!(!d.selected_yes);
269
270 let result = d.handle_key(key(KeyCode::Enter));
271 assert_eq!(result, Some(false));
272 assert!(!d.visible);
273 }
274
275 #[test]
276 fn unrecognized_key_returns_none() {
277 let mut d = ConfirmDialog::new();
278 d.show("Title", "Msg");
279
280 let result = d.handle_key(key(KeyCode::Char('x')));
281 assert_eq!(result, None);
282 assert!(d.visible);
284 }
285
286 #[test]
287 fn show_resets_selection_to_no() {
288 let mut d = ConfirmDialog::new();
289 d.show("First", "First message");
290 d.handle_key(key(KeyCode::Left));
292 assert!(d.selected_yes);
293
294 d.show("Second", "Second message");
296 assert!(!d.selected_yes);
297 assert_eq!(d.title, "Second");
298 }
299}