Skip to content

Commit

Permalink
Add 'without_filtering' to selectable prompts
Browse files Browse the repository at this point in the history
  • Loading branch information
mikaelmello committed Dec 27, 2023
1 parent 79f02db commit 73ed5fc
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 79 deletions.
20 changes: 20 additions & 0 deletions inquire/src/prompts/multiselect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ pub struct MultiSelect<'a, T> {
/// Defaults to true.
pub reset_cursor: bool,

/// Whether to allow the option list to be filtered by user input or not.
///
/// Defaults to true.
pub filter_input_enabled: bool,

/// Function called with the current user input to score the provided
/// options.
/// The list of options is sorted in descending order (highest score first)
Expand Down Expand Up @@ -201,6 +206,10 @@ where
/// Defaults to true.
pub const DEFAULT_RESET_CURSOR: bool = true;

/// Default filter input enabled behaviour.
/// Defaults to true.
pub const DEFAULT_FILTER_INPUT_ENABLED: bool = true;

/// Default behavior of keeping or cleaning the current filter value.
pub const DEFAULT_KEEP_FILTER: bool = true;

Expand All @@ -220,6 +229,7 @@ where
starting_cursor: Self::DEFAULT_STARTING_CURSOR,
starting_filter_input: None,
reset_cursor: Self::DEFAULT_RESET_CURSOR,
filter_input_enabled: Self::DEFAULT_FILTER_INPUT_ENABLED,
keep_filter: Self::DEFAULT_KEEP_FILTER,
scorer: Self::DEFAULT_SCORER,
formatter: Self::DEFAULT_FORMATTER,
Expand Down Expand Up @@ -325,6 +335,16 @@ where
self
}

/// Disables the filter input, which means the user will not be able to filter the options
/// by typing.
///
/// This is useful when you want to simplify the UX if the filter does not add any value,
/// such as when the list is already short.
pub fn without_filtering(mut self) -> Self {
self.filter_input_enabled = false;
self
}

/// Sets the provided color theme to this prompt.
///
/// Note: The default render config considers if the NO_COLOR environment variable
Expand Down
93 changes: 51 additions & 42 deletions inquire/src/prompts/multiselect/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub struct MultiSelectPrompt<'a, T> {
help_message: Option<&'a str>,
cursor_index: usize,
checked: BTreeSet<usize>,
input: Input,
input: Option<Input>,
scored_options: Vec<usize>,
scorer: Scorer<'a, T>,
formatter: MultiOptionFormatter<'a, T>,
Expand Down Expand Up @@ -66,6 +66,13 @@ where
})
.unwrap_or_default();

let input = match mso.filter_input_enabled {
true => Some(Input::new_with(
mso.starting_filter_input.unwrap_or_default(),
)),
false => None,
};

Ok(Self {
message: mso.message,
config: (&mso).into(),
Expand All @@ -74,10 +81,7 @@ where
scored_options,
help_message: mso.help_message,
cursor_index: mso.starting_cursor,
input: mso
.starting_filter_input
.map(Input::new_with)
.unwrap_or_else(Input::new),
input,
scorer: mso.scorer,
formatter: mso.formatter,
validator: mso.validator,
Expand All @@ -86,22 +90,6 @@ where
})
}

fn score_options(&self) -> Vec<(usize, i64)> {
self.options
.iter()
.enumerate()
.filter_map(|(i, opt)| {
(self.scorer)(
self.input.content(),
opt,
self.string_options.get(i).unwrap(),
i,
)
.map(|score| (i, score))
})
.collect::<Vec<(usize, i64)>>()
}

fn move_cursor_up(&mut self, qty: usize, wrap: bool) -> ActionResult {
let new_position = if wrap {
let after_wrap = qty.saturating_sub(self.cursor_index);
Expand Down Expand Up @@ -152,11 +140,23 @@ where
self.checked.insert(*idx);
}

ActionResult::NeedsRedraw
}

fn clear_input_if_needed(&mut self, action: MultiSelectPromptAction) -> ActionResult {
if !self.config.keep_filter {
self.input.clear();
return ActionResult::Clean;
}

ActionResult::NeedsRedraw
match action {
MultiSelectPromptAction::ToggleCurrentOption
| MultiSelectPromptAction::SelectAll
| MultiSelectPromptAction::ClearSelections => {
self.input.as_mut().map(Input::clear);
ActionResult::NeedsRedraw
}
_ => ActionResult::Clean,
}
}

fn validate_current_answer(&self) -> InquireResult<Validation> {
Expand Down Expand Up @@ -196,7 +196,21 @@ where
}

