Share

Share this URL with your friends and let them edit or delete the document.

Permissions

gobin
--- multi_select.rs	2024-02-19 14:22:35.885375959 +0100
+++ multi_select_plus.rs	2024-02-19 14:34:53.049117132 +0100
@@ -1,4 +1,4 @@
-use std::{io, iter::repeat, ops::Rem};
+use std::{io, ops::Rem};
 
 use console::{Key, Term};
 
@@ -12,14 +12,34 @@
 /// ## Example
 ///
 /// ```rust,no_run
-/// use dialoguer::MultiSelect;
+/// use dialoguer::MultiSelectPlus;
 ///
 /// fn main() {
-///     let items = vec!["foo", "bar", "baz"];
+///     use dialoguer::{MultiSelectPlusItem, MultiSelectPlusStatus};
+///     let items = vec![
+///         MultiSelectPlusItem {
+///             name: String::from("Foo"),
+///             summary_text: String::from("Foo"),
+///             status: MultiSelectPlusStatus::UNCHECKED
+///         },
+///         MultiSelectPlusItem {
+///             name: String::from("Bar (more details here)"),
+///             summary_text: String::from("Bar"),
+///             status: MultiSelectPlusStatus::CHECKED
+///         },
+///         MultiSelectPlusItem {
+///             name: String::from("Baz"),
+///             summary_text: String::from("Baz"),
+///             status: MultiSelectPlusStatus {
+///                 checked: false,
+///                 symbol: "-"
+///             }
+///         }
+///     ];
 ///
-///     let selection = MultiSelect::new()
+///     let selection = MultiSelectPlus::new()
 ///         .with_prompt("What do you choose?")
-///         .items(&items)
+///         .items(items)
 ///         .interact()
 ///         .unwrap();
 ///
@@ -30,10 +50,11 @@
 ///     }
 /// }
 /// ```
-#[derive(Clone)]
-pub struct MultiSelect<'a> {
-    defaults: Vec<bool>,
-    items: Vec<String>,
+pub struct MultiSelectPlus<'a> {
+    items: Vec<MultiSelectPlusItem>,
+    checked_status: MultiSelectPlusStatus,
+    unchecked_status: MultiSelectPlusStatus,
+    select_callback: Option<Box<SelectCallback<'a>>>,
     prompt: Option<String>,
     report: bool,
     clear: bool,
@@ -41,20 +62,63 @@
     theme: &'a dyn Theme,
 }
 
-impl Default for MultiSelect<'static> {
+#[derive(Clone)]
+pub struct MultiSelectPlusItem {
+    pub name: String,
+    pub summary_text: String,
+    pub status: MultiSelectPlusStatus,
+}
+
+impl MultiSelectPlusItem {
+    pub fn name(&self) -> &String {
+        &self.name
+    }
+
+    pub fn summary_text(&self) -> &String {
+        &self.summary_text
+    }
+
+    pub fn checked(&self) -> &MultiSelectPlusStatus {
+        &self.status
+    }
+}
+
+#[derive(Clone, PartialEq)]
+pub struct MultiSelectPlusStatus {
+    pub checked: bool,
+    pub symbol: &'static str,
+}
+
+impl MultiSelectPlusStatus {
+    pub const fn new(checked: bool, symbol: &'static str) -> Self {
+        Self { checked, symbol }
+    }
+
+    pub const CHECKED: Self = Self::new(true, "X");
+    pub const UNCHECKED: Self = Self::new(false, " ");
+}
+
+impl Default for MultiSelectPlus<'static> {
     fn default() -> Self {
         Self::new()
     }
 }
 
-impl MultiSelect<'static> {
+impl <'a> MultiSelectPlus<'a> {
     /// Creates a multi select prompt with default theme.
     pub fn new() -> Self {
         Self::with_theme(&SimpleTheme)
     }
 }
 
-impl MultiSelect<'_> {
+/// A callback that can be used to modify the items in the multi select prompt.
+/// Executed between the selection of an item and the rendering of the prompt.
+/// * `item` - The item that was selected
+/// * `items` - The current list of items
+pub type SelectCallback<'a> = dyn Fn(&MultiSelectPlusItem, &Vec<MultiSelectPlusItem>) -> Option<Vec<MultiSelectPlusItem>> + 'a;
+
+
+impl<'a> MultiSelectPlus<'a> {
     /// Sets the clear behavior of the menu.
     ///
     /// The default is to clear the menu.
@@ -63,18 +127,6 @@
         self
     }
 
-    /// Sets a defaults for the menu.
-    pub fn defaults(mut self, val: &[bool]) -> Self {
-        self.defaults = val
-            .to_vec()
-            .iter()
-            .copied()
-            .chain(repeat(false))
-            .take(self.items.len())
-            .collect();
-        self
-    }
-
     /// Sets an optional max length for a page
     ///
     /// Max length is disabled by None
@@ -87,38 +139,23 @@
         self
     }
 
