dialoguer/prompts/input.rs
1use std::{
2 cmp::Ordering,
3 io, iter,
4 str::FromStr,
5 sync::{Arc, Mutex},
6};
7
8use console::{Key, Term};
9
10#[cfg(feature = "completion")]
11use crate::completion::Completion;
12#[cfg(feature = "history")]
13use crate::history::History;
14use crate::{
15 theme::{render::TermThemeRenderer, SimpleTheme, Theme},
16 validate::InputValidator,
17 Result,
18};
19
20type InputValidatorCallback<'a, T> = Arc<Mutex<dyn FnMut(&T) -> Option<String> + 'a>>;
21
22/// Renders an input prompt.
23///
24/// ## Example
25///
26/// ```rust,no_run
27/// use dialoguer::Input;
28///
29/// fn main() {
30/// let name: String = Input::new()
31/// .with_prompt("Your name?")
32/// .interact_text()
33/// .unwrap();
34///
35/// println!("Your name is: {}", name);
36/// }
37/// ```
38///
39/// It can also be used with turbofish notation:
40///
41/// ```rust,no_run
42/// use dialoguer::Input;
43///
44/// fn main() {
45/// let name = Input::<String>::new()
46/// .with_prompt("Your name?")
47/// .interact_text()
48/// .unwrap();
49///
50/// println!("Your name is: {}", name);
51/// }
52/// ```
53#[derive(Clone)]
54pub struct Input<'a, T> {
55 prompt: String,
56 post_completion_text: Option<String>,
57 report: bool,
58 default: Option<T>,
59 show_default: bool,
60 initial_text: Option<String>,
61 theme: &'a dyn Theme,
62 permit_empty: bool,
63 validator: Option<InputValidatorCallback<'a, T>>,
64 #[cfg(feature = "history")]
65 history: Option<Arc<Mutex<&'a mut dyn History<T>>>>,
66 #[cfg(feature = "completion")]
67 completion: Option<&'a dyn Completion>,
68}
69
70impl<T> Default for Input<'static, T> {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76impl<T> Input<'_, T> {
77 /// Creates an input prompt with default theme.
78 pub fn new() -> Self {
79 Self::with_theme(&SimpleTheme)
80 }
81
82 /// Sets the input prompt.
83 pub fn with_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
84 self.prompt = prompt.into();
85 self
86 }
87
88 /// Changes the prompt text to the post completion text after input is complete
89 pub fn with_post_completion_text<S: Into<String>>(mut self, post_completion_text: S) -> Self {
90 self.post_completion_text = Some(post_completion_text.into());
91 self
92 }
93
94 /// Indicates whether to report the input value after interaction.
95 ///
96 /// The default is to report the input value.
97 pub fn report(mut self, val: bool) -> Self {
98 self.report = val;
99 self
100 }
101
102 /// Sets initial text that user can accept or erase.
103 pub fn with_initial_text<S: Into<String>>(mut self, val: S) -> Self {
104 self.initial_text = Some(val.into());
105 self
106 }
107
108 /// Sets a default.
109 ///
110 /// Out of the box the prompt does not have a default and will continue
111 /// to display until the user inputs something and hits enter. If a default is set the user
112 /// can instead accept the default with enter.
113 pub fn default(mut self, value: T) -> Self {
114 self.default = Some(value);
115 self
116 }
117
118 /// Enables or disables an empty input
119 ///
120 /// By default, if there is no default value set for the input, the user must input a non-empty string.
121 pub fn allow_empty(mut self, val: bool) -> Self {
122 self.permit_empty = val;
123 self
124 }
125
126 /// Disables or enables the default value display.
127 ///
128 /// The default behaviour is to append [`default`](#method.default) to the prompt to tell the
129 /// user what is the default value.
130 ///
131 /// This method does not affect existence of default value, only its display in the prompt!
132 pub fn show_default(mut self, val: bool) -> Self {
133 self.show_default = val;
134 self
135 }
136}
137
138impl<'a, T> Input<'a, T> {
139 /// Creates an input prompt with a specific theme.
140 ///
141 /// ## Example
142 ///
143 /// ```rust,no_run
144 /// use dialoguer::{theme::ColorfulTheme, Input};
145 ///
146 /// fn main() {
147 /// let name: String = Input::with_theme(&ColorfulTheme::default())
148 /// .interact()
149 /// .unwrap();
150 /// }
151 /// ```
152 pub fn with_theme(theme: &'a dyn Theme) -> Self {
153 Self {
154 prompt: "".into(),
155 post_completion_text: None,
156 report: true,
157 default: None,
158 show_default: true,
159 initial_text: None,
160 theme,
161 permit_empty: false,
162 validator: None,
163 #[cfg(feature = "history")]
164 history: None,
165 #[cfg(feature = "completion")]
166 completion: None,
167 }
168 }
169
170 /// Enable history processing
171 ///
172 /// ## Example
173 ///
174 /// ```rust,no_run
175 /// use std::{collections::VecDeque, fmt::Display};
176 /// use dialoguer::{History, Input};
177 ///
178 /// struct MyHistory {
179 /// history: VecDeque<String>,
180 /// }
181 ///
182 /// impl Default for MyHistory {
183 /// fn default() -> Self {
184 /// MyHistory {
185 /// history: VecDeque::new(),
186 /// }
187 /// }
188 /// }
189 ///
190 /// impl<T: ToString> History<T> for MyHistory {
191 /// fn read(&self, pos: usize) -> Option<String> {
192 /// self.history.get(pos).cloned()
193 /// }
194 ///
195 /// fn write(&mut self, val: &T)
196 /// where
197 /// {
198 /// self.history.push_front(val.to_string());
199 /// }
200 /// }
201 ///
202 /// fn main() {
203 /// let mut history = MyHistory::default();
204 ///
205 /// let input = Input::<String>::new()
206 /// .history_with(&mut history)
207 /// .interact_text()
208 /// .unwrap();
209 /// }
210 /// ```
211 #[cfg(feature = "history")]
212 pub fn history_with<H>(mut self, history: &'a mut H) -> Self
213 where
214 H: History<T>,
215 {
216 self.history = Some(Arc::new(Mutex::new(history)));
217 self
218 }
219
220 /// Enable completion
221 #[cfg(feature = "completion")]
222 pub fn completion_with<C>(mut self, completion: &'a C) -> Self
223 where
224 C: Completion,
225 {
226 self.completion = Some(completion);
227 self
228 }
229}
230
231impl<'a, T> Input<'a, T>
232where
233 T: 'a,
234{
235 /// Registers a validator.
236 ///
237 /// # Example
238 ///
239 /// ```rust,no_run
240 /// use dialoguer::Input;
241 ///
242 /// fn main() {
243 /// let mail: String = Input::new()
244 /// .with_prompt("Enter email")
245 /// .validate_with(|input: &String| -> Result<(), &str> {
246 /// if input.contains('@') {
247 /// Ok(())
248 /// } else {
249 /// Err("This is not a mail address")
250 /// }
251 /// })
252 /// .interact()
253 /// .unwrap();
254 /// }
255 /// ```
256 pub fn validate_with<V>(mut self, mut validator: V) -> Self
257 where
258 V: InputValidator<T> + 'a,
259 V::Err: ToString,
260 {
261 let mut old_validator_func = self.validator.take();
262
263 self.validator = Some(Arc::new(Mutex::new(move |value: &T| -> Option<String> {
264 if let Some(old) = old_validator_func.as_mut() {
265 if let Some(err) = old.lock().unwrap()(value) {
266 return Some(err);
267 }
268 }
269
270 match validator.validate(value) {
271 Ok(()) => None,
272 Err(err) => Some(err.to_string()),
273 }
274 })));
275
276 self
277 }
278}
279
280impl<T> Input<'_, T>
281where
282 T: Clone + ToString + FromStr,
283 <T as FromStr>::Err: ToString,
284{
285 /// Enables the user to enter a printable ascii sequence and returns the result.
286 ///
287 /// Its difference from [`interact`](Self::interact) is that it only allows ascii characters for string,
288 /// while [`interact`](Self::interact) allows virtually any character to be used e.g arrow keys.
289 ///
290 /// The dialog is rendered on stderr.
291 pub fn interact_text(self) -> Result<T> {
292 self.interact_text_on(&Term::stderr())
293 }
294
295 /// Like [`interact_text`](Self::interact_text) but allows a specific terminal to be set.
296 pub fn interact_text_on(mut self, term: &Term) -> Result<T> {
297 if !term.is_term() {
298 return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into());
299 }
300
301 let mut render = TermThemeRenderer::new(term, self.theme);
302
303 loop {
304 let default_string = self.default.as_ref().map(ToString::to_string);
305
306 let prompt_len = render.input_prompt(
307 &self.prompt,
308 if self.show_default {
309 default_string.as_deref()
310 } else {
311 None
312 },
313 )?;
314
315 let mut chars: Vec<char> = Vec::new();
316 let mut position = 0;
317 #[cfg(feature = "history")]
318 let mut hist_pos = 0;
319
320 if let Some(initial) = self.initial_text.as_ref() {
321 term.write_str(initial)?;
322 chars = initial.chars().collect();
323 position = chars.len();
324 }
325 term.flush()?;
326
327 loop {
328 match term.read_key()? {
329 Key::Backspace if position > 0 => {
330 position -= 1;
331 chars.remove(position);
332 let line_size = term.size().1 as usize;
333 // Case we want to delete last char of a line so the cursor is at the beginning of the next line
334 if (position + prompt_len) % (line_size - 1) == 0 {
335 term.clear_line()?;
336 term.move_cursor_up(1)?;
337 term.move_cursor_right(line_size + 1)?;
338 } else {
339 term.clear_chars(1)?;
340 }
341
342 let tail: String = chars[position..].iter().collect();
343
344 if !tail.is_empty() {
345 term.write_str(&tail)?;
346
347 let total = position + prompt_len + tail.chars().count();
348 let total_line = total / line_size;
349 let line_cursor = (position + prompt_len) / line_size;
350 term.move_cursor_up(total_line - line_cursor)?;
351
352 term.move_cursor_left(line_size)?;
353 term.move_cursor_right((position + prompt_len) % line_size)?;
354 }
355
356 term.flush()?;
357 }
358 Key::Char(chr) if !chr.is_ascii_control() => {
359 chars.insert(position, chr);
360 position += 1;
361 let tail: String =
362 iter::once(&chr).chain(chars[position..].iter()).collect();
363 term.write_str(&tail)?;
364 term.move_cursor_left(tail.chars().count() - 1)?;
365 term.flush()?;
366 }
367 Key::ArrowLeft if position > 0 => {
368 if (position + prompt_len) % term.size().1 as usize == 0 {
369 term.move_cursor_up(1)?;
370 term.move_cursor_right(term.size().1 as usize)?;
371 } else {
372 term.move_cursor_left(1)?;
373 }
374 position -= 1;
375 term.flush()?;
376 }
377 Key::ArrowRight if position < chars.len() => {
378 if (position + prompt_len) % (term.size().1 as usize - 1) == 0 {
379 term.move_cursor_down(1)?;
380 term.move_cursor_left(term.size().1 as usize)?;
381 } else {
382 term.move_cursor_right(1)?;
383 }
384 position += 1;
385 term.flush()?;
386 }
387 Key::UnknownEscSeq(seq) if seq == vec!['b'] => {
388 let line_size = term.size().1 as usize;
389 let nb_space = chars[..position]
390 .iter()
391 .rev()
392 .take_while(|c| c.is_whitespace())
393 .count();
394 let find_last_space = chars[..position - nb_space]
395 .iter()
396 .rposition(|c| c.is_whitespace());
397
398 // If we find a space we set the cursor to the next char else we set it to the beginning of the input
399 if let Some(mut last_space) = find_last_space {
400 if last_space < position {
401 last_space += 1;
402 let new_line = (prompt_len + last_space) / line_size;
403 let old_line = (prompt_len + position) / line_size;
404 let diff_line = old_line - new_line;
405 if diff_line != 0 {
406 term.move_cursor_up(old_line - new_line)?;
407 }
408
409 let new_pos_x = (prompt_len + last_space) % line_size;
410 let old_pos_x = (prompt_len + position) % line_size;
411 let diff_pos_x = new_pos_x as i64 - old_pos_x as i64;
412 //println!("new_pos_x = {}, old_pos_x = {}, diff = {}", new_pos_x, old_pos_x, diff_pos_x);
413 if diff_pos_x < 0 {
414 term.move_cursor_left(-diff_pos_x as usize)?;
415 } else {
416 term.move_cursor_right((diff_pos_x) as usize)?;
417 }
418 position = last_space;
419 }
420 } else {
421 term.move_cursor_left(position)?;
422 position = 0;
423 }
424
425 term.flush()?;
426 }
427 Key::UnknownEscSeq(seq) if seq == vec!['f'] => {
428 let line_size = term.size().1 as usize;
429 let find_next_space =
430 chars[position..].iter().position(|c| c.is_whitespace());
431
432 // If we find a space we set the cursor to the next char else we set it to the beginning of the input
433 if let Some(mut next_space) = find_next_space {
434 let nb_space = chars[position + next_space..]
435 .iter()
436 .take_while(|c| c.is_whitespace())
437 .count();
438 next_space += nb_space;
439 let new_line = (prompt_len + position + next_space) / line_size;
440 let old_line = (prompt_len + position) / line_size;
441 term.move_cursor_down(new_line - old_line)?;
442
443 let new_pos_x = (prompt_len + position + next_space) % line_size;
444 let old_pos_x = (prompt_len + position) % line_size;
445 let diff_pos_x = new_pos_x as i64 - old_pos_x as i64;
446 if diff_pos_x < 0 {
447 term.move_cursor_left(-diff_pos_x as usize)?;
448 } else {
449 term.move_cursor_right((diff_pos_x) as usize)?;
450 }
451 position += next_space;
452 } else {
453 let new_line = (prompt_len + chars.len()) / line_size;
454 let old_line = (prompt_len + position) / line_size;
455 term.move_cursor_down(new_line - old_line)?;
456
457 let new_pos_x = (prompt_len + chars.len()) % line_size;
458 let old_pos_x = (prompt_len + position) % line_size;
459 let diff_pos_x = new_pos_x as i64 - old_pos_x as i64;
460 match diff_pos_x.cmp(&0) {
461 Ordering::Less => {
462 term.move_cursor_left((-diff_pos_x - 1) as usize)?;
463 }
464 Ordering::Equal => {}
465 Ordering::Greater => {
466 term.move_cursor_right((diff_pos_x) as usize)?;
467 }
468 }
469 position = chars.len();
470 }
471
472 term.flush()?;
473 }
474 #[cfg(feature = "completion")]
475 Key::ArrowRight | Key::Tab => {
476 if let Some(completion) = &self.completion {
477 let input: String = chars.clone().into_iter().collect();
478 if let Some(x) = completion.get(&input) {
479 term.clear_chars(chars.len())?;
480 chars.clear();
481 position = 0;
482 for ch in x.chars() {
483 chars.insert(position, ch);
484 position += 1;
485 }
486 term.write_str(&x)?;
487 term.flush()?;
488 }
489 }
490 }
491 #[cfg(feature = "history")]
492 Key::ArrowUp => {
493 let line_size = term.size().1 as usize;
494 if let Some(history) = &self.history {
495 if let Some(previous) = history.lock().unwrap().read(hist_pos) {
496 hist_pos += 1;
497 let mut chars_len = chars.len();
498 while ((prompt_len + chars_len) / line_size) > 0 {
499 term.clear_chars(chars_len)?;
500 if (prompt_len + chars_len) % line_size == 0 {
501 chars_len -= std::cmp::min(chars_len, line_size);
502 } else {
503 chars_len -= std::cmp::min(
504 chars_len,
505 (prompt_len + chars_len + 1) % line_size,
506 );
507 }
508 if chars_len > 0 {
509 term.move_cursor_up(1)?;
510 term.move_cursor_right(line_size)?;
511 }
512 }
513 term.clear_chars(chars_len)?;
514 chars.clear();
515 position = 0;
516 for ch in previous.chars() {
517 chars.insert(position, ch);
518 position += 1;
519 }
520 term.write_str(&previous)?;
521 term.flush()?;
522 }
523 }
524 }
525 #[cfg(feature = "history")]
526 Key::ArrowDown => {
527 let line_size = term.size().1 as usize;
528 if let Some(history) = &self.history {
529 let mut chars_len = chars.len();
530 while ((prompt_len + chars_len) / line_size) > 0 {
531 term.clear_chars(chars_len)?;
532 if (prompt_len + chars_len) % line_size == 0 {
533 chars_len -= std::cmp::min(chars_len, line_size);
534 } else {
535 chars_len -= std::cmp::min(
536 chars_len,
537 (prompt_len + chars_len + 1) % line_size,
538 );
539 }
540 if chars_len > 0 {
541 term.move_cursor_up(1)?;
542 term.move_cursor_right(line_size)?;
543 }
544 }
545 term.clear_chars(chars_len)?;
546 chars.clear();
547 position = 0;
548 // Move the history position back one in case we have up arrowed into it
549 // and the position is sitting on the next to read
550 if let Some(pos) = hist_pos.checked_sub(1) {
551 hist_pos = pos;
552 // Move it back again to get the previous history entry
553 if let Some(pos) = pos.checked_sub(1) {
554 if let Some(previous) = history.lock().unwrap().read(pos) {
555 for ch in previous.chars() {
556 chars.insert(position, ch);
557 position += 1;
558 }
559 term.write_str(&previous)?;
560 }
561 }
562 }
563 term.flush()?;
564 }
565 }
566 Key::Enter => break,
567 _ => (),
568 }
569 }
570 let input = chars.iter().collect::<String>();
571
572 term.clear_line()?;
573 render.clear()?;
574
575 if chars.is_empty() {
576 if let Some(ref default) = self.default {
577 if let Some(ref mut validator) = self.validator {
578 if let Some(err) = validator.lock().unwrap()(default) {
579 render.error(&err)?;
580 continue;
581 }
582 }
583
584 if self.report {
585 render.input_prompt_selection(&self.prompt, &default.to_string())?;
586 }
587 term.flush()?;
588 return Ok(default.clone());
589 } else if !self.permit_empty {
590 continue;
591 }
592 }
593
594 match input.parse::<T>() {
595 Ok(value) => {
596 #[cfg(feature = "history")]
597 if let Some(history) = &mut self.history {
598 history.lock().unwrap().write(&value);
599 }
600
601 if let Some(ref mut validator) = self.validator {
602 if let Some(err) = validator.lock().unwrap()(&value) {
603 render.error(&err)?;
604 continue;
605 }
606 }
607
608 if self.report {
609 if let Some(post_completion_text) = &self.post_completion_text {
610 render.input_prompt_selection(post_completion_text, &input)?;
611 } else {
612 render.input_prompt_selection(&self.prompt, &input)?;
613 }
614 }
615 term.flush()?;
616
617 return Ok(value);
618 }
619 Err(err) => {
620 render.error(&err.to_string())?;
621 continue;
622 }
623 }
624 }
625 }
626
627 /// Enables user interaction and returns the result.
628 ///
629 /// Allows any characters as input, including e.g arrow keys.
630 /// Some of the keys might have undesired behavior.
631 /// For more limited version, see [`interact_text`](Self::interact_text).
632 ///
633 /// If the user confirms the result is `true`, `false` otherwise.
634 /// The dialog is rendered on stderr.
635 pub fn interact(self) -> Result<T> {
636 self.interact_on(&Term::stderr())
637 }
638
639 /// Like [`interact`](Self::interact) but allows a specific terminal to be set.
640 pub fn interact_on(mut self, term: &Term) -> Result<T> {
641 if !term.is_term() {
642 return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into());
643 }
644
645 let mut render = TermThemeRenderer::new(term, self.theme);
646
647 loop {
648 let default_string = self.default.as_ref().map(ToString::to_string);
649
650 render.input_prompt(
651 &self.prompt,
652 if self.show_default {
653 default_string.as_deref()
654 } else {
655 None
656 },
657 )?;
658 term.flush()?;
659
660 let input = if let Some(initial_text) = self.initial_text.as_ref() {
661 term.read_line_initial_text(initial_text)?
662 } else {
663 term.read_line()?
664 };
665
666 render.add_line();
667 term.clear_line()?;
668 render.clear()?;
669
670 if input.is_empty() {
671 if let Some(ref default) = self.default {
672 if let Some(ref mut validator) = self.validator {
673 if let Some(err) = validator.lock().unwrap()(default) {
674 render.error(&err)?;
675 continue;
676 }
677 }
678
679 if self.report {
680 render.input_prompt_selection(&self.prompt, &default.to_string())?;
681 }
682 term.flush()?;
683 return Ok(default.clone());
684 } else if !self.permit_empty {
685 continue;
686 }
687 }
688
689 match input.parse::<T>() {
690 Ok(value) => {
691 if let Some(ref mut validator) = self.validator {
692 if let Some(err) = validator.lock().unwrap()(&value) {
693 render.error(&err)?;
694 continue;
695 }
696 }
697
698 if self.report {
699 render.input_prompt_selection(&self.prompt, &input)?;
700 }
701 term.flush()?;
702
703 return Ok(value);
704 }
705 Err(err) => {
706 render.error(&err.to_string())?;
707 continue;
708 }
709 }
710 }
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717
718 #[test]
719 fn test_clone() {
720 let input = Input::<String>::new().with_prompt("Your name");
721
722 let _ = input.clone();
723 }
724}