Skip to content

Commit

Permalink
Merge branch 'feat/placeholder' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
mikaelmello committed Aug 17, 2021
2 parents 675c2e3 + e479158 commit 7564695
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 19 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ It provides several different prompts in order to interactively ask the user for
- Several kinds of prompts to suit your needs;
- Standardized error handling (thanks to [thiserror](https://crates.io/crates/thiserror));
- Support for fine-grained configuration for each prompt type, allowing you to customize:
- Rendering configuration (aka color theme + other components).
- Rendering configuration (aka color theme + other components);
- Default values;
- Placeholders;
- Input validators and formatters;
- Help messages;
- Auto-completion for [`Text`] prompts;
Expand Down Expand Up @@ -170,6 +171,7 @@ With `Text`, you can customize several aspects:
- **Prompt message**: Main message when prompting the user for input, `"What is your name?"` in the example above.
- **Help message**: Message displayed at the line below the prompt.
- **Default value**: Default value returned when the user submits an empty response.
- **Placeholder**: Short hint that describes the expected value of the input.
- **Validators**: Custom validators to the user's input, displaying an error message if the input does not pass the requirements.
- **Formatter**: Custom formatter in case you need to pre-process the user input before showing it as the final answer.
- **Suggester**: Custom function that returns a list of input suggestions based on the current text input. See more on "Autocomplete" below.
Expand Down Expand Up @@ -347,7 +349,7 @@ match amount {

This prompt has all of the validation, parsing and error handling features built-in to reduce as much boilerplaste as possible from your prompts. Its defaults are necessarily very simple in order to cover a large range of generic cases, for example a "Invalid input" error message.

You can customize as many aspects of this prompt as you like: prompt message, help message, default value, value parser and value formatter.
You can customize as many aspects of this prompt as you like: prompt message, help message, default value, placeholder, value parser and value formatter.

**Behavior**

Expand Down Expand Up @@ -407,6 +409,7 @@ Confirm prompts provide several options of configuration:

- **Prompt message**: Required when creating the prompt.
- **Default value**: Default value returned when the user submits an empty response.
- **Placeholder**: Short hint that describes the expected value of the input.
- **Help message**: Message displayed at the line below the prompt.
- **Formatter**: Custom formatter in case you need to pre-process the user input before showing it as the final answer.
- Formats `true` to "Yes" and `false` to "No", by default.
Expand Down
1 change: 1 addition & 0 deletions examples/confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ fn main() {
let ans = Confirm {
message: "Are you happy?",
default: Some(false),
placeholder: Some("si|no"),
help_message: Some("It's alright if you're not"),
formatter: Confirm::DEFAULT_FORMATTER,
parser: &|ans| match ans {
Expand Down
1 change: 1 addition & 0 deletions examples/text_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ fn main() {
let _input = Text {
message: "How are you feeling?",
default: None,
placeholder: Some("Good"),
help_message: None,
formatter: Text::DEFAULT_FORMATTER,
validators: Vec::new(),
Expand Down
33 changes: 22 additions & 11 deletions src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::ui::{Key, KeyModifiers};
#[derive(Clone, Debug)]
pub struct Input {
content: String,
placeholder: Option<String>,
cursor: usize,
length: usize,
}
Expand All @@ -23,24 +24,26 @@ impl Input {
pub fn new() -> Self {
Self {
content: String::new(),
placeholder: None,
cursor: 0,
length: 0,
}
}

#[cfg(test)]
pub fn with_content(mut self, content: &str) -> Self {
self.content = String::from(content);
self.length = content.graphemes(true).count();
self.cursor = std::cmp::min(self.cursor, self.length);
pub fn new_with(content: &str) -> Self {
let len = content.graphemes(true).count();

self
Self {
content: String::from(content),
placeholder: None,
length: len,
cursor: len,
}
}

pub fn reset_with(&mut self, content: &str) {
self.content = String::from(content);
self.length = content.graphemes(true).count();
self.cursor = self.length;
pub fn with_placeholder(mut self, placeholder: &str) -> Self {
self.placeholder = Some(String::from(placeholder));
self
}

#[cfg(test)]
Expand All @@ -56,6 +59,14 @@ impl Input {
self
}

pub fn is_empty(&self) -> bool {
self.length == 0
}

pub fn placeholder(&self) -> Option<&str> {
self.placeholder.as_deref()
}

pub fn handle_key(&mut self, key: Key) -> bool {
match key {
Key::Backspace => self.backspace(),
Expand Down Expand Up @@ -261,7 +272,7 @@ mod test {
let content = "great 🌍, 🍞, 🚗, 1231321📞, 🎉, 🍆xsa232 s2da ake iak eaik";

let assert = |expected, initial| {
let mut input = Input::new().with_content(content).with_cursor(initial);
let mut input = Input::new_with(content).with_cursor(initial);

let dirty = input.handle_key(Key::Left(KeyModifiers::CONTROL));
assert_eq!(expected != initial, dirty,
Expand Down
12 changes: 12 additions & 0 deletions src/prompts/confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::{
///
/// - **Prompt message**: Required when creating the prompt.
/// - **Default value**: Default value returned when the user submits an empty response.
/// - **Placeholder**: Short hint that describes the expected value of the input.
/// - **Help message**: Message displayed at the line below the prompt.
/// - **Formatter**: Custom formatter in case you need to pre-process the user input before showing it as the final answer.
/// - Formats `true` to "Yes" and `false` to "No", by default.
Expand Down Expand Up @@ -57,6 +58,9 @@ pub struct Confirm<'a> {
/// Default value, returned when the user input is empty.
pub default: Option<bool>,

/// Short hint that describes the expected value of the input.
pub placeholder: Option<&'a str>,

/// Help message to be presented to the user.
pub help_message: Option<&'a str>,

Expand Down Expand Up @@ -99,6 +103,7 @@ impl<'a> Confirm<'a> {
Self {
message,
default: None,
placeholder: None,
help_message: None,
formatter: Self::DEFAULT_FORMATTER,
parser: Self::DEFAULT_PARSER,
Expand All @@ -114,6 +119,12 @@ impl<'a> Confirm<'a> {
self
}

/// Sets the placeholder.
pub fn with_placeholder(mut self, placeholder: &'a str) -> Self {
self.placeholder = Some(placeholder);
self
}

/// Sets the help message of the prompt.
pub fn with_help_message(mut self, message: &'a str) -> Self {
self.help_message = Some(message);
Expand Down Expand Up @@ -180,6 +191,7 @@ impl<'a> From<Confirm<'a>> for CustomType<'a, bool> {
Some(val) => Some((val, co.default_value_formatter)),
None => None,
},
placeholder: co.placeholder,
help_message: co.help_message,
formatter: co.formatter,
parser: co.parser,
Expand Down
18 changes: 16 additions & 2 deletions src/prompts/custom_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{
///
/// This prompt has all of the validation, parsing and error handling features built-in to reduce as much boilerplaste as possible from your prompts. Its defaults are necessarily very simple in order to cover a large range of generic cases, for example a "Invalid input" error message.
///
/// You can customize as many aspects of this prompt as you like: prompt message, help message, default value, value parser and value formatter.
/// You can customize as many aspects of this prompt as you like: prompt message, help message, default value, placeholder, value parser and value formatter.
///
/// # Behavior
///
Expand All @@ -34,6 +34,7 @@ use crate::{
/// message: "How much is your travel going to cost?",
/// formatter: &|i| format!("${:.2}", i),
/// default: None,
/// placeholder: Some("123.45"),
/// error_message: "Please type a valid number.".into(),
/// help_message: "Do not use currency and the number should use dots as the decimal separator.".into(),
/// parser: &|i| match i.parse::<f64>() {
Expand Down Expand Up @@ -70,6 +71,9 @@ pub struct CustomType<'a, T> {
/// Default value, returned when the user input is empty.
pub default: Option<(T, CustomTypeFormatter<'a, T>)>,

/// Short hint that describes the expected value of the input.
pub placeholder: Option<&'a str>,

/// Help message to be presented to the user.
pub help_message: Option<&'a str>,

Expand Down Expand Up @@ -98,6 +102,7 @@ where
Self {
message,
default: None,
placeholder: None,
help_message: None,
formatter: &|val| val.to_string(),
parser: parse_type!(T),
Expand All @@ -112,6 +117,12 @@ where
self
}

/// Sets the placeholder.
pub fn with_placeholder(mut self, placeholder: &'a str) -> Self {
self.placeholder = Some(placeholder);
self
}

/// Sets the help message of the prompt.
pub fn with_help_message(mut self, message: &'a str) -> Self {
self.help_message = Some(message);
Expand Down Expand Up @@ -181,7 +192,10 @@ where
help_message: co.help_message,
formatter: co.formatter,
parser: co.parser,
input: Input::new(),
input: co
.placeholder
.map(|p| Input::new().with_placeholder(p))
.unwrap_or_else(|| Input::new()),
error_message: co.error_message,
}
}
Expand Down
18 changes: 16 additions & 2 deletions src/prompts/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const DEFAULT_HELP_MESSAGE: &str = "↑↓ to move, tab to auto-complete, enter
/// - **Prompt message**: Main message when prompting the user for input, `"What is your name?"` in the example below.
/// - **Help message**: Message displayed at the line below the prompt.
/// - **Default value**: Default value returned when the user submits an empty response.
/// - **Placeholder**: Short hint that describes the expected value of the input.
/// - **Validators**: Custom validators to the user's input, displaying an error message if the input does not pass the requirements.
/// - **Formatter**: Custom formatter in case you need to pre-process the user input before showing it as the final answer.
/// - **Suggester**: Custom function that returns a list of input suggestions based on the current text input. See more on "Autocomplete" below.
Expand Down Expand Up @@ -65,6 +66,9 @@ pub struct Text<'a> {
/// Default value, returned when the user input is empty.
pub default: Option<&'a str>,

/// Short hint that describes the expected value of the input.
pub placeholder: Option<&'a str>,

/// Help message to be presented to the user.
pub help_message: Option<&'a str>,

Expand Down Expand Up @@ -106,6 +110,7 @@ impl<'a> Text<'a> {
pub fn new(message: &'a str) -> Self {
Self {
message,
placeholder: None,
default: None,
help_message: Self::DEFAULT_HELP_MESSAGE,
validators: Self::DEFAULT_VALIDATORS,
Expand All @@ -128,6 +133,12 @@ impl<'a> Text<'a> {
self
}

/// Sets the placeholder.
pub fn with_placeholder(mut self, placeholder: &'a str) -> Self {
self.placeholder = Some(placeholder);
self
}

/// Sets the suggester.
pub fn with_suggester(mut self, suggester: Suggester<'a>) -> Self {
self.suggester = Some(suggester);
Expand Down Expand Up @@ -220,7 +231,10 @@ impl<'a> From<Text<'a>> for TextPrompt<'a> {
formatter: so.formatter,
validators: so.validators,
suggester: so.suggester,
input: Input::new(),
input: so
.placeholder
.map(|p| Input::new().with_placeholder(p))
.unwrap_or_else(|| Input::new()),
original_input: None,
error: None,
cursor_index: 0,
Expand Down Expand Up @@ -293,7 +307,7 @@ impl<'a> TextPrompt<'a> {
if self.original_input.is_none() {
self.original_input = Some(self.input.clone());
}
self.input.reset_with(suggestion);
self.input = Input::new_with(suggestion);
}
}
}
Expand Down
34 changes: 32 additions & 2 deletions src/ui/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,44 @@ where
}

fn print_input(&mut self, input: &Input) -> Result<()> {
self.terminal.write(' ')?;

if input.is_empty() {
if let Some(placeholder) = input.placeholder() {
if !placeholder.is_empty() {
let mut chars = placeholder.chars();

let first_char = chars.next();
let rest: String = chars.collect();

match first_char {
Some(c) => self.terminal.write_styled(
&Styled::new(c).with_style_sheet(self.render_config.placeholder_cursor),
)?,
None => {}
}

self.terminal.write_styled(
&Styled::new(rest).with_style_sheet(self.render_config.placeholder),
)?;

return Ok(());
}
}

self.terminal.write_styled(
&Styled::new(' ').with_style_sheet(self.render_config.text_input.cursor),
)?;

return Ok(());
}

let (before, mut at, after) = input.split();

if at.is_empty() {
at.push(' ');
}

self.terminal.write(' ')?;

self.terminal.write_styled(
&Styled::new(before).with_style_sheet(self.render_config.text_input.text),
)?;
Expand Down
20 changes: 20 additions & 0 deletions src/ui/render_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ pub struct RenderConfig {
/// and after the default value, as separators.
pub default_value: StyleSheet,

/// Render configuration of placeholders.
///
/// Note: placeholders are displayed wrapped in parenthesis, e.g. (yes).
/// Non-styled space characters is added before the default value display
/// and after the default value, as separators.
pub placeholder: StyleSheet,

/// Render configuration of placeholder cursors.
///
/// Note: placeholders are displayed wrapped in parenthesis, e.g. (yes).
/// Non-styled space characters is added before the default value display
/// and after the default value, as separators.
pub placeholder_cursor: StyleSheet,

/// Render configuration of help messages.
///
/// Note: help messages are displayed wrapped in brackets, e.g. [Be careful!].
Expand Down Expand Up @@ -98,6 +112,8 @@ impl RenderConfig {
prompt_prefix: Styled::new("?"),
prompt: StyleSheet::empty(),
default_value: StyleSheet::empty(),
placeholder: StyleSheet::empty(),
placeholder_cursor: StyleSheet::empty(),
help_message: StyleSheet::empty(),
text_input: InputRenderConfig::empty(),
error_message: ErrorMessageRenderConfig::empty(),
Expand Down Expand Up @@ -204,6 +220,10 @@ impl Default for RenderConfig {
prompt_prefix: Styled::new("?").with_fg(Color::Green),
prompt: StyleSheet::empty(),
default_value: StyleSheet::empty(),
placeholder: StyleSheet::new().with_fg(Color::DarkGrey),
placeholder_cursor: StyleSheet::new()
.with_fg(Color::Black)
.with_bg(Color::DarkGrey),
help_message: StyleSheet::empty().with_fg(Color::Cyan),
text_input: InputRenderConfig::default(),
error_message: ErrorMessageRenderConfig::default(),
Expand Down

0 comments on commit 7564695

Please sign in to comment.