mod action;
mod config;
mod prompt;
#[cfg(test)]
#[cfg(feature = "crossterm")]
mod test;
pub use action::*;
use std::fmt::Display;
use crate::{
config::get_configuration,
error::{InquireError, InquireResult},
formatter::MultiOptionFormatter,
list_option::ListOption,
prompts::prompt::Prompt,
terminal::get_default_terminal,
type_aliases::Scorer,
ui::{Backend, MultiSelectBackend, RenderConfig},
validator::MultiOptionValidator,
};
use self::prompt::MultiSelectPrompt;
#[cfg(feature = "fuzzy")]
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
#[cfg(feature = "fuzzy")]
use std::sync::LazyLock;
#[cfg(feature = "fuzzy")]
static DEFAULT_MATCHER: LazyLock<SkimMatcherV2> =
LazyLock::new(|| SkimMatcherV2::default().ignore_case());
#[derive(Clone)]
pub struct MultiSelect<'a, T> {
pub message: &'a str,
pub options: Vec<T>,
pub default: Option<Vec<usize>>,
pub help_message: Option<&'a str>,
pub page_size: usize,
pub vim_mode: bool,
pub starting_cursor: usize,
pub starting_filter_input: Option<&'a str>,
pub reset_cursor: bool,
pub filter_input_enabled: bool,
pub scorer: Scorer<'a, T>,
pub keep_filter: bool,
pub formatter: MultiOptionFormatter<'a, T>,
pub validator: Option<Box<dyn MultiOptionValidator<T>>>,
pub render_config: RenderConfig<'a>,
}
impl<'a, T> MultiSelect<'a, T>
where
T: Display,
{
pub const DEFAULT_FORMATTER: MultiOptionFormatter<'a, T> = &|ans| {
ans.iter()
.map(|opt| opt.to_string())
.collect::<Vec<String>>()
.join(", ")
};
#[cfg(feature = "fuzzy")]
pub const DEFAULT_SCORER: Scorer<'a, T> =
&|input, _option, string_value, _idx| -> Option<i64> {
DEFAULT_MATCHER.fuzzy_match(string_value, input)
};
#[cfg(not(feature = "fuzzy"))]
pub const DEFAULT_SCORER: Scorer<'a, T> =
&|input, _option, string_value, _idx| -> Option<i64> {
let filter = input.to_lowercase();
match string_value.to_lowercase().contains(&filter) {
true => Some(0),
false => None,
}
};
pub const DEFAULT_PAGE_SIZE: usize = crate::config::DEFAULT_PAGE_SIZE;
pub const DEFAULT_VIM_MODE: bool = crate::config::DEFAULT_VIM_MODE;
pub const DEFAULT_STARTING_CURSOR: usize = 0;
pub const DEFAULT_RESET_CURSOR: bool = true;
pub const DEFAULT_FILTER_INPUT_ENABLED: bool = true;
pub const DEFAULT_KEEP_FILTER: bool = true;
pub const DEFAULT_HELP_MESSAGE: Option<&'a str> =
Some("↑↓ to move, space to select one, → to all, ← to none, type to filter");
pub fn new(message: &'a str, options: Vec<T>) -> Self {
Self {
message,
options,
default: None,
help_message: Self::DEFAULT_HELP_MESSAGE,
page_size: Self::DEFAULT_PAGE_SIZE,
vim_mode: Self::DEFAULT_VIM_MODE,
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,
validator: None,
render_config: get_configuration(),
}
}
pub fn with_help_message(mut self, message: &'a str) -> Self {
self.help_message = Some(message);
self
}
pub fn without_help_message(mut self) -> Self {
self.help_message = None;
self
}
pub fn with_page_size(mut self, page_size: usize) -> Self {
self.page_size = page_size;
self
}
pub fn with_vim_mode(mut self, vim_mode: bool) -> Self {
self.vim_mode = vim_mode;
self
}
pub fn with_keep_filter(mut self, keep_filter: bool) -> Self {
self.keep_filter = keep_filter;
self
}
pub fn with_scorer(mut self, scorer: Scorer<'a, T>) -> Self {
self.scorer = scorer;
self
}
pub fn with_formatter(mut self, formatter: MultiOptionFormatter<'a, T>) -> Self {
self.formatter = formatter;
self
}
pub fn with_validator<V>(mut self, validator: V) -> Self
where
V: MultiOptionValidator<T> + 'static,
{
self.validator = Some(Box::new(validator));
self
}
pub fn with_default(mut self, default: &'a [usize]) -> Self {
self.default = Some(default.to_vec());
self
}
pub fn with_all_selected_by_default(mut self) -> Self {
self.default = Some((0..self.options.len()).collect::<Vec<_>>());
self
}
pub fn with_starting_cursor(mut self, starting_cursor: usize) -> Self {
self.starting_cursor = starting_cursor;
self
}
pub fn with_starting_filter_input(mut self, starting_filter_input: &'a str) -> Self {
self.starting_filter_input = Some(starting_filter_input);
self
}
pub fn with_reset_cursor(mut self, reset_cursor: bool) -> Self {
self.reset_cursor = reset_cursor;
self
}
pub fn without_filtering(mut self) -> Self {
self.filter_input_enabled = false;
self
}
pub fn with_render_config(mut self, render_config: RenderConfig<'a>) -> Self {
self.render_config = render_config;
self
}
pub fn prompt_skippable(self) -> InquireResult<Option<Vec<T>>> {
match self.prompt() {
Ok(answer) => Ok(Some(answer)),
Err(InquireError::OperationCanceled) => Ok(None),
Err(err) => Err(err),
}
}
pub fn prompt(self) -> InquireResult<Vec<T>> {
self.raw_prompt()
.map(|op| op.into_iter().map(|o| o.value).collect())
}
pub fn raw_prompt_skippable(self) -> InquireResult<Option<Vec<ListOption<T>>>> {
match self.raw_prompt() {
Ok(answer) => Ok(Some(answer)),
Err(InquireError::OperationCanceled) => Ok(None),
Err(err) => Err(err),
}
}
pub fn raw_prompt(self) -> InquireResult<Vec<ListOption<T>>> {
let (input_reader, terminal) = get_default_terminal()?;
let mut backend = Backend::new(input_reader, terminal, self.render_config)?;
self.prompt_with_backend(&mut backend)
}
pub(crate) fn prompt_with_backend<B: MultiSelectBackend>(
self,
backend: &mut B,
) -> InquireResult<Vec<ListOption<T>>> {
MultiSelectPrompt::new(self)?.prompt(backend)
}
}