--- 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..]);
- }
-}