Skip to content

Commit 81f33c5

Browse files
authored
Add 'without_filtering' to selectable prompts (#203)
* Add 'without_filtering' to selectable prompts * Add changelog entry
1 parent 2948f2a commit 81f33c5

File tree

9 files changed

+240
-79
lines changed

9 files changed

+240
-79
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
- Ctrl-b/Ctrl-f for left/right
2424
- Ctrl-j/Ctrl-g for enter/cancel
2525
- Added 'with_starting_filter_input' to both Select and MultiSelect, which allows for setting an initial value to the filter section of the prompt.
26+
- Added 'without_filtering' to both Select and MultiSelect, useful when you want to simplify the UX if the filter does not add any value, such as when the list is already short.
2627
- Added 'with_answered_prompt_prefix' to RenderConfig to allow customization of answered prompt prefix
2728

2829
### Fixes

inquire/src/prompts/multiselect/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ pub struct MultiSelect<'a, T> {
9191
/// Defaults to true.
9292
pub reset_cursor: bool,
9393

94+
/// Whether to allow the option list to be filtered by user input or not.
95+
///
96+
/// Defaults to true.
97+
pub filter_input_enabled: bool,
98+
9499
/// Function called with the current user input to score the provided
95100
/// options.
96101
/// The list of options is sorted in descending order (highest score first)
@@ -201,6 +206,10 @@ where
201206
/// Defaults to true.
202207
pub const DEFAULT_RESET_CURSOR: bool = true;
203208

209+
/// Default filter input enabled behaviour.
210+
/// Defaults to true.
211+
pub const DEFAULT_FILTER_INPUT_ENABLED: bool = true;
212+
204213
/// Default behavior of keeping or cleaning the current filter value.
205214
pub const DEFAULT_KEEP_FILTER: bool = true;
206215

@@ -220,6 +229,7 @@ where
220229
starting_cursor: Self::DEFAULT_STARTING_CURSOR,
221230
starting_filter_input: None,
222231
reset_cursor: Self::DEFAULT_RESET_CURSOR,
232+
filter_input_enabled: Self::DEFAULT_FILTER_INPUT_ENABLED,
223233
keep_filter: Self::DEFAULT_KEEP_FILTER,
224234
scorer: Self::DEFAULT_SCORER,
225235
formatter: Self::DEFAULT_FORMATTER,
@@ -325,6 +335,16 @@ where
325335
self
326336
}
327337

338+
/// Disables the filter input, which means the user will not be able to filter the options
339+
/// by typing.
340+
///
341+
/// This is useful when you want to simplify the UX if the filter does not add any value,
342+
/// such as when the list is already short.
343+
pub fn without_filtering(mut self) -> Self {
344+
self.filter_input_enabled = false;
345+
self
346+
}
347+
328348
/// Sets the provided color theme to this prompt.
329349
///
330350
/// Note: The default render config considers if the NO_COLOR environment variable

inquire/src/prompts/multiselect/prompt.rs

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub struct MultiSelectPrompt<'a, T> {
2323
help_message: Option<&'a str>,
2424
cursor_index: usize,
2525
checked: BTreeSet<usize>,
26-
input: Input,
26+
input: Option<Input>,
2727
scored_options: Vec<usize>,
2828
scorer: Scorer<'a, T>,
2929
formatter: MultiOptionFormatter<'a, T>,
@@ -66,6 +66,13 @@ where
6666
})
6767
.unwrap_or_default();
6868

69+
let input = match mso.filter_input_enabled {
70+
true => Some(Input::new_with(
71+
mso.starting_filter_input.unwrap_or_default(),
72+
)),
73+
false => None,
74+
};
75+
6976
Ok(Self {
7077
message: mso.message,
7178
config: (&mso).into(),
@@ -74,10 +81,7 @@ where
7481
scored_options,
7582
help_message: mso.help_message,
7683
cursor_index: mso.starting_cursor,
77-
input: mso
78-
.starting_filter_input
79-
.map(Input::new_with)
80-
.unwrap_or_else(Input::new),
84+
input,
8185
scorer: mso.scorer,
8286
formatter: mso.formatter,
8387
validator: mso.validator,
@@ -86,22 +90,6 @@ where
8690
})
8791
}
8892

