1use std::{convert::TryFrom, io};
2
3use crate::{
4 backend::Backend,
5 events,
6 layout::Layout,
7 style::{Color, Stylize},
8 Widget,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum Delimiter {
14 Parentheses,
16 Braces,
18 SquareBracket,
20 AngleBracket,
22 Other(char, char),
24 None,
26}
27
28impl From<Delimiter> for Option<(char, char)> {
29 fn from(delim: Delimiter) -> Self {
30 match delim {
31 Delimiter::Parentheses => Some(('(', ')')),
32 Delimiter::Braces => Some(('{', '}')),
33 Delimiter::SquareBracket => Some(('[', ']')),
34 Delimiter::AngleBracket => Some(('<', '>')),
35 Delimiter::Other(start, end) => Some((start, end)),
36 Delimiter::None => None,
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct Prompt<M, H = &'static str> {
44 message: M,
45 hint: Option<H>,
46 delim: Delimiter,
47 message_len: u16,
48 hint_len: u16,
49}
50
51impl<M: AsRef<str>, H: AsRef<str>> Prompt<M, H> {
52 pub fn new(message: M) -> Self {
54 Self {
55 message_len: u16::try_from(textwrap::core::display_width(message.as_ref()))
56 .expect("message must fit within a u16"),
57 message,
58 hint: None,
59 delim: Delimiter::Parentheses,
60 hint_len: 0,
61 }
62 }
63
64 pub fn with_hint(mut self, hint: H) -> Self {
66 self.hint_len = u16::try_from(textwrap::core::display_width(hint.as_ref()))
67 .expect("hint must fit within a u16");
68 self.hint = Some(hint);
69 self
70 }
71
72 pub fn with_optional_hint(self, hint: Option<H>) -> Self {
74 match hint {
75 Some(hint) => self.with_hint(hint),
76 None => self,
77 }
78 }
79
80 pub fn with_delim(mut self, delim: Delimiter) -> Self {
82 self.delim = delim;
83 self
84 }
85
86 pub fn message(&self) -> &M {
88 &self.message
89 }
90
91 pub fn hint(&self) -> Option<&H> {
93 self.hint.as_ref()
94 }
95
96 pub fn delim(&self) -> Delimiter {
98 self.delim
99 }
100
101 pub fn into_message(self) -> M {
103 self.message
104 }
105
106 pub fn into_hint(self) -> Option<H> {
108 self.hint
109 }
110
111 pub fn into_message_and_hint(self) -> (M, Option<H>) {
113 (self.message, self.hint)
114 }
115
116 pub fn message_len(&self) -> u16 {
118 self.message_len
119 }
120
121 pub fn hint_len(&self) -> u16 {
123 if self.hint.is_some() {
124 match self.delim {
125 Delimiter::None => self.hint_len,
126 _ => self.hint_len + 2,
127 }
128 } else {
129 0
130 }
131 }
132
133 pub fn width(&self) -> u16 {
135 if self.hint.is_some() {
136 2 + self.message_len + 1 + self.hint_len() + 1
138 } else {
139 2 + self.message_len + 3
141 }
142 }
143
144 fn cursor_pos_impl(&self, layout: Layout) -> (u16, u16) {
145 let mut width = self.width();
146 let relative_pos = if width > layout.line_width() {
147 width -= layout.line_width();
148
149 (width % layout.width, 1 + width / layout.width)
150 } else {
151 (layout.line_offset + width, 0)
152 };
153
154 layout.offset_cursor(relative_pos)
155 }
156}
157
158impl<M: AsRef<str>> Prompt<M, &'static str> {
159 pub fn write_finished_message<B: Backend>(
161 message: &M,
162 skipped: bool,
163 backend: &mut B,
164 ) -> io::Result<()> {
165 let symbol_set = crate::symbols::current();
166 if skipped {
167 backend.write_styled(&symbol_set.cross.yellow())?;
168 } else {
169 backend.write_styled(&symbol_set.completed.light_green())?;
170 }
171 backend.write_all(b" ")?;
172 backend.write_styled(&message.as_ref().bold())?;
173 backend.write_all(b" ")?;
174 backend.write_styled(&symbol_set.middle_dot.dark_grey())?;
175 backend.write_all(b" ")
176 }
177}
178
179impl<M: AsRef<str>, H: AsRef<str>> Widget for Prompt<M, H> {
180 fn render<B: Backend>(&mut self, layout: &mut Layout, b: &mut B) -> io::Result<()> {
181 b.write_styled(&"? ".light_green())?;
182 b.write_styled(&self.message.as_ref().bold())?;
183 b.write_all(b" ")?;
184
185 b.set_fg(Color::DarkGrey)?;
186
187 match (&self.hint, self.delim.into()) {
188 (Some(hint), Some((start, end))) => write!(b, "{}{}{}", start, hint.as_ref(), end)?,
189 (Some(hint), None) => write!(b, "{}", hint.as_ref())?,
190 (None, _) => {
191 write!(b, "{}", crate::symbols::current().arrow)?;
192 }
193 }
194
195 b.set_fg(Color::Reset)?;
196 b.write_all(b" ")?;
197
198 *layout = layout.with_cursor_pos(self.cursor_pos_impl(*layout));
199
200 Ok(())
201 }
202
203 fn height(&mut self, layout: &mut Layout) -> u16 {
204 let offset_y = layout.offset_y;
206
207 let cursor_pos = self.cursor_pos_impl(*layout);
208 *layout = layout.with_cursor_pos(cursor_pos);
209
210 cursor_pos.1 + 1 - offset_y
211 }
212
213 fn cursor_pos(&mut self, layout: Layout) -> (u16, u16) {
214 self.cursor_pos_impl(layout)
215 }
216
217 fn handle_key(&mut self, _: events::KeyEvent) -> bool {
218 false
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use crate::{backend::TestBackend, test_consts::*};
225
226 use super::*;
227
228 type Prompt = super::Prompt<&'static str, &'static str>;
229
230 #[test]
231 fn test_width() {
232 assert_eq!(Prompt::new("Hello").width(), 10);
233 assert_eq!(Prompt::new("Hello").with_hint("world").width(), 16);
234 assert_eq!(
235 Prompt::new("Hello")
236 .with_hint("world")
237 .with_delim(Delimiter::None)
238 .width(),
239 14
240 );
241 assert_eq!(Prompt::new(LOREM).with_hint(UNICODE).width(), 946);
242 }
243
244 #[test]
245 fn test_render() {
246 fn test(
247 message: &'static str,
248 hint: Option<&'static str>,
249 delim: Delimiter,
250 expected_layout: Layout,
251 ) {
252 let size = (100, 20).into();
253 let mut layout = Layout::new(5, size);
254 let mut prompt = Prompt::new(message)
255 .with_optional_hint(hint)
256 .with_delim(delim);
257 let mut backend = TestBackend::new_with_layout(size, layout);
258
259 prompt.render(&mut layout, &mut backend).unwrap();
260
261 crate::assert_backend_snapshot!(backend);
262 assert_eq!(
263 layout,
264 expected_layout,
265 "\ncursor pos = {:?}, width = {:?}",
266 prompt.cursor_pos(Layout::new(5, size)),
267 prompt.width(),
268 );
269 }
270
271 let layout = Layout::new(5, (100, 20).into());
272
273 test("Hello", None, Delimiter::None, layout.with_line_offset(15));
274
275 test(
276 "Hello",
277 Some("world"),
278 Delimiter::Parentheses,
279 layout.with_line_offset(21),
280 );
281
282 test(
283 "Hello",
284 Some("world"),
285 Delimiter::Braces,
286 layout.with_line_offset(21),
287 );
288
289 test(
290 "Hello",
291 Some("world"),
292 Delimiter::SquareBracket,
293 layout.with_line_offset(21),
294 );
295
296 test(
297 "Hello",
298 Some("world"),
299 Delimiter::AngleBracket,
300 layout.with_line_offset(21),
301 );
302
303 test(
304 "Hello",
305 Some("world"),
306 Delimiter::Other('-', '|'),
307 layout.with_line_offset(21),
308 );
309
310 test(
311 LOREM,
312 Some(UNICODE),
313 Delimiter::None,
314 layout.with_line_offset(49).with_offset(0, 9),
315 );
316 }
317
318 #[test]
319 fn test_height() {
320 let mut layout = Layout::new(5, (100, 20).into());
321
322 assert_eq!(Prompt::new("Hello").height(&mut layout.clone()), 1);
323 assert_eq!(
324 Prompt::new("Hello")
325 .with_hint("world")
326 .height(&mut layout.clone()),
327 1
328 );
329 assert_eq!(
330 Prompt::new(LOREM).with_hint(UNICODE).height(&mut layout),
331 10
332 );
333 }
334
335 #[test]
336 fn test_cursor_pos() {
337 let layout = Layout::new(5, (100, 20).into());
338
339 assert_eq!(Prompt::new("Hello").cursor_pos_impl(layout), (15, 0));
340 assert_eq!(
341 Prompt::new("Hello")
342 .with_hint("world")
343 .cursor_pos_impl(layout),
344 (21, 0)
345 );
346 assert_eq!(
347 Prompt::new("Hello")
348 .with_hint("world")
349 .with_delim(Delimiter::None)
350 .cursor_pos_impl(layout),
351 (19, 0)
352 );
353 assert_eq!(
354 Prompt::new(LOREM)
355 .with_hint(UNICODE)
356 .cursor_pos_impl(layout),
357 (51, 9)
358 );
359
360 assert_eq!(
361 Prompt::new(LOREM)
362 .with_hint(UNICODE)
363 .cursor_pos_impl(layout.with_offset(0, 3)),
364 (51, 12)
365 );
366 }
367}