aboutsummaryrefslogtreecommitdiff
path: root/vendor/dialoguer/src/prompts/fuzzy_select.rs
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/dialoguer/src/prompts/fuzzy_select.rs')
-rw-r--r--vendor/dialoguer/src/prompts/fuzzy_select.rs326
1 files changed, 326 insertions, 0 deletions
diff --git a/vendor/dialoguer/src/prompts/fuzzy_select.rs b/vendor/dialoguer/src/prompts/fuzzy_select.rs
new file mode 100644
index 0000000..9b7f992
--- /dev/null
+++ b/vendor/dialoguer/src/prompts/fuzzy_select.rs
@@ -0,0 +1,326 @@
+use crate::theme::{SimpleTheme, TermThemeRenderer, Theme};
+use console::{Key, Term};
+use fuzzy_matcher::FuzzyMatcher;
+use std::{io, ops::Rem};
+
+/// Renders a selection menu that user can fuzzy match to reduce set.
+///
+/// User can use fuzzy search to limit selectable items.
+/// Interaction returns index of an item selected in the order they appear in `item` invocation or `items` slice.
+///
+/// ## Examples
+///
+/// ```rust,no_run
+/// use dialoguer::{
+/// FuzzySelect,
+/// theme::ColorfulTheme
+/// };
+/// use console::Term;
+///
+/// fn main() -> std::io::Result<()> {
+/// let items = vec!["Item 1", "item 2"];
+/// let selection = FuzzySelect::with_theme(&ColorfulTheme::default())
+/// .items(&items)
+/// .default(0)
+/// .interact_on_opt(&Term::stderr())?;
+///
+/// match selection {
+/// Some(index) => println!("User selected item : {}", items[index]),
+/// None => println!("User did not select anything")
+/// }
+///
+/// Ok(())
+/// }
+/// ```
+
+pub struct FuzzySelect<'a> {
+ default: Option<usize>,
+ items: Vec<String>,
+ prompt: String,
+ report: bool,
+ clear: bool,
+ highlight_matches: bool,
+ max_length: Option<usize>,
+ theme: &'a dyn Theme,
+ /// Search string that a fuzzy search with start with.
+ /// Defaults to an empty string.
+ initial_text: String,
+}
+
+impl Default for FuzzySelect<'static> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl FuzzySelect<'static> {
+ /// Creates the prompt with a specific text.
+ pub fn new() -> Self {
+ Self::with_theme(&SimpleTheme)
+ }
+}
+
+impl FuzzySelect<'_> {
+ /// Sets the clear behavior of the menu.
+ ///
+ /// The default is to clear the menu.
+ pub fn clear(&mut self, val: bool) -> &mut Self {
+ self.clear = val;
+ self
+ }
+
+ /// Sets a default for the menu
+ pub fn default(&mut self, val: usize) -> &mut Self {
+ self.default = Some(val);
+ self
+ }
+
+ /// Add a single item to the fuzzy selector.
+ pub fn item<T: ToString>(&mut self, item: T) -> &mut Self {
+ self.items.push(item.to_string());
+ self
+ }
+
+ /// Adds multiple items to the fuzzy selector.
+ pub fn items<T: ToString>(&mut self, items: &[T]) -> &mut Self {
+ for item in items {
+ self.items.push(item.to_string());
+ }
+ self
+ }
+
+ /// Sets the search text that a fuzzy search starts with.
+ pub fn with_initial_text<S: Into<String>>(&mut self, initial_text: S) -> &mut Self {
+ self.initial_text = initial_text.into();
+ self
+ }
+
+ /// Prefaces the menu with a prompt.
+ ///
+ /// When a prompt is set the system also prints out a confirmation after
+ /// the fuzzy selection.
+ pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut Self {
+ self.prompt = prompt.into();
+ self
+ }
+
+ /// Indicates whether to report the selected value after interaction.
+ ///
+ /// The default is to report the selection.
+ pub fn report(&mut self, val: bool) -> &mut Self {
+ self.report = val;
+ self
+ }
+
+ /// Indicates whether to highlight matched indices
+ ///
+ /// The default is to highlight the indices
+ pub fn highlight_matches(&mut self, val: bool) -> &mut Self {
+ self.highlight_matches = val;
+ self
+ }
+
+ /// Sets the maximum number of visible options.
+ ///
+ /// The default is the height of the terminal minus 2.
+ pub fn max_length(&mut self, rows: usize) -> &mut Self {
+ self.max_length = Some(rows);
+ self
+ }
+
+ /// Enables user interaction and returns the result.
+ ///
+ /// The user can select the items using 'Enter' and the index of selected item will be returned.
+ /// The dialog is rendered on stderr.
+ /// Result contains `index` of selected item if user hit 'Enter'.
+ /// This unlike [interact_opt](#method.interact_opt) does not allow to quit with 'Esc' or 'q'.
+ #[inline]
+ pub fn interact(&self) -> io::Result<usize> {
+ self.interact_on(&Term::stderr())
+ }
+
+ /// Enables user interaction and returns the result.
+ ///
+ /// The user can select the items using 'Enter' and the index of selected item will be returned.
+ /// The dialog is rendered on stderr.
+ /// Result contains `Some(index)` if user hit 'Enter' or `None` if user cancelled with 'Esc' or 'q'.
+ #[inline]
+ pub fn interact_opt(&self) -> io::Result<Option<usize>> {
+ self.interact_on_opt(&Term::stderr())
+ }
+
+ /// Like `interact` but allows a specific terminal to be set.
+ #[inline]
+ pub fn interact_on(&self, term: &Term) -> io::Result<usize> {
+ self._interact_on(term, false)?
+ .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))
+ }
+
+ /// Like `interact` but allows a specific terminal to be set.
+ #[inline]
+ pub fn interact_on_opt(&self, term: &Term) -> io::Result<Option<usize>> {
+ self._interact_on(term, true)
+ }
+
+ /// Like `interact` but allows a specific terminal to be set.
+ fn _interact_on(&self, term: &Term, allow_quit: bool) -> io::Result<Option<usize>> {
+ // Place cursor at the end of the search term
+ let mut position = self.initial_text.len();
+ let mut search_term = self.initial_text.to_owned();
+
+ let mut render = TermThemeRenderer::new(term, self.theme);
+ let mut sel = self.default;
+
+ let mut size_vec = Vec::new();
+ for items in self.items.iter().as_slice() {
+ let size = &items.len();
+ size_vec.push(*size);
+ }
+
+ // Fuzzy matcher
+ let matcher = fuzzy_matcher::skim::SkimMatcherV2::default();
+
+ // Subtract -2 because we need space to render the prompt.
+ let visible_term_rows = (term.size().0 as usize).max(3) - 2;
+ let visible_term_rows = self
+ .max_length
+ .unwrap_or(visible_term_rows)
+ .min(visible_term_rows);
+ // Variable used to determine if we need to scroll through the list.
+ let mut starting_row = 0;
+
+ term.hide_cursor()?;
+
+ loop {
+ render.clear()?;
+ render.fuzzy_select_prompt(self.prompt.as_str(), &search_term, position)?;
+
+ // Maps all items to a tuple of item and its match score.
+ let mut filtered_list = self
+ .items
+ .iter()
+ .map(|item| (item, matcher.fuzzy_match(item, &search_term)))
+ .filter_map(|(item, score)| score.map(|s| (item, s)))
+ .collect::<Vec<_>>();
+
+ // Renders all matching items, from best match to worst.
+ filtered_list.sort_unstable_by(|(_, s1), (_, s2)| s2.cmp(s1));
+
+ for (idx, (item, _)) in filtered_list
+ .iter()
+ .enumerate()
+ .skip(starting_row)
+ .take(visible_term_rows)
+ {
+ render.fuzzy_select_prompt_item(
+ item,
+ Some(idx) == sel,
+ self.highlight_matches,
+ &matcher,
+ &search_term,
+ )?;
+ }
+ term.flush()?;
+
+ match (term.read_key()?, sel) {
+ (Key::Escape, _) if allow_quit => {
+ if self.clear {
+ render.clear()?;
+ term.flush()?;
+ }
+ term.show_cursor()?;
+ return Ok(None);
+ }
+ (Key::ArrowUp | Key::BackTab, _) if !filtered_list.is_empty() => {
+ if sel == Some(0) {
+ starting_row =
+ filtered_list.len().max(visible_term_rows) - visible_term_rows;
+ } else if sel == Some(starting_row) {
+ starting_row -= 1;
+ }
+ sel = match sel {
+ None => Some(filtered_list.len() - 1),
+ Some(sel) => Some(
+ ((sel as i64 - 1 + filtered_list.len() as i64)
+ % (filtered_list.len() as i64))
+ as usize,
+ ),
+ };
+ term.flush()?;
+ }
+ (Key::ArrowDown | Key::Tab, _) if !filtered_list.is_empty() => {
+ sel = match sel {
+ None => Some(0),
+ Some(sel) => {
+ Some((sel as u64 + 1).rem(filtered_list.len() as u64) as usize)
+ }
+ };
+ if sel == Some(visible_term_rows + starting_row) {
+ starting_row += 1;
+ } else if sel == Some(0) {
+ starting_row = 0;
+ }
+ term.flush()?;
+ }
+ (Key::ArrowLeft, _) if position > 0 => {
+ position -= 1;
+ term.flush()?;
+ }
+ (Key::ArrowRight, _) if position < search_term.len() => {
+ position += 1;
+ term.flush()?;
+ }
+ (Key::Enter, Some(sel)) if !filtered_list.is_empty() => {
+ if self.clear {
+ render.clear()?;
+ }
+
+ if self.report {
+ render
+ .input_prompt_selection(self.prompt.as_str(), filtered_list[sel].0)?;
+ }
+
+ let sel_string = filtered_list[sel].0;
+ let sel_string_pos_in_items =
+ self.items.iter().position(|item| item.eq(sel_string));
+
+ term.show_cursor()?;
+ return Ok(sel_string_pos_in_items);
+ }
+ (Key::Backspace, _) if position > 0 => {
+ position -= 1;
+ search_term.remove(position);
+ term.flush()?;
+ }
+ (Key::Char(chr), _) if !chr.is_ascii_control() => {
+ search_term.insert(position, chr);
+ position += 1;
+ term.flush()?;
+ sel = Some(0);
+ starting_row = 0;
+ }
+
+ _ => {}
+ }
+
+ render.clear_preserve_prompt(&size_vec)?;
+ }
+ }
+}
+
+impl<'a> FuzzySelect<'a> {
+ /// Same as `new` but with a specific theme.
+ pub fn with_theme(theme: &'a dyn Theme) -> Self {
+ Self {
+ default: None,
+ items: vec![],
+ prompt: "".into(),
+ report: true,
+ clear: true,
+ highlight_matches: true,
+ max_length: None,
+ theme,
+ initial_text: "".into(),
+ }
+ }
+}