Skip to main content

script_wizard/
ask.rs

1use std::process::Command;
2
3use chrono::{NaiveDate, Weekday};
4use clap::ValueEnum;
5use inquire::{
6    autocompletion::Replacement, error::CustomUserError, Confirm, DateSelect, Editor, InquireError,
7    MultiSelect, Select, Text,
8};
9
10#[derive(Clone, ValueEnum)]
11pub enum Confirmation {
12    Yes,
13    No,
14}
15
16fn read_json_array(json: &str) -> Result<Vec<String>, CustomUserError> {
17    let a: Vec<String> = serde_json::from_str(json).expect("invalid json array");
18    Ok(a)
19}
20
21#[derive(Clone, Default)]
22pub struct AskAutoCompleter {
23    input: String,
24    suggestions_json: String,
25    suggestions: Vec<String>,
26    suggestion_index: usize,
27}
28
29impl AskAutoCompleter {
30    fn update_input(&mut self, input: &str) -> Result<(), CustomUserError> {
31        if input == self.input {
32            // No change:
33            return Ok(());
34        }
35        self.input = input.to_string();
36        self.suggestion_index = 0;
37        Ok(())
38    }
39}
40
41impl inquire::Autocomplete for AskAutoCompleter {
42    fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, CustomUserError> {
43        self.update_input(input)?;
44        self.suggestions = read_json_array(&self.suggestions_json)
45            .expect("Couldn't parse suggestions")
46            .iter()
47            .filter(|s| s.to_lowercase().contains(&input.to_lowercase()))
48            .map(|s| String::from(s.clone()))
49            .collect();
50        Ok(self.suggestions.clone())
51    }
52
53    fn get_completion(
54        &mut self,
55        input: &str,
56        highlighted_suggestion: Option<String>,
57    ) -> Result<Replacement, CustomUserError> {
58        self.update_input(input)?;
59        match highlighted_suggestion {
60            Some(suggestion) => Ok(Replacement::Some(suggestion)),
61            None => {
62                if self.suggestions.len() > 0 {
63                    self.suggestion_index = (self.suggestion_index + 1) % self.suggestions.len();
64                    Ok(Replacement::Some(
65                        self.suggestions
66                            .get(self.suggestion_index)
67                            .unwrap()
68                            .to_string(),
69                    ))
70                } else {
71                    Ok(Replacement::None)
72                }
73            }
74        }
75    }
76}
77
78pub fn ask_prompt(
79    question: &str,
80    default: &str,
81    allow_blank: bool,
82    suggestions_json: &str,
83    cancel_code: u8,
84) -> String {
85    if question == "" {
86        panic!("Blank question")
87    }
88    let mut auto_completer = AskAutoCompleter::default();
89    auto_completer.suggestions_json = suggestions_json.to_string();
90    match allow_blank {
91        true => {
92            let r: Result<String, InquireError>;
93            match default {
94                "" => {
95                    r = Text::new(question)
96                        .with_autocomplete(auto_completer.clone())
97                        .prompt();
98                }
99                _ => {
100                    r = Text::new(question)
101                        .with_autocomplete(auto_completer.clone())
102                        .with_default(default)
103                        .prompt();
104                }
105            }
106            if r.is_err() {
107                std::process::exit(cancel_code.into());
108            }
109            r.unwrap()
110        }
111        false => {
112            let mut a = String::from("");
113            while a == "" {
114                let r: Result<String, InquireError>;
115                match default {
116                    "" => {
117                        r = Text::new(question)
118                            .with_autocomplete(auto_completer.clone())
119                            .prompt();
120                    }
121                    _ => {
122                        r = Text::new(question)
123                            .with_default(default)
124                            .with_autocomplete(auto_completer.clone())
125                            .prompt();
126                    }
127                }
128                if r.is_err() {
129                    std::process::exit(cancel_code.into());
130                }
131                a = r.unwrap();
132            }
133            a
134        }
135    }
136}
137
138#[macro_export]
139macro_rules! ask {
140    ($question: expr, $default: expr, $allow_blank: expr, $suggestions_json: expr, $cancel_code: expr) => {
141        ask::ask_prompt($question, $default, $allow_blank, $suggestions_json, $cancel_code)
142    };
143    ($question: expr, $default: expr, $allow_blank: expr, $suggestions_json: expr) => {
144        ask::ask_prompt($question, $default, $allow_blank, $suggestions_json, 1)
145    };
146    ($question: expr, $default: expr, $allow_blank: expr) => {
147        ask::ask_prompt($question, $default, $allow_blank, "", 1)
148    };
149    ($question: expr, $default: expr) => {
150        ask::ask_prompt($question, $default, false, "", 1)
151    };
152    ($question: expr) => {
153        ask::ask_prompt($question, "", false, "", 1)
154    };
155}
156pub use ask;
157
158pub fn confirm(question: &str, default_answer: Option<Confirmation>, cancel_code: u8) -> bool {
159    let mut c = Confirm::new(question);
160    match default_answer {
161        Some(Confirmation::Yes) => c = c.with_default(true),
162        Some(Confirmation::No) => c = c.with_default(false),
163        _ => (),
164    }
165    match c.prompt() {
166        Ok(true) => true,
167        Ok(false) => false,
168        Err(_) => std::process::exit(cancel_code.into()),
169    }
170}
171
172pub fn choose(
173    question: &str,
174    default: &str,
175    options: Vec<&str>,
176    numeric: &bool,
177    cancel_code: u8,
178) -> String {
179    let default_index: usize;
180    match default.trim().parse::<usize>() {
181        Ok(n) => {
182            default_index = n;
183        }
184        Err(_) => {
185            default_index = options.iter().position(|&r| r == default).unwrap_or(0);
186        }
187    }
188    let ans: Result<&str, InquireError> = Select::new(question, options.clone())
189        .with_starting_cursor(default_index)
190        .with_help_message("↑↓ to move, enter to select, type to filter, ESC to cancel")
191        .prompt();
192    match ans {
193        Ok(selection) => match numeric {
194            true => {
195                let index = options.iter().position(|&r| r == selection).unwrap();
196                format!("{}", index)
197            }
198            false => String::from(selection),
199        },
200        Err(_) => std::process::exit(cancel_code.into()),
201    }
202}
203
204pub fn select(question: &str, default: &str, options: Vec<&str>, cancel_code: u8) -> Vec<String> {
205    let defaults: Vec<&str> = serde_json::from_str(default).unwrap_or(vec![]);
206    let mut default_indices = vec![];
207    for (index, item) in options.iter().enumerate() {
208        match defaults.iter().find(|&r| r == item) {
209            Some(_) => default_indices.append(&mut vec![index]),
210            None => {}
211        };
212    }
213    let ans = MultiSelect::new(question, options)
214        .with_default(&default_indices)
215        .with_help_message("↑↓ to move, space to select one, → to all, ← to none, type to filter, ESC to cancel")
216        .prompt();
217    match ans {
218        Ok(selection) => selection.iter().map(|&x| x.into()).collect(),
219        Err(_) => std::process::exit(cancel_code.into()),
220    }
221}
222
223pub fn date(
224    question: &str,
225    default: &str,
226    min_date: &str,
227    max_date: &str,
228    starting_date: &str,
229    week_start: Weekday,
230    help_message: &str,
231    date_format: &str,
232    cancel_code: u8,
233) -> String {
234    let mut picker = DateSelect::new(question)
235        .with_starting_date(
236            NaiveDate::parse_from_str(default, date_format)
237                .unwrap_or(chrono::Local::now().naive_local().into()),
238        )
239        .with_min_date(NaiveDate::parse_from_str(min_date, date_format).unwrap_or(NaiveDate::MIN))
240        .with_max_date(NaiveDate::parse_from_str(max_date, date_format).unwrap_or(NaiveDate::MAX))
241        .with_week_start(week_start)
242        .with_help_message(help_message);
243    if let Ok(d) = NaiveDate::parse_from_str(starting_date, date_format) {
244        picker = picker.with_starting_date(d);
245    }
246    match picker.prompt() {
247        Ok(date) => date.format(date_format).to_string(),
248        Err(_) => std::process::exit(cancel_code.into()),
249    }
250}
251
252pub fn editor(message: &str, default: &str, help_message: &str, file_extension: &str, cancel_code: u8) -> String {
253    let ans = Editor::new(message)
254        .with_predefined_text(default)
255        .with_help_message(help_message)
256        .with_file_extension(file_extension)
257        .prompt();
258    match ans {
259        Ok(text) => text,
260        Err(_) => std::process::exit(cancel_code.into()),
261    }
262}
263
264pub fn menu(
265    heading: &str,
266    entries: &Vec<String>,
267    default: &Option<String>,
268    once: &bool,
269    cancel_code: u8,
270) -> Result<usize, u8> {
271    let mut new_default: String = default.clone().unwrap_or("".to_string());
272    loop {
273        eprintln!("");
274        let titles: Vec<&str> = entries
275            .iter()
276            .map(|e| e.split(" = ").collect::<Vec<&str>>()[0])
277            .collect();
278        let commands: Vec<&str> = entries
279            .iter()
280            .map(|e| e.split(" = ").collect::<Vec<&str>>()[1])
281            .collect();
282        let command_index = choose(heading, new_default.as_str(), titles, &true, cancel_code)
283            .parse::<usize>()
284            .unwrap_or(1);
285
286        new_default = command_index.to_string();
287
288        // Run the command:
289        let cmd = commands[command_index];
290        let status = Command::new("/bin/bash")
291            .args(["-c", cmd])
292            .status()
293            .unwrap();
294
295        match status.code().unwrap_or(1) {
296            0 => {
297                //Keep looping unless --once is given:
298                if *once {
299                    return Ok(0);
300                }
301            }
302            2 => {
303                // Ok(2) signals to quit the loop:
304                return Ok(2);
305            }
306            _ => {
307                return Err(1);
308            }
309        }
310    }
311}