89-
fn score_options(&self) -> Vec<(usize, i64)> {
90-
self.options
91-
.iter()
92-
.enumerate()
93-
.filter_map(|(i, opt)| {
94-
(self.scorer)(
95-
self.input.content(),
96-
opt,
97-
self.string_options.get(i).unwrap(),
98-
i,
99-
)
100-
.map(|score| (i, score))
101-
})
102-
.collect::<Vec<(usize, i64)>>()
103-
}
104-
10593
fn move_cursor_up(&mut self, qty: usize, wrap: bool) -> ActionResult {
10694
let new_position = if wrap {
10795
let after_wrap = qty.saturating_sub(self.cursor_index);
@@ -152,11 +140,23 @@ where
152140
self.checked.insert(*idx);
153141
}
154142

143+
ActionResult::NeedsRedraw
144+
}
145+
146+
fn clear_input_if_needed(&mut self, action: MultiSelectPromptAction) -> ActionResult {
155147
if !self.config.keep_filter {
156-
self.input.clear();
148+
return ActionResult::Clean;
157149
}
158150

159-
ActionResult::NeedsRedraw
151+
match action {
152+
MultiSelectPromptAction::ToggleCurrentOption
153+
| MultiSelectPromptAction::SelectAll
154+
| MultiSelectPromptAction::ClearSelections => {
155+
self.input.as_mut().map(Input::clear);
156+
ActionResult::NeedsRedraw
157+
}
158+
_ => ActionResult::Clean,
159+
}
160160
}
161161

162162
fn validate_current_answer(&self) -> InquireResult<Validation> {
@@ -196,7 +196,21 @@ where
196196
}
197197

198198
fn run_scorer(&mut self) {
199-
let mut options = self.score_options();
199+
let content = match &self.input {
200+
Some(input) => input.content(),
201+
None => return,
202+
};
203+
204+
let mut options = self
205+
.options
206+
.iter()
207+
.enumerate()
208+
.filter_map(|(i, opt)| {
209+
(self.scorer)(content, opt, self.string_options.get(i).unwrap(), i)
210+
.map(|score| (i, score))
211+
})
212+
.collect::<Vec<(usize, i64)>>();
213+
200214
options.sort_unstable_by_key(|(_idx, score)| Reverse(*score));
201215

202216
let new_scored_options = options.iter().map(|(idx, _)| *idx).collect::<Vec<usize>>();
@@ -270,33 +284,28 @@ where
270284
for idx in &self.scored_options {
271285
self.checked.insert(*idx);
272286
}
273-
274-
if !self.config.keep_filter {
275-
self.input.clear();
276-
}
277-
278287
ActionResult::NeedsRedraw
279288
}
280289
MultiSelectPromptAction::ClearSelections => {
281290
self.checked.clear();
282-
283-
if !self.config.keep_filter {
284-
self.input.clear();
285-
}
286-
287291
ActionResult::NeedsRedraw
288292
}
289-
MultiSelectPromptAction::FilterInput(input_action) => {
290-
let result = self.input.handle(input_action);
293+
MultiSelectPromptAction::FilterInput(input_action) => match self.input.as_mut() {
294+
Some(input) => {
295+
let result = input.handle(input_action);
291296

292-
if let InputActionResult::ContentChanged = result {
293-
self.run_scorer();
294-
}
297+
if let InputActionResult::ContentChanged = result {
298+
self.run_scorer();
299+
}
295300

296-
result.into()
297-
}
301+
result.into()
302+
}
303+
None => ActionResult::Clean,
304+
},
298305
};
299306

307+
let result = self.clear_input_if_needed(action).merge(result);
308+
300309
Ok(result)
301310
}
302311

@@ -307,7 +316,7 @@ where
307316
backend.render_error_message(err)?;
308317
}
309318

310-
backend.render_multiselect_prompt(prompt, &self.input)?;
319+
backend.render_multiselect_prompt(prompt, self.input.as_ref())?;
311320

312321
let choices = self
313322
.scored_options

