├── .gitignore ├── .vscode └── settings.json ├── deny.toml ├── src ├── completion.rs ├── theme │ ├── simple.rs │ ├── mod.rs │ ├── render.rs │ └── colorful.rs ├── prompts │ ├── mod.rs │ ├── password.rs │ ├── confirm.rs │ ├── select.rs │ ├── sort.rs │ ├── multi_select.rs │ ├── fuzzy_select.rs │ └── input.rs ├── error.rs ├── validate.rs ├── lib.rs ├── history.rs ├── edit.rs └── paging.rs ├── .cargo └── config.toml ├── examples ├── editor.rs ├── password.rs ├── history.rs ├── sort.rs ├── paging.rs ├── fuzzy_select.rs ├── buffered.rs ├── completion.rs ├── select.rs ├── multi_select.rs ├── history_custom.rs ├── input.rs ├── wizard.rs └── confirm.rs ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── README.md ├── LICENSE ├── Cargo.toml └── CHANGELOG-OLD.md /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.cargo.features": "all" 3 | } 4 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [advisories] 2 | yanked = "deny" 3 | 4 | [licenses] 5 | version = 2 6 | allow = ["Apache-2.0", "MIT"] 7 | -------------------------------------------------------------------------------- /src/completion.rs: -------------------------------------------------------------------------------- 1 | /// Trait for completion handling. 2 | pub trait Completion { 3 | fn get(&self, input: &str) -> Option; 4 | } 5 | -------------------------------------------------------------------------------- /src/theme/simple.rs: -------------------------------------------------------------------------------- 1 | use crate::theme::Theme; 2 | 3 | /// The default theme. 4 | pub struct SimpleTheme; 5 | 6 | impl Theme for SimpleTheme {} 7 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | format = "fmt" 3 | format-check = "fmt --check" 4 | lint = "clippy --all-targets --all-features -- -D warnings" 5 | test-cover = "llvm-cov --all-features --lcov --output-path lcov.info" 6 | -------------------------------------------------------------------------------- /examples/editor.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::Editor; 2 | 3 | fn main() { 4 | if let Some(rv) = Editor::new().edit("Enter a commit message").unwrap() { 5 | println!("Your message:"); 6 | println!("{}", rv); 7 | } else { 8 | println!("Abort!"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "05:39" 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: weekly 12 | time: "06:28" 13 | -------------------------------------------------------------------------------- /src/prompts/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::needless_doctest_main)] 2 | 3 | pub mod confirm; 4 | pub mod input; 5 | pub mod multi_select; 6 | pub mod select; 7 | pub mod sort; 8 | 9 | #[cfg(feature = "fuzzy-select")] 10 | pub mod fuzzy_select; 11 | 12 | #[cfg(feature = "password")] 13 | pub mod password; 14 | -------------------------------------------------------------------------------- /examples/password.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::{theme::ColorfulTheme, Password}; 2 | 3 | fn main() { 4 | let password = Password::with_theme(&ColorfulTheme::default()) 5 | .with_prompt("Password") 6 | .with_confirmation("Repeat password", "Error: the passwords don't match.") 7 | .validate_with(|input: &String| -> Result<(), &str> { 8 | if input.chars().count() > 3 { 9 | Ok(()) 10 | } else { 11 | Err("Password must be longer than 3") 12 | } 13 | }) 14 | .interact() 15 | .unwrap(); 16 | 17 | println!( 18 | "Your password is {} characters long", 19 | password.chars().count() 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /examples/history.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::{theme::ColorfulTheme, BasicHistory, Input}; 2 | use std::process; 3 | 4 | fn main() { 5 | println!("Use 'exit' to quit the prompt"); 6 | println!("In this example, history is limited to 8 entries and contains no duplicates"); 7 | println!("Use the Up/Down arrows to scroll through history"); 8 | println!(); 9 | 10 | let mut history = BasicHistory::new().max_entries(8).no_duplicates(true); 11 | 12 | loop { 13 | if let Ok(cmd) = Input::::with_theme(&ColorfulTheme::default()) 14 | .with_prompt("dialoguer") 15 | .history_with(&mut history) 16 | .interact_text() 17 | { 18 | if cmd == "exit" { 19 | process::exit(0); 20 | } 21 | println!("Entered {}", cmd); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dialoguer 2 | 3 | [![Build Status](https://github.com/console-rs/dialoguer/workflows/CI/badge.svg)](https://github.com/console-rs/dialoguer/actions?query=branch%3Amain) 4 | [![Latest version](https://img.shields.io/crates/v/dialoguer.svg)](https://crates.io/crates/dialoguer) 5 | [![Documentation](https://docs.rs/dialoguer/badge.svg)](https://docs.rs/dialoguer) 6 | 7 | A rust library for command line prompts and similar things. 8 | 9 | Best paired with other libraries in the family: 10 | 11 | * [console](https://github.com/console-rs/console) 12 | * [indicatif](https://github.com/console-rs/indicatif) 13 | 14 | ## License and Links 15 | 16 | * [Documentation](https://docs.rs/dialoguer/) 17 | * [Issue Tracker](https://github.com/console-rs/dialoguer/issues) 18 | * [Examples](https://github.com/console-rs/dialoguer/tree/main/examples) 19 | * License: [MIT](https://github.com/console-rs/dialoguer/blob/main/LICENSE) 20 | -------------------------------------------------------------------------------- /examples/sort.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::{theme::ColorfulTheme, Sort}; 2 | 3 | fn main() { 4 | let list = &[ 5 | "Ice Cream", 6 | "Vanilla Cupcake", 7 | "Chocolate Muffin", 8 | "A Pile of sweet, sweet mustard", 9 | ]; 10 | let sorted = Sort::with_theme(&ColorfulTheme::default()) 11 | .with_prompt("Order your foods by preference") 12 | .items(&list[..]) 13 | .interact() 14 | .unwrap(); 15 | 16 | println!("Your favorite item:"); 17 | println!(" {}", list[sorted[0]]); 18 | println!("Your least favorite item:"); 19 | println!(" {}", list[sorted[sorted.len() - 1]]); 20 | 21 | let sorted = Sort::with_theme(&ColorfulTheme::default()) 22 | .with_prompt("Order your foods by preference") 23 | .items(&list[..]) 24 | .max_length(2) 25 | .interact() 26 | .unwrap(); 27 | 28 | println!("Your favorite item:"); 29 | println!(" {}", list[sorted[0]]); 30 | println!("Your least favorite item:"); 31 | println!(" {}", list[sorted[sorted.len() - 1]]); 32 | } 33 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, io::Error as IoError, result::Result as StdResult}; 2 | 3 | /// Possible errors returned by prompts. 4 | #[derive(Debug)] 5 | pub enum Error { 6 | /// Error while executing IO operations. 7 | IO(IoError), 8 | } 9 | 10 | impl fmt::Display for Error { 11 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 12 | match self { 13 | Self::IO(io) => write!(f, "IO error: {}", io), 14 | } 15 | } 16 | } 17 | 18 | impl std::error::Error for Error {} 19 | 20 | impl From for Error { 21 | fn from(err: IoError) -> Self { 22 | Self::IO(err) 23 | } 24 | } 25 | 26 | /// Result type where errors are of type [Error](enum@Error). 27 | pub type Result = StdResult; 28 | 29 | impl From for IoError { 30 | fn from(value: Error) -> Self { 31 | match value { 32 | Error::IO(err) => err, 33 | // If other error types are added in the future: 34 | // err => IoError::new(std::io::ErrorKind::Other, err), 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/paging.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::{theme::ColorfulTheme, Select}; 2 | 3 | fn main() { 4 | let selections = &[ 5 | "Ice Cream", 6 | "Vanilla Cupcake", 7 | "Chocolate Muffin", 8 | "A Pile of sweet, sweet mustard", 9 | "Carrots", 10 | "Peas", 11 | "Pistacio", 12 | "Mustard", 13 | "Cream", 14 | "Banana", 15 | "Chocolate", 16 | "Flakes", 17 | "Corn", 18 | "Cake", 19 | "Tarte", 20 | "Cheddar", 21 | "Vanilla", 22 | "Hazelnut", 23 | "Flour", 24 | "Sugar", 25 | "Salt", 26 | "Potato", 27 | "French Fries", 28 | "Pizza", 29 | "Mousse au chocolat", 30 | "Brown sugar", 31 | "Blueberry", 32 | "Burger", 33 | ]; 34 | 35 | let selection = Select::with_theme(&ColorfulTheme::default()) 36 | .with_prompt("Pick your flavor") 37 | .default(0) 38 | .items(&selections[..]) 39 | .interact() 40 | .unwrap(); 41 | 42 | println!("Enjoy your {}!", selections[selection]); 43 | } 44 | -------------------------------------------------------------------------------- /examples/fuzzy_select.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::{theme::ColorfulTheme, FuzzySelect}; 2 | 3 | fn main() { 4 | let selections = &[ 5 | "Ice Cream", 6 | "Vanilla Cupcake", 7 | "Chocolate Muffin", 8 | "A Pile of sweet, sweet mustard", 9 | "Carrots", 10 | "Peas", 11 | "Pistacio", 12 | "Mustard", 13 | "Cream", 14 | "Banana", 15 | "Chocolate", 16 | "Flakes", 17 | "Corn", 18 | "Cake", 19 | "Tarte", 20 | "Cheddar", 21 | "Vanilla", 22 | "Hazelnut", 23 | "Flour", 24 | "Sugar", 25 | "Salt", 26 | "Potato", 27 | "French Fries", 28 | "Pizza", 29 | "Mousse au chocolat", 30 | "Brown sugar", 31 | "Blueberry", 32 | "Burger", 33 | ]; 34 | 35 | let selection = FuzzySelect::with_theme(&ColorfulTheme::default()) 36 | .with_prompt("Pick your flavor") 37 | .default(0) 38 | .items(&selections[..]) 39 | .interact() 40 | .unwrap(); 41 | 42 | println!("Enjoy your {}!", selections[selection]); 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Armin Ronacher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /examples/buffered.rs: -------------------------------------------------------------------------------- 1 | use console::Term; 2 | use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select, Sort}; 3 | 4 | fn main() { 5 | let items = &[ 6 | "Ice Cream", 7 | "Vanilla Cupcake", 8 | "Chocolate Muffin", 9 | "A Pile of sweet, sweet mustard", 10 | ]; 11 | let term = Term::buffered_stderr(); 12 | let theme = ColorfulTheme::default(); 13 | 14 | println!("All the following controls are run in a buffered terminal"); 15 | Confirm::with_theme(&theme) 16 | .with_prompt("Do you want to continue?") 17 | .interact_on(&term) 18 | .unwrap(); 19 | 20 | let _: String = Input::with_theme(&theme) 21 | .with_prompt("Your name") 22 | .interact_on(&term) 23 | .unwrap(); 24 | 25 | Select::with_theme(&theme) 26 | .with_prompt("Pick an item") 27 | .items(items) 28 | .interact_on(&term) 29 | .unwrap(); 30 | 31 | MultiSelect::with_theme(&theme) 32 | .with_prompt("Pick some items") 33 | .items(items) 34 | .interact_on(&term) 35 | .unwrap(); 36 | 37 | Sort::with_theme(&theme) 38 | .with_prompt("Order these items") 39 | .items(items) 40 | .interact_on(&term) 41 | .unwrap(); 42 | } 43 | -------------------------------------------------------------------------------- /examples/completion.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::{theme::ColorfulTheme, Completion, Input}; 2 | 3 | fn main() { 4 | println!("Use the Right arrow or Tab to complete your command"); 5 | 6 | let completion = MyCompletion::default(); 7 | 8 | Input::::with_theme(&ColorfulTheme::default()) 9 | .with_prompt("dialoguer") 10 | .completion_with(&completion) 11 | .interact_text() 12 | .unwrap(); 13 | } 14 | 15 | struct MyCompletion { 16 | options: Vec, 17 | } 18 | 19 | impl Default for MyCompletion { 20 | fn default() -> Self { 21 | MyCompletion { 22 | options: vec![ 23 | "orange".to_string(), 24 | "apple".to_string(), 25 | "banana".to_string(), 26 | ], 27 | } 28 | } 29 | } 30 | 31 | impl Completion for MyCompletion { 32 | /// Simple completion implementation based on substring 33 | fn get(&self, input: &str) -> Option { 34 | let matches = self 35 | .options 36 | .iter() 37 | .filter(|option| option.starts_with(input)) 38 | .collect::>(); 39 | 40 | if matches.len() == 1 { 41 | Some(matches[0].to_string()) 42 | } else { 43 | None 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/select.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::{theme::ColorfulTheme, Select}; 2 | 3 | fn main() { 4 | let selections = &[ 5 | "Ice Cream", 6 | "Vanilla Cupcake", 7 | "Chocolate Muffin", 8 | "A Pile of sweet, sweet mustard", 9 | ]; 10 | 11 | let selection = Select::with_theme(&ColorfulTheme::default()) 12 | .with_prompt("Pick your flavor") 13 | .default(0) 14 | .items(&selections[..]) 15 | .interact() 16 | .unwrap(); 17 | 18 | println!("Enjoy your {}!", selections[selection]); 19 | 20 | let selection = Select::with_theme(&ColorfulTheme::default()) 21 | .with_prompt("Optionally pick your flavor") 22 | .default(0) 23 | .items(&selections[..]) 24 | .interact_opt() 25 | .unwrap(); 26 | 27 | if let Some(selection) = selection { 28 | println!("Enjoy your {}!", selections[selection]); 29 | } else { 30 | println!("You didn't select anything!"); 31 | } 32 | 33 | let selection = Select::with_theme(&ColorfulTheme::default()) 34 | .with_prompt("Pick your flavor, hint it might be on the second page") 35 | .default(0) 36 | .max_length(2) 37 | .items(&selections[..]) 38 | .interact() 39 | .unwrap(); 40 | 41 | println!("Enjoy your {}!", selections[selection]); 42 | } 43 | -------------------------------------------------------------------------------- /examples/multi_select.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::{theme::ColorfulTheme, MultiSelect}; 2 | 3 | fn main() { 4 | let multiselected = &[ 5 | "Ice Cream", 6 | "Vanilla Cupcake", 7 | "Chocolate Muffin", 8 | "A Pile of sweet, sweet mustard", 9 | ]; 10 | let defaults = &[false, false, true, false]; 11 | let selections = MultiSelect::with_theme(&ColorfulTheme::default()) 12 | .with_prompt("Pick your food") 13 | .items(&multiselected[..]) 14 | .defaults(&defaults[..]) 15 | .interact() 16 | .unwrap(); 17 | 18 | if selections.is_empty() { 19 | println!("You did not select anything :("); 20 | } else { 21 | println!("You selected these things:"); 22 | for selection in selections { 23 | println!(" {}", multiselected[selection]); 24 | } 25 | } 26 | 27 | let selections = MultiSelect::with_theme(&ColorfulTheme::default()) 28 | .with_prompt("Pick your food") 29 | .items(&multiselected[..]) 30 | .defaults(&defaults[..]) 31 | .max_length(2) 32 | .interact() 33 | .unwrap(); 34 | if selections.is_empty() { 35 | println!("You did not select anything :("); 36 | } else { 37 | println!("You selected these things:"); 38 | for selection in selections { 39 | println!(" {}", multiselected[selection]); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/validate.rs: -------------------------------------------------------------------------------- 1 | //! Provides validation for text inputs 2 | 3 | /// Trait for input validators. 4 | /// 5 | /// A generic implementation for `Fn(&str) -> Result<(), E>` is provided 6 | /// to facilitate development. 7 | pub trait InputValidator { 8 | type Err; 9 | 10 | /// Invoked with the value to validate. 11 | /// 12 | /// If this produces `Ok(())` then the value is used and parsed, if 13 | /// an error is returned validation fails with that error. 14 | fn validate(&mut self, input: &T) -> Result<(), Self::Err>; 15 | } 16 | 17 | impl InputValidator for F 18 | where 19 | F: FnMut(&T) -> Result<(), E>, 20 | { 21 | type Err = E; 22 | 23 | fn validate(&mut self, input: &T) -> Result<(), Self::Err> { 24 | self(input) 25 | } 26 | } 27 | 28 | /// Trait for password validators. 29 | #[allow(clippy::ptr_arg)] 30 | pub trait PasswordValidator { 31 | type Err; 32 | 33 | /// Invoked with the value to validate. 34 | /// 35 | /// If this produces `Ok(())` then the value is used and parsed, if 36 | /// an error is returned validation fails with that error. 37 | fn validate(&self, input: &String) -> Result<(), Self::Err>; 38 | } 39 | 40 | impl PasswordValidator for F 41 | where 42 | F: Fn(&String) -> Result<(), E>, 43 | { 44 | type Err = E; 45 | 46 | fn validate(&self, input: &String) -> Result<(), Self::Err> { 47 | self(input) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/history_custom.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::{theme::ColorfulTheme, History, Input}; 2 | use std::{collections::VecDeque, process}; 3 | 4 | fn main() { 5 | println!("Use 'exit' to quit the prompt"); 6 | println!("In this example, history is limited to 4 entries"); 7 | println!("Use the Up/Down arrows to scroll through history"); 8 | println!(); 9 | 10 | let mut history = MyHistory::default(); 11 | 12 | loop { 13 | if let Ok(cmd) = Input::::with_theme(&ColorfulTheme::default()) 14 | .with_prompt("dialoguer") 15 | .history_with(&mut history) 16 | .interact_text() 17 | { 18 | if cmd == "exit" { 19 | process::exit(0); 20 | } 21 | println!("Entered {}", cmd); 22 | } 23 | } 24 | } 25 | 26 | struct MyHistory { 27 | max: usize, 28 | history: VecDeque, 29 | } 30 | 31 | impl Default for MyHistory { 32 | fn default() -> Self { 33 | MyHistory { 34 | max: 4, 35 | history: VecDeque::new(), 36 | } 37 | } 38 | } 39 | 40 | impl History for MyHistory { 41 | fn read(&self, pos: usize) -> Option { 42 | self.history.get(pos).cloned() 43 | } 44 | 45 | fn write(&mut self, val: &T) { 46 | if self.history.len() == self.max { 47 | self.history.pop_back(); 48 | } 49 | self.history.push_front(val.to_string()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/input.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::{theme::ColorfulTheme, Input}; 2 | 3 | fn main() { 4 | let input: String = Input::with_theme(&ColorfulTheme::default()) 5 | .with_prompt("Your name") 6 | .interact_text() 7 | .unwrap(); 8 | 9 | println!("Hello {}!", input); 10 | 11 | let mail: String = Input::with_theme(&ColorfulTheme::default()) 12 | .with_prompt("Your email") 13 | .validate_with({ 14 | let mut force = None; 15 | move |input: &String| -> Result<(), &str> { 16 | if input.contains('@') || (force.as_ref() == Some(input)) { 17 | Ok(()) 18 | } else { 19 | force = Some(input.clone()); 20 | Err("This is not a mail address; type the same value again to force use") 21 | } 22 | } 23 | }) 24 | .interact_text() 25 | .unwrap(); 26 | 27 | println!("Email: {}", mail); 28 | 29 | let mail: String = Input::with_theme(&ColorfulTheme::default()) 30 | .with_prompt("Your planet") 31 | .default("Earth".to_string()) 32 | .interact_text() 33 | .unwrap(); 34 | 35 | println!("Planet: {}", mail); 36 | 37 | let mail: String = Input::with_theme(&ColorfulTheme::default()) 38 | .with_prompt("Your galaxy") 39 | .with_initial_text("Milky Way".to_string()) 40 | .interact_text() 41 | .unwrap(); 42 | 43 | println!("Galaxy: {}", mail); 44 | } 45 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dialoguer" 3 | description = "A command line prompting library." 4 | version = "0.12.0" 5 | edition = "2021" 6 | rust-version = "1.71" 7 | keywords = ["cli", "menu", "prompt"] 8 | categories = ["command-line-interface"] 9 | license = "MIT" 10 | homepage = "https://github.com/console-rs/dialoguer" 11 | repository = "https://github.com/console-rs/dialoguer" 12 | documentation = "https://docs.rs/dialoguer" 13 | readme = "README.md" 14 | 15 | [features] 16 | default = ["editor", "password"] 17 | editor = ["tempfile"] 18 | fuzzy-select = ["fuzzy-matcher"] 19 | history = [] 20 | password = ["zeroize"] 21 | completion = [] 22 | 23 | [dependencies] 24 | console = "0.16.0" 25 | tempfile = { version = "3", optional = true } 26 | zeroize = { version = "1.1.1", optional = true } 27 | fuzzy-matcher = { version = "0.3.7", optional = true } 28 | shell-words = "1.1.0" 29 | 30 | [[example]] 31 | name = "password" 32 | required-features = ["password"] 33 | 34 | [[example]] 35 | name = "editor" 36 | required-features = ["editor"] 37 | 38 | [[example]] 39 | name = "fuzzy_select" 40 | required-features = ["fuzzy-select"] 41 | 42 | [[example]] 43 | name = "history" 44 | required-features = ["history"] 45 | 46 | [[example]] 47 | name = "history_custom" 48 | required-features = ["history"] 49 | 50 | [[example]] 51 | name = "completion" 52 | required-features = ["completion"] 53 | 54 | [workspace.metadata.workspaces] 55 | no_individual_tags = true 56 | 57 | [package.metadata.docs.rs] 58 | rustdoc-args = ["--cfg", "docsrs"] 59 | all-features = true 60 | 61 | [package.metadata.cargo_check_external_types] 62 | allowed_external_types = [ 63 | "console", "console::*", "fuzzy_matcher::skim::SkimMatcherV2", 64 | ] 65 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! dialoguer is a library for Rust that helps you build useful small 2 | //! interactive user inputs for the command line. It provides utilities 3 | //! to render various simple dialogs like confirmation prompts, text 4 | //! inputs and more. 5 | //! 6 | //! Best paired with other libraries in the family: 7 | //! 8 | //! * [indicatif](https://docs.rs/indicatif) 9 | //! * [console](https://docs.rs/console) 10 | //! 11 | //! # Crate Contents 12 | //! 13 | //! * Confirmation prompts 14 | //! * Input prompts (regular and password) 15 | //! * Input validation 16 | //! * Selections prompts (single and multi) 17 | //! * Fuzzy select prompt 18 | //! * Other kind of prompts 19 | //! * Editor launching 20 | //! 21 | //! # Crate Features 22 | //! 23 | //! The following crate features are available: 24 | //! * `editor`: enables bindings to launch editor to edit strings 25 | //! * `fuzzy-select`: enables fuzzy select prompt 26 | //! * `history`: enables input prompts to be able to track history of inputs 27 | //! * `password`: enables password input prompt 28 | //! * `completion`: enables ability to implement custom tab-completion for input prompts 29 | //! 30 | //! By default `editor` and `password` are enabled. 31 | 32 | #![deny(clippy::all)] 33 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 34 | 35 | pub use console; 36 | 37 | #[cfg(feature = "completion")] 38 | pub use completion::Completion; 39 | #[cfg(feature = "editor")] 40 | pub use edit::Editor; 41 | pub use error::{Error, Result}; 42 | #[cfg(feature = "history")] 43 | pub use history::{BasicHistory, History}; 44 | use paging::Paging; 45 | pub use validate::{InputValidator, PasswordValidator}; 46 | 47 | #[cfg(feature = "fuzzy-select")] 48 | pub use prompts::fuzzy_select::FuzzySelect; 49 | #[cfg(feature = "password")] 50 | pub use prompts::password::Password; 51 | pub use prompts::{ 52 | confirm::Confirm, input::Input, multi_select::MultiSelect, select::Select, sort::Sort, 53 | }; 54 | 55 | #[cfg(feature = "completion")] 56 | mod completion; 57 | #[cfg(feature = "editor")] 58 | mod edit; 59 | mod error; 60 | #[cfg(feature = "history")] 61 | mod history; 62 | mod paging; 63 | mod prompts; 64 | pub mod theme; 65 | mod validate; 66 | -------------------------------------------------------------------------------- /examples/wizard.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::net::IpAddr; 3 | 4 | use console::Style; 5 | use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select}; 6 | 7 | #[derive(Debug)] 8 | #[allow(dead_code)] 9 | struct Config { 10 | interface: IpAddr, 11 | hostname: String, 12 | use_acme: bool, 13 | private_key: Option, 14 | cert: Option, 15 | } 16 | 17 | fn init_config() -> Result, Box> { 18 | let theme = ColorfulTheme { 19 | values_style: Style::new().yellow().dim(), 20 | ..ColorfulTheme::default() 21 | }; 22 | println!("Welcome to the setup wizard"); 23 | 24 | if !Confirm::with_theme(&theme) 25 | .with_prompt("Do you want to continue?") 26 | .interact()? 27 | { 28 | return Ok(None); 29 | } 30 | 31 | let interface = Input::with_theme(&theme) 32 | .with_prompt("Interface") 33 | .default("127.0.0.1".parse().unwrap()) 34 | .interact()?; 35 | 36 | let hostname = Input::with_theme(&theme) 37 | .with_prompt("Hostname") 38 | .interact()?; 39 | 40 | let tls = Select::with_theme(&theme) 41 | .with_prompt("Configure TLS") 42 | .default(0) 43 | .item("automatic with ACME") 44 | .item("manual") 45 | .item("no") 46 | .interact()?; 47 | 48 | let (private_key, cert, use_acme) = match tls { 49 | 0 => (Some("acme.pkey".into()), Some("acme.cert".into()), true), 50 | 1 => ( 51 | Some( 52 | Input::with_theme(&theme) 53 | .with_prompt(" Path to private key") 54 | .interact()?, 55 | ), 56 | Some( 57 | Input::with_theme(&theme) 58 | .with_prompt(" Path to certificate") 59 | .interact()?, 60 | ), 61 | false, 62 | ), 63 | _ => (None, None, false), 64 | }; 65 | 66 | Ok(Some(Config { 67 | hostname, 68 | interface, 69 | private_key, 70 | cert, 71 | use_acme, 72 | })) 73 | } 74 | 75 | fn main() { 76 | match init_config() { 77 | Ok(None) => println!("Aborted."), 78 | Ok(Some(config)) => println!("{:#?}", config), 79 | Err(err) => println!("error: {}", err), 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /examples/confirm.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::{theme::ColorfulTheme, Confirm}; 2 | 3 | fn main() { 4 | if Confirm::with_theme(&ColorfulTheme::default()) 5 | .with_prompt("Do you want to continue?") 6 | .interact() 7 | .unwrap() 8 | { 9 | println!("Looks like you want to continue"); 10 | } else { 11 | println!("nevermind then :("); 12 | } 13 | 14 | if Confirm::with_theme(&ColorfulTheme::default()) 15 | .with_prompt("Do you really want to continue?") 16 | .default(true) 17 | .interact() 18 | .unwrap() 19 | { 20 | println!("Looks like you want to continue"); 21 | } else { 22 | println!("nevermind then :("); 23 | } 24 | 25 | if Confirm::with_theme(&ColorfulTheme::default()) 26 | .with_prompt("Do you really really want to continue?") 27 | .default(true) 28 | .show_default(false) 29 | .wait_for_newline(true) 30 | .interact() 31 | .unwrap() 32 | { 33 | println!("Looks like you want to continue"); 34 | } else { 35 | println!("nevermind then :("); 36 | } 37 | 38 | if Confirm::with_theme(&ColorfulTheme::default()) 39 | .with_prompt("Do you really really really want to continue?") 40 | .wait_for_newline(true) 41 | .interact() 42 | .unwrap() 43 | { 44 | println!("Looks like you want to continue"); 45 | } else { 46 | println!("nevermind then :("); 47 | } 48 | 49 | match Confirm::with_theme(&ColorfulTheme::default()) 50 | .with_prompt("Do you really really really really want to continue?") 51 | .interact_opt() 52 | .unwrap() 53 | { 54 | Some(true) => println!("Looks like you want to continue"), 55 | Some(false) => println!("nevermind then :("), 56 | None => println!("Ok, we can start over later"), 57 | } 58 | 59 | match Confirm::with_theme(&ColorfulTheme::default()) 60 | .with_prompt("Do you really really really really really want to continue?") 61 | .default(true) 62 | .wait_for_newline(true) 63 | .interact_opt() 64 | .unwrap() 65 | { 66 | Some(true) => println!("Looks like you want to continue"), 67 | Some(false) => println!("nevermind then :("), 68 | None => println!("Ok, we can start over later"), 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/history.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | /// Trait for history handling. 4 | pub trait History { 5 | /// This is called with the current position that should 6 | /// be read from history. The `pos` represents the number 7 | /// of times the `Up`/`Down` arrow key has been pressed. 8 | /// This would normally be used as an index to some sort 9 | /// of vector. If the `pos` does not have an entry, `None` 10 | /// should be returned. 11 | fn read(&self, pos: usize) -> Option; 12 | 13 | /// This is called with the next value you should store 14 | /// in history at the first location. Normally history 15 | /// is implemented as a FIFO queue. 16 | fn write(&mut self, val: &T); 17 | } 18 | 19 | pub struct BasicHistory { 20 | max_entries: Option, 21 | deque: VecDeque, 22 | no_duplicates: bool, 23 | } 24 | 25 | impl BasicHistory { 26 | /// Creates a new basic history value which has no limit on the number of 27 | /// entries and allows for duplicates. 28 | /// 29 | /// # Example 30 | /// 31 | /// A history with at most 8 entries and no duplicates: 32 | /// 33 | /// ```rs 34 | /// let mut history = BasicHistory::new().max_entries(8).no_duplicates(true); 35 | /// ``` 36 | pub fn new() -> Self { 37 | Self { 38 | max_entries: None, 39 | deque: VecDeque::new(), 40 | no_duplicates: false, 41 | } 42 | } 43 | 44 | /// Limit the number of entries stored in the history. 45 | pub fn max_entries(self, max_entries: usize) -> Self { 46 | Self { 47 | max_entries: Some(max_entries), 48 | ..self 49 | } 50 | } 51 | 52 | /// Prevent duplicates in the history. This means that any previous entries 53 | /// that are equal to a new entry are removed before the new entry is added. 54 | pub fn no_duplicates(self, no_duplicates: bool) -> Self { 55 | Self { 56 | no_duplicates, 57 | ..self 58 | } 59 | } 60 | } 61 | 62 | impl History for BasicHistory { 63 | fn read(&self, pos: usize) -> Option { 64 | self.deque.get(pos).cloned() 65 | } 66 | 67 | fn write(&mut self, val: &T) { 68 | let val = val.to_string(); 69 | 70 | if self.no_duplicates { 71 | self.deque.retain(|v| v != &val); 72 | } 73 | 74 | self.deque.push_front(val); 75 | 76 | if let Some(max_entries) = self.max_entries { 77 | self.deque.truncate(max_entries); 78 | } 79 | } 80 | } 81 | 82 | impl Default for BasicHistory { 83 | fn default() -> Self { 84 | Self::new() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | jobs: 8 | test: 9 | name: Tests 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | include: 14 | - os: macos-latest 15 | rust: stable 16 | - os: ubuntu-latest 17 | rust: stable 18 | - os: windows-latest 19 | rust: stable 20 | - os: ubuntu-latest 21 | rust: 1.71.0 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - name: Install rust 25 | uses: dtolnay/rust-toolchain@master 26 | with: 27 | toolchain: ${{ matrix.rust }} 28 | - name: Checkout 29 | uses: actions/checkout@v6 30 | - uses: Swatinem/rust-cache@v2 31 | - name: Test 32 | run: cargo test --all-features 33 | 34 | lint: 35 | name: Linting (fmt + clippy) 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: Install rust 39 | uses: dtolnay/rust-toolchain@stable 40 | with: 41 | components: rustfmt, clippy 42 | - name: Checkout 43 | uses: actions/checkout@v6 44 | - name: Lint check 45 | run: cargo lint 46 | - name: Format check 47 | run: cargo format-check 48 | 49 | docs: 50 | name: Check for documentation errors 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v6 54 | - uses: dtolnay/rust-toolchain@stable 55 | - run: cargo doc --all-features --no-deps --document-private-items 56 | env: 57 | RUSTDOCFLAGS: -Dwarnings 58 | 59 | semver: 60 | name: Check semver compatibility 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Checkout sources 64 | uses: actions/checkout@v6 65 | with: 66 | persist-credentials: false 67 | - name: Check semver 68 | uses: obi1kenobi/cargo-semver-checks-action@v2 69 | 70 | check-external-types: 71 | name: Validate external types appearing in public API 72 | runs-on: ubuntu-latest 73 | steps: 74 | - name: Checkout sources 75 | uses: actions/checkout@v6 76 | with: 77 | persist-credentials: false 78 | - name: Install rust toolchain 79 | uses: dtolnay/rust-toolchain@master 80 | with: 81 | toolchain: nightly-2025-08-06 82 | # ^ sync with https://github.com/awslabs/cargo-check-external-types/blob/main/rust-toolchain.toml 83 | - run: cargo install --locked cargo-check-external-types 84 | - name: run cargo-check-external-types 85 | run: cargo check-external-types --all-features 86 | 87 | audit: 88 | name: Audit dependencies 89 | runs-on: ubuntu-latest 90 | steps: 91 | - uses: actions/checkout@v6 92 | - uses: EmbarkStudios/cargo-deny-action@v2 93 | -------------------------------------------------------------------------------- /src/edit.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | ffi::{OsStr, OsString}, 4 | fs, 5 | io::{Read, Write}, 6 | process, 7 | }; 8 | 9 | use crate::Result; 10 | 11 | /// Launches the default editor to edit a string. 12 | /// 13 | /// ## Example 14 | /// 15 | /// ```rust,no_run 16 | /// use dialoguer::Editor; 17 | /// 18 | /// if let Some(rv) = Editor::new().edit("Enter a commit message").unwrap() { 19 | /// println!("Your message:"); 20 | /// println!("{}", rv); 21 | /// } else { 22 | /// println!("Abort!"); 23 | /// } 24 | /// ``` 25 | pub struct Editor { 26 | editor: OsString, 27 | extension: String, 28 | require_save: bool, 29 | trim_newlines: bool, 30 | } 31 | 32 | fn get_default_editor() -> OsString { 33 | if let Some(prog) = env::var_os("VISUAL") { 34 | return prog; 35 | } 36 | if let Some(prog) = env::var_os("EDITOR") { 37 | return prog; 38 | } 39 | if cfg!(windows) { 40 | "notepad.exe".into() 41 | } else { 42 | "vi".into() 43 | } 44 | } 45 | 46 | impl Default for Editor { 47 | fn default() -> Self { 48 | Self::new() 49 | } 50 | } 51 | 52 | impl Editor { 53 | /// Creates a new editor. 54 | pub fn new() -> Self { 55 | Self { 56 | editor: get_default_editor(), 57 | extension: ".txt".into(), 58 | require_save: true, 59 | trim_newlines: true, 60 | } 61 | } 62 | 63 | /// Sets a specific editor executable. 64 | pub fn executable>(&mut self, val: S) -> &mut Self { 65 | self.editor = val.as_ref().into(); 66 | self 67 | } 68 | 69 | /// Sets a specific extension 70 | pub fn extension(&mut self, val: &str) -> &mut Self { 71 | self.extension = val.into(); 72 | self 73 | } 74 | 75 | /// Enables or disables the save requirement. 76 | pub fn require_save(&mut self, val: bool) -> &mut Self { 77 | self.require_save = val; 78 | self 79 | } 80 | 81 | /// Enables or disables trailing newline stripping. 82 | /// 83 | /// This is on by default. 84 | pub fn trim_newlines(&mut self, val: bool) -> &mut Self { 85 | self.trim_newlines = val; 86 | self 87 | } 88 | 89 | /// Launches the editor to edit a string. 90 | /// 91 | /// Returns `None` if the file was not saved or otherwise the 92 | /// entered text. 93 | pub fn edit(&self, s: &str) -> Result> { 94 | let mut f = tempfile::Builder::new() 95 | .prefix("edit-") 96 | .suffix(&self.extension) 97 | .rand_bytes(12) 98 | .tempfile()?; 99 | f.write_all(s.as_bytes())?; 100 | f.flush()?; 101 | let ts = fs::metadata(f.path())?.modified()?; 102 | 103 | let s: String = self.editor.clone().into_string().unwrap(); 104 | let (cmd, args) = match shell_words::split(&s) { 105 | Ok(mut parts) => { 106 | let cmd = parts.remove(0); 107 | (cmd, parts) 108 | } 109 | Err(_) => (s, vec![]), 110 | }; 111 | 112 | let rv = process::Command::new(cmd) 113 | .args(args) 114 | .arg(f.path()) 115 | .spawn()? 116 | .wait()?; 117 | 118 | if rv.success() && self.require_save && ts >= fs::metadata(f.path())?.modified()? { 119 | return Ok(None); 120 | } 121 | 122 | let mut new_f = fs::File::open(f.path())?; 123 | let mut rv = String::new(); 124 | new_f.read_to_string(&mut rv)?; 125 | 126 | if self.trim_newlines { 127 | let len = rv.trim_end_matches(&['\n', '\r'][..]).len(); 128 | rv.truncate(len); 129 | } 130 | 131 | Ok(Some(rv)) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/paging.rs: -------------------------------------------------------------------------------- 1 | use console::Term; 2 | 3 | use crate::Result; 4 | 5 | /// Creates a paging module 6 | /// 7 | /// The paging module serves as tracking structure to allow paged views 8 | /// and automatically (de-)activates paging depending on the current terminal size. 9 | pub struct Paging<'a> { 10 | pub pages: usize, 11 | pub current_page: usize, 12 | pub capacity: usize, 13 | pub active: bool, 14 | pub max_capacity: Option, 15 | term: &'a Term, 16 | current_term_size: (u16, u16), 17 | items_len: usize, 18 | activity_transition: bool, 19 | } 20 | 21 | impl<'a> Paging<'a> { 22 | pub fn new(term: &'a Term, items_len: usize, max_capacity: Option) -> Paging<'a> { 23 | let term_size = term.size(); 24 | // Subtract -2 because we need space to render the prompt, if paging is active 25 | let capacity = max_capacity 26 | .unwrap_or(usize::MAX) 27 | .clamp(3, term_size.0 as usize) 28 | - 2; 29 | let pages = (items_len as f64 / capacity as f64).ceil() as usize; 30 | 31 | Paging { 32 | pages, 33 | current_page: 0, 34 | capacity, 35 | active: pages > 1, 36 | term, 37 | current_term_size: term_size, 38 | items_len, 39 | max_capacity, 40 | // Set transition initially to true to trigger prompt rendering for inactive paging on start 41 | activity_transition: true, 42 | } 43 | } 44 | 45 | pub fn update_page(&mut self, cursor_pos: usize) { 46 | if cursor_pos != !0 47 | && (cursor_pos < self.current_page * self.capacity 48 | || cursor_pos >= (self.current_page + 1) * self.capacity) 49 | { 50 | self.current_page = cursor_pos / self.capacity; 51 | } 52 | } 53 | 54 | /// Updates all internal based on the current terminal size and cursor position 55 | pub fn update(&mut self, cursor_pos: usize) -> Result { 56 | let new_term_size = self.term.size(); 57 | 58 | if self.current_term_size != new_term_size { 59 | self.current_term_size = new_term_size; 60 | self.capacity = self 61 | .max_capacity 62 | .unwrap_or(usize::MAX) 63 | .clamp(3, self.current_term_size.0 as usize) 64 | - 2; 65 | self.pages = (self.items_len as f64 / self.capacity as f64).ceil() as usize; 66 | } 67 | 68 | if self.active == (self.pages > 1) { 69 | self.activity_transition = false; 70 | } else { 71 | self.active = self.pages > 1; 72 | self.activity_transition = true; 73 | // Clear everything to prevent "ghost" lines in terminal when a resize happened 74 | self.term.clear_last_lines(self.capacity)?; 75 | } 76 | 77 | self.update_page(cursor_pos); 78 | 79 | Ok(()) 80 | } 81 | 82 | /// Renders a prompt when the following conditions are met: 83 | /// * Paging is active 84 | /// * Transition of the paging activity happened (active -> inactive / inactive -> active) 85 | pub fn render_prompt(&mut self, mut render_prompt: F) -> Result 86 | where 87 | F: FnMut(Option<(usize, usize)>) -> Result, 88 | { 89 | if self.active { 90 | let paging_info = Some((self.current_page + 1, self.pages)); 91 | render_prompt(paging_info)?; 92 | } else if self.activity_transition { 93 | render_prompt(None)?; 94 | } 95 | 96 | self.term.flush()?; 97 | 98 | Ok(()) 99 | } 100 | 101 | /// Navigates to the next page 102 | pub fn next_page(&mut self) -> usize { 103 | if self.current_page == self.pages - 1 { 104 | self.current_page = 0; 105 | } else { 106 | self.current_page += 1; 107 | } 108 | 109 | self.current_page * self.capacity 110 | } 111 | 112 | /// Navigates to the previous page 113 | pub fn previous_page(&mut self) -> usize { 114 | if self.current_page == 0 { 115 | self.current_page = self.pages - 1; 116 | } else { 117 | self.current_page -= 1; 118 | } 119 | 120 | self.current_page * self.capacity 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /CHANGELOG-OLD.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | For newer releases, please see [releases](https://github.com/console-rs/dialoguer/releases). 4 | 5 | ## 0.11.0 6 | 7 | ### Enhancements 8 | 9 | * Added `dialoguer::Result` and `dialoguer::Error` 10 | * Added a `BasicHistory` implementation for `History` 11 | * Added vim mode for `FuzzySelect` 12 | * All prompts implement `Clone` 13 | * Add handling of `Delete` key for `FuzzySelect` 14 | 15 | ### Bug fixes 16 | 17 | * Resolve some issues on Windows where pressing shift keys sometimes aborted dialogs 18 | * Resolve `MultiSelect` checked and unchecked variants looking the same on Windows 19 | * `Input` values that are invalid are now also stored in `History` 20 | * Resolve some issues with cursor positioning in `Input` when using `utf-8` characters 21 | * Correct page is shown when default selected option is not on the first page for `Select` 22 | * Fix panic in `FuzzySelect` when using non-ASCII characters 23 | 24 | ### Breaking 25 | 26 | * Updated MSRV to `1.63.0` due to multiple dependencies on different platforms: `rustix`, `tempfile`,`linux-raw-sys` 27 | * Removed deprecated `Confirm::with_text` 28 | * Removed deprecated `ColorfulTheme::inline_selections` 29 | * Prompt builder functions now take `mut self` instead of `&mut self` 30 | * Prompt builder functions now return `Self` instead of `&mut Self` 31 | * Prompt interaction functions now take `self` instead of `&self` 32 | * Prompt interaction functions and other operations now return `dialoguer::Result` instead of `std::io::Result` 33 | * Rename `Validator` to `InputValidator` 34 | * The trait method `Theme::format_fuzzy_select_prompt()` now takes a byte position instead of a cursor position in order to support UTF-8. 35 | 36 | ## 0.10.4 37 | 38 | ### Enhancements 39 | 40 | * Added validator for password input 41 | 42 | ## 0.10.3 43 | 44 | ### Enhancements 45 | 46 | * Fix various issues with fuzzy select 47 | * Enable customization of number of rows for fuzzy select 48 | * Added post completion text for input 49 | * Various cursor movement improvements 50 | * Correctly ignore unknown keys. 51 | * Reset prompt height in `TermThemeRenderer::clear`. 52 | 53 | ## 0.10.2 54 | 55 | ### Enhancements 56 | 57 | * Fix fuzzy select active item colors. 58 | * Fix fuzzy search clear on cancel. 59 | * Clear everything on cancel via escape key. 60 | 61 | ## 0.10.1 62 | 63 | ### Enhancements 64 | 65 | * Allow matches highlighting for `FuzzySelect` 66 | 67 | ## 0.10.0 68 | 69 | ### Enhancements 70 | 71 | * Loosen some trait bounds 72 | * Improve keyboard interactions (#141, #162) 73 | * Added `max_length` to `MultiSelect`, `Select` and `Sort` 74 | * Allow completion support for `Input::interact_text*` behind `completion` feature 75 | 76 | ### Breaking 77 | 78 | * All prompts `*::new` will now don't report selected values unless `report(true)` is called on them. 79 | 80 | ## 0.9.0 81 | 82 | ### Enhancements 83 | 84 | * Apply input validation to the default value too in `Input` 85 | * Added `FuzzySelect` behind `fuzzy-select` feature 86 | * Allow history processing for `Input::interact_text*` behind `history` feature 87 | * Added `interact_*_opt` methods for `MultiSelect` and `Sort`. 88 | 89 | ### Breaking 90 | 91 | * Updated MSRV to `1.51.0` 92 | * `Editor` is gated behind `editor` feature 93 | * `Password`, `Theme::format_password_prompt` and `Theme::format_password_prompt_selection` are gated behind `password` feature 94 | * Remove `Select::paged()`, `Sort::paged()` and `MultiSelect::paged()` in favor of automatic paging based on terminal size 95 | 96 | ## 0.8.0 97 | 98 | ### Enhancements 99 | 100 | * `Input::validate_with` can take a `FnMut` (allowing multiple references) 101 | 102 | ### Breaking 103 | 104 | * `Input::interact*` methods take `&mut self` instead of `&self` 105 | 106 | ## 0.7.0 107 | 108 | ### Enhancements 109 | 110 | * Added `wait_for_newline` to `Confirm` 111 | * More secure password prompt 112 | * More documentation 113 | * Added `interact_text` method for `Input` prompt 114 | * Added `inline_selections` to `ColorfulTheme` 115 | 116 | ### Breaking 117 | 118 | * Removed `theme::CustomPromptCharacterTheme` 119 | * `Input` validators now take the input type `T` as arg 120 | * `Confirm` has no `default` value by default now 121 | 122 | ## 0.6.2 123 | 124 | ### Enhancements 125 | 126 | * Updating some docs 127 | 128 | ## 0.6.1 129 | 130 | ### Bug fixes 131 | 132 | * `theme::ColorfulTheme` default styles are for stderr 133 | 134 | ## 0.6.0 135 | 136 | ### Breaking 137 | 138 | * Removed `theme::SelectionStyle` enum 139 | * Allowed more customization for `theme::Theme` trait by changing methods 140 | * Allowed more customization for `theme::ColorfulTheme` by changing members 141 | * Renamed prompt `Confirmation` to `Confirm` 142 | * Renamed prompt `PasswordInput` to `Password` 143 | * Renamed prompt `OrderList` to `Sort` 144 | * Renamed prompt `Checkboxes` to `MultiSelect` 145 | 146 | ### Enhancements 147 | 148 | * Improved colored theme 149 | * Improved cursor visibility manipulation 150 | -------------------------------------------------------------------------------- /src/prompts/password.rs: -------------------------------------------------------------------------------- 1 | use std::{io, sync::Arc}; 2 | 3 | use console::Term; 4 | use zeroize::Zeroizing; 5 | 6 | use crate::{ 7 | theme::{render::TermThemeRenderer, SimpleTheme, Theme}, 8 | validate::PasswordValidator, 9 | Result, 10 | }; 11 | 12 | type PasswordValidatorCallback<'a> = Arc Option + 'a>; 13 | 14 | /// Renders a password input prompt. 15 | /// 16 | /// ## Example 17 | /// 18 | /// ```rust,no_run 19 | /// use dialoguer::Password; 20 | /// 21 | /// fn main() { 22 | /// let password = Password::new() 23 | /// .with_prompt("New Password") 24 | /// .with_confirmation("Confirm password", "Passwords mismatching") 25 | /// .interact() 26 | /// .unwrap(); 27 | /// 28 | /// println!("Your password length is: {}", password.len()); 29 | /// } 30 | /// ``` 31 | #[derive(Clone)] 32 | pub struct Password<'a> { 33 | prompt: String, 34 | report: bool, 35 | theme: &'a dyn Theme, 36 | allow_empty_password: bool, 37 | confirmation_prompt: Option<(String, String)>, 38 | validator: Option>, 39 | } 40 | 41 | impl Default for Password<'static> { 42 | fn default() -> Password<'static> { 43 | Self::new() 44 | } 45 | } 46 | 47 | impl Password<'static> { 48 | /// Creates a password input prompt with default theme. 49 | pub fn new() -> Password<'static> { 50 | Self::with_theme(&SimpleTheme) 51 | } 52 | } 53 | 54 | impl Password<'_> { 55 | /// Sets the password input prompt. 56 | pub fn with_prompt>(mut self, prompt: S) -> Self { 57 | self.prompt = prompt.into(); 58 | self 59 | } 60 | 61 | /// Indicates whether to report confirmation after interaction. 62 | /// 63 | /// The default is to report. 64 | pub fn report(mut self, val: bool) -> Self { 65 | self.report = val; 66 | self 67 | } 68 | 69 | /// Enables confirmation prompting. 70 | pub fn with_confirmation(mut self, prompt: A, mismatch_err: B) -> Self 71 | where 72 | A: Into, 73 | B: Into, 74 | { 75 | self.confirmation_prompt = Some((prompt.into(), mismatch_err.into())); 76 | self 77 | } 78 | 79 | /// Allows/Disables empty password. 80 | /// 81 | /// By default this setting is set to false (i.e. password is not empty). 82 | pub fn allow_empty_password(mut self, allow_empty_password: bool) -> Self { 83 | self.allow_empty_password = allow_empty_password; 84 | self 85 | } 86 | 87 | /// Enables user interaction and returns the result. 88 | /// 89 | /// If the user confirms the result is `Ok()`, `Err()` otherwise. 90 | /// The dialog is rendered on stderr. 91 | pub fn interact(self) -> Result { 92 | self.interact_on(&Term::stderr()) 93 | } 94 | 95 | /// Like [`interact`](Self::interact) but allows a specific terminal to be set. 96 | pub fn interact_on(self, term: &Term) -> Result { 97 | if !term.is_term() { 98 | return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into()); 99 | } 100 | 101 | let mut render = TermThemeRenderer::new(term, self.theme); 102 | render.set_prompts_reset_height(false); 103 | 104 | loop { 105 | let password = Zeroizing::new(self.prompt_password(&mut render, &self.prompt)?); 106 | 107 | if let Some(ref validator) = self.validator { 108 | if let Some(err) = validator(&password) { 109 | render.error(&err)?; 110 | continue; 111 | } 112 | } 113 | 114 | if let Some((ref prompt, ref err)) = self.confirmation_prompt { 115 | let pw2 = Zeroizing::new(self.prompt_password(&mut render, prompt)?); 116 | 117 | if *password != *pw2 { 118 | render.error(err)?; 119 | continue; 120 | } 121 | } 122 | 123 | render.clear()?; 124 | 125 | if self.report { 126 | render.password_prompt_selection(&self.prompt)?; 127 | } 128 | term.flush()?; 129 | 130 | return Ok((*password).clone()); 131 | } 132 | } 133 | 134 | fn prompt_password(&self, render: &mut TermThemeRenderer, prompt: &str) -> Result { 135 | loop { 136 | render.password_prompt(prompt)?; 137 | render.term().flush()?; 138 | 139 | let input = render.term().read_secure_line()?; 140 | 141 | render.add_line(); 142 | 143 | if !input.is_empty() || self.allow_empty_password { 144 | return Ok(input); 145 | } 146 | } 147 | } 148 | } 149 | 150 | impl<'a> Password<'a> { 151 | /// Registers a validator. 152 | /// 153 | /// # Example 154 | /// 155 | /// ```rust,no_run 156 | /// use dialoguer::Password; 157 | /// 158 | /// fn main() { 159 | /// let password: String = Password::new() 160 | /// .with_prompt("Enter password") 161 | /// .validate_with(|input: &String| -> Result<(), &str> { 162 | /// if input.chars().count() > 8 { 163 | /// Ok(()) 164 | /// } else { 165 | /// Err("Password must be longer than 8") 166 | /// } 167 | /// }) 168 | /// .interact() 169 | /// .unwrap(); 170 | /// } 171 | /// ``` 172 | pub fn validate_with(mut self, validator: V) -> Self 173 | where 174 | V: PasswordValidator + 'a, 175 | V::Err: ToString, 176 | { 177 | let old_validator_func = self.validator.take(); 178 | 179 | self.validator = Some(Arc::new(move |value: &String| -> Option { 180 | if let Some(old) = &old_validator_func { 181 | if let Some(err) = old(value) { 182 | return Some(err); 183 | } 184 | } 185 | 186 | match validator.validate(value) { 187 | Ok(()) => None, 188 | Err(err) => Some(err.to_string()), 189 | } 190 | })); 191 | 192 | self 193 | } 194 | 195 | /// Creates a password input prompt with a specific theme. 196 | /// 197 | /// ## Example 198 | /// 199 | /// ```rust,no_run 200 | /// use dialoguer::{theme::ColorfulTheme, Password}; 201 | /// 202 | /// fn main() { 203 | /// let password = Password::with_theme(&ColorfulTheme::default()) 204 | /// .interact() 205 | /// .unwrap(); 206 | /// } 207 | /// ``` 208 | pub fn with_theme(theme: &'a dyn Theme) -> Self { 209 | Self { 210 | prompt: "".into(), 211 | report: true, 212 | theme, 213 | allow_empty_password: false, 214 | confirmation_prompt: None, 215 | validator: None, 216 | } 217 | } 218 | } 219 | 220 | #[cfg(test)] 221 | mod tests { 222 | use super::*; 223 | 224 | #[test] 225 | fn test_clone() { 226 | let password = Password::new().with_prompt("Enter password"); 227 | 228 | let _ = password.clone(); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/theme/mod.rs: -------------------------------------------------------------------------------- 1 | //! Customizes the rendering of the elements. 2 | use std::fmt; 3 | 4 | #[cfg(feature = "fuzzy-select")] 5 | use console::style; 6 | #[cfg(feature = "fuzzy-select")] 7 | use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; 8 | 9 | mod colorful; 10 | pub(crate) mod render; 11 | mod simple; 12 | 13 | pub use colorful::ColorfulTheme; 14 | pub use simple::SimpleTheme; 15 | 16 | /// Implements a theme for dialoguer. 17 | pub trait Theme { 18 | /// Formats a prompt. 19 | #[inline] 20 | fn format_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { 21 | write!(f, "{}:", prompt) 22 | } 23 | 24 | /// Formats out an error. 25 | #[inline] 26 | fn format_error(&self, f: &mut dyn fmt::Write, err: &str) -> fmt::Result { 27 | write!(f, "error: {}", err) 28 | } 29 | 30 | /// Formats a confirm prompt. 31 | fn format_confirm_prompt( 32 | &self, 33 | f: &mut dyn fmt::Write, 34 | prompt: &str, 35 | default: Option, 36 | ) -> fmt::Result { 37 | if !prompt.is_empty() { 38 | write!(f, "{} ", &prompt)?; 39 | } 40 | match default { 41 | None => write!(f, "[y/n] ")?, 42 | Some(true) => write!(f, "[Y/n] ")?, 43 | Some(false) => write!(f, "[y/N] ")?, 44 | } 45 | Ok(()) 46 | } 47 | 48 | /// Formats a confirm prompt after selection. 49 | fn format_confirm_prompt_selection( 50 | &self, 51 | f: &mut dyn fmt::Write, 52 | prompt: &str, 53 | selection: Option, 54 | ) -> fmt::Result { 55 | let selection = selection.map(|b| if b { "yes" } else { "no" }); 56 | 57 | match selection { 58 | Some(selection) if prompt.is_empty() => { 59 | write!(f, "{}", selection) 60 | } 61 | Some(selection) => { 62 | write!(f, "{} {}", &prompt, selection) 63 | } 64 | None if prompt.is_empty() => Ok(()), 65 | None => { 66 | write!(f, "{}", &prompt) 67 | } 68 | } 69 | } 70 | 71 | /// Formats an input prompt. 72 | fn format_input_prompt( 73 | &self, 74 | f: &mut dyn fmt::Write, 75 | prompt: &str, 76 | default: Option<&str>, 77 | ) -> fmt::Result { 78 | match default { 79 | Some(default) if prompt.is_empty() => write!(f, "[{}]: ", default), 80 | Some(default) => write!(f, "{} [{}]: ", prompt, default), 81 | None => write!(f, "{}: ", prompt), 82 | } 83 | } 84 | 85 | /// Formats an input prompt after selection. 86 | #[inline] 87 | fn format_input_prompt_selection( 88 | &self, 89 | f: &mut dyn fmt::Write, 90 | prompt: &str, 91 | sel: &str, 92 | ) -> fmt::Result { 93 | write!(f, "{}: {}", prompt, sel) 94 | } 95 | 96 | /// Formats a password prompt. 97 | #[inline] 98 | #[cfg(feature = "password")] 99 | fn format_password_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { 100 | self.format_input_prompt(f, prompt, None) 101 | } 102 | 103 | /// Formats a password prompt after selection. 104 | #[inline] 105 | #[cfg(feature = "password")] 106 | fn format_password_prompt_selection( 107 | &self, 108 | f: &mut dyn fmt::Write, 109 | prompt: &str, 110 | ) -> fmt::Result { 111 | self.format_input_prompt_selection(f, prompt, "[hidden]") 112 | } 113 | 114 | /// Formats a select prompt. 115 | #[inline] 116 | fn format_select_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { 117 | self.format_prompt(f, prompt) 118 | } 119 | 120 | /// Formats a select prompt after selection. 121 | #[inline] 122 | fn format_select_prompt_selection( 123 | &self, 124 | f: &mut dyn fmt::Write, 125 | prompt: &str, 126 | sel: &str, 127 | ) -> fmt::Result { 128 | self.format_input_prompt_selection(f, prompt, sel) 129 | } 130 | 131 | /// Formats a multi select prompt. 132 | #[inline] 133 | fn format_multi_select_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { 134 | self.format_prompt(f, prompt) 135 | } 136 | 137 | /// Formats a sort prompt. 138 | #[inline] 139 | fn format_sort_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { 140 | self.format_prompt(f, prompt) 141 | } 142 | 143 | /// Formats a multi_select prompt after selection. 144 | fn format_multi_select_prompt_selection( 145 | &self, 146 | f: &mut dyn fmt::Write, 147 | prompt: &str, 148 | selections: &[&str], 149 | ) -> fmt::Result { 150 | write!(f, "{}: ", prompt)?; 151 | for (idx, sel) in selections.iter().enumerate() { 152 | write!(f, "{}{}", if idx == 0 { "" } else { ", " }, sel)?; 153 | } 154 | Ok(()) 155 | } 156 | 157 | /// Formats a sort prompt after selection. 158 | #[inline] 159 | fn format_sort_prompt_selection( 160 | &self, 161 | f: &mut dyn fmt::Write, 162 | prompt: &str, 163 | selections: &[&str], 164 | ) -> fmt::Result { 165 | self.format_multi_select_prompt_selection(f, prompt, selections) 166 | } 167 | 168 | /// Formats a select prompt item. 169 | fn format_select_prompt_item( 170 | &self, 171 | f: &mut dyn fmt::Write, 172 | text: &str, 173 | active: bool, 174 | ) -> fmt::Result { 175 | write!(f, "{} {}", if active { ">" } else { " " }, text) 176 | } 177 | 178 | /// Formats a multi select prompt item. 179 | fn format_multi_select_prompt_item( 180 | &self, 181 | f: &mut dyn fmt::Write, 182 | text: &str, 183 | checked: bool, 184 | active: bool, 185 | ) -> fmt::Result { 186 | write!( 187 | f, 188 | "{} {}", 189 | match (checked, active) { 190 | (true, true) => "> [x]", 191 | (true, false) => " [x]", 192 | (false, true) => "> [ ]", 193 | (false, false) => " [ ]", 194 | }, 195 | text 196 | ) 197 | } 198 | 199 | /// Formats a sort prompt item. 200 | fn format_sort_prompt_item( 201 | &self, 202 | f: &mut dyn fmt::Write, 203 | text: &str, 204 | picked: bool, 205 | active: bool, 206 | ) -> fmt::Result { 207 | write!( 208 | f, 209 | "{} {}", 210 | match (picked, active) { 211 | (true, true) => "> [x]", 212 | (false, true) => "> [ ]", 213 | (_, false) => " [ ]", 214 | }, 215 | text 216 | ) 217 | } 218 | 219 | /// Formats a fuzzy select prompt item. 220 | #[cfg(feature = "fuzzy-select")] 221 | fn format_fuzzy_select_prompt_item( 222 | &self, 223 | f: &mut dyn fmt::Write, 224 | text: &str, 225 | active: bool, 226 | highlight_matches: bool, 227 | matcher: &SkimMatcherV2, 228 | search_term: &str, 229 | ) -> fmt::Result { 230 | write!(f, "{} ", if active { ">" } else { " " })?; 231 | 232 | if highlight_matches { 233 | if let Some((_score, indices)) = matcher.fuzzy_indices(text, search_term) { 234 | for (idx, c) in text.chars().enumerate() { 235 | if indices.contains(&idx) { 236 | write!(f, "{}", style(c).for_stderr().bold())?; 237 | } else { 238 | write!(f, "{}", c)?; 239 | } 240 | } 241 | 242 | return Ok(()); 243 | } 244 | } 245 | 246 | write!(f, "{}", text) 247 | } 248 | 249 | /// Formats a fuzzy select prompt. 250 | #[cfg(feature = "fuzzy-select")] 251 | fn format_fuzzy_select_prompt( 252 | &self, 253 | f: &mut dyn fmt::Write, 254 | prompt: &str, 255 | search_term: &str, 256 | bytes_pos: usize, 257 | ) -> fmt::Result { 258 | if !prompt.is_empty() { 259 | write!(f, "{prompt} ")?; 260 | } 261 | 262 | let (st_head, st_tail) = search_term.split_at(bytes_pos); 263 | write!(f, "{st_head}|{st_tail}") 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/theme/render.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, io}; 2 | 3 | use console::{measure_text_width, Term}; 4 | #[cfg(feature = "fuzzy-select")] 5 | use fuzzy_matcher::skim::SkimMatcherV2; 6 | 7 | use crate::{theme::Theme, Result}; 8 | 9 | /// Helper struct to conveniently render a theme. 10 | pub(crate) struct TermThemeRenderer<'a> { 11 | term: &'a Term, 12 | theme: &'a dyn Theme, 13 | height: usize, 14 | prompt_height: usize, 15 | prompts_reset_height: bool, 16 | } 17 | 18 | impl<'a> TermThemeRenderer<'a> { 19 | pub fn new(term: &'a Term, theme: &'a dyn Theme) -> TermThemeRenderer<'a> { 20 | TermThemeRenderer { 21 | term, 22 | theme, 23 | height: 0, 24 | prompt_height: 0, 25 | prompts_reset_height: true, 26 | } 27 | } 28 | 29 | #[cfg(feature = "password")] 30 | pub fn set_prompts_reset_height(&mut self, val: bool) { 31 | self.prompts_reset_height = val; 32 | } 33 | 34 | #[cfg(feature = "password")] 35 | pub fn term(&self) -> &Term { 36 | self.term 37 | } 38 | 39 | pub fn add_line(&mut self) { 40 | self.height += 1; 41 | } 42 | 43 | fn write_formatted_str< 44 | F: FnOnce(&mut TermThemeRenderer, &mut dyn fmt::Write) -> fmt::Result, 45 | >( 46 | &mut self, 47 | f: F, 48 | ) -> Result { 49 | let mut buf = String::new(); 50 | f(self, &mut buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; 51 | self.height += buf.chars().filter(|&x| x == '\n').count(); 52 | self.term.write_str(&buf)?; 53 | Ok(measure_text_width(&buf)) 54 | } 55 | 56 | fn write_formatted_line< 57 | F: FnOnce(&mut TermThemeRenderer, &mut dyn fmt::Write) -> fmt::Result, 58 | >( 59 | &mut self, 60 | f: F, 61 | ) -> Result { 62 | let mut buf = String::new(); 63 | f(self, &mut buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; 64 | self.height += buf.chars().filter(|&x| x == '\n').count() + 1; 65 | Ok(self.term.write_line(&buf)?) 66 | } 67 | 68 | fn write_formatted_prompt< 69 | F: FnOnce(&mut TermThemeRenderer, &mut dyn fmt::Write) -> fmt::Result, 70 | >( 71 | &mut self, 72 | f: F, 73 | ) -> Result { 74 | self.write_formatted_line(f)?; 75 | if self.prompts_reset_height { 76 | self.prompt_height = self.height; 77 | self.height = 0; 78 | } 79 | Ok(()) 80 | } 81 | 82 | fn write_paging_info(buf: &mut dyn fmt::Write, paging_info: (usize, usize)) -> fmt::Result { 83 | write!(buf, " [Page {}/{}] ", paging_info.0, paging_info.1) 84 | } 85 | 86 | pub fn error(&mut self, err: &str) -> Result { 87 | self.write_formatted_line(|this, buf| this.theme.format_error(buf, err)) 88 | } 89 | 90 | pub fn confirm_prompt(&mut self, prompt: &str, default: Option) -> Result { 91 | self.write_formatted_str(|this, buf| this.theme.format_confirm_prompt(buf, prompt, default)) 92 | } 93 | 94 | pub fn confirm_prompt_selection(&mut self, prompt: &str, sel: Option) -> Result { 95 | self.write_formatted_prompt(|this, buf| { 96 | this.theme.format_confirm_prompt_selection(buf, prompt, sel) 97 | }) 98 | } 99 | 100 | #[cfg(feature = "fuzzy-select")] 101 | pub fn fuzzy_select_prompt( 102 | &mut self, 103 | prompt: &str, 104 | search_term: &str, 105 | cursor_pos: usize, 106 | ) -> Result { 107 | self.write_formatted_prompt(|this, buf| { 108 | this.theme 109 | .format_fuzzy_select_prompt(buf, prompt, search_term, cursor_pos) 110 | }) 111 | } 112 | 113 | pub fn input_prompt(&mut self, prompt: &str, default: Option<&str>) -> Result { 114 | self.write_formatted_str(|this, buf| this.theme.format_input_prompt(buf, prompt, default)) 115 | } 116 | 117 | pub fn input_prompt_selection(&mut self, prompt: &str, sel: &str) -> Result { 118 | self.write_formatted_prompt(|this, buf| { 119 | this.theme.format_input_prompt_selection(buf, prompt, sel) 120 | }) 121 | } 122 | 123 | #[cfg(feature = "password")] 124 | pub fn password_prompt(&mut self, prompt: &str) -> Result { 125 | self.write_formatted_str(|this, buf| { 126 | write!(buf, "\r")?; 127 | this.theme.format_password_prompt(buf, prompt) 128 | }) 129 | } 130 | 131 | #[cfg(feature = "password")] 132 | pub fn password_prompt_selection(&mut self, prompt: &str) -> Result { 133 | self.write_formatted_prompt(|this, buf| { 134 | this.theme.format_password_prompt_selection(buf, prompt) 135 | }) 136 | } 137 | 138 | pub fn select_prompt(&mut self, prompt: &str, paging_info: Option<(usize, usize)>) -> Result { 139 | self.write_formatted_prompt(|this, buf| { 140 | this.theme.format_select_prompt(buf, prompt)?; 141 | 142 | if let Some(paging_info) = paging_info { 143 | TermThemeRenderer::write_paging_info(buf, paging_info)?; 144 | } 145 | 146 | Ok(()) 147 | }) 148 | } 149 | 150 | pub fn select_prompt_selection(&mut self, prompt: &str, sel: &str) -> Result { 151 | self.write_formatted_prompt(|this, buf| { 152 | this.theme.format_select_prompt_selection(buf, prompt, sel) 153 | }) 154 | } 155 | 156 | pub fn select_prompt_item(&mut self, text: &str, active: bool) -> Result { 157 | self.write_formatted_line(|this, buf| { 158 | this.theme.format_select_prompt_item(buf, text, active) 159 | }) 160 | } 161 | 162 | #[cfg(feature = "fuzzy-select")] 163 | pub fn fuzzy_select_prompt_item( 164 | &mut self, 165 | text: &str, 166 | active: bool, 167 | highlight: bool, 168 | matcher: &SkimMatcherV2, 169 | search_term: &str, 170 | ) -> Result { 171 | self.write_formatted_line(|this, buf| { 172 | this.theme.format_fuzzy_select_prompt_item( 173 | buf, 174 | text, 175 | active, 176 | highlight, 177 | matcher, 178 | search_term, 179 | ) 180 | }) 181 | } 182 | 183 | pub fn multi_select_prompt( 184 | &mut self, 185 | prompt: &str, 186 | paging_info: Option<(usize, usize)>, 187 | ) -> Result { 188 | self.write_formatted_prompt(|this, buf| { 189 | this.theme.format_multi_select_prompt(buf, prompt)?; 190 | 191 | if let Some(paging_info) = paging_info { 192 | TermThemeRenderer::write_paging_info(buf, paging_info)?; 193 | } 194 | 195 | Ok(()) 196 | }) 197 | } 198 | 199 | pub fn multi_select_prompt_selection(&mut self, prompt: &str, sel: &[&str]) -> Result { 200 | self.write_formatted_prompt(|this, buf| { 201 | this.theme 202 | .format_multi_select_prompt_selection(buf, prompt, sel) 203 | }) 204 | } 205 | 206 | pub fn multi_select_prompt_item(&mut self, text: &str, checked: bool, active: bool) -> Result { 207 | self.write_formatted_line(|this, buf| { 208 | this.theme 209 | .format_multi_select_prompt_item(buf, text, checked, active) 210 | }) 211 | } 212 | 213 | pub fn sort_prompt(&mut self, prompt: &str, paging_info: Option<(usize, usize)>) -> Result { 214 | self.write_formatted_prompt(|this, buf| { 215 | this.theme.format_sort_prompt(buf, prompt)?; 216 | 217 | if let Some(paging_info) = paging_info { 218 | TermThemeRenderer::write_paging_info(buf, paging_info)?; 219 | } 220 | 221 | Ok(()) 222 | }) 223 | } 224 | 225 | pub fn sort_prompt_selection(&mut self, prompt: &str, sel: &[&str]) -> Result { 226 | self.write_formatted_prompt(|this, buf| { 227 | this.theme.format_sort_prompt_selection(buf, prompt, sel) 228 | }) 229 | } 230 | 231 | pub fn sort_prompt_item(&mut self, text: &str, picked: bool, active: bool) -> Result { 232 | self.write_formatted_line(|this, buf| { 233 | this.theme 234 | .format_sort_prompt_item(buf, text, picked, active) 235 | }) 236 | } 237 | 238 | pub fn clear(&mut self) -> Result { 239 | self.term 240 | .clear_last_lines(self.height + self.prompt_height)?; 241 | self.height = 0; 242 | self.prompt_height = 0; 243 | Ok(()) 244 | } 245 | 246 | pub fn clear_preserve_prompt(&mut self, size_vec: &[usize]) -> Result { 247 | let mut new_height = self.height; 248 | let prefix_width = 2; 249 | //Check each item size, increment on finding an overflow 250 | for size in size_vec { 251 | if *size > self.term.size().1 as usize { 252 | new_height += (((*size as f64 + prefix_width as f64) / self.term.size().1 as f64) 253 | .ceil()) as usize 254 | - 1; 255 | } 256 | } 257 | 258 | self.term.clear_last_lines(new_height)?; 259 | self.height = 0; 260 | Ok(()) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/prompts/confirm.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use console::{Key, Term}; 4 | 5 | use crate::{ 6 | theme::{render::TermThemeRenderer, SimpleTheme, Theme}, 7 | Result, 8 | }; 9 | 10 | /// Renders a confirm prompt. 11 | /// 12 | /// ## Example 13 | /// 14 | /// ```rust,no_run 15 | /// use dialoguer::Confirm; 16 | /// 17 | /// fn main() { 18 | /// let confirmation = Confirm::new() 19 | /// .with_prompt("Do you want to continue?") 20 | /// .interact() 21 | /// .unwrap(); 22 | /// 23 | /// if confirmation { 24 | /// println!("Looks like you want to continue"); 25 | /// } else { 26 | /// println!("nevermind then :("); 27 | /// } 28 | /// } 29 | /// ``` 30 | #[derive(Clone)] 31 | pub struct Confirm<'a> { 32 | prompt: String, 33 | report: bool, 34 | default: Option, 35 | show_default: bool, 36 | wait_for_newline: bool, 37 | theme: &'a dyn Theme, 38 | } 39 | 40 | impl Default for Confirm<'static> { 41 | fn default() -> Self { 42 | Self::new() 43 | } 44 | } 45 | 46 | impl Confirm<'static> { 47 | /// Creates a confirm prompt with default theme. 48 | pub fn new() -> Self { 49 | Self::with_theme(&SimpleTheme) 50 | } 51 | } 52 | 53 | impl Confirm<'_> { 54 | /// Sets the confirm prompt. 55 | pub fn with_prompt>(mut self, prompt: S) -> Self { 56 | self.prompt = prompt.into(); 57 | self 58 | } 59 | 60 | /// Indicates whether or not to report the chosen selection after interaction. 61 | /// 62 | /// The default is to report the chosen selection. 63 | pub fn report(mut self, val: bool) -> Self { 64 | self.report = val; 65 | self 66 | } 67 | 68 | /// Sets when to react to user input. 69 | /// 70 | /// When `false` (default), we check on each user keystroke immediately as 71 | /// it is typed. Valid inputs can be one of 'y', 'n', or a newline to accept 72 | /// the default. 73 | /// 74 | /// When `true`, the user must type their choice and hit the Enter key before 75 | /// proceeding. Valid inputs can be "yes", "no", "y", "n", or an empty string 76 | /// to accept the default. 77 | pub fn wait_for_newline(mut self, wait: bool) -> Self { 78 | self.wait_for_newline = wait; 79 | self 80 | } 81 | 82 | /// Sets a default. 83 | /// 84 | /// Out of the box the prompt does not have a default and will continue 85 | /// to display until the user inputs something and hits enter. If a default is set the user 86 | /// can instead accept the default with enter. 87 | pub fn default(mut self, val: bool) -> Self { 88 | self.default = Some(val); 89 | self 90 | } 91 | 92 | /// Disables or enables the default value display. 93 | /// 94 | /// The default is to append the default value to the prompt to tell the user. 95 | pub fn show_default(mut self, val: bool) -> Self { 96 | self.show_default = val; 97 | self 98 | } 99 | 100 | /// Enables user interaction and returns the result. 101 | /// 102 | /// The dialog is rendered on stderr. 103 | /// 104 | /// Result contains `bool` if user answered "yes" or "no" or `default` (configured in [`default`](Self::default) if pushes enter. 105 | /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'. 106 | #[inline] 107 | pub fn interact(self) -> Result { 108 | self.interact_on(&Term::stderr()) 109 | } 110 | 111 | /// Enables user interaction and returns the result. 112 | /// 113 | /// The dialog is rendered on stderr. 114 | /// 115 | /// Result contains `Some(bool)` if user answered "yes" or "no" or `Some(default)` (configured in [`default`](Self::default)) if pushes enter, 116 | /// or `None` if user cancelled with 'Esc' or 'q'. 117 | /// 118 | /// ## Example 119 | /// 120 | /// ```rust,no_run 121 | /// use dialoguer::Confirm; 122 | /// 123 | /// fn main() { 124 | /// let confirmation = Confirm::new() 125 | /// .interact_opt() 126 | /// .unwrap(); 127 | /// 128 | /// match confirmation { 129 | /// Some(answer) => println!("User answered {}", if answer { "yes" } else { "no " }), 130 | /// None => println!("User did not answer") 131 | /// } 132 | /// } 133 | /// ``` 134 | #[inline] 135 | pub fn interact_opt(self) -> Result> { 136 | self.interact_on_opt(&Term::stderr()) 137 | } 138 | 139 | /// Like [`interact`](Self::interact) but allows a specific terminal to be set. 140 | #[inline] 141 | pub fn interact_on(self, term: &Term) -> Result { 142 | Ok(self 143 | ._interact_on(term, false)? 144 | .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))?) 145 | } 146 | 147 | /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set. 148 | #[inline] 149 | pub fn interact_on_opt(self, term: &Term) -> Result> { 150 | self._interact_on(term, true) 151 | } 152 | 153 | fn _interact_on(self, term: &Term, allow_quit: bool) -> Result> { 154 | if !term.is_term() { 155 | return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into()); 156 | } 157 | 158 | let mut render = TermThemeRenderer::new(term, self.theme); 159 | 160 | let default_if_show = if self.show_default { 161 | self.default 162 | } else { 163 | None 164 | }; 165 | 166 | render.confirm_prompt(&self.prompt, default_if_show)?; 167 | 168 | term.hide_cursor()?; 169 | term.flush()?; 170 | 171 | let rv; 172 | 173 | if self.wait_for_newline { 174 | // Waits for user input and for the user to hit the Enter key 175 | // before validation. 176 | let mut value = default_if_show; 177 | 178 | loop { 179 | let input = term.read_key()?; 180 | 181 | match input { 182 | Key::Char('y') | Key::Char('Y') => { 183 | value = Some(true); 184 | } 185 | Key::Char('n') | Key::Char('N') => { 186 | value = Some(false); 187 | } 188 | Key::Enter => { 189 | if !allow_quit { 190 | value = value.or(self.default); 191 | } 192 | 193 | if value.is_some() || allow_quit { 194 | rv = value; 195 | break; 196 | } 197 | continue; 198 | } 199 | Key::Escape | Key::Char('q') if allow_quit => { 200 | value = None; 201 | } 202 | _ => { 203 | continue; 204 | } 205 | }; 206 | 207 | term.clear_line()?; 208 | render.confirm_prompt(&self.prompt, value)?; 209 | } 210 | } else { 211 | // Default behavior: matches continuously on every keystroke, 212 | // and does not wait for user to hit the Enter key. 213 | loop { 214 | let input = term.read_key()?; 215 | let value = match input { 216 | Key::Char('y') | Key::Char('Y') => Some(true), 217 | Key::Char('n') | Key::Char('N') => Some(false), 218 | Key::Enter if self.default.is_some() => Some(self.default.unwrap()), 219 | Key::Escape | Key::Char('q') if allow_quit => None, 220 | _ => { 221 | continue; 222 | } 223 | }; 224 | 225 | rv = value; 226 | break; 227 | } 228 | } 229 | 230 | term.clear_line()?; 231 | if self.report { 232 | render.confirm_prompt_selection(&self.prompt, rv)?; 233 | } 234 | term.show_cursor()?; 235 | term.flush()?; 236 | 237 | Ok(rv) 238 | } 239 | } 240 | 241 | impl<'a> Confirm<'a> { 242 | /// Creates a confirm prompt with a specific theme. 243 | /// 244 | /// ## Example 245 | /// 246 | /// ```rust,no_run 247 | /// use dialoguer::{theme::ColorfulTheme, Confirm}; 248 | /// 249 | /// fn main() { 250 | /// let confirmation = Confirm::with_theme(&ColorfulTheme::default()) 251 | /// .interact() 252 | /// .unwrap(); 253 | /// } 254 | /// ``` 255 | pub fn with_theme(theme: &'a dyn Theme) -> Self { 256 | Self { 257 | prompt: "".into(), 258 | report: true, 259 | default: None, 260 | show_default: true, 261 | wait_for_newline: false, 262 | theme, 263 | } 264 | } 265 | } 266 | 267 | #[cfg(test)] 268 | mod tests { 269 | use super::*; 270 | 271 | #[test] 272 | fn test_clone() { 273 | let confirm = Confirm::new().with_prompt("Do you want to continue?"); 274 | 275 | let _ = confirm.clone(); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/prompts/select.rs: -------------------------------------------------------------------------------- 1 | use std::{io, ops::Rem}; 2 | 3 | use console::{Key, Term}; 4 | 5 | use crate::{ 6 | theme::{render::TermThemeRenderer, SimpleTheme, Theme}, 7 | Paging, Result, 8 | }; 9 | 10 | /// Renders a select prompt. 11 | /// 12 | /// User can select from one or more options. 13 | /// Interaction returns index of an item selected in the order they appear in `item` invocation or `items` slice. 14 | /// 15 | /// ## Example 16 | /// 17 | /// ```rust,no_run 18 | /// use dialoguer::Select; 19 | /// 20 | /// fn main() { 21 | /// let items = vec!["foo", "bar", "baz"]; 22 | /// 23 | /// let selection = Select::new() 24 | /// .with_prompt("What do you choose?") 25 | /// .items(&items) 26 | /// .interact() 27 | /// .unwrap(); 28 | /// 29 | /// println!("You chose: {}", items[selection]); 30 | /// } 31 | /// ``` 32 | #[derive(Clone)] 33 | pub struct Select<'a> { 34 | default: usize, 35 | items: Vec, 36 | prompt: Option, 37 | report: bool, 38 | clear: bool, 39 | theme: &'a dyn Theme, 40 | max_length: Option, 41 | } 42 | 43 | impl Default for Select<'static> { 44 | fn default() -> Self { 45 | Self::new() 46 | } 47 | } 48 | 49 | impl Select<'static> { 50 | /// Creates a select prompt with default theme. 51 | pub fn new() -> Self { 52 | Self::with_theme(&SimpleTheme) 53 | } 54 | } 55 | 56 | impl Select<'_> { 57 | /// Indicates whether select menu should be erased from the screen after interaction. 58 | /// 59 | /// The default is to clear the menu. 60 | pub fn clear(mut self, val: bool) -> Self { 61 | self.clear = val; 62 | self 63 | } 64 | 65 | /// Sets initial selected element when select menu is rendered 66 | /// 67 | /// Element is indicated by the index at which it appears in [`item`](Self::item) method invocation or [`items`](Self::items) slice. 68 | pub fn default(mut self, val: usize) -> Self { 69 | self.default = val; 70 | self 71 | } 72 | 73 | /// Sets an optional max length for a page. 74 | /// 75 | /// Max length is disabled by None 76 | pub fn max_length(mut self, val: usize) -> Self { 77 | // Paging subtracts two from the capacity, paging does this to 78 | // make an offset for the page indicator. So to make sure that 79 | // we can show the intended amount of items we need to add two 80 | // to our value. 81 | self.max_length = Some(val + 2); 82 | self 83 | } 84 | 85 | /// Add a single item to the selector. 86 | /// 87 | /// ## Example 88 | /// 89 | /// ```rust,no_run 90 | /// use dialoguer::Select; 91 | /// 92 | /// fn main() { 93 | /// let selection = Select::new() 94 | /// .item("Item 1") 95 | /// .item("Item 2") 96 | /// .interact() 97 | /// .unwrap(); 98 | /// } 99 | /// ``` 100 | pub fn item(mut self, item: T) -> Self { 101 | self.items.push(item.to_string()); 102 | 103 | self 104 | } 105 | 106 | /// Adds multiple items to the selector. 107 | pub fn items(mut self, items: I) -> Self 108 | where 109 | T: ToString, 110 | I: IntoIterator, 111 | { 112 | self.items 113 | .extend(items.into_iter().map(|item| item.to_string())); 114 | 115 | self 116 | } 117 | 118 | /// Sets the select prompt. 119 | /// 120 | /// By default, when a prompt is set the system also prints out a confirmation after 121 | /// the selection. You can opt-out of this with [`report`](Self::report). 122 | pub fn with_prompt>(mut self, prompt: S) -> Self { 123 | self.prompt = Some(prompt.into()); 124 | self.report = true; 125 | self 126 | } 127 | 128 | /// Indicates whether to report the selected value after interaction. 129 | /// 130 | /// The default is to report the selection. 131 | pub fn report(mut self, val: bool) -> Self { 132 | self.report = val; 133 | self 134 | } 135 | 136 | /// Enables user interaction and returns the result. 137 | /// 138 | /// The user can select the items with the 'Space' bar or 'Enter' and the index of selected item will be returned. 139 | /// The dialog is rendered on stderr. 140 | /// Result contains `index` if user selected one of items using 'Enter'. 141 | /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'. 142 | #[inline] 143 | pub fn interact(self) -> Result { 144 | self.interact_on(&Term::stderr()) 145 | } 146 | 147 | /// Enables user interaction and returns the result. 148 | /// 149 | /// The user can select the items with the 'Space' bar or 'Enter' and the index of selected item will be returned. 150 | /// The dialog is rendered on stderr. 151 | /// Result contains `Some(index)` if user selected one of items using 'Enter' or `None` if user cancelled with 'Esc' or 'q'. 152 | /// 153 | /// ## Example 154 | /// 155 | ///```rust,no_run 156 | /// use dialoguer::Select; 157 | /// 158 | /// fn main() { 159 | /// let items = vec!["foo", "bar", "baz"]; 160 | /// 161 | /// let selection = Select::new() 162 | /// .with_prompt("What do you choose?") 163 | /// .items(&items) 164 | /// .interact_opt() 165 | /// .unwrap(); 166 | /// 167 | /// match selection { 168 | /// Some(index) => println!("You chose: {}", items[index]), 169 | /// None => println!("You did not choose anything.") 170 | /// } 171 | /// } 172 | ///``` 173 | #[inline] 174 | pub fn interact_opt(self) -> Result> { 175 | self.interact_on_opt(&Term::stderr()) 176 | } 177 | 178 | /// Like [`interact`](Self::interact) but allows a specific terminal to be set. 179 | #[inline] 180 | pub fn interact_on(self, term: &Term) -> Result { 181 | Ok(self 182 | ._interact_on(term, false)? 183 | .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))?) 184 | } 185 | 186 | /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set. 187 | #[inline] 188 | pub fn interact_on_opt(self, term: &Term) -> Result> { 189 | self._interact_on(term, true) 190 | } 191 | 192 | /// Like `interact` but allows a specific terminal to be set. 193 | fn _interact_on(self, term: &Term, allow_quit: bool) -> Result> { 194 | if !term.is_term() { 195 | return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into()); 196 | } 197 | 198 | if self.items.is_empty() { 199 | return Err(io::Error::new( 200 | io::ErrorKind::Other, 201 | "Empty list of items given to `Select`", 202 | ))?; 203 | } 204 | 205 | let mut paging = Paging::new(term, self.items.len(), self.max_length); 206 | let mut render = TermThemeRenderer::new(term, self.theme); 207 | let mut sel = self.default; 208 | 209 | let mut size_vec = Vec::new(); 210 | 211 | for items in self 212 | .items 213 | .iter() 214 | .flat_map(|i| i.split('\n')) 215 | .collect::>() 216 | { 217 | let size = &items.len(); 218 | size_vec.push(*size); 219 | } 220 | 221 | term.hide_cursor()?; 222 | paging.update_page(sel); 223 | 224 | loop { 225 | if let Some(ref prompt) = self.prompt { 226 | paging.render_prompt(|paging_info| render.select_prompt(prompt, paging_info))?; 227 | } 228 | 229 | for (idx, item) in self 230 | .items 231 | .iter() 232 | .enumerate() 233 | .skip(paging.current_page * paging.capacity) 234 | .take(paging.capacity) 235 | { 236 | render.select_prompt_item(item, sel == idx)?; 237 | } 238 | 239 | term.flush()?; 240 | 241 | match term.read_key()? { 242 | Key::ArrowDown | Key::Tab | Key::Char('j') => { 243 | if sel == !0 { 244 | sel = 0; 245 | } else { 246 | sel = (sel as u64 + 1).rem(self.items.len() as u64) as usize; 247 | } 248 | } 249 | Key::Escape | Key::Char('q') => { 250 | if allow_quit { 251 | if self.clear { 252 | render.clear()?; 253 | } else { 254 | term.clear_last_lines(paging.capacity)?; 255 | } 256 | 257 | term.show_cursor()?; 258 | term.flush()?; 259 | 260 | return Ok(None); 261 | } 262 | } 263 | Key::ArrowUp | Key::BackTab | Key::Char('k') => { 264 | if sel == !0 { 265 | sel = self.items.len() - 1; 266 | } else { 267 | sel = ((sel as i64 - 1 + self.items.len() as i64) 268 | % (self.items.len() as i64)) as usize; 269 | } 270 | } 271 | Key::ArrowLeft | Key::Char('h') => { 272 | if paging.active { 273 | sel = paging.previous_page(); 274 | } 275 | } 276 | Key::ArrowRight | Key::Char('l') => { 277 | if paging.active { 278 | sel = paging.next_page(); 279 | } 280 | } 281 | 282 | Key::Enter | Key::Char(' ') if sel != !0 => { 283 | if self.clear { 284 | render.clear()?; 285 | } 286 | 287 | if let Some(ref prompt) = self.prompt { 288 | if self.report { 289 | render.select_prompt_selection(prompt, &self.items[sel])?; 290 | } 291 | } 292 | 293 | term.show_cursor()?; 294 | term.flush()?; 295 | 296 | return Ok(Some(sel)); 297 | } 298 | _ => {} 299 | } 300 | 301 | paging.update(sel)?; 302 | 303 | if paging.active { 304 | render.clear()?; 305 | } else { 306 | render.clear_preserve_prompt(&size_vec)?; 307 | } 308 | } 309 | } 310 | } 311 | 312 | impl<'a> Select<'a> { 313 | /// Creates a select prompt with a specific theme. 314 | /// 315 | /// ## Example 316 | /// 317 | /// ```rust,no_run 318 | /// use dialoguer::{theme::ColorfulTheme, Select}; 319 | /// 320 | /// fn main() { 321 | /// let selection = Select::with_theme(&ColorfulTheme::default()) 322 | /// .items(&["foo", "bar", "baz"]) 323 | /// .interact() 324 | /// .unwrap(); 325 | /// } 326 | /// ``` 327 | pub fn with_theme(theme: &'a dyn Theme) -> Self { 328 | Self { 329 | default: !0, 330 | items: vec![], 331 | prompt: None, 332 | report: false, 333 | clear: true, 334 | max_length: None, 335 | theme, 336 | } 337 | } 338 | } 339 | 340 | #[cfg(test)] 341 | mod tests { 342 | use super::*; 343 | 344 | #[test] 345 | fn test_clone() { 346 | let select = Select::new().with_prompt("Do you want to continue?"); 347 | 348 | let _ = select.clone(); 349 | } 350 | 351 | #[test] 352 | fn test_str() { 353 | let selections = &[ 354 | "Ice Cream", 355 | "Vanilla Cupcake", 356 | "Chocolate Muffin", 357 | "A Pile of sweet, sweet mustard", 358 | ]; 359 | 360 | assert_eq!( 361 | Select::new().default(0).items(&selections[..]).items, 362 | selections 363 | ); 364 | } 365 | 366 | #[test] 367 | fn test_string() { 368 | let selections = vec!["a".to_string(), "b".to_string()]; 369 | 370 | assert_eq!( 371 | Select::new().default(0).items(&selections).items, 372 | selections 373 | ); 374 | } 375 | 376 | #[test] 377 | fn test_ref_str() { 378 | let a = "a"; 379 | let b = "b"; 380 | 381 | let selections = &[a, b]; 382 | 383 | assert_eq!(Select::new().default(0).items(selections).items, selections); 384 | } 385 | 386 | #[test] 387 | fn test_iterator() { 388 | let items = ["First", "Second", "Third"]; 389 | let iterator = items.iter().skip(1); 390 | 391 | assert_eq!(Select::new().default(0).items(iterator).items, &items[1..]); 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/prompts/sort.rs: -------------------------------------------------------------------------------- 1 | use std::{io, ops::Rem}; 2 | 3 | use console::{Key, Term}; 4 | 5 | use crate::{ 6 | theme::{render::TermThemeRenderer, SimpleTheme, Theme}, 7 | Paging, Result, 8 | }; 9 | 10 | /// Renders a sort prompt. 11 | /// 12 | /// Returns list of indices in original items list sorted according to user input. 13 | /// 14 | /// ## Example 15 | /// 16 | /// ```rust,no_run 17 | /// use dialoguer::Sort; 18 | /// 19 | /// fn main() { 20 | /// let items = vec!["foo", "bar", "baz"]; 21 | /// 22 | /// let ordered = Sort::new() 23 | /// .with_prompt("Which order do you prefer?") 24 | /// .items(&items) 25 | /// .interact() 26 | /// .unwrap(); 27 | /// 28 | /// println!("You prefer:"); 29 | /// 30 | /// for i in ordered { 31 | /// println!("{}", items[i]); 32 | /// } 33 | /// } 34 | /// ``` 35 | #[derive(Clone)] 36 | pub struct Sort<'a> { 37 | items: Vec, 38 | prompt: Option, 39 | report: bool, 40 | clear: bool, 41 | max_length: Option, 42 | theme: &'a dyn Theme, 43 | } 44 | 45 | impl Default for Sort<'static> { 46 | fn default() -> Self { 47 | Self::new() 48 | } 49 | } 50 | 51 | impl Sort<'static> { 52 | /// Creates a sort prompt with default theme. 53 | pub fn new() -> Self { 54 | Self::with_theme(&SimpleTheme) 55 | } 56 | } 57 | 58 | impl Sort<'_> { 59 | /// Sets the clear behavior of the menu. 60 | /// 61 | /// The default is to clear the menu after user interaction. 62 | pub fn clear(mut self, val: bool) -> Self { 63 | self.clear = val; 64 | self 65 | } 66 | 67 | /// Sets an optional max length for a page 68 | /// 69 | /// Max length is disabled by None 70 | pub fn max_length(mut self, val: usize) -> Self { 71 | // Paging subtracts two from the capacity, paging does this to 72 | // make an offset for the page indicator. So to make sure that 73 | // we can show the intended amount of items we need to add two 74 | // to our value. 75 | self.max_length = Some(val + 2); 76 | self 77 | } 78 | 79 | /// Add a single item to the selector. 80 | pub fn item(mut self, item: T) -> Self { 81 | self.items.push(item.to_string()); 82 | self 83 | } 84 | 85 | /// Adds multiple items to the selector. 86 | pub fn items(mut self, items: I) -> Self 87 | where 88 | T: ToString, 89 | I: IntoIterator, 90 | { 91 | self.items 92 | .extend(items.into_iter().map(|item| item.to_string())); 93 | 94 | self 95 | } 96 | 97 | /// Prefaces the menu with a prompt. 98 | /// 99 | /// By default, when a prompt is set the system also prints out a confirmation after 100 | /// the selection. You can opt-out of this with [`report`](#method.report). 101 | pub fn with_prompt>(mut self, prompt: S) -> Self { 102 | self.prompt = Some(prompt.into()); 103 | self 104 | } 105 | 106 | /// Indicates whether to report the selected order after interaction. 107 | /// 108 | /// The default is to report the selected order. 109 | pub fn report(mut self, val: bool) -> Self { 110 | self.report = val; 111 | self 112 | } 113 | 114 | /// Enables user interaction and returns the result. 115 | /// 116 | /// The user can order the items with the 'Space' bar and the arrows. On 'Enter' ordered list of the incides of items will be returned. 117 | /// The dialog is rendered on stderr. 118 | /// Result contains `Vec` if user hit 'Enter'. 119 | /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'. 120 | #[inline] 121 | pub fn interact(self) -> Result> { 122 | self.interact_on(&Term::stderr()) 123 | } 124 | 125 | /// Enables user interaction and returns the result. 126 | /// 127 | /// The user can order the items with the 'Space' bar and the arrows. On 'Enter' ordered list of the incides of items will be returned. 128 | /// The dialog is rendered on stderr. 129 | /// Result contains `Some(Vec)` if user hit 'Enter' or `None` if user cancelled with 'Esc' or 'q'. 130 | /// 131 | /// ## Example 132 | /// 133 | /// ```rust,no_run 134 | /// use dialoguer::Sort; 135 | /// 136 | /// fn main() { 137 | /// let items = vec!["foo", "bar", "baz"]; 138 | /// 139 | /// let ordered = Sort::new() 140 | /// .items(&items) 141 | /// .interact_opt() 142 | /// .unwrap(); 143 | /// 144 | /// match ordered { 145 | /// Some(positions) => { 146 | /// println!("You prefer:"); 147 | /// 148 | /// for i in positions { 149 | /// println!("{}", items[i]); 150 | /// } 151 | /// }, 152 | /// None => println!("You did not prefer anything.") 153 | /// } 154 | /// } 155 | /// ``` 156 | #[inline] 157 | pub fn interact_opt(self) -> Result>> { 158 | self.interact_on_opt(&Term::stderr()) 159 | } 160 | 161 | /// Like [`interact`](Self::interact) but allows a specific terminal to be set. 162 | #[inline] 163 | pub fn interact_on(self, term: &Term) -> Result> { 164 | Ok(self 165 | ._interact_on(term, false)? 166 | .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))?) 167 | } 168 | 169 | /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set. 170 | #[inline] 171 | pub fn interact_on_opt(self, term: &Term) -> Result>> { 172 | self._interact_on(term, true) 173 | } 174 | 175 | fn _interact_on(self, term: &Term, allow_quit: bool) -> Result>> { 176 | if !term.is_term() { 177 | return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into()); 178 | } 179 | 180 | if self.items.is_empty() { 181 | return Err(io::Error::new( 182 | io::ErrorKind::Other, 183 | "Empty list of items given to `Sort`", 184 | ))?; 185 | } 186 | 187 | let mut paging = Paging::new(term, self.items.len(), self.max_length); 188 | let mut render = TermThemeRenderer::new(term, self.theme); 189 | let mut sel = 0; 190 | 191 | let mut size_vec = Vec::new(); 192 | 193 | for items in self.items.iter().as_slice() { 194 | let size = &items.len(); 195 | size_vec.push(*size); 196 | } 197 | 198 | let mut order: Vec<_> = (0..self.items.len()).collect(); 199 | let mut checked: bool = false; 200 | 201 | term.hide_cursor()?; 202 | 203 | loop { 204 | if let Some(ref prompt) = self.prompt { 205 | paging.render_prompt(|paging_info| render.sort_prompt(prompt, paging_info))?; 206 | } 207 | 208 | for (idx, item) in order 209 | .iter() 210 | .enumerate() 211 | .skip(paging.current_page * paging.capacity) 212 | .take(paging.capacity) 213 | { 214 | render.sort_prompt_item(&self.items[*item], checked, sel == idx)?; 215 | } 216 | 217 | term.flush()?; 218 | 219 | match term.read_key()? { 220 | Key::ArrowDown | Key::Tab | Key::Char('j') => { 221 | let old_sel = sel; 222 | 223 | if sel == !0 { 224 | sel = 0; 225 | } else { 226 | sel = (sel as u64 + 1).rem(self.items.len() as u64) as usize; 227 | } 228 | 229 | if checked && old_sel != sel { 230 | order.swap(old_sel, sel); 231 | } 232 | } 233 | Key::ArrowUp | Key::BackTab | Key::Char('k') => { 234 | let old_sel = sel; 235 | 236 | if sel == !0 { 237 | sel = self.items.len() - 1; 238 | } else { 239 | sel = ((sel as i64 - 1 + self.items.len() as i64) 240 | % (self.items.len() as i64)) as usize; 241 | } 242 | 243 | if checked && old_sel != sel { 244 | order.swap(old_sel, sel); 245 | } 246 | } 247 | Key::ArrowLeft | Key::Char('h') => { 248 | if paging.active { 249 | let old_sel = sel; 250 | let old_page = paging.current_page; 251 | 252 | sel = paging.previous_page(); 253 | 254 | if checked { 255 | let indexes: Vec<_> = if old_page == 0 { 256 | let indexes1: Vec<_> = (0..=old_sel).rev().collect(); 257 | let indexes2: Vec<_> = (sel..self.items.len()).rev().collect(); 258 | [indexes1, indexes2].concat() 259 | } else { 260 | (sel..=old_sel).rev().collect() 261 | }; 262 | 263 | for index in 0..(indexes.len() - 1) { 264 | order.swap(indexes[index], indexes[index + 1]); 265 | } 266 | } 267 | } 268 | } 269 | Key::ArrowRight | Key::Char('l') => { 270 | if paging.active { 271 | let old_sel = sel; 272 | let old_page = paging.current_page; 273 | 274 | sel = paging.next_page(); 275 | 276 | if checked { 277 | let indexes: Vec<_> = if old_page == paging.pages - 1 { 278 | let indexes1: Vec<_> = (old_sel..self.items.len()).collect(); 279 | let indexes2: Vec<_> = vec![0]; 280 | [indexes1, indexes2].concat() 281 | } else { 282 | (old_sel..=sel).collect() 283 | }; 284 | 285 | for index in 0..(indexes.len() - 1) { 286 | order.swap(indexes[index], indexes[index + 1]); 287 | } 288 | } 289 | } 290 | } 291 | Key::Char(' ') => { 292 | checked = !checked; 293 | } 294 | Key::Escape | Key::Char('q') => { 295 | if allow_quit { 296 | if self.clear { 297 | render.clear()?; 298 | } else { 299 | term.clear_last_lines(paging.capacity)?; 300 | } 301 | 302 | term.show_cursor()?; 303 | term.flush()?; 304 | 305 | return Ok(None); 306 | } 307 | } 308 | Key::Enter => { 309 | if self.clear { 310 | render.clear()?; 311 | } 312 | 313 | if let Some(ref prompt) = self.prompt { 314 | if self.report { 315 | let list: Vec<_> = order 316 | .iter() 317 | .map(|item| self.items[*item].as_str()) 318 | .collect(); 319 | render.sort_prompt_selection(prompt, &list[..])?; 320 | } 321 | } 322 | 323 | term.show_cursor()?; 324 | term.flush()?; 325 | 326 | return Ok(Some(order)); 327 | } 328 | _ => {} 329 | } 330 | 331 | paging.update(sel)?; 332 | 333 | if paging.active { 334 | render.clear()?; 335 | } else { 336 | render.clear_preserve_prompt(&size_vec)?; 337 | } 338 | } 339 | } 340 | } 341 | 342 | impl<'a> Sort<'a> { 343 | /// Creates a sort prompt with a specific theme. 344 | /// 345 | /// ## Example 346 | /// 347 | /// ```rust,no_run 348 | /// use dialoguer::{theme::ColorfulTheme, Sort}; 349 | /// 350 | /// fn main() { 351 | /// let ordered = Sort::with_theme(&ColorfulTheme::default()) 352 | /// .items(&["foo", "bar", "baz"]) 353 | /// .interact() 354 | /// .unwrap(); 355 | /// } 356 | /// ``` 357 | pub fn with_theme(theme: &'a dyn Theme) -> Self { 358 | Self { 359 | items: vec![], 360 | clear: true, 361 | prompt: None, 362 | report: true, 363 | max_length: None, 364 | theme, 365 | } 366 | } 367 | } 368 | 369 | #[cfg(test)] 370 | mod tests { 371 | use super::*; 372 | 373 | #[test] 374 | fn test_clone() { 375 | let sort = Sort::new().with_prompt("Which order do you prefer?"); 376 | 377 | let _ = sort.clone(); 378 | } 379 | 380 | #[test] 381 | fn test_iterator() { 382 | let items = ["First", "Second", "Third"]; 383 | let iterator = items.iter().skip(1); 384 | 385 | assert_eq!(Sort::new().items(iterator).items, &items[1..]); 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /src/prompts/multi_select.rs: -------------------------------------------------------------------------------- 1 | use std::{io, iter::repeat, ops::Rem}; 2 | 3 | use console::{Key, Term}; 4 | 5 | use crate::{ 6 | theme::{render::TermThemeRenderer, SimpleTheme, Theme}, 7 | Paging, Result, 8 | }; 9 | 10 | /// Renders a multi select prompt. 11 | /// 12 | /// ## Example 13 | /// 14 | /// ```rust,no_run 15 | /// use dialoguer::MultiSelect; 16 | /// 17 | /// fn main() { 18 | /// let items = vec!["foo", "bar", "baz"]; 19 | /// 20 | /// let selection = MultiSelect::new() 21 | /// .with_prompt("What do you choose?") 22 | /// .items(&items) 23 | /// .interact() 24 | /// .unwrap(); 25 | /// 26 | /// println!("You chose:"); 27 | /// 28 | /// for i in selection { 29 | /// println!("{}", items[i]); 30 | /// } 31 | /// } 32 | /// ``` 33 | #[derive(Clone)] 34 | pub struct MultiSelect<'a> { 35 | defaults: Vec, 36 | items: Vec, 37 | prompt: Option, 38 | report: bool, 39 | clear: bool, 40 | max_length: Option, 41 | theme: &'a dyn Theme, 42 | } 43 | 44 | impl Default for MultiSelect<'static> { 45 | fn default() -> Self { 46 | Self::new() 47 | } 48 | } 49 | 50 | impl MultiSelect<'static> { 51 | /// Creates a multi select prompt with default theme. 52 | pub fn new() -> Self { 53 | Self::with_theme(&SimpleTheme) 54 | } 55 | } 56 | 57 | impl MultiSelect<'_> { 58 | /// Sets the clear behavior of the menu. 59 | /// 60 | /// The default is to clear the menu. 61 | pub fn clear(mut self, val: bool) -> Self { 62 | self.clear = val; 63 | self 64 | } 65 | 66 | /// Sets a defaults for the menu. 67 | pub fn defaults(mut self, val: &[bool]) -> Self { 68 | self.defaults = val 69 | .to_vec() 70 | .iter() 71 | .copied() 72 | .chain(repeat(false)) 73 | .take(self.items.len()) 74 | .collect(); 75 | self 76 | } 77 | 78 | /// Sets an optional max length for a page 79 | /// 80 | /// Max length is disabled by None 81 | pub fn max_length(mut self, val: usize) -> Self { 82 | // Paging subtracts two from the capacity, paging does this to 83 | // make an offset for the page indicator. So to make sure that 84 | // we can show the intended amount of items we need to add two 85 | // to our value. 86 | self.max_length = Some(val + 2); 87 | self 88 | } 89 | 90 | /// Add a single item to the selector. 91 | #[inline] 92 | pub fn item(self, item: T) -> Self { 93 | self.item_checked(item, false) 94 | } 95 | 96 | /// Add a single item to the selector with a default checked state. 97 | pub fn item_checked(mut self, item: T, checked: bool) -> Self { 98 | self.items.push(item.to_string()); 99 | self.defaults.push(checked); 100 | self 101 | } 102 | 103 | /// Adds multiple items to the selector. 104 | pub fn items(self, items: I) -> Self 105 | where 106 | T: ToString, 107 | I: IntoIterator, 108 | { 109 | self.items_checked(items.into_iter().map(|item| (item, false))) 110 | } 111 | 112 | /// Adds multiple items to the selector with checked state 113 | pub fn items_checked(mut self, items: I) -> Self 114 | where 115 | T: ToString, 116 | I: IntoIterator, 117 | { 118 | for (item, checked) in items.into_iter() { 119 | self.items.push(item.to_string()); 120 | self.defaults.push(checked); 121 | } 122 | self 123 | } 124 | 125 | /// Prefaces the menu with a prompt. 126 | /// 127 | /// By default, when a prompt is set the system also prints out a confirmation after 128 | /// the selection. You can opt-out of this with [`report`](Self::report). 129 | pub fn with_prompt>(mut self, prompt: S) -> Self { 130 | self.prompt = Some(prompt.into()); 131 | self 132 | } 133 | 134 | /// Indicates whether to report the selected values after interaction. 135 | /// 136 | /// The default is to report the selections. 137 | pub fn report(mut self, val: bool) -> Self { 138 | self.report = val; 139 | self 140 | } 141 | 142 | /// Enables user interaction and returns the result. 143 | /// 144 | /// The user can select the items with the 'Space' bar and on 'Enter' the indices of selected items will be returned. 145 | /// The dialog is rendered on stderr. 146 | /// Result contains `Vec` if user hit 'Enter'. 147 | /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'. 148 | #[inline] 149 | pub fn interact(self) -> Result> { 150 | self.interact_on(&Term::stderr()) 151 | } 152 | 153 | /// Enables user interaction and returns the result. 154 | /// 155 | /// The user can select the items with the 'Space' bar and on 'Enter' the indices of selected items will be returned. 156 | /// The dialog is rendered on stderr. 157 | /// Result contains `Some(Vec)` if user hit 'Enter' or `None` if user cancelled with 'Esc' or 'q'. 158 | /// 159 | /// ## Example 160 | /// 161 | /// ```rust,no_run 162 | /// use dialoguer::MultiSelect; 163 | /// 164 | /// fn main() { 165 | /// let items = vec!["foo", "bar", "baz"]; 166 | /// 167 | /// let ordered = MultiSelect::new() 168 | /// .items(&items) 169 | /// .interact_opt() 170 | /// .unwrap(); 171 | /// 172 | /// match ordered { 173 | /// Some(positions) => { 174 | /// println!("You chose:"); 175 | /// 176 | /// for i in positions { 177 | /// println!("{}", items[i]); 178 | /// } 179 | /// }, 180 | /// None => println!("You did not choose anything.") 181 | /// } 182 | /// } 183 | /// ``` 184 | #[inline] 185 | pub fn interact_opt(self) -> Result>> { 186 | self.interact_on_opt(&Term::stderr()) 187 | } 188 | 189 | /// Like [`interact`](Self::interact) but allows a specific terminal to be set. 190 | #[inline] 191 | pub fn interact_on(self, term: &Term) -> Result> { 192 | Ok(self 193 | ._interact_on(term, false)? 194 | .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))?) 195 | } 196 | 197 | /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set. 198 | #[inline] 199 | pub fn interact_on_opt(self, term: &Term) -> Result>> { 200 | self._interact_on(term, true) 201 | } 202 | 203 | fn _interact_on(self, term: &Term, allow_quit: bool) -> Result>> { 204 | if !term.is_term() { 205 | return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into()); 206 | } 207 | 208 | if self.items.is_empty() { 209 | return Err(io::Error::new( 210 | io::ErrorKind::Other, 211 | "Empty list of items given to `MultiSelect`", 212 | ))?; 213 | } 214 | 215 | let mut paging = Paging::new(term, self.items.len(), self.max_length); 216 | let mut render = TermThemeRenderer::new(term, self.theme); 217 | let mut sel = 0; 218 | 219 | let mut size_vec = Vec::new(); 220 | 221 | for items in self 222 | .items 223 | .iter() 224 | .flat_map(|i| i.split('\n')) 225 | .collect::>() 226 | { 227 | let size = &items.len(); 228 | size_vec.push(*size); 229 | } 230 | 231 | let mut checked: Vec = self.defaults.clone(); 232 | 233 | term.hide_cursor()?; 234 | 235 | loop { 236 | if let Some(ref prompt) = self.prompt { 237 | paging 238 | .render_prompt(|paging_info| render.multi_select_prompt(prompt, paging_info))?; 239 | } 240 | 241 | for (idx, item) in self 242 | .items 243 | .iter() 244 | .enumerate() 245 | .skip(paging.current_page * paging.capacity) 246 | .take(paging.capacity) 247 | { 248 | render.multi_select_prompt_item(item, checked[idx], sel == idx)?; 249 | } 250 | 251 | term.flush()?; 252 | 253 | match term.read_key()? { 254 | Key::ArrowDown | Key::Tab | Key::Char('j') => { 255 | if sel == !0 { 256 | sel = 0; 257 | } else { 258 | sel = (sel as u64 + 1).rem(self.items.len() as u64) as usize; 259 | } 260 | } 261 | Key::ArrowUp | Key::BackTab | Key::Char('k') => { 262 | if sel == !0 { 263 | sel = self.items.len() - 1; 264 | } else { 265 | sel = ((sel as i64 - 1 + self.items.len() as i64) 266 | % (self.items.len() as i64)) as usize; 267 | } 268 | } 269 | Key::ArrowLeft | Key::Char('h') => { 270 | if paging.active { 271 | sel = paging.previous_page(); 272 | } 273 | } 274 | Key::ArrowRight | Key::Char('l') => { 275 | if paging.active { 276 | sel = paging.next_page(); 277 | } 278 | } 279 | Key::Char(' ') => { 280 | checked[sel] = !checked[sel]; 281 | } 282 | Key::Char('a') => { 283 | if checked.iter().all(|&item_checked| item_checked) { 284 | checked.fill(false); 285 | } else { 286 | checked.fill(true); 287 | } 288 | } 289 | Key::Escape | Key::Char('q') => { 290 | if allow_quit { 291 | if self.clear { 292 | render.clear()?; 293 | } else { 294 | term.clear_last_lines(paging.capacity)?; 295 | } 296 | 297 | term.show_cursor()?; 298 | term.flush()?; 299 | 300 | return Ok(None); 301 | } 302 | } 303 | Key::Enter => { 304 | if self.clear { 305 | render.clear()?; 306 | } 307 | 308 | if let Some(ref prompt) = self.prompt { 309 | if self.report { 310 | let selections: Vec<_> = checked 311 | .iter() 312 | .enumerate() 313 | .filter_map(|(idx, &checked)| { 314 | if checked { 315 | Some(self.items[idx].as_str()) 316 | } else { 317 | None 318 | } 319 | }) 320 | .collect(); 321 | 322 | render.multi_select_prompt_selection(prompt, &selections[..])?; 323 | } 324 | } 325 | 326 | term.show_cursor()?; 327 | term.flush()?; 328 | 329 | return Ok(Some( 330 | checked 331 | .into_iter() 332 | .enumerate() 333 | .filter_map(|(idx, checked)| if checked { Some(idx) } else { None }) 334 | .collect(), 335 | )); 336 | } 337 | _ => {} 338 | } 339 | 340 | paging.update(sel)?; 341 | 342 | if paging.active { 343 | render.clear()?; 344 | } else { 345 | render.clear_preserve_prompt(&size_vec)?; 346 | } 347 | } 348 | } 349 | } 350 | 351 | impl<'a> MultiSelect<'a> { 352 | /// Creates a multi select prompt with a specific theme. 353 | /// 354 | /// ## Example 355 | /// 356 | /// ```rust,no_run 357 | /// use dialoguer::{theme::ColorfulTheme, MultiSelect}; 358 | /// 359 | /// fn main() { 360 | /// let selection = MultiSelect::with_theme(&ColorfulTheme::default()) 361 | /// .items(&["foo", "bar", "baz"]) 362 | /// .interact() 363 | /// .unwrap(); 364 | /// } 365 | /// ``` 366 | pub fn with_theme(theme: &'a dyn Theme) -> Self { 367 | Self { 368 | items: vec![], 369 | defaults: vec![], 370 | clear: true, 371 | prompt: None, 372 | report: true, 373 | max_length: None, 374 | theme, 375 | } 376 | } 377 | } 378 | 379 | #[cfg(test)] 380 | mod tests { 381 | use super::*; 382 | 383 | #[test] 384 | fn test_clone() { 385 | let multi_select = MultiSelect::new().with_prompt("Select your favorite(s)"); 386 | 387 | let _ = multi_select.clone(); 388 | } 389 | 390 | #[test] 391 | fn test_iterator() { 392 | let items = ["First", "Second", "Third"]; 393 | let iterator = items.iter().skip(1); 394 | 395 | assert_eq!(MultiSelect::new().items(iterator).items, &items[1..]); 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /src/theme/colorful.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use console::{style, Style, StyledObject}; 4 | #[cfg(feature = "fuzzy-select")] 5 | use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; 6 | 7 | use crate::theme::Theme; 8 | 9 | /// A colorful theme 10 | pub struct ColorfulTheme { 11 | /// The style for default values 12 | pub defaults_style: Style, 13 | /// The style for prompt 14 | pub prompt_style: Style, 15 | /// Prompt prefix value and style 16 | pub prompt_prefix: StyledObject, 17 | /// Prompt suffix value and style 18 | pub prompt_suffix: StyledObject, 19 | /// Prompt on success prefix value and style 20 | pub success_prefix: StyledObject, 21 | /// Prompt on success suffix value and style 22 | pub success_suffix: StyledObject, 23 | /// Error prefix value and style 24 | pub error_prefix: StyledObject, 25 | /// The style for error message 26 | pub error_style: Style, 27 | /// The style for hints 28 | pub hint_style: Style, 29 | /// The style for values on prompt success 30 | pub values_style: Style, 31 | /// The style for active items 32 | pub active_item_style: Style, 33 | /// The style for inactive items 34 | pub inactive_item_style: Style, 35 | /// Active item in select prefix value and style 36 | pub active_item_prefix: StyledObject, 37 | /// Inctive item in select prefix value and style 38 | pub inactive_item_prefix: StyledObject, 39 | /// Checked item in multi select prefix value and style 40 | pub checked_item_prefix: StyledObject, 41 | /// Unchecked item in multi select prefix value and style 42 | pub unchecked_item_prefix: StyledObject, 43 | /// Picked item in sort prefix value and style 44 | pub picked_item_prefix: StyledObject, 45 | /// Unpicked item in sort prefix value and style 46 | pub unpicked_item_prefix: StyledObject, 47 | /// Formats the cursor for a fuzzy select prompt 48 | #[cfg(feature = "fuzzy-select")] 49 | pub fuzzy_cursor_style: Style, 50 | // Formats the highlighting if matched characters 51 | #[cfg(feature = "fuzzy-select")] 52 | pub fuzzy_match_highlight_style: Style, 53 | } 54 | 55 | impl Default for ColorfulTheme { 56 | fn default() -> ColorfulTheme { 57 | ColorfulTheme { 58 | defaults_style: Style::new().for_stderr().cyan(), 59 | prompt_style: Style::new().for_stderr().bold(), 60 | prompt_prefix: style("?".to_string()).for_stderr().yellow(), 61 | prompt_suffix: style("›".to_string()).for_stderr().black().bright(), 62 | success_prefix: style("✔".to_string()).for_stderr().green(), 63 | success_suffix: style("·".to_string()).for_stderr().black().bright(), 64 | error_prefix: style("✘".to_string()).for_stderr().red(), 65 | error_style: Style::new().for_stderr().red(), 66 | hint_style: Style::new().for_stderr().black().bright(), 67 | values_style: Style::new().for_stderr().green(), 68 | active_item_style: Style::new().for_stderr().cyan(), 69 | inactive_item_style: Style::new().for_stderr(), 70 | active_item_prefix: style("❯".to_string()).for_stderr().green(), 71 | inactive_item_prefix: style(" ".to_string()).for_stderr(), 72 | checked_item_prefix: style("✔".to_string()).for_stderr().green(), 73 | unchecked_item_prefix: style("⬚".to_string()).for_stderr().magenta(), 74 | picked_item_prefix: style("❯".to_string()).for_stderr().green(), 75 | unpicked_item_prefix: style(" ".to_string()).for_stderr(), 76 | #[cfg(feature = "fuzzy-select")] 77 | fuzzy_cursor_style: Style::new().for_stderr().black().on_white(), 78 | #[cfg(feature = "fuzzy-select")] 79 | fuzzy_match_highlight_style: Style::new().for_stderr().bold(), 80 | } 81 | } 82 | } 83 | 84 | impl Theme for ColorfulTheme { 85 | /// Formats a prompt. 86 | fn format_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { 87 | if !prompt.is_empty() { 88 | write!( 89 | f, 90 | "{} {} ", 91 | &self.prompt_prefix, 92 | self.prompt_style.apply_to(prompt) 93 | )?; 94 | } 95 | 96 | write!(f, "{}", &self.prompt_suffix) 97 | } 98 | 99 | /// Formats an error 100 | fn format_error(&self, f: &mut dyn fmt::Write, err: &str) -> fmt::Result { 101 | write!( 102 | f, 103 | "{} {}", 104 | &self.error_prefix, 105 | self.error_style.apply_to(err) 106 | ) 107 | } 108 | 109 | /// Formats an input prompt. 110 | fn format_input_prompt( 111 | &self, 112 | f: &mut dyn fmt::Write, 113 | prompt: &str, 114 | default: Option<&str>, 115 | ) -> fmt::Result { 116 | if !prompt.is_empty() { 117 | write!( 118 | f, 119 | "{} {} ", 120 | &self.prompt_prefix, 121 | self.prompt_style.apply_to(prompt) 122 | )?; 123 | } 124 | 125 | match default { 126 | Some(default) => write!( 127 | f, 128 | "{} {} ", 129 | self.hint_style.apply_to(&format!("({})", default)), 130 | &self.prompt_suffix 131 | ), 132 | None => write!(f, "{} ", &self.prompt_suffix), 133 | } 134 | } 135 | 136 | /// Formats a confirm prompt. 137 | fn format_confirm_prompt( 138 | &self, 139 | f: &mut dyn fmt::Write, 140 | prompt: &str, 141 | default: Option, 142 | ) -> fmt::Result { 143 | if !prompt.is_empty() { 144 | write!( 145 | f, 146 | "{} {} ", 147 | &self.prompt_prefix, 148 | self.prompt_style.apply_to(prompt) 149 | )?; 150 | } 151 | 152 | match default { 153 | None => write!( 154 | f, 155 | "{} {}", 156 | self.hint_style.apply_to("(y/n)"), 157 | &self.prompt_suffix 158 | ), 159 | Some(true) => write!( 160 | f, 161 | "{} {} {}", 162 | self.hint_style.apply_to("(y/n)"), 163 | &self.prompt_suffix, 164 | self.defaults_style.apply_to("yes") 165 | ), 166 | Some(false) => write!( 167 | f, 168 | "{} {} {}", 169 | self.hint_style.apply_to("(y/n)"), 170 | &self.prompt_suffix, 171 | self.defaults_style.apply_to("no") 172 | ), 173 | } 174 | } 175 | 176 | /// Formats a confirm prompt after selection. 177 | fn format_confirm_prompt_selection( 178 | &self, 179 | f: &mut dyn fmt::Write, 180 | prompt: &str, 181 | selection: Option, 182 | ) -> fmt::Result { 183 | if !prompt.is_empty() { 184 | write!( 185 | f, 186 | "{} {} ", 187 | &self.success_prefix, 188 | self.prompt_style.apply_to(prompt) 189 | )?; 190 | } 191 | let selection = selection.map(|b| if b { "yes" } else { "no" }); 192 | 193 | match selection { 194 | Some(selection) => { 195 | write!( 196 | f, 197 | "{} {}", 198 | &self.success_suffix, 199 | self.values_style.apply_to(selection) 200 | ) 201 | } 202 | None => { 203 | write!(f, "{}", &self.success_suffix) 204 | } 205 | } 206 | } 207 | 208 | /// Formats an input prompt after selection. 209 | fn format_input_prompt_selection( 210 | &self, 211 | f: &mut dyn fmt::Write, 212 | prompt: &str, 213 | sel: &str, 214 | ) -> fmt::Result { 215 | if !prompt.is_empty() { 216 | write!( 217 | f, 218 | "{} {} ", 219 | &self.success_prefix, 220 | self.prompt_style.apply_to(prompt) 221 | )?; 222 | } 223 | 224 | write!( 225 | f, 226 | "{} {}", 227 | &self.success_suffix, 228 | self.values_style.apply_to(sel) 229 | ) 230 | } 231 | 232 | /// Formats a password prompt after selection. 233 | #[cfg(feature = "password")] 234 | fn format_password_prompt_selection( 235 | &self, 236 | f: &mut dyn fmt::Write, 237 | prompt: &str, 238 | ) -> fmt::Result { 239 | self.format_input_prompt_selection(f, prompt, "********") 240 | } 241 | 242 | /// Formats a multi select prompt after selection. 243 | fn format_multi_select_prompt_selection( 244 | &self, 245 | f: &mut dyn fmt::Write, 246 | prompt: &str, 247 | selections: &[&str], 248 | ) -> fmt::Result { 249 | if !prompt.is_empty() { 250 | write!( 251 | f, 252 | "{} {} ", 253 | &self.success_prefix, 254 | self.prompt_style.apply_to(prompt) 255 | )?; 256 | } 257 | 258 | write!(f, "{} ", &self.success_suffix)?; 259 | 260 | for (idx, sel) in selections.iter().enumerate() { 261 | write!( 262 | f, 263 | "{}{}", 264 | if idx == 0 { "" } else { ", " }, 265 | self.values_style.apply_to(sel) 266 | )?; 267 | } 268 | 269 | Ok(()) 270 | } 271 | 272 | /// Formats a select prompt item. 273 | fn format_select_prompt_item( 274 | &self, 275 | f: &mut dyn fmt::Write, 276 | text: &str, 277 | active: bool, 278 | ) -> fmt::Result { 279 | let details = if active { 280 | ( 281 | &self.active_item_prefix, 282 | self.active_item_style.apply_to(text), 283 | ) 284 | } else { 285 | ( 286 | &self.inactive_item_prefix, 287 | self.inactive_item_style.apply_to(text), 288 | ) 289 | }; 290 | 291 | write!(f, "{} {}", details.0, details.1) 292 | } 293 | 294 | /// Formats a multi select prompt item. 295 | fn format_multi_select_prompt_item( 296 | &self, 297 | f: &mut dyn fmt::Write, 298 | text: &str, 299 | checked: bool, 300 | active: bool, 301 | ) -> fmt::Result { 302 | let details = match (checked, active) { 303 | (true, true) => ( 304 | &self.checked_item_prefix, 305 | self.active_item_style.apply_to(text), 306 | ), 307 | (true, false) => ( 308 | &self.checked_item_prefix, 309 | self.inactive_item_style.apply_to(text), 310 | ), 311 | (false, true) => ( 312 | &self.unchecked_item_prefix, 313 | self.active_item_style.apply_to(text), 314 | ), 315 | (false, false) => ( 316 | &self.unchecked_item_prefix, 317 | self.inactive_item_style.apply_to(text), 318 | ), 319 | }; 320 | 321 | write!(f, "{} {}", details.0, details.1) 322 | } 323 | 324 | /// Formats a sort prompt item. 325 | fn format_sort_prompt_item( 326 | &self, 327 | f: &mut dyn fmt::Write, 328 | text: &str, 329 | picked: bool, 330 | active: bool, 331 | ) -> fmt::Result { 332 | let details = match (picked, active) { 333 | (true, true) => ( 334 | &self.picked_item_prefix, 335 | self.active_item_style.apply_to(text), 336 | ), 337 | (false, true) => ( 338 | &self.unpicked_item_prefix, 339 | self.active_item_style.apply_to(text), 340 | ), 341 | (_, false) => ( 342 | &self.unpicked_item_prefix, 343 | self.inactive_item_style.apply_to(text), 344 | ), 345 | }; 346 | 347 | write!(f, "{} {}", details.0, details.1) 348 | } 349 | 350 | /// Formats a fuzzy select prompt item. 351 | #[cfg(feature = "fuzzy-select")] 352 | fn format_fuzzy_select_prompt_item( 353 | &self, 354 | f: &mut dyn fmt::Write, 355 | text: &str, 356 | active: bool, 357 | highlight_matches: bool, 358 | matcher: &SkimMatcherV2, 359 | search_term: &str, 360 | ) -> fmt::Result { 361 | write!( 362 | f, 363 | "{} ", 364 | if active { 365 | &self.active_item_prefix 366 | } else { 367 | &self.inactive_item_prefix 368 | } 369 | )?; 370 | 371 | if highlight_matches { 372 | if let Some((_score, indices)) = matcher.fuzzy_indices(text, search_term) { 373 | for (idx, c) in text.chars().enumerate() { 374 | if indices.contains(&idx) { 375 | if active { 376 | write!( 377 | f, 378 | "{}", 379 | self.active_item_style 380 | .apply_to(self.fuzzy_match_highlight_style.apply_to(c)) 381 | )?; 382 | } else { 383 | write!(f, "{}", self.fuzzy_match_highlight_style.apply_to(c))?; 384 | } 385 | } else if active { 386 | write!(f, "{}", self.active_item_style.apply_to(c))?; 387 | } else { 388 | write!(f, "{}", c)?; 389 | } 390 | } 391 | 392 | return Ok(()); 393 | } 394 | } 395 | 396 | if active { 397 | write!(f, "{}", self.active_item_style.apply_to(text)) 398 | } else { 399 | write!(f, "{}", text) 400 | } 401 | } 402 | 403 | /// Formats a fuzzy-selectprompt after selection. 404 | #[cfg(feature = "fuzzy-select")] 405 | fn format_fuzzy_select_prompt( 406 | &self, 407 | f: &mut dyn fmt::Write, 408 | prompt: &str, 409 | search_term: &str, 410 | bytes_pos: usize, 411 | ) -> fmt::Result { 412 | if !prompt.is_empty() { 413 | write!( 414 | f, 415 | "{} {} ", 416 | self.prompt_prefix, 417 | self.prompt_style.apply_to(prompt) 418 | )?; 419 | } 420 | 421 | let (st_head, remaining) = search_term.split_at(bytes_pos); 422 | let mut chars = remaining.chars(); 423 | let chr = chars.next().unwrap_or(' '); 424 | let st_cursor = self.fuzzy_cursor_style.apply_to(chr); 425 | let st_tail = chars.as_str(); 426 | 427 | let prompt_suffix = &self.prompt_suffix; 428 | write!(f, "{prompt_suffix} {st_head}{st_cursor}{st_tail}",) 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /src/prompts/fuzzy_select.rs: -------------------------------------------------------------------------------- 1 | use std::{io, ops::Rem}; 2 | 3 | use console::{Key, Term}; 4 | use fuzzy_matcher::FuzzyMatcher; 5 | 6 | use crate::{ 7 | theme::{render::TermThemeRenderer, SimpleTheme, Theme}, 8 | Result, 9 | }; 10 | 11 | /// Renders a select prompt with fuzzy search. 12 | /// 13 | /// User can use fuzzy search to limit selectable items. 14 | /// Interaction returns index of an item selected in the order they appear in `item` invocation or `items` slice. 15 | /// 16 | /// ## Example 17 | /// 18 | /// ```rust,no_run 19 | /// use dialoguer::FuzzySelect; 20 | /// 21 | /// fn main() { 22 | /// let items = vec!["foo", "bar", "baz"]; 23 | /// 24 | /// let selection = FuzzySelect::new() 25 | /// .with_prompt("What do you choose?") 26 | /// .items(&items) 27 | /// .interact() 28 | /// .unwrap(); 29 | /// 30 | /// println!("You chose: {}", items[selection]); 31 | /// } 32 | /// ``` 33 | #[derive(Clone)] 34 | pub struct FuzzySelect<'a> { 35 | default: Option, 36 | items: Vec, 37 | prompt: String, 38 | report: bool, 39 | clear: bool, 40 | highlight_matches: bool, 41 | enable_vim_mode: bool, 42 | max_length: Option, 43 | theme: &'a dyn Theme, 44 | /// Search string that a fuzzy search with start with. 45 | /// Defaults to an empty string. 46 | initial_text: String, 47 | } 48 | 49 | impl Default for FuzzySelect<'static> { 50 | fn default() -> Self { 51 | Self::new() 52 | } 53 | } 54 | 55 | impl FuzzySelect<'static> { 56 | /// Creates a fuzzy select prompt with default theme. 57 | pub fn new() -> Self { 58 | Self::with_theme(&SimpleTheme) 59 | } 60 | } 61 | 62 | impl FuzzySelect<'_> { 63 | /// Sets the clear behavior of the menu. 64 | /// 65 | /// The default is to clear the menu. 66 | pub fn clear(mut self, val: bool) -> Self { 67 | self.clear = val; 68 | self 69 | } 70 | 71 | /// Sets a default for the menu 72 | pub fn default(mut self, val: usize) -> Self { 73 | self.default = Some(val); 74 | self 75 | } 76 | 77 | /// Add a single item to the fuzzy selector. 78 | pub fn item(mut self, item: T) -> Self { 79 | self.items.push(item.to_string()); 80 | self 81 | } 82 | 83 | /// Adds multiple items to the fuzzy selector. 84 | pub fn items(mut self, items: I) -> Self 85 | where 86 | T: ToString, 87 | I: IntoIterator, 88 | { 89 | self.items 90 | .extend(items.into_iter().map(|item| item.to_string())); 91 | 92 | self 93 | } 94 | 95 | /// Sets the search text that a fuzzy search starts with. 96 | pub fn with_initial_text>(mut self, initial_text: S) -> Self { 97 | self.initial_text = initial_text.into(); 98 | self 99 | } 100 | 101 | /// Prefaces the menu with a prompt. 102 | /// 103 | /// When a prompt is set the system also prints out a confirmation after 104 | /// the fuzzy selection. 105 | pub fn with_prompt>(mut self, prompt: S) -> Self { 106 | self.prompt = prompt.into(); 107 | self 108 | } 109 | 110 | /// Indicates whether to report the selected value after interaction. 111 | /// 112 | /// The default is to report the selection. 113 | pub fn report(mut self, val: bool) -> Self { 114 | self.report = val; 115 | self 116 | } 117 | 118 | /// Indicates whether to highlight matched indices 119 | /// 120 | /// The default is to highlight the indices 121 | pub fn highlight_matches(mut self, val: bool) -> Self { 122 | self.highlight_matches = val; 123 | self 124 | } 125 | 126 | /// Indicated whether to allow the use of vim mode 127 | /// 128 | /// Vim mode can be entered by pressing Escape. 129 | /// This then allows the user to navigate using hjkl. 130 | /// 131 | /// The default is to disable vim mode. 132 | pub fn vim_mode(mut self, val: bool) -> Self { 133 | self.enable_vim_mode = val; 134 | self 135 | } 136 | 137 | /// Sets the maximum number of visible options. 138 | /// 139 | /// The default is the height of the terminal minus 2. 140 | pub fn max_length(mut self, rows: usize) -> Self { 141 | self.max_length = Some(rows); 142 | self 143 | } 144 | 145 | /// Enables user interaction and returns the result. 146 | /// 147 | /// The user can select the items using 'Enter' and the index of selected item will be returned. 148 | /// The dialog is rendered on stderr. 149 | /// Result contains `index` of selected item if user hit 'Enter'. 150 | /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'. 151 | #[inline] 152 | pub fn interact(self) -> Result { 153 | self.interact_on(&Term::stderr()) 154 | } 155 | 156 | /// Enables user interaction and returns the result. 157 | /// 158 | /// The user can select the items using 'Enter' and the index of selected item will be returned. 159 | /// The dialog is rendered on stderr. 160 | /// Result contains `Some(index)` if user hit 'Enter' or `None` if user cancelled with 'Esc' or 'q'. 161 | /// 162 | /// ## Example 163 | /// 164 | /// ```rust,no_run 165 | /// use dialoguer::FuzzySelect; 166 | /// 167 | /// fn main() { 168 | /// let items = vec!["foo", "bar", "baz"]; 169 | /// 170 | /// let selection = FuzzySelect::new() 171 | /// .items(&items) 172 | /// .interact_opt() 173 | /// .unwrap(); 174 | /// 175 | /// match selection { 176 | /// Some(index) => println!("You chose: {}", items[index]), 177 | /// None => println!("You did not choose anything.") 178 | /// } 179 | /// } 180 | /// ``` 181 | #[inline] 182 | pub fn interact_opt(self) -> Result> { 183 | self.interact_on_opt(&Term::stderr()) 184 | } 185 | 186 | /// Like [`interact`](Self::interact) but allows a specific terminal to be set. 187 | #[inline] 188 | pub fn interact_on(self, term: &Term) -> Result { 189 | Ok(self 190 | ._interact_on(term, false)? 191 | .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))?) 192 | } 193 | 194 | /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set. 195 | #[inline] 196 | pub fn interact_on_opt(self, term: &Term) -> Result> { 197 | self._interact_on(term, true) 198 | } 199 | 200 | fn _interact_on(self, term: &Term, allow_quit: bool) -> Result> { 201 | // Place cursor at the end of the search term 202 | let mut cursor = self.initial_text.chars().count(); 203 | let mut search_term = self.initial_text.to_owned(); 204 | 205 | let mut render = TermThemeRenderer::new(term, self.theme); 206 | let mut sel = self.default; 207 | 208 | let mut size_vec = Vec::new(); 209 | for items in self.items.iter().as_slice() { 210 | let size = &items.len(); 211 | size_vec.push(*size); 212 | } 213 | 214 | // Fuzzy matcher 215 | let matcher = fuzzy_matcher::skim::SkimMatcherV2::default(); 216 | 217 | // Subtract -2 because we need space to render the prompt. 218 | let visible_term_rows = (term.size().0 as usize).max(3) - 2; 219 | let visible_term_rows = self 220 | .max_length 221 | .unwrap_or(visible_term_rows) 222 | .min(visible_term_rows); 223 | // Variable used to determine if we need to scroll through the list. 224 | let mut starting_row = 0; 225 | 226 | term.hide_cursor()?; 227 | 228 | let mut vim_mode = false; 229 | 230 | loop { 231 | let mut byte_indices = search_term 232 | .char_indices() 233 | .map(|(index, _)| index) 234 | .collect::>(); 235 | 236 | byte_indices.push(search_term.len()); 237 | 238 | render.clear()?; 239 | render.fuzzy_select_prompt(self.prompt.as_str(), &search_term, byte_indices[cursor])?; 240 | 241 | // Maps all items to a tuple of item and its match score. 242 | let mut filtered_list = self 243 | .items 244 | .iter() 245 | .map(|item| (item, matcher.fuzzy_match(item, &search_term))) 246 | .filter_map(|(item, score)| score.map(|s| (item, s))) 247 | .collect::>(); 248 | 249 | // Renders all matching items, from best match to worst. 250 | filtered_list.sort_unstable_by(|(_, s1), (_, s2)| s2.cmp(s1)); 251 | 252 | for (idx, (item, _)) in filtered_list 253 | .iter() 254 | .enumerate() 255 | .skip(starting_row) 256 | .take(visible_term_rows) 257 | { 258 | render.fuzzy_select_prompt_item( 259 | item, 260 | Some(idx) == sel, 261 | self.highlight_matches, 262 | &matcher, 263 | &search_term, 264 | )?; 265 | } 266 | term.flush()?; 267 | 268 | match (term.read_key()?, sel, vim_mode) { 269 | (Key::Escape, _, false) if self.enable_vim_mode => { 270 | vim_mode = true; 271 | } 272 | (Key::Escape, _, false) | (Key::Char('q'), _, true) if allow_quit => { 273 | if self.clear { 274 | render.clear()?; 275 | term.flush()?; 276 | } 277 | term.show_cursor()?; 278 | return Ok(None); 279 | } 280 | (Key::Char('i' | 'a'), _, true) => { 281 | vim_mode = false; 282 | } 283 | (Key::ArrowUp | Key::BackTab, _, _) | (Key::Char('k'), _, true) 284 | if !filtered_list.is_empty() => 285 | { 286 | if sel == Some(0) { 287 | starting_row = 288 | filtered_list.len().max(visible_term_rows) - visible_term_rows; 289 | } else if sel == Some(starting_row) { 290 | starting_row -= 1; 291 | } 292 | sel = match sel { 293 | None => Some(filtered_list.len() - 1), 294 | Some(sel) => Some( 295 | ((sel as i64 - 1 + filtered_list.len() as i64) 296 | % (filtered_list.len() as i64)) 297 | as usize, 298 | ), 299 | }; 300 | term.flush()?; 301 | } 302 | (Key::ArrowDown | Key::Tab, _, _) | (Key::Char('j'), _, true) 303 | if !filtered_list.is_empty() => 304 | { 305 | sel = match sel { 306 | None => Some(0), 307 | Some(sel) => { 308 | Some((sel as u64 + 1).rem(filtered_list.len() as u64) as usize) 309 | } 310 | }; 311 | if sel == Some(visible_term_rows + starting_row) { 312 | starting_row += 1; 313 | } else if sel == Some(0) { 314 | starting_row = 0; 315 | } 316 | term.flush()?; 317 | } 318 | (Key::ArrowLeft, _, _) | (Key::Char('h'), _, true) if cursor > 0 => { 319 | cursor -= 1; 320 | term.flush()?; 321 | } 322 | (Key::ArrowRight, _, _) | (Key::Char('l'), _, true) 323 | if cursor < byte_indices.len() - 1 => 324 | { 325 | cursor += 1; 326 | term.flush()?; 327 | } 328 | (Key::Enter, Some(sel), _) if !filtered_list.is_empty() => { 329 | if self.clear { 330 | render.clear()?; 331 | } 332 | 333 | if self.report { 334 | render 335 | .input_prompt_selection(self.prompt.as_str(), filtered_list[sel].0)?; 336 | } 337 | 338 | let sel_string = filtered_list[sel].0; 339 | let sel_string_pos_in_items = 340 | self.items.iter().position(|item| item.eq(sel_string)); 341 | 342 | term.show_cursor()?; 343 | return Ok(sel_string_pos_in_items); 344 | } 345 | (Key::Backspace, _, _) if cursor > 0 => { 346 | cursor -= 1; 347 | search_term.remove(byte_indices[cursor]); 348 | term.flush()?; 349 | } 350 | (Key::Del, _, _) if cursor < byte_indices.len() - 1 => { 351 | search_term.remove(byte_indices[cursor]); 352 | term.flush()?; 353 | } 354 | (Key::Char(chr), _, _) if !chr.is_ascii_control() => { 355 | search_term.insert(byte_indices[cursor], chr); 356 | cursor += 1; 357 | term.flush()?; 358 | sel = Some(0); 359 | starting_row = 0; 360 | } 361 | 362 | _ => {} 363 | } 364 | 365 | render.clear_preserve_prompt(&size_vec)?; 366 | } 367 | } 368 | } 369 | 370 | impl<'a> FuzzySelect<'a> { 371 | /// Creates a fuzzy select prompt with a specific theme. 372 | /// 373 | /// ## Example 374 | /// 375 | /// ```rust,no_run 376 | /// use dialoguer::{theme::ColorfulTheme, FuzzySelect}; 377 | /// 378 | /// fn main() { 379 | /// let selection = FuzzySelect::with_theme(&ColorfulTheme::default()) 380 | /// .items(&["foo", "bar", "baz"]) 381 | /// .interact() 382 | /// .unwrap(); 383 | /// } 384 | /// ``` 385 | pub fn with_theme(theme: &'a dyn Theme) -> Self { 386 | Self { 387 | default: None, 388 | items: vec![], 389 | prompt: "".into(), 390 | report: true, 391 | clear: true, 392 | highlight_matches: true, 393 | enable_vim_mode: false, 394 | max_length: None, 395 | theme, 396 | initial_text: "".into(), 397 | } 398 | } 399 | } 400 | 401 | #[cfg(test)] 402 | mod tests { 403 | use super::*; 404 | 405 | #[test] 406 | fn test_clone() { 407 | let fuzzy_select = FuzzySelect::new().with_prompt("Do you want to continue?"); 408 | 409 | let _ = fuzzy_select.clone(); 410 | } 411 | 412 | #[test] 413 | fn test_iterator() { 414 | let items = ["First", "Second", "Third"]; 415 | let iterator = items.iter().skip(1); 416 | 417 | assert_eq!(FuzzySelect::new().items(iterator).items, &items[1..]); 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /src/prompts/input.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp::Ordering, 3 | io, iter, 4 | str::FromStr, 5 | sync::{Arc, Mutex}, 6 | }; 7 | 8 | use console::{Key, Term}; 9 | 10 | #[cfg(feature = "completion")] 11 | use crate::completion::Completion; 12 | #[cfg(feature = "history")] 13 | use crate::history::History; 14 | use crate::{ 15 | theme::{render::TermThemeRenderer, SimpleTheme, Theme}, 16 | validate::InputValidator, 17 | Result, 18 | }; 19 | 20 | type InputValidatorCallback<'a, T> = Arc Option + '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::::new() 46 | /// .with_prompt("Your name?") 47 | /// .interact_text() 48 | /// .unwrap(); 49 | /// 50 | /// println!("Your name is: {}", name); 51 | /// } 52 | /// ``` 53 | #[derive(Clone)] 54 | pub struct Input<'a, T> { 55 | prompt: String, 56 | post_completion_text: Option, 57 | report: bool, 58 | default: Option, 59 | show_default: bool, 60 | initial_text: Option, 61 | theme: &'a dyn Theme, 62 | permit_empty: bool, 63 | validator: Option>, 64 | #[cfg(feature = "history")] 65 | history: Option>>>, 66 | #[cfg(feature = "completion")] 67 | completion: Option<&'a dyn Completion>, 68 | } 69 | 70 | impl Default for Input<'static, T> { 71 | fn default() -> Self { 72 | Self::new() 73 | } 74 | } 75 | 76 | impl 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>(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>(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>(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 | 138 | impl<'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, 180 | /// } 181 | /// 182 | /// impl Default for MyHistory { 183 | /// fn default() -> Self { 184 | /// MyHistory { 185 | /// history: VecDeque::new(), 186 | /// } 187 | /// } 188 | /// } 189 | /// 190 | /// impl History for MyHistory { 191 | /// fn read(&self, pos: usize) -> Option { 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::::new() 206 | /// .history_with(&mut history) 207 | /// .interact_text() 208 | /// .unwrap(); 209 | /// } 210 | /// ``` 211 | #[cfg(feature = "history")] 212 | pub fn history_with(mut self, history: &'a mut H) -> Self 213 | where 214 | H: History, 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(mut self, completion: &'a C) -> Self 223 | where 224 | C: Completion, 225 | { 226 | self.completion = Some(completion); 227 | self 228 | } 229 | } 230 | 231 | impl<'a, T> Input<'a, T> 232 | where 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(mut self, mut validator: V) -> Self 257 | where 258 | V: InputValidator + '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 { 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 | 280 | impl Input<'_, T> 281 | where 282 | T: Clone + ToString + FromStr, 283 | ::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 { 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 { 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 = 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::(); 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::() { 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 { 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 { 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::() { 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)] 715 | mod tests { 716 | use super::*; 717 | 718 | #[test] 719 | fn test_clone() { 720 | let input = Input::::new().with_prompt("Your name"); 721 | 722 | let _ = input.clone(); 723 | } 724 | } 725 | --------------------------------------------------------------------------------