rab/agent/ui/components/
confirm_overlay.rs1use crate::agent::ui::theme::current_theme;
7use crate::tui::Component;
8use crate::tui::keybindings::{ACTION_SELECT_CANCEL, ACTION_SELECT_CONFIRM, get_keybindings};
9use crossterm::event::KeyEvent;
10
11pub struct ConfirmOverlay {
13 title: String,
14 message: String,
15 on_confirm: Option<Box<dyn FnOnce()>>,
16 on_cancel: Option<Box<dyn FnOnce()>>,
17 selected: bool, done: bool,
19}
20
21impl ConfirmOverlay {
22 pub fn new(title: impl Into<String>, message: impl Into<String>) -> Self {
23 Self {
24 title: title.into(),
25 message: message.into(),
26 on_confirm: None,
27 on_cancel: None,
28 selected: true, done: false,
30 }
31 }
32
33 pub fn on_confirm<F>(&mut self, f: F)
35 where
36 F: FnOnce() + 'static,
37 {
38 self.on_confirm = Some(Box::new(f));
39 }
40
41 pub fn on_cancel<F>(&mut self, f: F)
43 where
44 F: FnOnce() + 'static,
45 {
46 self.on_cancel = Some(Box::new(f));
47 }
48}
49
50impl Component for ConfirmOverlay {
51 fn render(&mut self, width: usize) -> Vec<String> {
52 let theme = current_theme();
53 let mut lines: Vec<String> = Vec::new();
54
55 lines.push(theme.dim(&"─".repeat(width.saturating_sub(2))));
57 lines.push(String::new());
58
59 lines.push(format!(" {}", theme.bold(&theme.accent(&self.title))));
61 lines.push(String::new());
62
63 let max_text_width = width.saturating_sub(4); if max_text_width > 10 {
66 let mut remaining = self.message.as_str();
67 while !remaining.is_empty() {
68 let break_at = if remaining.len() <= max_text_width {
69 remaining.len()
70 } else {
71 let slice = &remaining[..max_text_width];
73 let last_space = slice.rfind(' ').unwrap_or(max_text_width);
74 if last_space == 0 {
75 max_text_width
76 } else {
77 last_space
78 }
79 };
80 lines.push(format!(" {}", &remaining[..break_at]));
81 remaining = remaining[break_at..].trim_start();
82 }
83 } else {
84 lines.push(format!(" {}", self.message));
85 }
86 lines.push(String::new());
87
88 let yes_style = if self.selected {
90 theme.bold(&theme.fg_key(crate::agent::ui::theme::ThemeKey::Success, "[Y] Yes"))
91 } else {
92 theme.dim("[Y] Yes")
93 };
94 let no_style = if !self.selected {
95 theme.bold(&theme.fg_key(crate::agent::ui::theme::ThemeKey::Error, "[N] No"))
96 } else {
97 theme.dim("[N] No")
98 };
99
100 lines.push(format!(" {} {}", yes_style, no_style));
101 lines.push(String::new());
102
103 lines.push(format!(
105 " {}",
106 theme.dim("Tab/← →: switch · Enter: confirm · Esc: cancel")
107 ));
108
109 lines.push(String::new());
110 lines.push(theme.dim(&"─".repeat(width.saturating_sub(2))));
112
113 lines
114 }
115
116 fn handle_input(&mut self, key: &KeyEvent) -> bool {
117 if self.done {
118 return false;
119 }
120
121 let kb = get_keybindings();
122
123 if kb.matches(key, ACTION_SELECT_CANCEL) {
125 self.done = true;
126 if let Some(cb) = self.on_cancel.take() {
127 cb();
128 }
129 return false;
130 }
131
132 if kb.matches(key, ACTION_SELECT_CONFIRM)
134 || key.code == crossterm::event::KeyCode::Char('y')
135 {
136 self.done = true;
137 if let Some(cb) = self.on_confirm.take() {
138 cb();
139 }
140 return false;
141 }
142
143 if key.code == crossterm::event::KeyCode::Char('n') {
145 self.done = true;
146 if let Some(cb) = self.on_cancel.take() {
147 cb();
148 }
149 return false;
150 }
151
152 if key.code == crossterm::event::KeyCode::Tab
154 || key.code == crossterm::event::KeyCode::Right
155 || key.code == crossterm::event::KeyCode::Left
156 {
157 self.selected = !self.selected;
158 return true;
159 }
160
161 false
162 }
163}