1use ratatui::{
2 buffer::Buffer,
3 layout::{Position, Rect},
4 style::{Modifier, Style},
5 widgets::Widget,
6};
7
8use crate::key::Key;
9use crate::styles::{Styles, Tone};
10use crate::text::{self, Pen};
11
12const MARK: &str = "▸ ";
13const BEFORE: u16 = 2;
14const BETWEEN: u16 = 3;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum Answer {
19 Yes,
20 No,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[non_exhaustive]
25pub struct ConfirmKeys {
26 pub yes: &'static [Key],
27 pub no: &'static [Key],
28 pub choose: bool,
29 pub toggle: &'static [Key],
30 pub accept: &'static [Key],
31}
32
33impl ConfirmKeys {
34 pub const HEY: ConfirmKeys = ConfirmKeys {
35 yes: &[Key::Char('y'), Key::Char('Y')],
36 no: &[Key::Char('n'), Key::Char('N'), Key::Esc],
37 choose: true,
38 toggle: &[
39 Key::Left,
40 Key::Right,
41 Key::Tab,
42 Key::BackTab,
43 Key::Char('h'),
44 Key::Char('l'),
45 ],
46 accept: &[Key::Enter],
47 };
48
49 pub const ENTER: ConfirmKeys = ConfirmKeys {
50 yes: &[Key::Enter],
51 no: &[Key::Esc],
52 choose: false,
53 toggle: &[],
54 accept: &[],
55 };
56
57 pub const fn yes(mut self, keys: &'static [Key]) -> Self {
58 self.yes = keys;
59 self
60 }
61
62 pub const fn no(mut self, keys: &'static [Key]) -> Self {
63 self.no = keys;
64 self
65 }
66
67 pub const fn choose(mut self, choose: bool) -> Self {
68 self.choose = choose;
69 self
70 }
71
72 pub const fn toggle(mut self, keys: &'static [Key]) -> Self {
73 self.toggle = keys;
74 self
75 }
76
77 pub const fn accept(mut self, keys: &'static [Key]) -> Self {
78 self.accept = keys;
79 self
80 }
81}
82
83impl Default for ConfirmKeys {
84 fn default() -> Self {
85 ConfirmKeys::HEY
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct Confirm {
91 yes: bool,
92 keys: ConfirmKeys,
93}
94
95impl Default for Confirm {
96 fn default() -> Self {
97 Confirm::new()
98 }
99}
100
101impl Confirm {
102 pub const fn new() -> Self {
103 Confirm {
104 yes: false,
105 keys: ConfirmKeys::HEY,
106 }
107 }
108
109 pub const fn keys(mut self, keys: ConfirmKeys) -> Self {
110 self.keys = keys;
111 self
112 }
113
114 pub const fn start(mut self, answer: Answer) -> Self {
115 self.yes = matches!(answer, Answer::Yes);
116 self
117 }
118
119 pub fn yes(&self) -> bool {
120 self.yes
121 }
122
123 pub fn choosing(&self) -> bool {
124 self.keys.choose
125 }
126
127 pub fn key(&mut self, key: Key) -> Option<Answer> {
128 if self.keys.yes.contains(&key) {
129 return Some(Answer::Yes);
130 }
131 if self.keys.no.contains(&key) {
132 return Some(Answer::No);
133 }
134 if !self.keys.choose {
135 return None;
136 }
137 if self.keys.accept.contains(&key) {
138 return Some(if self.yes { Answer::Yes } else { Answer::No });
139 }
140 if self.keys.toggle.contains(&key) {
141 self.yes = !self.yes;
142 }
143 None
144 }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148#[non_exhaustive]
149pub struct Words<'a> {
150 pub yes: &'a str,
151 pub no: &'a str,
152 pub hint: Option<&'a str>,
153}
154
155impl<'a> Words<'a> {
156 pub const fn new(yes: &'a str, no: &'a str) -> Self {
157 Words {
158 yes,
159 no,
160 hint: None,
161 }
162 }
163
164 pub const fn hint(mut self, hint: &'a str) -> Self {
165 self.hint = Some(hint);
166 self
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub struct ConfirmBar<'a> {
172 question: &'a str,
173 confirm: Confirm,
174 words: Words<'a>,
175 rule: bool,
176 inline: bool,
177 tone: Tone,
178 styles: Option<Styles>,
179}
180
181impl<'a> ConfirmBar<'a> {
182 pub fn new(question: &'a str, confirm: &Confirm, words: Words<'a>) -> Self {
183 ConfirmBar {
184 question,
185 confirm: *confirm,
186 words,
187 rule: true,
188 inline: false,
189 tone: Tone::Ink,
190 styles: None,
191 }
192 }
193
194 pub fn rule(mut self, rule: bool) -> Self {
195 self.rule = rule;
196 self
197 }
198
199 pub fn inline(mut self, inline: bool) -> Self {
200 self.inline = inline;
201 self
202 }
203
204 pub fn tone(mut self, tone: Tone) -> Self {
205 self.tone = tone;
206 self
207 }
208
209 pub fn styles(mut self, styles: Styles) -> Self {
210 self.styles = Some(styles);
211 self
212 }
213
214 pub(crate) fn styled(mut self, fallback: Styles) -> Self {
215 self.styles = Some(self.styles.unwrap_or(fallback));
216 self
217 }
218
219 fn look(&self) -> Styles {
220 self.styles.unwrap_or_default()
221 }
222
223 pub fn height(&self) -> u16 {
224 let hint = !self.inline && self.words.hint.is_some();
225 1 + u16::from(self.rule) + u16::from(hint)
226 }
227
228 fn choices_width(&self) -> u16 {
229 if !self.confirm.keys.choose {
230 return 0;
231 }
232 BEFORE
233 .saturating_add(text::width(MARK))
234 .saturating_add(text::width(self.words.yes))
235 .saturating_add(BETWEEN)
236 .saturating_add(text::width(self.words.no))
237 }
238
239 fn question_style(&self) -> Style {
240 self.tone.style(&self.look()).add_modifier(Modifier::BOLD)
241 }
242
243 fn draw_choices(&self, pen: &mut Pen) {
244 if !self.confirm.keys.choose {
245 return;
246 }
247 pen.skip(BEFORE);
248 let styles = self.look();
249 let lit = styles.lit();
250 let muted = styles.muted;
251 if self.confirm.yes {
252 pen.put(MARK, lit);
253 pen.put(self.words.yes, lit);
254 pen.skip(BETWEEN);
255 pen.put(self.words.no, muted);
256 } else {
257 pen.put(self.words.yes, muted);
258 pen.skip(BETWEEN);
259 pen.put(MARK, lit);
260 pen.put(self.words.no, lit);
261 }
262 }
263}
264
265impl Widget for ConfirmBar<'_> {
266 fn render(self, area: Rect, buf: &mut Buffer) {
267 (&self).render(area, buf);
268 }
269}
270
271impl Widget for &ConfirmBar<'_> {
272 fn render(self, area: Rect, buf: &mut Buffer) {
273 self.paint(area, buf);
274 }
275}
276
277impl ConfirmBar<'_> {
278 pub(crate) fn paint(&self, area: Rect, buf: &mut Buffer) -> Option<Position> {
279 if area.is_empty() {
280 return None;
281 }
282 let mut end = None;
283 let mut y = area.y;
284 let styles = self.look();
285 if self.rule {
286 text::rule(buf, area, styles.rule);
287 y += 1;
288 }
289 let muted = styles.muted;
290 if let Some(mut pen) = Pen::new(buf, area, area.x, y) {
291 let choices = self.choices_width();
292 let room = area.width.saturating_sub(choices);
293 let least = text::width(self.question).min(area.width / 2).max(1);
294 if room >= least {
295 pen.clip(self.question, self.question_style(), room);
296 self.draw_choices(&mut pen);
297 } else {
298 pen.clip(self.question, self.question_style(), area.width);
299 }
300 if self.inline
301 && let Some(hint) = self.words.hint
302 {
303 pen.indented(hint, muted);
304 return Some(Position::new(pen.x, y));
305 }
306 end = Some(Position::new(pen.x, y));
307 y += 1;
308 }
309 if let Some(hint) = self.words.hint
310 && let Some(mut pen) = Pen::new(buf, area, area.x, y)
311 {
312 pen.indented(hint, muted);
313 end = Some(Position::new(pen.x, y));
314 }
315 end
316 }
317}