keybindings/
dialog.rs

1//! # Interactive dialog prompts
2//!
3//! ## Overview
4//!
5//! Dialogs allow interacting with the user outside of the normal keybindings. For example, if the
6//! user has tried to take an action that requires confirmation, then you can use [PromptYesNo] to
7//! show them a message and fetch the `y` or `Y` keypresses.
8use std::borrow::Cow;
9use std::fmt::Debug;
10
11use textwrap::wrap;
12use unicode_segmentation::UnicodeSegmentation;
13use unicode_width::UnicodeWidthStr;
14
15/// An interactive dialog at the bottom of the screen.
16pub trait Dialog<A>: Debug + Send {
17    /// Render the lines to show to the user within the available area.
18    fn render(&mut self, max_rows: usize, max_cols: usize) -> Vec<Cow<'_, str>>;
19
20    /// The user's response to this interactive dialog. The user will be repeatedly
21    /// prompted until this returns Some.
22    fn input(&mut self, c: char) -> Option<Vec<A>>;
23}
24
25/// Interactively prompt the user for "y" or "n"
26#[derive(Clone, Debug)]
27pub struct PromptYesNo<A: Clone + Debug> {
28    res: Vec<A>,
29    msg: Cow<'static, str>,
30}
31
32impl<A: Debug> PromptYesNo<A>
33where
34    A: Clone + Debug,
35{
36    /// Create a new prompt with the given message and resulting actions.
37    pub fn new<T>(prompt: T, actions: Vec<A>) -> Self
38    where
39        T: Into<Cow<'static, str>>,
40    {
41        PromptYesNo { res: actions, msg: prompt.into() }
42    }
43}
44
45impl<A> Dialog<A> for PromptYesNo<A>
46where
47    A: Clone + Debug + Send + 'static,
48{
49    fn render(&mut self, max_rows: usize, max_cols: usize) -> Vec<Cow<'_, str>> {
50        if max_rows == 0 {
51            return vec![];
52        }
53
54        let mut lines = wrap(self.msg.as_ref(), max_cols);
55
56        if let Some(last) = lines.last_mut() {
57            last.to_mut().push_str(" (y/N)");
58        }
59
60        return lines;
61    }
62
63    fn input(&mut self, c: char) -> Option<Vec<A>> {
64        match c {
65            'y' | 'Y' => Some(self.res.clone()),
66            _ => Some(vec![]),
67        }
68    }
69}
70
71fn find_end(s: &str, start: usize, mut rows: usize, width: usize) -> usize {
72    let mut idx = 0;
73    let mut full = true;
74    let mut cols = width;
75
76    if rows == 0 || width == 0 {
77        // No room to show the string in, so just show it all as one page that
78        // the user can press space to escape from.
79        return s.len();
80    }
81
82    for (i, grapheme) in UnicodeSegmentation::grapheme_indices(&s[start..], false) {
83        idx = i;
84
85        if rows == 0 {
86            full = false;
87            break;
88        }
89
90        if let "\n" | "\r" | "\r\n" = grapheme {
91            cols = width;
92            rows = rows.saturating_sub(1);
93            continue;
94        }
95
96        if cols == 0 {
97            cols = width;
98            rows = rows.saturating_sub(1);
99        }
100
101        cols -= UnicodeWidthStr::width(grapheme);
102    }
103
104    if full {
105        return s.len();
106    } else {
107        return start + idx;
108    }
109}
110
111/// Allow the user to interactively page through some text.
112#[derive(Clone, Debug)]
113pub struct Pager<A: Clone + Debug> {
114    text: Cow<'static, str>,
115    idx_start: usize,
116    idx_end: usize,
117    area: (usize, usize),
118    res: Vec<A>,
119}
120
121impl<A> Pager<A>
122where
123    A: Clone + Debug,
124{
125    /// Create a new pager to display within the given boundaries.
126    pub fn new<T>(text: T, actions: Vec<A>) -> Self
127    where
128        T: Into<Cow<'static, str>>,
129    {
130        let text = text.into();
131        let idx_start = 0;
132        let idx_end = text.len();
133
134        Pager {
135            text,
136            idx_start,
137            idx_end,
138            area: (0, 0),
139            res: actions,
140        }
141    }
142
143    fn next_page(&mut self) -> bool {
144        self.idx_start = self.idx_end;
145        self.idx_end = self.text.len();
146
147        return self.idx_start == self.idx_end;
148    }
149}
150
151impl<A> Dialog<A> for Pager<A>
152where
153    A: Clone + Debug + Send + 'static,
154{
155    fn render(&mut self, max_rows: usize, max_cols: usize) -> Vec<Cow<'_, str>> {
156        if max_rows == 0 {
157            return vec![];
158        }
159
160        let max_rows = max_rows.saturating_sub(1);
161
162        if (max_rows, max_cols) != self.area {
163            self.idx_end = find_end(self.text.as_ref(), self.idx_start, max_rows, max_cols);
164            self.area = (max_rows, max_cols);
165        }
166
167        let s = &self.text[self.idx_start..self.idx_end];
168        let options = textwrap::Options::new(max_cols).break_words(true);
169        let mut lines = wrap(s.trim_end(), options);
170        lines.push("--- Press Space To Continue ---".into());
171        lines
172    }
173
174    fn input(&mut self, c: char) -> Option<Vec<A>> {
175        if c == ' ' {
176            if self.next_page() {
177                return Some(self.res.clone());
178            } else {
179                return None;
180            }
181        } else {
182            return None;
183        }
184    }
185}
186
187/// One of the choices for a [MultiChoice] prompt.
188#[derive(Clone, Debug)]
189pub struct MultiChoiceItem<A: Clone + Debug> {
190    choice: char,
191    text: Cow<'static, str>,
192    actions: Vec<A>,
193}
194
195impl<A> MultiChoiceItem<A>
196where
197    A: Clone + Debug,
198{
199    /// Create a new choice.
200    pub fn new<T>(choice: char, text: T, actions: Vec<A>) -> Self
201    where
202        T: Into<Cow<'static, str>>,
203    {
204        let text = text.into();
205
206        MultiChoiceItem { text, choice, actions }
207    }
208}
209
210/// A prompt that has multiple choices resulting in different actions.
211#[derive(Clone, Debug)]
212pub struct MultiChoice<A: Clone + Debug> {
213    choices: Vec<MultiChoiceItem<A>>,
214    idx_start: usize,
215    idx_end: usize,
216    area: (usize, usize),
217}
218
219impl<A> MultiChoice<A>
220where
221    A: Clone + Debug,
222{
223    /// Create a new prompt with multiple choices.
224    pub fn new(choices: Vec<MultiChoiceItem<A>>) -> Self {
225        MultiChoice {
226            idx_start: 0,
227            idx_end: choices.len(),
228            area: (0, 0),
229            choices,
230        }
231    }
232
233    fn next_page(&mut self) -> bool {
234        self.idx_start = self.idx_end;
235        self.idx_end = self.choices.len();
236
237        return self.idx_start == self.idx_end;
238    }
239}
240
241impl<A> Dialog<A> for MultiChoice<A>
242where
243    A: Clone + Debug + Send + 'static,
244{
245    fn render(&mut self, max_rows: usize, max_cols: usize) -> Vec<Cow<'_, str>> {
246        if max_rows == 0 {
247            return vec![];
248        }
249
250        let max_rows = max_rows.saturating_sub(1);
251
252        if (max_rows, max_cols) != self.area {
253            self.idx_end = self.choices.len().min(self.idx_start + max_rows);
254            self.area = (max_rows, max_cols);
255        }
256
257        let mut lines = self.choices[self.idx_start..self.idx_end]
258            .iter()
259            .map(|c| format!("({}) {}", c.choice, c.text))
260            .map(Cow::Owned)
261            .collect::<Vec<_>>();
262        lines.push("--- Select A Choice Or Press Space To Continue ---".into());
263
264        return lines;
265    }
266
267    fn input(&mut self, c: char) -> Option<Vec<A>> {
268        if c == ' ' {
269            if self.next_page() {
270                return Some(vec![]);
271            } else {
272                return None;
273            }
274        }
275
276        for item in self.choices.iter() {
277            if item.choice == c {
278                return Some(item.actions.clone());
279            }
280        }
281
282        return None;
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_yes_no() {
292        let mut dialog = PromptYesNo::new("Are you sure?", vec![1, 2, 3]);
293
294        let lines = dialog.render(1, 100);
295        assert_eq!(lines.len(), 1);
296        assert_eq!(lines[0].as_ref(), "Are you sure? (y/N)");
297
298        // Lower- and uppercase 'y' return actions.
299        assert_eq!(dialog.input('y'), Some(vec![1, 2, 3]));
300        assert_eq!(dialog.input('Y'), Some(vec![1, 2, 3]));
301
302        // Lower- and uppercase 'n' don't return actions.
303        assert_eq!(dialog.input('n'), Some(vec![]));
304        assert_eq!(dialog.input('N'), Some(vec![]));
305
306        // Defaults to not returning an action.
307        assert_eq!(dialog.input('q'), Some(vec![]));
308    }
309
310    #[test]
311    fn test_pager() {
312        let mut dialog =
313            Pager::new("This is Line 1\nThis is Line 2\nThis is Line 3\nThis is Line 4", vec![5]);
314
315        // Only returns lines that we have room for.
316        let lines = dialog.render(3, 15);
317        assert_eq!(lines.len(), 3);
318        assert_eq!(lines[0].as_ref(), "This is Line 1");
319        assert_eq!(lines[1].as_ref(), "This is Line 2");
320        assert_eq!(lines[2].as_ref(), "--- Press Space To Continue ---");
321
322        // Wraps the original text (but not press space message).
323        let lines = dialog.render(3, 10);
324        assert_eq!(lines.len(), 3);
325        assert_eq!(lines[0].as_ref(), "This is");
326        assert_eq!(lines[1].as_ref(), "Line 1");
327        assert_eq!(lines[2].as_ref(), "--- Press Space To Continue ---");
328
329        // We can give the pager more space, and it resizes.
330        let lines = dialog.render(5, 10);
331        assert_eq!(lines.len(), 5);
332        assert_eq!(lines[0].as_ref(), "This is");
333        assert_eq!(lines[1].as_ref(), "Line 1");
334        assert_eq!(lines[2].as_ref(), "This is");
335        assert_eq!(lines[3].as_ref(), "Line 2");
336        assert_eq!(lines[4].as_ref(), "--- Press Space To Continue ---");
337
338        // Press space bar.
339        let res = dialog.input(' ');
340        assert_eq!(res, None);
341
342        // Now renders the second page.
343        let lines = dialog.render(5, 10);
344        assert_eq!(lines.len(), 5);
345        assert_eq!(lines[0].as_ref(), "This is");
346        assert_eq!(lines[1].as_ref(), "Line 3");
347        assert_eq!(lines[2].as_ref(), "This is");
348        assert_eq!(lines[3].as_ref(), "Line 4");
349        assert_eq!(lines[4].as_ref(), "--- Press Space To Continue ---");
350
351        // Press space bar again, and we're done.
352        let res = dialog.input(' ');
353        assert_eq!(res, Some(vec![5]));
354    }
355
356    #[test]
357    fn test_multi_choice() {
358        let choice1 = MultiChoiceItem::new('a', "Choice A", vec![0, 1]);
359        let choice2 = MultiChoiceItem::new('q', "Choice Q", vec![2]);
360        let choice3 = MultiChoiceItem::new('5', "Choice 5", vec![3]);
361        let choices = vec![choice1, choice2, choice3];
362        let mut dialog = MultiChoice::new(choices.clone());
363
364        // Only returns lines that we have room for.
365        let lines = dialog.render(2, 15);
366        assert_eq!(lines.len(), 2);
367        assert_eq!(lines[0].as_ref(), "(a) Choice A");
368        assert_eq!(lines[1].as_ref(), "--- Select A Choice Or Press Space To Continue ---");
369
370        // Increase visible lines.
371        let lines = dialog.render(3, 15);
372        assert_eq!(lines.len(), 3);
373        assert_eq!(lines[0].as_ref(), "(a) Choice A");
374        assert_eq!(lines[1].as_ref(), "(q) Choice Q");
375        assert_eq!(lines[2].as_ref(), "--- Select A Choice Or Press Space To Continue ---");
376
377        // Press Space.
378        assert_eq!(dialog.input(' '), None);
379
380        // Next page renders.
381        let lines = dialog.render(3, 15);
382        assert_eq!(lines.len(), 2);
383        assert_eq!(lines[0].as_ref(), "(5) Choice 5");
384        assert_eq!(lines[1].as_ref(), "--- Select A Choice Or Press Space To Continue ---");
385
386        // Select choice from previous page.
387        assert_eq!(dialog.input('q'), Some(vec![2]));
388
389        // Restart and select a different choice.
390        let mut dialog = MultiChoice::new(choices.clone());
391        assert_eq!(dialog.input('a'), Some(vec![0, 1]));
392
393        // Restart and select a different choice.
394        let mut dialog = MultiChoice::new(choices.clone());
395        assert_eq!(dialog.input('5'), Some(vec![3]));
396    }
397}