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}