termimad/ask.rs
1use {
2 crate::*,
3 std::io,
4};
5
6/// a question that can be asked to the user, requiring
7/// him to type the key of the desired answer
8///
9/// A question can be built using [`Question::new`] or with
10/// the [ask!] macro
11pub struct Question {
12 pub md: Option<String>,
13 pub answers: Vec<Answer>,
14 pub default_answer: Option<String>,
15}
16
17/// one of the proposed answers to a question
18pub struct Answer {
19 pub key: String,
20 pub md: String,
21}
22
23impl Question {
24 /// Create a new question with some text.
25 pub fn new<S: Into<String>>(md: S) -> Self {
26 Self {
27 md: Some(md.into()),
28 answers: Vec::new(),
29 default_answer: None,
30 }
31 }
32
33 /// add a proposed answer, with a key
34 ///
35 /// The user will have to type the result of calling `to_string()` on
36 /// the key (numbers, chars, or strings are naturally good options for keys)
37 pub fn add_answer<K: std::fmt::Display, S: Into<String>>(&mut self, key: K, md: S) {
38 self.answers.push(Answer {
39 key: key.to_string(),
40 md: md.into(),
41 });
42 }
43
44 /// set the value which will be returned if the user only hits enter.
45 ///
46 /// It does *not* have to be one of the answers' key, except when you
47 /// use the [ask!] macro.
48 pub fn set_default<K: std::fmt::Display>(&mut self, default_answer: K) {
49 self.default_answer = Some(default_answer.to_string());
50 }
51
52 /// has a default been defined which isn't among the list of answers?
53 pub fn has_exotic_default(&self) -> bool {
54 if let Some(da) = self.default_answer.as_ref() {
55 for answer in &self.answers {
56 if &answer.key == da {
57 return false;
58 }
59 }
60 true
61 } else {
62 false
63 }
64 }
65
66 /// Does the asking and returns the inputted string, unless
67 /// the user just typed *enter* and there was a default value.
68 ///
69 /// If the user types something not recognized, he's asking to
70 /// try again.
71 pub fn ask(&self, skin: &MadSkin) -> io::Result<String> {
72 if let Some(md) = &self.md {
73 skin.print_text(md);
74 }
75 for a in &self.answers {
76 if self.default_answer.as_ref() == Some(&a.key) {
77 mad_print_inline!(skin, "[**$0**] ", a.key);
78 } else {
79 mad_print_inline!(skin, "[$0] ", a.key);
80 }
81 skin.print_text(&a.md);
82 }
83 loop {
84 let mut input = String::new();
85 io::stdin().read_line(&mut input)?;
86 input.truncate(input.trim_end().len());
87 if input.is_empty() {
88 if let Some(da) = &self.default_answer {
89 return Ok(da.clone());
90 }
91 }
92 for a in &self.answers {
93 if a.key == input {
94 return Ok(input);
95 }
96 }
97 println!("answer {:?} not understood", input);
98 }
99 }
100}
101
102/// ask the user to choose among proposed answers.
103///
104/// This macro makes it possible to propose several choices, with
105/// an optional default one, to execute blocks, to optionaly return
106/// a value.
107///
108/// Example of a simple `confirmation`:
109///
110/// ```no_run
111/// let confirmed = termimad::ask!(
112/// termimad::get_default_skin(),
113/// "Do you want to erase the disk ?", ('n') {
114/// ('y', "**Y**es") => { true }
115/// ('n', "**N**o") => { false }
116/// }
117/// );
118/// ```
119///
120/// Example of chained questions:
121///
122/// ```no_run
123/// use termimad::*;
124///
125/// let skin = get_default_skin();
126/// let beverage = ask!(skin, "What do I serve you ?", {
127/// ('b', "**B**eer") => {
128/// ask!(skin, "Really ? We have wine and orange juice too", (2) {
129/// ("oj", "**o**range **j**uice") => { "orange juice" }
130/// ('w' , "ok for some wine") => { "wine" }
131/// ('b' , "I said **beer**") => { "beer" }
132/// ( 2 , "Make it **2** beer glasses!") => { "beer x 2" }
133/// })
134/// }
135/// ('w', "**W**ine") => {
136/// println!("An excellent choice!");
137/// "wine"
138/// }
139/// });
140/// ```
141///
142/// Limits compared to the [Question] API:
143/// - the default answer, if any, must be among the declared ones
144///
145/// Note that examples/ask contains several examples of this macro.
146#[macro_export]
147macro_rules! ask {
148 (
149 $skin: expr,
150 $question: expr,
151 { $(($key: expr, $answer: expr) => $r: block)+ }
152 ) => {{
153 let mut question = $crate::Question {
154 md: Some($question.to_string()),
155 answers: vec![$($crate::Answer { key: $key.to_string(), md: $answer.to_string() }),*],
156 default_answer: None,
157 };
158 let key = question.ask($skin).unwrap();
159 let mut answers = question.answers.drain(..);
160 match key {
161 $(
162 _ if answers.next().unwrap().key == key => { $r }
163 )*
164 _ => { unreachable!(); }
165 }
166 }};
167 (
168 $skin: expr,
169 $question: expr,
170 ($default_answer: expr)
171 { $(($key: expr, $answer: expr) => $r: block)+ }
172 ) => {{
173 let mut question = $crate::Question {
174 md: Some($question.to_string()),
175 answers: vec![$($crate::Answer { key: $key.to_string(), md: $answer.to_string() }),*],
176 default_answer: Some($default_answer.to_string()),
177 };
178 if question.has_exotic_default() {
179 // I should rewrite this macro as a proc macro...
180 panic!("default answer when using the ask! macro must be among declared answers");
181 }
182 let key = question.ask($skin).unwrap();
183 let mut answers = question.answers.drain(..);
184 match key {
185 $(
186 _ if answers.next().unwrap().key == key => { $r }
187 )*
188 _ => { unreachable!() }
189 }
190 }}
191}