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 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 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 if *once {
299 return Ok(0);
300 }
301 }
302 2 => {
303 return Ok(2);
305 }
306 _ => {
307 return Err(1);
308 }
309 }
310 }
311}