1use std::cmp::min;
2
3use crate::{
4 autocompletion::{Autocomplete, NoAutoCompletion, Replacement},
5 config::{self, get_configuration},
6 error::{InquireError, InquireResult},
7 formatter::{StringFormatter, DEFAULT_STRING_FORMATTER},
8 input::Input,
9 list_option::ListOption,
10 terminal::get_default_terminal,
11 ui::{Backend, Key, KeyModifiers, RenderConfig, TextBackend},
12 utils::paginate,
13 validator::{ErrorMessage, StringValidator, Validation},
14};
15
16const DEFAULT_HELP_MESSAGE_WITH_AC: &str = "↑↓ to move, tab to autocomplete, enter to submit";
17
18#[derive(Clone)]
70pub struct Text<'a> {
71 pub message: &'a str,
73
74 pub initial_value: Option<&'a str>,
80
81 pub default: Option<&'a str>,
83
84 pub placeholder: Option<&'a str>,
86
87 pub help_message: Option<&'a str>,
89
90 pub formatter: StringFormatter<'a>,
92
93 pub autocompleter: Option<Box<dyn Autocomplete>>,
95
96 pub validators: Vec<Box<dyn StringValidator>>,
103
104 pub page_size: usize,
106
107 pub render_config: RenderConfig,
116}
117
118impl<'a> Text<'a> {
119 pub const DEFAULT_FORMATTER: StringFormatter<'a> = DEFAULT_STRING_FORMATTER;
121
122 pub const DEFAULT_PAGE_SIZE: usize = config::DEFAULT_PAGE_SIZE;
124
125 pub const DEFAULT_VALIDATORS: Vec<Box<dyn StringValidator>> = vec![];
127
128 pub const DEFAULT_HELP_MESSAGE: Option<&'a str> = None;
130
131 pub fn new(message: &'a str) -> Self {
133 Self {
134 message,
135 placeholder: None,
136 initial_value: None,
137 default: None,
138 help_message: Self::DEFAULT_HELP_MESSAGE,
139 validators: Self::DEFAULT_VALIDATORS,
140 formatter: Self::DEFAULT_FORMATTER,
141 page_size: Self::DEFAULT_PAGE_SIZE,
142 autocompleter: None,
143 render_config: get_configuration(),
144 }
145 }
146
147 pub fn with_help_message(mut self, message: &'a str) -> Self {
149 self.help_message = Some(message);
150 self
151 }
152
153 pub fn with_initial_value(mut self, message: &'a str) -> Self {
159 self.initial_value = Some(message);
160 self
161 }
162
163 pub fn with_default(mut self, message: &'a str) -> Self {
165 self.default = Some(message);
166 self
167 }
168
169 pub fn with_placeholder(mut self, placeholder: &'a str) -> Self {
171 self.placeholder = Some(placeholder);
172 self
173 }
174
175 pub fn with_autocomplete<AC>(mut self, ac: AC) -> Self
177 where
178 AC: Autocomplete + 'static,
179 {
180 self.autocompleter = Some(Box::new(ac));
181 self
182 }
183
184 pub fn with_formatter(mut self, formatter: StringFormatter<'a>) -> Self {
186 self.formatter = formatter;
187 self
188 }
189
190 pub fn with_page_size(mut self, page_size: usize) -> Self {
192 self.page_size = page_size;
193 self
194 }
195
196 pub fn with_validator<V>(mut self, validator: V) -> Self
205 where
206 V: StringValidator + 'static,
207 {
208 if self.validators.capacity() == 0 {
211 self.validators.reserve(5);
212 }
213
214 self.validators.push(Box::new(validator));
215 self
216 }
217
218 pub fn with_validators(mut self, validators: &[Box<dyn StringValidator>]) -> Self {
227 for validator in validators {
228 #[allow(clippy::clone_double_ref)]
229 self.validators.push(validator.clone());
230 }
231 self
232 }
233
234 pub fn with_render_config(mut self, render_config: RenderConfig) -> Self {
243 self.render_config = render_config;
244 self
245 }
246
247 pub fn prompt_skippable(self) -> InquireResult<Option<String>> {
257 match self.prompt() {
258 Ok(answer) => Ok(Some(answer)),
259 Err(InquireError::OperationCanceled) => Ok(None),
260 Err(err) => Err(err),
261 }
262 }
263
264 pub fn prompt(self) -> InquireResult<String> {
267 let terminal = get_default_terminal()?;
268 let mut backend = Backend::new(terminal, self.render_config)?;
269 self.prompt_with_backend(&mut backend)
270 }
271
272 pub(crate) fn prompt_with_backend<B: TextBackend>(
273 self,
274 backend: &mut B,
275 ) -> InquireResult<String> {
276 TextPrompt::from(self).prompt(backend)
277 }
278}
279
280struct TextPrompt<'a> {
281 message: &'a str,
282 default: Option<&'a str>,
283 help_message: Option<&'a str>,
284 input: Input,
285 formatter: StringFormatter<'a>,
286 validators: Vec<Box<dyn StringValidator>>,
287 error: Option<ErrorMessage>,
288 autocompleter: Box<dyn Autocomplete>,
289 suggested_options: Vec<String>,
290 suggestion_cursor_index: Option<usize>,
291 page_size: usize,
292}
293
294impl<'a> From<Text<'a>> for TextPrompt<'a> {
295 fn from(so: Text<'a>) -> Self {
296 let input = Input::new_with(so.initial_value.unwrap_or_default());
297 let input = if let Some(placeholder) = so.placeholder {
298 input.with_placeholder(placeholder)
299 } else {
300 input
301 };
302
303 Self {
304 message: so.message,
305 default: so.default,
306 help_message: so.help_message,
307 formatter: so.formatter,
308 autocompleter: so
309 .autocompleter
310 .unwrap_or_else(|| Box::<NoAutoCompletion>::default()),
311 input,
312 error: None,
313 suggestion_cursor_index: None,
314 page_size: so.page_size,
315 suggested_options: vec![],
316 validators: so.validators,
317 }
318 }
319}
320
321impl<'a> From<&'a str> for Text<'a> {
322 fn from(val: &'a str) -> Self {
323 Text::new(val)
324 }
325}
326
327impl<'a> TextPrompt<'a> {
328 fn update_suggestions(&mut self) -> InquireResult<()> {
329 self.suggested_options = self.autocompleter.get_suggestions(self.input.content())?;
330 self.suggestion_cursor_index = None;
331
332 Ok(())
333 }
334
335 fn get_highlighted_suggestion(&self) -> Option<&str> {
336 if let Some(cursor) = self.suggestion_cursor_index {
337 let suggestion = self.suggested_options.get(cursor).unwrap().as_ref();
338 Some(suggestion)
339 } else {
340 None
341 }
342 }
343
344 fn move_cursor_up(&mut self, qty: usize) -> bool {
345 self.suggestion_cursor_index = match self.suggestion_cursor_index {
346 None => None,
347 Some(index) if index < qty => None,
348 Some(index) => Some(index.saturating_sub(qty)),
349 };
350
351 false
352 }
353
354 fn move_cursor_down(&mut self, qty: usize) -> bool {
355 self.suggestion_cursor_index = match self.suggested_options.is_empty() {
356 true => None,
357 false => match self.suggestion_cursor_index {
358 None if qty == 0 => None,
359 None => Some(min(
360 qty.saturating_sub(1),
361 self.suggested_options.len().saturating_sub(1),
362 )),
363 Some(index) => Some(min(
364 index.saturating_add(qty),
365 self.suggested_options.len().saturating_sub(1),
366 )),
367 },
368 };
369
370 false
371 }
372
373 fn handle_tab_key(&mut self) -> InquireResult<bool> {
374 let suggestion = self.get_highlighted_suggestion().map(|s| s.to_owned());
375 match self
376 .autocompleter
377 .get_completion(self.input.content(), suggestion)?
378 {
379 Replacement::Some(value) => {
380 self.input = Input::new_with(value);
381 Ok(true)
382 }
383 Replacement::None => Ok(false),
384 }
385 }
386
387 fn on_change(&mut self, key: Key) -> InquireResult<()> {
388 let dirty = match key {
389 Key::Up(KeyModifiers::NONE) => self.move_cursor_up(1),
390 Key::PageUp => self.move_cursor_up(self.page_size),
391
392 Key::Down(KeyModifiers::NONE) => self.move_cursor_down(1),
393 Key::PageDown => self.move_cursor_down(self.page_size),
394
395 Key::Tab => self.handle_tab_key()?,
396
397 key => self.input.handle_key(key),
398 };
399
400 if dirty {
401 self.update_suggestions()?;
402 }
403
404 Ok(())
405 }
406
407 fn get_current_answer(&self) -> &str {
408 if let Some(suggestion) = self.get_highlighted_suggestion() {
411 return suggestion;
412 }
413
414 if self.input.content().is_empty() {
416 if let Some(val) = self.default {
417 return val;
418 }
419 }
420
421 self.input.content()
422 }
423
424 fn validate_current_answer(&self) -> InquireResult<Validation> {
425 for validator in &self.validators {
426 match validator.validate(self.get_current_answer()) {
427 Ok(Validation::Valid) => {}
428 Ok(Validation::Invalid(msg)) => return Ok(Validation::Invalid(msg)),
429 Err(err) => return Err(InquireError::Custom(err)),
430 }
431 }
432
433 Ok(Validation::Valid)
434 }
435
436 fn render<B: TextBackend>(&mut self, backend: &mut B) -> InquireResult<()> {
437 let prompt = &self.message;
438
439 backend.frame_setup()?;
440
441 if let Some(err) = &self.error {
442 backend.render_error_message(err)?;
443 }
444
445 backend.render_prompt(prompt, self.default, &self.input)?;
446
447 let choices = self
448 .suggested_options
449 .iter()
450 .enumerate()
451 .map(|(i, val)| ListOption::new(i, val.as_ref()))
452 .collect::<Vec<ListOption<&str>>>();
453
454 let page = paginate(self.page_size, &choices, self.suggestion_cursor_index);
455
456 backend.render_suggestions(page)?;
457
458 if let Some(message) = self.help_message {
459 backend.render_help_message(message)?;
460 } else if !choices.is_empty() {
461 backend.render_help_message(DEFAULT_HELP_MESSAGE_WITH_AC)?;
462 }
463
464 backend.frame_finish()?;
465
466 Ok(())
467 }
468
469 fn prompt<B: TextBackend>(mut self, backend: &mut B) -> InquireResult<String> {
470 let final_answer: String;
471 self.update_suggestions()?;
472
473 loop {
474 self.render(backend)?;
475
476 let key = backend.read_key()?;
477
478 match key {
479 Key::Interrupt => interrupt_prompt!(),
480 Key::Cancel => cancel_prompt!(backend, self.message),
481 Key::Submit => match self.validate_current_answer()? {
482 Validation::Valid => {
483 final_answer = self.get_current_answer().to_owned();
484 break;
485 }
486 Validation::Invalid(msg) => self.error = Some(msg),
487 },
488 key => self.on_change(key)?,
489 }
490 }
491
492 let formatted = (self.formatter)(&final_answer);
493
494 finish_prompt_with_answer!(backend, self.message, &formatted, final_answer);
495 }
496}
497
498#[cfg(test)]
499#[cfg(feature = "crossterm")]
500mod test {
501 use super::Text;
502 use crate::{
503 terminal::crossterm::CrosstermTerminal,
504 ui::{Backend, RenderConfig},
505 validator::{ErrorMessage, Validation},
506 };
507 use crossterm::event::{KeyCode, KeyEvent};
508
509 fn default<'a>() -> Text<'a> {
510 Text::new("Question?")
511 }
512
513 macro_rules! text_to_events {
514 ($text:expr) => {{
515 $text.chars().map(KeyCode::Char)
516 }};
517 }
518
519 macro_rules! text_test {
520 ($name:ident,$input:expr,$output:expr) => {
521 text_test! {$name, $input, $output, default()}
522 };
523
524 ($name:ident,$input:expr,$output:expr,$prompt:expr) => {
525 #[test]
526 fn $name() {
527 let read: Vec<KeyEvent> = $input.into_iter().map(KeyEvent::from).collect();
528 let mut read = read.iter();
529
530 let mut write: Vec<u8> = Vec::new();
531
532 let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read);
533 let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap();
534
535 let ans = $prompt.prompt_with_backend(&mut backend).unwrap();
536
537 assert_eq!($output, ans);
538 }
539 };
540 }
541
542 text_test!(empty, vec![KeyCode::Enter], "");
543
544 text_test!(single_letter, vec![KeyCode::Char('b'), KeyCode::Enter], "b");
545
546 text_test!(
547 letters_and_enter,
548 text_to_events!("normal input\n"),
549 "normal input"
550 );
551
552 text_test!(
553 letters_and_enter_with_emoji,
554 text_to_events!("with emoji 🧘🏻♂️, 🌍, 🍞, 🚗, 📞\n"),
555 "with emoji 🧘🏻♂️, 🌍, 🍞, 🚗, 📞"
556 );
557
558 text_test!(
559 input_and_correction,
560 {
561 let mut events = vec![];
562 events.append(&mut text_to_events!("anor").collect());
563 events.push(KeyCode::Backspace);
564 events.push(KeyCode::Backspace);
565 events.push(KeyCode::Backspace);
566 events.push(KeyCode::Backspace);
567 events.append(&mut text_to_events!("normal input").collect());
568 events.push(KeyCode::Enter);
569 events
570 },
571 "normal input"
572 );
573
574 text_test!(
575 input_and_excessive_correction,
576 {
577 let mut events = vec![];
578 events.append(&mut text_to_events!("anor").collect());
579 events.push(KeyCode::Backspace);
580 events.push(KeyCode::Backspace);
581 events.push(KeyCode::Backspace);
582 events.push(KeyCode::Backspace);
583 events.push(KeyCode::Backspace);
584 events.push(KeyCode::Backspace);
585 events.push(KeyCode::Backspace);
586 events.push(KeyCode::Backspace);
587 events.push(KeyCode::Backspace);
588 events.push(KeyCode::Backspace);
589 events.append(&mut text_to_events!("normal input").collect());
590 events.push(KeyCode::Enter);
591 events
592 },
593 "normal input"
594 );
595
596 text_test!(
597 input_correction_after_validation,
598 {
599 let mut events = vec![];
600 events.append(&mut text_to_events!("1234567890").collect());
601 events.push(KeyCode::Enter);
602 events.push(KeyCode::Backspace);
603 events.push(KeyCode::Backspace);
604 events.push(KeyCode::Backspace);
605 events.push(KeyCode::Backspace);
606 events.push(KeyCode::Backspace);
607 events.append(&mut text_to_events!("yes").collect());
608 events.push(KeyCode::Enter);
609 events
610 },
611 "12345yes",
612 Text::new("").with_validator(|ans: &str| match ans.len() {
613 len if len > 5 && len < 10 => Ok(Validation::Valid),
614 _ => Ok(Validation::Invalid(ErrorMessage::Default)),
615 })
616 );
617}