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}