diff --git a/CHANGELOG.md b/CHANGELOG.md index a54d3a90..b14f07f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - **Breaking**. Allow lifetime customization of RenderConfig. [#101](https://github.com/mikaelmello/inquire/pull/101). Thanks to @arturfast for the suggestion [#95](https://github.com/mikaelmello/inquire/issues/95). - Add new option on MultiSelect prompts to set all options to be selected by default. Thanks to @conikeec for the suggestion (#151)! - **Breaking**. Improved user experience on Password prompts. When there is a validation error, the input is cleared if the password is rendered using the `Hidden` display mode, matching the user expectation of having to write the password from scratch again. Thanks to @CM-IV for the questions on #149! +- **Breaking**. Help messages are now defined through a new type, `HelpMessage`, containing three variants: `None`, `Default` and `Custom(String)`. This allows better default help messages tailored to the customizations of each prompt, e.g. hint `esc` to leave the prompt only when applicable. Thanks to @dariocurr for the request on #143! #161. - Add strict clippy lints to improve code consistency and readability. - Expand workflow clippy task to lint all-features in workspace. - Add docs badge to readme. diff --git a/inquire/examples/confirm.rs b/inquire/examples/confirm.rs index 1967b6ea..c1ab8f3a 100644 --- a/inquire/examples/confirm.rs +++ b/inquire/examples/confirm.rs @@ -19,7 +19,7 @@ fn main() { message: "Are you happy?", default: Some(false), placeholder: Some("si|no"), - help_message: Some("It's alright if you're not"), + help_message: "It's alright if you're not".into(), formatter: &|ans| match ans { true => "si".to_owned(), false => "no".to_owned(), diff --git a/inquire/examples/text_options.rs b/inquire/examples/text_options.rs index 28fd3006..63b44c34 100644 --- a/inquire/examples/text_options.rs +++ b/inquire/examples/text_options.rs @@ -15,7 +15,7 @@ fn main() { initial_value: None, default: None, placeholder: Some("Good"), - help_message: None, + help_message: None.into(), formatter: Text::DEFAULT_FORMATTER, validators: Vec::new(), page_size: Text::DEFAULT_PAGE_SIZE, diff --git a/inquire/src/autocompletion.rs b/inquire/src/autocompletion.rs index 513c0dc4..0e458990 100644 --- a/inquire/src/autocompletion.rs +++ b/inquire/src/autocompletion.rs @@ -66,8 +66,12 @@ impl Clone for Box { /// Empty struct and implementation of Autocomplete trait. Used for the default /// autocompleter of `Text` prompts. #[derive(Clone, Default)] +#[deprecated( + note = "All applicable APIs accept a Option, so use None instead. This struct will be removed in a future release." +)] pub struct NoAutoCompletion; +#[allow(deprecated)] impl Autocomplete for NoAutoCompletion { fn get_suggestions(&mut self, _: &str) -> Result, CustomUserError> { Ok(vec![]) diff --git a/inquire/src/prompts/confirm/mod.rs b/inquire/src/prompts/confirm/mod.rs index 8eea0142..401decf6 100644 --- a/inquire/src/prompts/confirm/mod.rs +++ b/inquire/src/prompts/confirm/mod.rs @@ -8,7 +8,7 @@ use crate::{ formatter::{BoolFormatter, DEFAULT_BOOL_FORMATTER}, parser::{BoolParser, DEFAULT_BOOL_PARSER}, terminal::{get_default_terminal, Terminal}, - ui::{Backend, RenderConfig}, + ui::{Backend, HelpMessage, RenderConfig}, CustomType, }; @@ -68,7 +68,7 @@ pub struct Confirm<'a> { pub placeholder: Option<&'a str>, /// Help message to be presented to the user. - pub help_message: Option<&'a str>, + pub help_message: HelpMessage, /// Function that formats the user input and presents it to the user as the final rendering of the prompt. pub formatter: BoolFormatter<'a>, @@ -117,7 +117,7 @@ impl<'a> Confirm<'a> { message, default: None, placeholder: None, - help_message: None, + help_message: HelpMessage::default(), formatter: Self::DEFAULT_FORMATTER, parser: Self::DEFAULT_PARSER, default_value_formatter: Self::DEFAULT_DEFAULT_VALUE_FORMATTER, @@ -139,8 +139,14 @@ impl<'a> Confirm<'a> { } /// Sets the help message of the prompt. - pub fn with_help_message(mut self, message: &'a str) -> Self { - self.help_message = Some(message); + pub fn with_help_message(mut self, message: &str) -> Self { + self.help_message = message.into(); + self + } + + /// Sets the prompt to not display a help message. + pub fn without_help_message(mut self) -> Self { + self.help_message = HelpMessage::None; self } diff --git a/inquire/src/prompts/custom_type/mod.rs b/inquire/src/prompts/custom_type/mod.rs index c80dac90..01bd7aa9 100644 --- a/inquire/src/prompts/custom_type/mod.rs +++ b/inquire/src/prompts/custom_type/mod.rs @@ -13,7 +13,7 @@ use crate::{ parser::CustomTypeParser, prompts::prompt::Prompt, terminal::get_default_terminal, - ui::{Backend, CustomTypeBackend, RenderConfig}, + ui::{Backend, CustomTypeBackend, HelpMessage, RenderConfig}, validator::CustomTypeValidator, }; @@ -87,7 +87,7 @@ pub struct CustomType<'a, T> { pub placeholder: Option<&'a str>, /// Help message to be presented to the user. - pub help_message: Option<&'a str>, + pub help_message: HelpMessage, /// Function that formats the user input and presents it to the user as the final rendering of the prompt. pub formatter: CustomTypeFormatter<'a, T>, @@ -136,7 +136,7 @@ where message, default: None, placeholder: None, - help_message: None, + help_message: HelpMessage::default(), formatter: &|val| val.to_string(), default_value_formatter: &|val| val.to_string(), parser: &|a| a.parse::().map_err(|_e| ()), @@ -159,8 +159,14 @@ where } /// Sets the help message of the prompt. - pub fn with_help_message(mut self, message: &'a str) -> Self { - self.help_message = Some(message); + pub fn with_help_message(mut self, message: &str) -> Self { + self.help_message = message.into(); + self + } + + /// Sets the prompt to not display a help message. + pub fn without_help_message(mut self) -> Self { + self.help_message = HelpMessage::None; self } diff --git a/inquire/src/prompts/custom_type/prompt.rs b/inquire/src/prompts/custom_type/prompt.rs index d2f93bf9..56312047 100644 --- a/inquire/src/prompts/custom_type/prompt.rs +++ b/inquire/src/prompts/custom_type/prompt.rs @@ -15,7 +15,7 @@ pub struct CustomTypePrompt<'a, T> { message: &'a str, config: CustomTypeConfig, error: Option, - help_message: Option<&'a str>, + help_message: Option, default: Option, input: Input, formatter: CustomTypeFormatter<'a, T>, @@ -35,7 +35,7 @@ where config: (&co).into(), error: None, default: co.default, - help_message: co.help_message, + help_message: co.help_message.into_or_default(None), formatter: co.formatter, default_value_formatter: co.default_value_formatter, validators: co.validators, @@ -78,7 +78,8 @@ where } } -impl<'a, B, T> Prompt for CustomTypePrompt<'a, T> +impl<'a, B, T> Prompt<'a, B, CustomTypeConfig, CustomTypePromptAction, T> + for CustomTypePrompt<'a, T> where B: CustomTypeBackend, T: Clone, @@ -87,6 +88,10 @@ where self.message } + fn help_message(&self) -> Option<&str> { + self.help_message.as_deref() + } + fn config(&self) -> &CustomTypeConfig { &self.config } @@ -138,10 +143,6 @@ where backend.render_prompt(prompt, default_message.as_deref(), &self.input)?; - if let Some(message) = self.help_message { - backend.render_help_message(message)?; - } - Ok(()) } } diff --git a/inquire/src/prompts/dateselect/mod.rs b/inquire/src/prompts/dateselect/mod.rs index 515b4457..bb681796 100644 --- a/inquire/src/prompts/dateselect/mod.rs +++ b/inquire/src/prompts/dateselect/mod.rs @@ -16,7 +16,7 @@ use crate::{ formatter::{self, DateFormatter}, prompts::prompt::Prompt, terminal::{get_default_terminal, Terminal}, - ui::{Backend, RenderConfig}, + ui::{Backend, HelpMessage, RenderConfig}, validator::DateValidator, }; @@ -82,7 +82,7 @@ pub struct DateSelect<'a> { pub max_date: Option, /// Help message to be presented to the user. - pub help_message: Option<&'a str>, + pub help_message: HelpMessage, /// Whether vim mode is enabled. When enabled, the user can /// navigate through the options using hjkl. @@ -117,10 +117,6 @@ impl<'a> DateSelect<'a> { /// Default value of vim mode. It is true because there is no typing functionality to be lost here. pub const DEFAULT_VIM_MODE: bool = true; - /// Default help message. - pub const DEFAULT_HELP_MESSAGE: Option<&'a str> = - Some("arrows to move, with ctrl to move months and years, enter to select"); - /// Default validators added to the [DateSelect] prompt, none. pub const DEFAULT_VALIDATORS: Vec> = vec![]; @@ -140,7 +136,7 @@ impl<'a> DateSelect<'a> { starting_date: get_current_date(), min_date: Self::DEFAULT_MIN_DATE, max_date: Self::DEFAULT_MAX_DATE, - help_message: Self::DEFAULT_HELP_MESSAGE, + help_message: HelpMessage::default(), vim_mode: Self::DEFAULT_VIM_MODE, formatter: Self::DEFAULT_FORMATTER, validators: Self::DEFAULT_VALIDATORS, @@ -150,14 +146,14 @@ impl<'a> DateSelect<'a> { } /// Sets the help message of the prompt. - pub fn with_help_message(mut self, message: &'a str) -> Self { - self.help_message = Some(message); + pub fn with_help_message(mut self, message: &str) -> Self { + self.help_message = message.into(); self } - /// Removes the set help message. + /// Sets the prompt to not display a help message. pub fn without_help_message(mut self) -> Self { - self.help_message = None; + self.help_message = HelpMessage::None; self } diff --git a/inquire/src/prompts/dateselect/prompt.rs b/inquire/src/prompts/dateselect/prompt.rs index 76874c4f..4c5aad83 100644 --- a/inquire/src/prompts/dateselect/prompt.rs +++ b/inquire/src/prompts/dateselect/prompt.rs @@ -21,7 +21,7 @@ pub struct DateSelectPrompt<'a> { message: &'a str, config: DateSelectConfig, current_date: NaiveDate, - help_message: Option<&'a str>, + help_message: Option, formatter: DateFormatter<'a>, validators: Vec>, error: Option, @@ -29,6 +29,7 @@ pub struct DateSelectPrompt<'a> { impl<'a> DateSelectPrompt<'a> { pub fn new(so: DateSelect<'a>) -> InquireResult { + let config = (&so).into(); if let Some(min_date) = so.min_date { if min_date > so.starting_date { return Err(InquireError::InvalidConfiguration( @@ -44,11 +45,16 @@ impl<'a> DateSelectPrompt<'a> { } } + let default_help_message = + Some("arrows to move, with ctrl to move months and years, enter to select"); + let help_message = so + .help_message + .into_or_default(default_help_message.map(|s| s.into())); Ok(Self { message: so.message, current_date: so.starting_date, - config: (&so).into(), - help_message: so.help_message, + config, + help_message, formatter: so.formatter, validators: so.validators, error: None, @@ -116,7 +122,8 @@ impl<'a> DateSelectPrompt<'a> { } } -impl<'a, B> Prompt for DateSelectPrompt<'a> +impl<'a, B> Prompt<'a, B, DateSelectConfig, DateSelectPromptAction, NaiveDate> + for DateSelectPrompt<'a> where B: DateSelectBackend, { @@ -124,6 +131,10 @@ where self.message } + fn help_message(&self) -> Option<&str> { + self.help_message.as_deref() + } + fn format_answer(&self, answer: &NaiveDate) -> String { (self.formatter)(*answer) } @@ -178,10 +189,6 @@ where self.config.max_date, )?; - if let Some(help_message) = self.help_message { - backend.render_help_message(help_message)?; - } - Ok(()) } } diff --git a/inquire/src/prompts/editor/mod.rs b/inquire/src/prompts/editor/mod.rs index 11f0a94f..a6bb71c0 100644 --- a/inquire/src/prompts/editor/mod.rs +++ b/inquire/src/prompts/editor/mod.rs @@ -16,7 +16,7 @@ use crate::{ formatter::StringFormatter, prompts::prompt::Prompt, terminal::get_default_terminal, - ui::{Backend, EditorBackend, RenderConfig}, + ui::{Backend, EditorBackend, HelpMessage, RenderConfig}, validator::StringValidator, }; @@ -66,7 +66,7 @@ pub struct Editor<'a> { pub predefined_text: Option<&'a str>, /// Help message to be presented to the user. - pub help_message: Option<&'a str>, + pub help_message: HelpMessage, /// Function that formats the user input and presents it to the user as the final rendering of the prompt. pub formatter: StringFormatter<'a>, @@ -97,9 +97,6 @@ impl<'a> Editor<'a> { /// Default validators added to the [Editor] prompt, none. pub const DEFAULT_VALIDATORS: Vec> = vec![]; - /// Default help message. - pub const DEFAULT_HELP_MESSAGE: Option<&'a str> = None; - /// Creates a [Editor] with the provided message and default options. pub fn new(message: &'a str) -> Self { Self { @@ -108,7 +105,7 @@ impl<'a> Editor<'a> { editor_command_args: &[], file_extension: ".txt", predefined_text: None, - help_message: Self::DEFAULT_HELP_MESSAGE, + help_message: HelpMessage::default(), validators: Self::DEFAULT_VALIDATORS, formatter: Self::DEFAULT_FORMATTER, render_config: RenderConfig::default(), @@ -116,8 +113,14 @@ impl<'a> Editor<'a> { } /// Sets the help message of the prompt. - pub fn with_help_message(mut self, message: &'a str) -> Self { - self.help_message = Some(message); + pub fn with_help_message(mut self, message: &str) -> Self { + self.help_message = message.into(); + self + } + + /// Sets the prompt to not display a help message. + pub fn without_help_message(mut self) -> Self { + self.help_message = HelpMessage::None; self } diff --git a/inquire/src/prompts/editor/prompt.rs b/inquire/src/prompts/editor/prompt.rs index d821b72b..5a315345 100644 --- a/inquire/src/prompts/editor/prompt.rs +++ b/inquire/src/prompts/editor/prompt.rs @@ -16,7 +16,7 @@ use super::{action::EditorPromptAction, config::EditorConfig}; pub struct EditorPrompt<'a> { message: &'a str, config: EditorConfig<'a>, - help_message: Option<&'a str>, + help_message: Option, formatter: StringFormatter<'a>, validators: Vec>, error: Option, @@ -34,7 +34,7 @@ impl<'a> EditorPrompt<'a> { Ok(Self { message: so.message, config: (&so).into(), - help_message: so.help_message, + help_message: so.help_message.into_or_default(None), formatter: so.formatter, validators: so.validators, error: None, @@ -92,7 +92,7 @@ impl<'a> EditorPrompt<'a> { } } -impl<'a, B> Prompt, EditorPromptAction, String> for EditorPrompt<'a> +impl<'a, B> Prompt<'a, B, EditorConfig<'a>, EditorPromptAction, String> for EditorPrompt<'a> where B: EditorBackend, { @@ -100,6 +100,10 @@ where self.message } + fn help_message(&self) -> Option<&str> { + self.help_message.as_deref() + } + fn config(&self) -> &EditorConfig<'a> { &self.config } @@ -144,10 +148,6 @@ where backend.render_prompt(prompt, editor_name)?; - if let Some(message) = self.help_message { - backend.render_help_message(message)?; - } - Ok(()) } } diff --git a/inquire/src/prompts/multiselect/mod.rs b/inquire/src/prompts/multiselect/mod.rs index 025ee52c..71c453cd 100644 --- a/inquire/src/prompts/multiselect/mod.rs +++ b/inquire/src/prompts/multiselect/mod.rs @@ -17,7 +17,7 @@ use crate::{ prompts::prompt::Prompt, terminal::get_default_terminal, type_aliases::Filter, - ui::{Backend, MultiSelectBackend, RenderConfig}, + ui::{Backend, HelpMessage, MultiSelectBackend, RenderConfig}, validator::MultiOptionValidator, }; @@ -65,7 +65,7 @@ pub struct MultiSelect<'a, T> { pub default: Option>, /// Help message to be presented to the user. - pub help_message: Option<&'a str>, + pub help_message: HelpMessage, /// Page size of the options displayed to the user. pub page_size: usize, @@ -175,17 +175,13 @@ where /// Default behavior of keeping or cleaning the current filter value. pub const DEFAULT_KEEP_FILTER: bool = true; - /// Default help message. - pub const DEFAULT_HELP_MESSAGE: Option<&'a str> = - Some("↑↓ to move, space to select one, → to all, ← to none, type to filter"); - /// Creates a [MultiSelect] with the provided message and options, along with default configuration values. pub fn new(message: &'a str, options: Vec) -> Self { Self { message, options, default: None, - help_message: Self::DEFAULT_HELP_MESSAGE, + help_message: HelpMessage::default(), page_size: Self::DEFAULT_PAGE_SIZE, vim_mode: Self::DEFAULT_VIM_MODE, starting_cursor: Self::DEFAULT_STARTING_CURSOR, @@ -198,14 +194,14 @@ where } /// Sets the help message of the prompt. - pub fn with_help_message(mut self, message: &'a str) -> Self { - self.help_message = Some(message); + pub fn with_help_message(mut self, message: &str) -> Self { + self.help_message = message.into(); self } - /// Removes the set help message. + /// Sets the prompt to not display a help message. pub fn without_help_message(mut self) -> Self { - self.help_message = None; + self.help_message = HelpMessage::None; self } diff --git a/inquire/src/prompts/multiselect/prompt.rs b/inquire/src/prompts/multiselect/prompt.rs index c24cbc57..e2383bb9 100644 --- a/inquire/src/prompts/multiselect/prompt.rs +++ b/inquire/src/prompts/multiselect/prompt.rs @@ -20,7 +20,7 @@ pub struct MultiSelectPrompt<'a, T> { config: MultiSelectConfig, options: Vec, string_options: Vec, - help_message: Option<&'a str>, + help_message: Option, cursor_index: usize, checked: BTreeSet, input: Input, @@ -36,6 +36,7 @@ where T: Display, { pub fn new(mso: MultiSelect<'a, T>) -> InquireResult { + let config = (&mso).into(); if mso.options.is_empty() { return Err(InquireError::InvalidConfiguration( "Available options can not be empty".into(), @@ -66,13 +67,18 @@ where }) .unwrap_or_else(BTreeSet::new); + let default_help_message = + Some("↑↓ to move, space to select one, → to all, ← to none, type to filter"); + let help_message = mso + .help_message + .into_or_default(default_help_message.map(|s| s.into())); Ok(Self { message: mso.message, - config: (&mso).into(), + config, options: mso.options, string_options, filtered_options, - help_message: mso.help_message, + help_message, cursor_index: mso.starting_cursor, input: Input::new(), filter: mso.filter, @@ -189,7 +195,7 @@ where } } -impl<'a, B, T> Prompt>> +impl<'a, B, T> Prompt<'a, B, MultiSelectConfig, MultiSelectPromptAction, Vec>> for MultiSelectPrompt<'a, T> where B: MultiSelectBackend, @@ -199,6 +205,10 @@ where self.message } + fn help_message(&self) -> Option<&str> { + self.help_message.as_deref() + } + fn config(&self) -> &MultiSelectConfig { &self.config } @@ -291,10 +301,6 @@ where backend.render_options(page, &self.checked)?; - if let Some(help_message) = self.help_message { - backend.render_help_message(help_message)?; - } - Ok(()) } } diff --git a/inquire/src/prompts/password/mod.rs b/inquire/src/prompts/password/mod.rs index 5fa58696..58306e2a 100644 --- a/inquire/src/prompts/password/mod.rs +++ b/inquire/src/prompts/password/mod.rs @@ -13,7 +13,7 @@ use crate::{ formatter::StringFormatter, prompts::prompt::Prompt, terminal::get_default_terminal, - ui::{Backend, PasswordBackend, RenderConfig}, + ui::{Backend, HelpMessage, PasswordBackend, RenderConfig}, validator::StringValidator, }; @@ -96,7 +96,7 @@ pub struct Password<'a> { pub custom_confirmation_error_message: Option<&'a str>, /// Help message to be presented to the user. - pub help_message: Option<&'a str>, + pub help_message: HelpMessage, /// Function that formats the user input and presents it to the user as the final rendering of the prompt. pub formatter: StringFormatter<'a>, @@ -136,9 +136,6 @@ impl<'a> Password<'a> { /// Default validators added to the [Password] prompt, none. pub const DEFAULT_VALIDATORS: Vec> = vec![]; - /// Default help message. - pub const DEFAULT_HELP_MESSAGE: Option<&'a str> = None; - /// Default value for the allow display toggle variable. pub const DEFAULT_ENABLE_DISPLAY_TOGGLE: bool = false; @@ -157,7 +154,7 @@ impl<'a> Password<'a> { enable_confirmation: Self::DEFAULT_ENABLE_CONFIRMATION, enable_display_toggle: Self::DEFAULT_ENABLE_DISPLAY_TOGGLE, display_mode: Self::DEFAULT_DISPLAY_MODE, - help_message: Self::DEFAULT_HELP_MESSAGE, + help_message: HelpMessage::default(), formatter: Self::DEFAULT_FORMATTER, validators: Self::DEFAULT_VALIDATORS, render_config: get_configuration(), @@ -165,8 +162,14 @@ impl<'a> Password<'a> { } /// Sets the help message of the prompt. - pub fn with_help_message(mut self, message: &'a str) -> Self { - self.help_message = Some(message); + pub fn with_help_message(mut self, message: &str) -> Self { + self.help_message = message.into(); + self + } + + /// Sets the prompt to not display a help message. + pub fn without_help_message(mut self) -> Self { + self.help_message = HelpMessage::None; self } diff --git a/inquire/src/prompts/password/prompt.rs b/inquire/src/prompts/password/prompt.rs index db5ac897..62804f7b 100644 --- a/inquire/src/prompts/password/prompt.rs +++ b/inquire/src/prompts/password/prompt.rs @@ -25,7 +25,7 @@ struct PasswordConfirmation<'a> { pub struct PasswordPrompt<'a> { message: &'a str, config: PasswordConfig, - help_message: Option<&'a str>, + help_message: Option, input: Input, current_mode: PasswordDisplayMode, confirmation: Option>, // if `None`, confirmation is disabled, `Some(_)` confirmation is enabled @@ -51,7 +51,7 @@ impl<'a> From> for PasswordPrompt<'a> { Self { message: so.message, config: (&so).into(), - help_message: so.help_message, + help_message: so.help_message.into_or_default(None), current_mode: so.display_mode, confirmation, confirmation_stage: false, @@ -136,7 +136,7 @@ impl<'a> PasswordPrompt<'a> { } } -impl<'a, B> Prompt for PasswordPrompt<'a> +impl<'a, B> Prompt<'a, B, PasswordConfig, PasswordPromptAction, String> for PasswordPrompt<'a> where B: PasswordBackend, { @@ -144,6 +144,10 @@ where self.message } + fn help_message(&self) -> Option<&str> { + self.help_message.as_deref() + } + fn config(&self) -> &PasswordConfig { &self.config } @@ -246,10 +250,6 @@ where } } - if let Some(message) = self.help_message { - backend.render_help_message(message)?; - } - Ok(()) } } diff --git a/inquire/src/prompts/prompt.rs b/inquire/src/prompts/prompt.rs index bc3f9888..a8c914b6 100644 --- a/inquire/src/prompts/prompt.rs +++ b/inquire/src/prompts/prompt.rs @@ -27,7 +27,7 @@ impl From for ActionResult { } /// Shared behavior among all different prompt types. -pub trait Prompt +pub trait Prompt<'a, Backend, Config, IAction, ReturnType> where Backend: CommonBackend, IAction: InnerAction, @@ -52,6 +52,10 @@ where /// * `answer` - Answer returned by the prompt. fn format_answer(&self, answer: &ReturnType) -> String; + /// Hook called to retrieve the help message to display at the end + /// of the prompt. + fn help_message(&self) -> Option<&str>; + /// Hook called when a prompt is first started, before the first /// draw happens. fn setup(&mut self) -> InquireResult<()> { @@ -106,6 +110,7 @@ where if let ActionResult::NeedsRedraw = last_handle { backend.frame_setup()?; self.render(backend)?; + backend.render_help_message(self.help_message())?; backend.frame_finish()?; last_handle = ActionResult::Clean; } diff --git a/inquire/src/prompts/select/mod.rs b/inquire/src/prompts/select/mod.rs index be538bf0..1f4f258e 100644 --- a/inquire/src/prompts/select/mod.rs +++ b/inquire/src/prompts/select/mod.rs @@ -17,7 +17,7 @@ use crate::{ prompts::prompt::Prompt, terminal::get_default_terminal, type_aliases::Filter, - ui::{Backend, RenderConfig, SelectBackend}, + ui::{Backend, HelpMessage, RenderConfig, SelectBackend}, }; use self::prompt::SelectPrompt; @@ -72,7 +72,7 @@ pub struct Select<'a, T> { pub options: Vec, /// Help message to be presented to the user. - pub help_message: Option<&'a str>, + pub help_message: HelpMessage, /// Page size of the options displayed to the user. pub page_size: usize, @@ -159,16 +159,12 @@ where /// Default starting cursor index. pub const DEFAULT_STARTING_CURSOR: usize = 0; - /// Default help message. - pub const DEFAULT_HELP_MESSAGE: Option<&'a str> = - Some("↑↓ to move, enter to select, type to filter"); - /// Creates a [Select] with the provided message and options, along with default configuration values. pub fn new(message: &'a str, options: Vec) -> Self { Self { message, options, - help_message: Self::DEFAULT_HELP_MESSAGE, + help_message: HelpMessage::default(), page_size: Self::DEFAULT_PAGE_SIZE, vim_mode: Self::DEFAULT_VIM_MODE, starting_cursor: Self::DEFAULT_STARTING_CURSOR, @@ -179,14 +175,14 @@ where } /// Sets the help message of the prompt. - pub fn with_help_message(mut self, message: &'a str) -> Self { - self.help_message = Some(message); + pub fn with_help_message(mut self, message: &str) -> Self { + self.help_message = message.into(); self } - /// Removes the set help message. + /// Sets the prompt to not display a help message. pub fn without_help_message(mut self) -> Self { - self.help_message = None; + self.help_message = HelpMessage::None; self } diff --git a/inquire/src/prompts/select/prompt.rs b/inquire/src/prompts/select/prompt.rs index b9bfb429..d8147cd3 100644 --- a/inquire/src/prompts/select/prompt.rs +++ b/inquire/src/prompts/select/prompt.rs @@ -20,7 +20,7 @@ pub struct SelectPrompt<'a, T> { options: Vec, string_options: Vec, filtered_options: Vec, - help_message: Option<&'a str>, + help_message: Option, cursor_index: usize, input: Input, filter: Filter<'a, T>, @@ -32,6 +32,7 @@ where T: Display, { pub fn new(so: Select<'a, T>) -> InquireResult { + let config = (&so).into(); if so.options.is_empty() { return Err(InquireError::InvalidConfiguration( "Available options can not be empty".into(), @@ -49,13 +50,18 @@ where let string_options = so.options.iter().map(T::to_string).collect(); let filtered_options = (0..so.options.len()).collect(); + let default_help_message = Some("↑↓ to move, enter to select, type to filter"); + let help_message = so + .help_message + .into_or_default(default_help_message.map(|s| s.into())); + Ok(Self { message: so.message, - config: (&so).into(), + config, options: so.options, string_options, filtered_options, - help_message: so.help_message, + help_message, cursor_index: so.starting_cursor, input: Input::new(), filter: so.filter, @@ -128,7 +134,8 @@ where } } -impl<'a, B, T> Prompt> for SelectPrompt<'a, T> +impl<'a, B, T> Prompt<'a, B, SelectConfig, SelectPromptAction, ListOption> + for SelectPrompt<'a, T> where B: SelectBackend, T: Display, @@ -137,6 +144,10 @@ where self.message } + fn help_message(&self) -> Option<&str> { + self.help_message.as_deref() + } + fn config(&self) -> &SelectConfig { &self.config } @@ -197,10 +208,6 @@ where backend.render_options(page)?; - if let Some(help_message) = self.help_message { - backend.render_help_message(help_message)?; - } - Ok(()) } } diff --git a/inquire/src/prompts/text/mod.rs b/inquire/src/prompts/text/mod.rs index 8aa5886f..d206f6b7 100644 --- a/inquire/src/prompts/text/mod.rs +++ b/inquire/src/prompts/text/mod.rs @@ -14,14 +14,12 @@ use crate::{ formatter::{StringFormatter, DEFAULT_STRING_FORMATTER}, prompts::prompt::Prompt, terminal::get_default_terminal, - ui::{Backend, RenderConfig, TextBackend}, + ui::{Backend, HelpMessage, RenderConfig, TextBackend}, validator::StringValidator, }; use self::prompt::TextPrompt; -const DEFAULT_HELP_MESSAGE_WITH_AC: &str = "↑↓ to move, tab to autocomplete, enter to submit"; - /// Standard text prompt that returns the user string input. /// /// This is the standard the standard kind of prompt you would expect from a library like this one. It displays a message to the user, prompting them to type something back. The user's input is then stored in a `String` and returned to the prompt caller. @@ -92,7 +90,7 @@ pub struct Text<'a> { pub placeholder: Option<&'a str>, /// Help message to be presented to the user. - pub help_message: Option<&'a str>, + pub help_message: HelpMessage, /// Function that formats the user input and presents it to the user as the final rendering of the prompt. pub formatter: StringFormatter<'a>, @@ -132,9 +130,6 @@ impl<'a> Text<'a> { /// Default validators added to the [Text] prompt, none. pub const DEFAULT_VALIDATORS: Vec> = vec![]; - /// Default help message. - pub const DEFAULT_HELP_MESSAGE: Option<&'a str> = None; - /// Creates a [Text] with the provided message and default options. pub fn new(message: &'a str) -> Self { Self { @@ -142,7 +137,7 @@ impl<'a> Text<'a> { placeholder: None, initial_value: None, default: None, - help_message: Self::DEFAULT_HELP_MESSAGE, + help_message: HelpMessage::default(), validators: Self::DEFAULT_VALIDATORS, formatter: Self::DEFAULT_FORMATTER, page_size: Self::DEFAULT_PAGE_SIZE, @@ -152,8 +147,14 @@ impl<'a> Text<'a> { } /// Sets the help message of the prompt. - pub fn with_help_message(mut self, message: &'a str) -> Self { - self.help_message = Some(message); + pub fn with_help_message(mut self, message: &str) -> Self { + self.help_message = message.into(); + self + } + + /// Sets the prompt to not display a help message. + pub fn without_help_message(mut self) -> Self { + self.help_message = HelpMessage::None; self } @@ -283,3 +284,9 @@ impl<'a> Text<'a> { TextPrompt::from(self).prompt(backend) } } + +impl<'a> From<&'a str> for Text<'a> { + fn from(val: &'a str) -> Self { + Text::new(val) + } +} diff --git a/inquire/src/prompts/text/prompt.rs b/inquire/src/prompts/text/prompt.rs index ff9cdfb1..44a23a7e 100644 --- a/inquire/src/prompts/text/prompt.rs +++ b/inquire/src/prompts/text/prompt.rs @@ -1,7 +1,7 @@ use std::cmp::min; use crate::{ - autocompletion::{NoAutoCompletion, Replacement}, + autocompletion::Replacement, error::InquireResult, formatter::StringFormatter, input::{Input, InputActionResult}, @@ -13,24 +13,25 @@ use crate::{ Autocomplete, InquireError, Text, }; -use super::{action::TextPromptAction, config::TextConfig, DEFAULT_HELP_MESSAGE_WITH_AC}; +use super::{action::TextPromptAction, config::TextConfig}; pub struct TextPrompt<'a> { message: &'a str, config: TextConfig, default: Option<&'a str>, - help_message: Option<&'a str>, + help_message: Option, input: Input, formatter: StringFormatter<'a>, validators: Vec>, error: Option, - autocompleter: Box, + autocompleter: Option>, suggested_options: Vec, suggestion_cursor_index: Option, } impl<'a> From> for TextPrompt<'a> { fn from(so: Text<'a>) -> Self { + let config = (&so).into(); let input = Input::new_with(so.initial_value.unwrap_or_default()); let input = if let Some(placeholder) = so.placeholder { input.with_placeholder(placeholder) @@ -38,15 +39,22 @@ impl<'a> From> for TextPrompt<'a> { input }; + let default_help_message = if so.autocompleter.is_some() { + Some("↑↓ to move, tab to autocomplete, enter to submit") + } else { + None + }; + let help_message = so + .help_message + .into_or_default(default_help_message.map(|s| s.into())); + Self { message: so.message, - config: (&so).into(), + config, default: so.default, - help_message: so.help_message, + help_message, formatter: so.formatter, - autocompleter: so - .autocompleter - .unwrap_or_else(|| Box::::default()), + autocompleter: so.autocompleter, input, error: None, suggestion_cursor_index: None, @@ -56,16 +64,12 @@ impl<'a> From> for TextPrompt<'a> { } } -impl<'a> From<&'a str> for Text<'a> { - fn from(val: &'a str) -> Self { - Text::new(val) - } -} - impl<'a> TextPrompt<'a> { fn update_suggestions(&mut self) -> InquireResult<()> { - self.suggested_options = self.autocompleter.get_suggestions(self.input.content())?; - self.suggestion_cursor_index = None; + if let Some(autocompleter) = &mut self.autocompleter { + self.suggested_options = autocompleter.get_suggestions(self.input.content())?; + self.suggestion_cursor_index = None; + } Ok(()) } @@ -119,15 +123,16 @@ impl<'a> TextPrompt<'a> { fn use_current_suggestion(&mut self) -> InquireResult { let suggestion = self.get_highlighted_suggestion().map(|s| s.to_owned()); - match self - .autocompleter - .get_completion(self.input.content(), suggestion)? - { - Replacement::Some(value) => { - self.input = Input::new_with(value); - Ok(ActionResult::NeedsRedraw) + if let Some(autocompleter) = &mut self.autocompleter { + match autocompleter.get_completion(self.input.content(), suggestion)? { + Replacement::Some(value) => { + self.input = Input::new_with(value); + Ok(ActionResult::NeedsRedraw) + } + Replacement::None => Ok(ActionResult::Clean), } - Replacement::None => Ok(ActionResult::Clean), + } else { + Ok(ActionResult::Clean) } } @@ -161,7 +166,7 @@ impl<'a> TextPrompt<'a> { } } -impl<'a, B> Prompt for TextPrompt<'a> +impl<'a, B> Prompt<'a, B, TextConfig, TextPromptAction, String> for TextPrompt<'a> where B: TextBackend, { @@ -169,6 +174,10 @@ where self.message } + fn help_message(&self) -> Option<&str> { + self.help_message.as_deref() + } + fn config(&self) -> &TextConfig { &self.config } @@ -240,12 +249,6 @@ where backend.render_suggestions(page)?; - if let Some(message) = self.help_message { - backend.render_help_message(message)?; - } else if !choices.is_empty() { - backend.render_help_message(DEFAULT_HELP_MESSAGE_WITH_AC)?; - } - Ok(()) } } diff --git a/inquire/src/prompts/text/test.rs b/inquire/src/prompts/text/test.rs index 94f0099e..5b9fee81 100644 --- a/inquire/src/prompts/text/test.rs +++ b/inquire/src/prompts/text/test.rs @@ -114,3 +114,15 @@ text_test!( _ => Ok(Validation::Invalid(ErrorMessage::Default)), }) ); + +text_test!( + text_from_str_is_implemented, + { + let mut events = vec![]; + events.append(&mut text_to_events!("12345").collect()); + events.push(KeyCode::Enter); + events + }, + "12345", + Text::from("12345") +); diff --git a/inquire/src/ui/backend.rs b/inquire/src/ui/backend.rs index 676c11c3..c15d2ab3 100644 --- a/inquire/src/ui/backend.rs +++ b/inquire/src/ui/backend.rs @@ -26,7 +26,7 @@ pub trait CommonBackend { fn render_prompt_with_answer(&mut self, prompt: &str, answer: &str) -> Result<()>; fn render_error_message(&mut self, error: &ErrorMessage) -> Result<()>; - fn render_help_message(&mut self, help: &str) -> Result<()>; + fn render_help_message(&mut self, help: Option<&str>) -> Result<()>; } pub trait TextBackend: CommonBackend { @@ -435,17 +435,22 @@ where Ok(()) } - fn render_help_message(&mut self, help: &str) -> Result<()> { - self.terminal - .write_styled(&Styled::new("[").with_style_sheet(self.render_config.help_message))?; + fn render_help_message(&mut self, help_message: Option<&str>) -> Result<()> { + if let Some(help_message) = help_message { + self.terminal.write_styled( + &Styled::new("[").with_style_sheet(self.render_config.help_message), + )?; - self.terminal - .write_styled(&Styled::new(help).with_style_sheet(self.render_config.help_message))?; + self.terminal.write_styled( + &Styled::new(help_message).with_style_sheet(self.render_config.help_message), + )?; - self.terminal - .write_styled(&Styled::new("]").with_style_sheet(self.render_config.help_message))?; + self.terminal.write_styled( + &Styled::new("]").with_style_sheet(self.render_config.help_message), + )?; - self.new_line()?; + self.new_line()?; + } Ok(()) } diff --git a/inquire/src/ui/help_message.rs b/inquire/src/ui/help_message.rs new file mode 100644 index 00000000..567dfad1 --- /dev/null +++ b/inquire/src/ui/help_message.rs @@ -0,0 +1,76 @@ +//! Help message type. + +use std::borrow::Cow; + +/// Help message type. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum HelpMessage { + /// No help message displayed. + None, + + /// Default help message displayed. + /// + /// The actual default help message varies depending on the prompt, possibly + /// including no default message at all. + Default, + + /// Custom help message displayed. The content is the given string. + Custom(String), +} + +impl HelpMessage { + /// Returns the help message as a string, or `None` if the help message is `None` or the default + /// provided is `None`. + pub(crate) fn into_or_default(self, default: Option>) -> Option { + match self { + Self::None => None, + Self::Default => default.map(|s| s.into_owned()), + Self::Custom(s) => Some(s), + } + } +} + +impl Default for HelpMessage { + fn default() -> Self { + Self::Default + } +} + +impl From> for HelpMessage { + fn from(val: Option<&str>) -> Self { + match val { + Some(val) => Self::Custom(val.to_string()), + None => Self::None, + } + } +} + +impl From<&str> for HelpMessage { + fn from(s: &str) -> Self { + Self::Custom(s.to_owned()) + } +} + +impl From<&mut str> for HelpMessage { + fn from(s: &mut str) -> Self { + Self::Custom(s.to_owned()) + } +} + +impl From for HelpMessage { + fn from(s: String) -> Self { + Self::Custom(s) + } +} + +impl From<&String> for HelpMessage { + fn from(s: &String) -> Self { + Self::Custom(s.clone()) + } +} + +impl<'a> From> for HelpMessage { + fn from(s: Cow<'a, str>) -> Self { + Self::Custom(s.into_owned()) + } +} diff --git a/inquire/src/ui/mod.rs b/inquire/src/ui/mod.rs index a721ccf0..3c686e8d 100644 --- a/inquire/src/ui/mod.rs +++ b/inquire/src/ui/mod.rs @@ -2,6 +2,7 @@ mod backend; mod color; +mod help_message; mod input_reader; mod key; mod render_config; @@ -12,5 +13,6 @@ pub(crate) use input_reader::*; pub(crate) use key::*; pub use color::Color; +pub use help_message::*; pub use render_config::*; pub use style::{Attributes, StyleSheet, Styled};