fn run_scorer(&mut self) {
let mut options = self.score_options();
let content = match &self.input {
Some(input) => input.content(),
None => return,
};

let mut options = self
.options
.iter()
.enumerate()
.filter_map(|(i, opt)| {
(self.scorer)(content, opt, self.string_options.get(i).unwrap(), i)
.map(|score| (i, score))
})
.collect::<Vec<(usize, i64)>>();

options.sort_unstable_by_key(|(_idx, score)| Reverse(*score));

let new_scored_options = options.iter().map(|(idx, _)| *idx).collect::<Vec<usize>>();
Expand Down Expand Up @@ -270,33 +284,28 @@ where
for idx in &self.scored_options {
self.checked.insert(*idx);
}

if !self.config.keep_filter {
self.input.clear();
}

ActionResult::NeedsRedraw
}
MultiSelectPromptAction::ClearSelections => {
self.checked.clear();

if !self.config.keep_filter {
self.input.clear();
}

ActionResult::NeedsRedraw
}
MultiSelectPromptAction::FilterInput(input_action) => {
let result = self.input.handle(input_action);
MultiSelectPromptAction::FilterInput(input_action) => match self.input.as_mut() {
Some(input) => {
let result = input.handle(input_action);

if let InputActionResult::ContentChanged = result {
self.run_scorer();
}
if let InputActionResult::ContentChanged = result {
self.run_scorer();
}

result.into()
}
result.into()
}
None => ActionResult::Clean,
},
};

let result = self.clear_input_if_needed(action).merge(result);

Ok(result)
}

Expand All @@ -307,7 +316,7 @@ where
backend.render_error_message(err)?;
}

backend.render_multiselect_prompt(prompt, &self.input)?;
backend.render_multiselect_prompt(prompt, self.input.as_ref())?;

let choices = self
.scored_options
Expand Down
42 changes: 42 additions & 0 deletions inquire/src/prompts/multiselect/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,45 @@ fn naive_assert_fuzzy_match_as_default_scorer() {

assert_eq!(vec![ListOption::new(2, "Strawberry")], ans);
}

#[test]
fn chars_do_not_affect_prompt_without_filtering() {
let read: Vec<KeyEvent> = [
KeyCode::Char('w'),
KeyCode::Char('r'),
KeyCode::Char('r'),
KeyCode::Char('y'),
KeyCode::Char(' '),
KeyCode::Enter,
]
.iter()
.map(|c| KeyEvent::from(*c))
.collect();

let mut read = read.iter();

let options = vec![
"Banana",
"Apple",
"Strawberry",
"Grapes",
"Lemon",
"Tangerine",
"Watermelon",
"Orange",
"Pear",
"Avocado",
"Pineapple",
];

let mut write: Vec<u8> = Vec::new();
let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read);
let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap();

let ans = MultiSelect::new("Question", options)
.without_filtering()
.prompt_with_backend(&mut backend)
.unwrap();

assert_eq!(vec![ListOption::new(0, "Banana")], ans);
}
16 changes: 15 additions & 1 deletion inquire/src/prompts/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ pub enum ActionResult {
Clean,
}

impl ActionResult {
pub fn merge(self, other: Self) -> Self {
match (self, other) {
(Self::NeedsRedraw, _) | (_, Self::NeedsRedraw) => Self::NeedsRedraw,
(Self::Clean, Self::Clean) => Self::Clean,
}
}

/// Returns whether the action requires a redraw.
pub fn needs_redraw(&self) -> bool {
matches!(self, Self::NeedsRedraw)
}
}

impl From<InputActionResult> for ActionResult {
fn from(value: InputActionResult) -> Self {
if value.needs_redraw() {
Expand Down Expand Up @@ -106,7 +120,7 @@ where

let mut last_handle = ActionResult::NeedsRedraw;
let final_answer = loop {
if let ActionResult::NeedsRedraw = last_handle {
if last_handle.needs_redraw() {
backend.frame_setup()?;
self.render(backend)?;
backend.frame_finish()?;
Expand Down
20 changes: 20 additions & 0 deletions inquire/src/prompts/select/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ pub struct Select<'a, T> {
/// Defaults to true.
pub reset_cursor: bool,

/// Whether to allow the option list to be filtered by user input or not.
///
/// Defaults to true.
pub filter_input_enabled: bool,

/// Function called with the current user input to score the provided
/// options.
pub scorer: Scorer<'a, T>,
Expand Down Expand Up @@ -186,6 +191,10 @@ where
/// Defaults to true.
pub const DEFAULT_RESET_CURSOR: bool = true;

/// Default filter input enabled behaviour.
/// Defaults to true.
pub const DEFAULT_FILTER_INPUT_ENABLED: bool = true;

/// Default help message.
pub const DEFAULT_HELP_MESSAGE: Option<&'a str> =
Some("↑↓ to move, enter to select, type to filter");
Expand All @@ -200,6 +209,7 @@ where
vim_mode: Self::DEFAULT_VIM_MODE,
starting_cursor: Self::DEFAULT_STARTING_CURSOR,
reset_cursor: Self::DEFAULT_RESET_CURSOR,
filter_input_enabled: Self::DEFAULT_FILTER_INPUT_ENABLED,
scorer: Self::DEFAULT_SCORER,
formatter: Self::DEFAULT_FORMATTER,
render_config: get_configuration(),
Expand Down Expand Up @@ -267,6 +277,16 @@ where
self
}

/// Disables the filter input, which means the user will not be able to filter the options
/// by typing.
///
/// This is useful when you want to simplify the UX if the filter does not add any value,
/// such as when the list is already short.
pub fn without_filtering(mut self) -> Self {
self.filter_input_enabled = false;
self
}

/// Sets the provided color theme to this prompt.
///
/// Note: The default render config considers if the NO_COLOR environment variable
Expand Down
Loading

0 comments on commit 73ed5fc

Please sign in to comment.