inquire/prompts/text/
prompt.rs

1use std::cmp::min;
2
3use crate::{
4    autocompletion::{NoAutoCompletion, Replacement},
5    error::InquireResult,
6    formatter::StringFormatter,
7    input::{Input, InputActionResult},
8    list_option::ListOption,
9    prompts::prompt::{ActionResult, Prompt},
10    ui::TextBackend,
11    utils::paginate,
12    validator::{ErrorMessage, StringValidator, Validation},
13    Autocomplete, InquireError, Text,
14};
15
16use super::{action::TextPromptAction, config::TextConfig, DEFAULT_HELP_MESSAGE_WITH_AC};
17
18pub struct TextPrompt<'a, 'b> {
19    message: &'a str,
20    config: TextConfig,
21    default: Option<&'a str>,
22    help_message: Option<&'a str>,
23    input: Input,
24    formatter: StringFormatter<'a>,
25    validators: Vec<Box<dyn StringValidator + 'b>>,
26    error: Option<ErrorMessage>,
27    autocompleter: Box<dyn Autocomplete + 'b>,
28    suggested_options: Vec<String>,
29    suggestion_cursor_index: Option<usize>,
30}
31
32impl<'a, 'b> From<Text<'a, 'b>> for TextPrompt<'a, 'b> {
33    fn from(so: Text<'a, 'b>) -> Self {
34        let input = Input::new_with(so.initial_value.unwrap_or_default());
35        let input = if let Some(placeholder) = so.placeholder {
36            input.with_placeholder(placeholder)
37        } else {
38            input
39        };
40
41        Self {
42            message: so.message,
43            config: (&so).into(),
44            default: so.default,
45            help_message: so.help_message,
46            formatter: so.formatter,
47            autocompleter: so
48                .autocompleter
49                .unwrap_or_else(|| Box::<NoAutoCompletion>::default()),
50            input,
51            error: None,
52            suggestion_cursor_index: None,
53            suggested_options: vec![],
54            validators: so.validators,
55        }
56    }
57}
58
59impl<'a, 'b> From<&'a str> for Text<'a, 'b> {
60    fn from(val: &'a str) -> Self {
61        Text::new(val)
62    }
63}
64
65impl<'a, 'b> TextPrompt<'a, 'b> {
66    fn update_suggestions(&mut self) -> InquireResult<()> {
67        self.suggested_options = self.autocompleter.get_suggestions(self.input.content())?;
68        self.suggestion_cursor_index = None;
69
70        Ok(())
71    }
72
73    fn get_highlighted_suggestion(&self) -> Option<&str> {
74        if let Some(cursor) = self.suggestion_cursor_index {
75            let suggestion = self.suggested_options.get(cursor).unwrap().as_ref();
76            Some(suggestion)
77        } else {
78            None
79        }
80    }
81
82    fn move_cursor_up(&mut self, qty: usize) -> ActionResult {
83        let new_cursor_index = match self.suggestion_cursor_index {
84            None => None,
85            Some(index) if index < qty => None,
86            Some(index) => Some(index.saturating_sub(qty)),
87        };
88
89        self.update_suggestion_cursor_pos(new_cursor_index)
90    }
91
92    fn move_cursor_down(&mut self, qty: usize) -> ActionResult {
93        let new_cursor_index = match self.suggested_options.is_empty() {
94            true => None,
95            false => match self.suggestion_cursor_index {
96                None if qty == 0 => None,
97                None => Some(min(
98                    qty.saturating_sub(1),
99                    self.suggested_options.len().saturating_sub(1),
100                )),
101                Some(index) => Some(min(
102                    index.saturating_add(qty),
103                    self.suggested_options.len().saturating_sub(1),
104                )),
105            },
106        };
107
108        self.update_suggestion_cursor_pos(new_cursor_index)
109    }
110
111    fn update_suggestion_cursor_pos(&mut self, new_position: Option<usize>) -> ActionResult {
112        if new_position != self.suggestion_cursor_index {
113            self.suggestion_cursor_index = new_position;
114            ActionResult::NeedsRedraw
115        } else {
116            ActionResult::Clean
117        }
118    }
119
120    fn use_current_suggestion(&mut self) -> InquireResult<ActionResult> {
121        let suggestion = self.get_highlighted_suggestion().map(|s| s.to_owned());
122        match self
123            .autocompleter
124            .get_completion(self.input.content(), suggestion)?
125        {
126            Replacement::Some(value) => {
127                self.input = Input::new_with(value);
128                Ok(ActionResult::NeedsRedraw)
129            }
130            Replacement::None => Ok(ActionResult::Clean),
131        }
132    }
133
134    fn get_current_answer(&self) -> &str {
135        // If there is a highlighted suggestion, assume user wanted it as
136        // the answer.
137        if let Some(suggestion) = self.get_highlighted_suggestion() {
138            return suggestion;
139        }
140
141        // Empty input with default values override any validators.
142        if self.input.content().is_empty() {
143            if let Some(val) = self.default {
144                return val;
145            }
146        }
147
148        self.input.content()
149    }
150
151    fn validate_current_answer(&self) -> InquireResult<Validation> {
152        for validator in &self.validators {
153            match validator.validate(self.get_current_answer()) {
154                Ok(Validation::Valid) => {}
155                Ok(Validation::Invalid(msg)) => return Ok(Validation::Invalid(msg)),
156                Err(err) => return Err(InquireError::Custom(err)),
157            }
158        }
159
160        Ok(Validation::Valid)
161    }
162}
163
164impl<'a, 'b, Backend> Prompt<Backend> for TextPrompt<'a, 'b>
165where
166    Backend: TextBackend,
167{
168    type Config = TextConfig;
169    type InnerAction = TextPromptAction;
170    type Output = String;
171
172    fn message(&self) -> &str {
173        self.message
174    }
175
176    fn config(&self) -> &TextConfig {
177        &self.config
178    }
179
180    fn format_answer(&self, answer: &String) -> String {
181        (self.formatter)(answer)
182    }
183
184    fn setup(&mut self) -> InquireResult<()> {
185        self.update_suggestions()
186    }
187
188    fn submit(&mut self) -> InquireResult<Option<String>> {
189        let result = match self.validate_current_answer()? {
190            Validation::Valid => Some(self.get_current_answer().to_owned()),
191            Validation::Invalid(msg) => {
192                self.error = Some(msg);
193                None
194            }
195        };
196
197        Ok(result)
198    }
199
200    fn handle(&mut self, action: TextPromptAction) -> InquireResult<ActionResult> {
201        let result = match action {
202            TextPromptAction::ValueInput(input_action) => {
203                let result = self.input.handle(input_action);
204
205                if let InputActionResult::ContentChanged = result {
206                    self.update_suggestions()?;
207                }
208
209                result.into()
210            }
211            TextPromptAction::MoveToSuggestionAbove => self.move_cursor_up(1),
212            TextPromptAction::MoveToSuggestionBelow => self.move_cursor_down(1),
213            TextPromptAction::MoveToSuggestionPageUp => self.move_cursor_up(self.config.page_size),
214            TextPromptAction::MoveToSuggestionPageDown => {
215                self.move_cursor_down(self.config.page_size)
216            }
217            TextPromptAction::UseCurrentSuggestion => {
218                let result = self.use_current_suggestion()?;
219
220                if let ActionResult::NeedsRedraw = result {
221                    self.update_suggestions()?;
222                }
223
224                result
225            }
226        };
227
228        Ok(result)
229    }
230
231    fn render(&self, backend: &mut Backend) -> InquireResult<()> {
232        let prompt = &self.message;
233
234        if let Some(err) = &self.error {
235            backend.render_error_message(err)?;
236        }
237
238        backend.render_prompt(prompt, self.default, &self.input)?;
239
240        let choices = self
241            .suggested_options
242            .iter()
243            .enumerate()
244            .map(|(i, val)| ListOption::new(i, val.as_ref()))
245            .collect::<Vec<ListOption<&str>>>();
246
247        let page = paginate(
248            self.config.page_size,
249            &choices,
250            self.suggestion_cursor_index,
251        );
252
253        backend.render_suggestions(page)?;
254
255        if let Some(message) = self.help_message {
256            backend.render_help_message(message)?;
257        } else if !choices.is_empty() {
258            backend.render_help_message(DEFAULT_HELP_MESSAGE_WITH_AC)?;
259        }
260
261        Ok(())
262    }
263}