-    /// Add a single item to the selector.
-    #[inline]
-    pub fn item<T: ToString>(self, item: T) -> Self {
-        self.item_checked(item, false)
+    pub fn with_select_callback(mut self, val: Box<SelectCallback<'a>>) -> Self {
+        self.select_callback = Some(val);
+        self
     }
 
-    /// Add a single item to the selector with a default checked state.
-    pub fn item_checked<T: ToString>(mut self, item: T, checked: bool) -> Self {
-        self.items.push(item.to_string());
-        self.defaults.push(checked);
+    /// Add a single item to the selector.
+    pub fn item(mut self, item: MultiSelectPlusItem) -> Self {
+        self.items.push(item);
         self
     }
 
     /// Adds multiple items to the selector.
-    pub fn items<T, I>(self, items: I) -> Self
+    pub fn items<I>(mut self, items: I) -> Self
     where
-        T: ToString,
-        I: IntoIterator<Item = T>,
+        I: IntoIterator<Item = MultiSelectPlusItem>
     {
-        self.items_checked(items.into_iter().map(|item| (item, false)))
-    }
-
-    /// Adds multiple items to the selector with checked state
-    pub fn items_checked<T, I>(mut self, items: I) -> Self
-    where
-        T: ToString,
-        I: IntoIterator<Item = (T, bool)>,
-    {
-        for (item, checked) in items.into_iter() {
-            self.items.push(item.to_string());
-            self.defaults.push(checked);
-        }
+        self.items.extend(items);
         self
     }
 
@@ -126,7 +163,7 @@
     ///
     /// By default, when a prompt is set the system also prints out a confirmation after
     /// the selection. You can opt-out of this with [`report`](Self::report).
-    pub fn with_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
+    pub fn with_prompt<T: Into<String>>(mut self, prompt: T) -> Self {
         self.prompt = Some(prompt.into());
         self
     }
@@ -159,13 +196,34 @@
     /// ## Example
     ///
     /// ```rust,no_run
-    /// use dialoguer::MultiSelect;
+    /// use dialoguer::MultiSelectPlus;
+    /// use dialoguer::MultiSelectPlusItem;
+    /// use dialoguer::MultiSelectPlusStatus;
     ///
     /// fn main() {
-    ///     let items = vec!["foo", "bar", "baz"];
+    ///     let items = vec![
+    ///         MultiSelectPlusItem {
+    ///             name: String::from("Foo"),
+    ///             summary_text: String::from("Foo"),
+    ///             status: MultiSelectPlusStatus::UNCHECKED
+    ///         },
+    ///         MultiSelectPlusItem {
+    ///             name: String::from("Bar (more details here)"),
+    ///             summary_text: String::from("Bar"),
+    ///             status: MultiSelectPlusStatus::CHECKED
+    ///         },
+    ///         MultiSelectPlusItem {
+    ///             name: String::from("Baz"),
+    ///             summary_text: String::from("Baz"),
+    ///             status: MultiSelectPlusStatus {
+    ///                 checked: false,
+    ///                 symbol: "-"
+    ///             }
+    ///         }
+    ///     ];
     ///
-    ///     let ordered = MultiSelect::new()
-    ///         .items(&items)
+    ///     let ordered = MultiSelectPlus::new()
+    ///         .items(items)
     ///         .interact_opt()
     ///         .unwrap();
     ///
@@ -176,7 +234,7 @@
     ///             for i in positions {
     ///                 println!("{}", items[i]);
     ///             }
-    ///         },
+    ///         }
     ///         None => println!("You did not choose anything.")
     ///     }
     /// }
@@ -200,7 +258,7 @@
         self._interact_on(term, true)
     }
 
-    fn _interact_on(self, term: &Term, allow_quit: bool) -> Result<Option<Vec<usize>>> {
+    fn _interact_on(mut self, term: &Term, allow_quit: bool) -> Result<Option<Vec<usize>>> {
         if !term.is_term() {
             return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into());
         }
@@ -216,19 +274,16 @@
         let mut render = TermThemeRenderer::new(term, self.theme);
         let mut sel = 0;
 
-        let mut size_vec = Vec::new();
-
-        for items in self
+        let size_vec = self
             .items
             .iter()
-            .flat_map(|i| i.split('\n'))
-            .collect::<Vec<_>>()
-        {
-            let size = &items.len();
-            size_vec.push(*size);
-        }
-
-        let mut checked: Vec<bool> = self.defaults.clone();
+            .flat_map(|i|
+                i.summary_text
+                    .split('\n')
+                    .map(|s| s.len())
+                    .collect::<Vec<_>>()
+            )
+            .collect::<Vec<_>>();
 
         term.hide_cursor()?;
 
@@ -238,14 +293,16 @@
                     .render_prompt(|paging_info| render.multi_select_prompt(prompt, paging_info))?;
             }
 
-            for (idx, item) in self
-                .items
+            // clone to prevent mutating while waiting for input
+            let mut items = self.items.to_vec();
+
+            for (idx, item) in items
                 .iter()
                 .enumerate()
                 .skip(paging.current_page * paging.capacity)
                 .take(paging.capacity)
             {
-                render.multi_select_prompt_item(item, checked[idx], sel == idx)?;
+                render.multi_select_plus_prompt_item(item, sel == idx)?;
             }
 
             term.flush()?;
@@ -277,14 +334,29 @@
                     }
                 }
                 Key::Char(' ') => {
-                    checked[sel] = !checked[sel];
+                    items[sel].status = if items[sel].status.checked {
+                        self.unchecked_status.clone()
+                    } else {
+                        self.checked_status.clone()
+                    };
+                    // if the callback exists, try getting a value from it
+                    // if nothing is returned from the first step, use the `items` as a fallback
+                    self.items = self.select_callback.as_ref()
+                        .and_then(|callback| callback(&items[sel], &items))
+                        .unwrap_or(items)
+
                 }
                 Key::Char('a') => {
-                    if checked.iter().all(|&item_checked| item_checked) {
-                        checked.fill(false);
+                    if items.iter().all(|item| item.status.checked) {
+                        items
+                            .iter_mut()
+                            .for_each(|item| item.status = self.unchecked_status.clone());
                     } else {
-                        checked.fill(true);
+                        items
+                            .iter_mut()
+                            .for_each(|item| item.status = self.checked_status.clone());
                     }
+                    self.items = items;
                 }
                 Key::Escape | Key::Char('q') => {
                     if allow_quit {
@@ -307,19 +379,22 @@
 
                     if let Some(ref prompt) = self.prompt {
                         if self.report {
-                            let selections: Vec<_> = checked
+                            let selections: Vec<_> = items
                                 .iter()
                                 .enumerate()
-                                .filter_map(|(idx, &checked)| {
-                                    if checked {
-                                        Some(self.items[idx].as_str())
+                                .filter_map(|(_, item)| {
+                                    if item.status.checked {
+                                        Some(item.summary_text.to_string())
                                     } else {
                                         None
                                     }
                                 })
                                 .collect();
 
-                            render.multi_select_prompt_selection(prompt, &selections[..])?;
+                            render.multi_select_prompt_selection(
+                                prompt,
+                                &selections.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
+                            )?;
                         }
                     }
 
@@ -327,10 +402,12 @@
                     term.flush()?;
 
                     return Ok(Some(
-                        checked
+                        items
                             .into_iter()
                             .enumerate()
-                            .filter_map(|(idx, checked)| if checked { Some(idx) } else { None })
+                            .filter_map(
+                                |(idx, item)| if item.status.checked { Some(idx) } else { None }
+                            )
                             .collect(),
                     ));
                 }
@@ -348,17 +425,37 @@
     }
 }
 
-impl<'a> MultiSelect<'a> {
+impl<'a> MultiSelectPlus<'a> {
     /// Creates a multi select prompt with a specific theme.
     ///
     /// ## Example
     ///
     /// ```rust,no_run
-    /// use dialoguer::{theme::ColorfulTheme, MultiSelect};
+    /// use dialoguer::{theme::ColorfulTheme, MultiSelectPlus, MultiSelectPlusItem, MultiSelectPlusStatus};
     ///
     /// fn main() {
-    ///     let selection = MultiSelect::with_theme(&ColorfulTheme::default())
-    ///         .items(&["foo", "bar", "baz"])
+    ///     let items = vec![
+    ///         MultiSelectPlusItem {
+    ///             name: String::from("Foo"),
+    ///             summary_text: String::from("Foo"),
+    ///             status: MultiSelectPlusStatus::UNCHECKED
+    ///         },
+    ///         MultiSelectPlusItem {
+    ///             name: String::from("Bar (more details here)"),
+    ///             summary_text: String::from("Bar"),
+    ///             status: MultiSelectPlusStatus::CHECKED
+    ///         },
+    ///         MultiSelectPlusItem {
+    ///             name: String::from("Baz"),
+    ///             summary_text: String::from("Baz"),
+    ///             status: MultiSelectPlusStatus {
+    ///                 checked: false,
+    ///                 symbol: "-"
+    ///             }
+    ///         }
+    ///     ];
+    ///     let selection = MultiSelectPlus::with_theme(&ColorfulTheme::default())
+    ///         .items(items)
     ///         .interact()
     ///         .unwrap();
     /// }
@@ -366,7 +463,9 @@
     pub fn with_theme(theme: &'a dyn Theme) -> Self {
         Self {
             items: vec![],
-            defaults: vec![],
+            unchecked_status: MultiSelectPlusStatus::UNCHECKED,
+            checked_status: MultiSelectPlusStatus::CHECKED,
+            select_callback: None,
             clear: true,
             prompt: None,
             report: true,
@@ -375,23 +474,3 @@
         }
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_clone() {
-        let multi_select = MultiSelect::new().with_prompt("Select your favorite(s)");
-
-        let _ = multi_select.clone();
-    }
-
-    #[test]
-    fn test_iterator() {
-        let items = ["First", "Second", "Third"];
-        let iterator = items.iter().skip(1);
-
-        assert_eq!(MultiSelect::new().items(iterator).items, &items[1..]);
-    }
-}