inquire/src/prompts/multiselect/test.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,45 @@ fn naive_assert_fuzzy_match_as_default_scorer() {
194194

195195
assert_eq!(vec![ListOption::new(2, "Strawberry")], ans);
196196
}
197+
198+
#[test]
199+
fn chars_do_not_affect_prompt_without_filtering() {
200+
let read: Vec<KeyEvent> = [
201+
KeyCode::Char('w'),
202+
KeyCode::Char('r'),
203+
KeyCode::Char('r'),
204+
KeyCode::Char('y'),
205+
KeyCode::Char(' '),
206+
KeyCode::Enter,
207+
]
208+
.iter()
209+
.map(|c| KeyEvent::from(*c))
210+
.collect();
211+
212+
let mut read = read.iter();
213+
214+
let options = vec![
215+
"Banana",
216+
"Apple",
217+
"Strawberry",
218+
"Grapes",
219+
"Lemon",
220+
"Tangerine",
221+
"Watermelon",
222+
"Orange",
223+
"Pear",
224+
"Avocado",
225+
"Pineapple",
226+
];
227+
228+
let mut write: Vec<u8> = Vec::new();
229+
let terminal = CrosstermTerminal::new_with_io(&mut write, &mut read);
230+
let mut backend = Backend::new(terminal, RenderConfig::default()).unwrap();
231+
232+
let ans = MultiSelect::new("Question", options)
233+
.without_filtering()
234+
.prompt_with_backend(&mut backend)
235+
.unwrap();
236+
237+
assert_eq!(vec![ListOption::new(0, "Banana")], ans);
238+
}

inquire/src/prompts/prompt.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ pub enum ActionResult {
1616
Clean,
1717
}
1818

19+
impl ActionResult {
20+
pub fn merge(self, other: Self) -> Self {
21+
match (self, other) {
22+
(Self::NeedsRedraw, _) | (_, Self::NeedsRedraw) => Self::NeedsRedraw,
23+
(Self::Clean, Self::Clean) => Self::Clean,
24+
}
25+
}
26+
27+
/// Returns whether the action requires a redraw.
28+
pub fn needs_redraw(&self) -> bool {
29+
matches!(self, Self::NeedsRedraw)
30+
}
31+
}
32+
1933
impl From<InputActionResult> for ActionResult {
2034
fn from(value: InputActionResult) -> Self {
2135
if value.needs_redraw() {
@@ -106,7 +120,7 @@ where
106120

107121
let mut last_handle = ActionResult::NeedsRedraw;
108122
let final_answer = loop {
109-
if let ActionResult::NeedsRedraw = last_handle {
123+
if last_handle.needs_redraw() {
110124
backend.frame_setup()?;
111125
self.render(backend)?;
112126
backend.frame_finish()?;

inquire/src/prompts/select/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ pub struct Select<'a, T> {
9797
/// Defaults to true.
9898
pub reset_cursor: bool,
9999

100+
/// Whether to allow the option list to be filtered by user input or not.
101+
///
102+
/// Defaults to true.
103+
pub filter_input_enabled: bool,
104+
100105
/// Function called with the current user input to score the provided
101106
/// options.
102107
pub scorer: Scorer<'a, T>,
@@ -186,6 +191,10 @@ where
186191
/// Defaults to true.
187192
pub const DEFAULT_RESET_CURSOR: bool = true;
188193

194+
/// Default filter input enabled behaviour.
195+
/// Defaults to true.
196+
pub const DEFAULT_FILTER_INPUT_ENABLED: bool = true;
197+
189198
/// Default help message.
190199
pub const DEFAULT_HELP_MESSAGE: Option<&'a str> =
191200
Some("↑↓ to move, enter to select, type to filter");
@@ -200,6 +209,7 @@ where
200209
vim_mode: Self::DEFAULT_VIM_MODE,
201210
starting_cursor: Self::DEFAULT_STARTING_CURSOR,
202211
reset_cursor: Self::DEFAULT_RESET_CURSOR,
212+
filter_input_enabled: Self::DEFAULT_FILTER_INPUT_ENABLED,
203213
scorer: Self::DEFAULT_SCORER,
204214
formatter: Self::DEFAULT_FORMATTER,
205215
render_config: get_configuration(),
@@ -267,6 +277,16 @@ where
267277
self
268278
}
269279

280+
/// Disables the filter input, which means the user will not be able to filter the options
281+
/// by typing.
282+
///
283+
/// This is useful when you want to simplify the UX if the filter does not add any value,
284+
/// such as when the list is already short.
285+
pub fn without_filtering(mut self) -> Self {
286+
self.filter_input_enabled = false;
287+
self
288+
}
289+
270290
/// Sets the provided color theme to this prompt.
271291
///
272292
/// Note: The default render config considers if the NO_COLOR environment variable

0 commit comments

Comments
 (0)