nu_command/platform/input/
list.rs

1use dialoguer::{FuzzySelect, MultiSelect, Select, console::Term};
2use nu_engine::command_prelude::*;
3use nu_protocol::shell_error::io::IoError;
4
5use std::fmt::{Display, Formatter};
6
7enum InteractMode {
8    Single(Option<usize>),
9    Multi(Option<Vec<usize>>),
10}
11
12#[derive(Clone)]
13struct Options {
14    name: String,
15    value: Value,
16}
17
18impl Display for Options {
19    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
20        write!(f, "{}", self.name)
21    }
22}
23
24#[derive(Clone)]
25pub struct InputList;
26
27const INTERACT_ERROR: &str = "Interact error, could not process options";
28
29impl Command for InputList {
30    fn name(&self) -> &str {
31        "input list"
32    }
33
34    fn signature(&self) -> Signature {
35        Signature::build("input list")
36            .input_output_types(vec![
37                (Type::List(Box::new(Type::Any)), Type::Any),
38                (Type::Range, Type::Int),
39            ])
40            .optional("prompt", SyntaxShape::String, "The prompt to display.")
41            .switch(
42                "multi",
43                "Use multiple results, you can press a to toggle all options on/off",
44                Some('m'),
45            )
46            .switch("fuzzy", "Use a fuzzy select.", Some('f'))
47            .switch("index", "Returns list indexes.", Some('i'))
48            .named(
49                "display",
50                SyntaxShape::CellPath,
51                "Field to use as display value",
52                Some('d'),
53            )
54            .allow_variants_without_examples(true)
55            .category(Category::Platform)
56    }
57
58    fn description(&self) -> &str {
59        "Interactive list selection."
60    }
61
62    fn extra_description(&self) -> &str {
63        "Abort with esc or q."
64    }
65
66    fn search_terms(&self) -> Vec<&str> {
67        vec!["prompt", "ask", "menu"]
68    }
69
70    fn run(
71        &self,
72        engine_state: &EngineState,
73        stack: &mut Stack,
74        call: &Call,
75        input: PipelineData,
76    ) -> Result<PipelineData, ShellError> {
77        let head = call.head;
78        let prompt: Option<String> = call.opt(engine_state, stack, 0)?;
79        let multi = call.has_flag(engine_state, stack, "multi")?;
80        let fuzzy = call.has_flag(engine_state, stack, "fuzzy")?;
81        let index = call.has_flag(engine_state, stack, "index")?;
82        let display_path: Option<CellPath> = call.get_flag(engine_state, stack, "display")?;
83        let config = stack.get_config(engine_state);
84
85        let options: Vec<Options> = match input {
86            PipelineData::Value(Value::Range { .. }, ..)
87            | PipelineData::Value(Value::List { .. }, ..)
88            | PipelineData::ListStream { .. } => input
89                .into_iter()
90                .map(move |val| {
91                    let display_value = if let Some(ref cellpath) = display_path {
92                        val.follow_cell_path(&cellpath.members)?
93                            .to_expanded_string(", ", &config)
94                    } else {
95                        val.to_expanded_string(", ", &config)
96                    };
97                    Ok(Options {
98                        name: display_value,
99                        value: val,
100                    })
101                })
102                .collect::<Result<Vec<_>, ShellError>>()?,
103
104            _ => {
105                return Err(ShellError::TypeMismatch {
106                    err_message: "expected a list, a table, or a range".to_string(),
107                    span: head,
108                });
109            }
110        };
111
112        if options.is_empty() {
113            return Err(ShellError::TypeMismatch {
114                err_message: "expected a list or table, it can also be a problem with the an inner type of your list.".to_string(),
115                span: head,
116            });
117        }
118
119        if multi && fuzzy {
120            return Err(ShellError::TypeMismatch {
121                err_message: "Fuzzy search is not supported for multi select".to_string(),
122                span: head,
123            });
124        }
125
126        // could potentially be used to map the use theme colors at some point
127        // let theme = dialoguer::theme::ColorfulTheme {
128        //     active_item_style: Style::new().fg(Color::Cyan).bold(),
129        //     ..Default::default()
130        // };
131
132        let answer: InteractMode = if multi {
133            let multi_select = MultiSelect::new(); //::with_theme(&theme);
134
135            InteractMode::Multi(
136                if let Some(prompt) = prompt {
137                    multi_select.with_prompt(&prompt)
138                } else {
139                    multi_select
140                }
141                .items(&options)
142                .report(false)
143                .interact_on_opt(&Term::stderr())
144                .map_err(|dialoguer::Error::IO(err)| {
145                    IoError::new_with_additional_context(err, call.head, None, INTERACT_ERROR)
146                })?,
147            )
148        } else if fuzzy {
149            let fuzzy_select = FuzzySelect::new(); //::with_theme(&theme);
150
151            InteractMode::Single(
152                if let Some(prompt) = prompt {
153                    fuzzy_select.with_prompt(&prompt)
154                } else {
155                    fuzzy_select
156                }
157                .items(&options)
158                .default(0)
159                .report(false)
160                .interact_on_opt(&Term::stderr())
161                .map_err(|dialoguer::Error::IO(err)| {
162                    IoError::new_with_additional_context(err, call.head, None, INTERACT_ERROR)
163                })?,
164            )
165        } else {
166            let select = Select::new(); //::with_theme(&theme);
167            InteractMode::Single(
168                if let Some(prompt) = prompt {
169                    select.with_prompt(&prompt)
170                } else {
171                    select
172                }
173                .items(&options)
174                .default(0)
175                .report(false)
176                .interact_on_opt(&Term::stderr())
177                .map_err(|dialoguer::Error::IO(err)| {
178                    IoError::new_with_additional_context(err, call.head, None, INTERACT_ERROR)
179                })?,
180            )
181        };
182
183        Ok(match answer {
184            InteractMode::Multi(res) => {
185                if index {
186                    match res {
187                        Some(opts) => Value::list(
188                            opts.into_iter()
189                                .map(|s| Value::int(s as i64, head))
190                                .collect(),
191                            head,
192                        ),
193                        None => Value::nothing(head),
194                    }
195                } else {
196                    match res {
197                        Some(opts) => Value::list(
198                            opts.iter().map(|s| options[*s].value.clone()).collect(),
199                            head,
200                        ),
201                        None => Value::nothing(head),
202                    }
203                }
204            }
205            InteractMode::Single(res) => {
206                if index {
207                    match res {
208                        Some(opt) => Value::int(opt as i64, head),
209                        None => Value::nothing(head),
210                    }
211                } else {
212                    match res {
213                        Some(opt) => options[opt].value.clone(),
214                        None => Value::nothing(head),
215                    }
216                }
217            }
218        }
219        .into_pipeline_data())
220    }
221
222    fn examples(&self) -> Vec<Example> {
223        vec![
224            Example {
225                description: "Return a single value from a list",
226                example: r#"[1 2 3 4 5] | input list 'Rate it'"#,
227                result: None,
228            },
229            Example {
230                description: "Return multiple values from a list",
231                example: r#"[Banana Kiwi Pear Peach Strawberry] | input list --multi 'Add fruits to the basket'"#,
232                result: None,
233            },
234            Example {
235                description: "Return a single record from a table with fuzzy search",
236                example: r#"ls | input list --fuzzy 'Select the target'"#,
237                result: None,
238            },
239            Example {
240                description: "Choose an item from a range",
241                example: r#"1..10 | input list"#,
242                result: None,
243            },
244            Example {
245                description: "Return the index of a selected item",
246                example: r#"[Banana Kiwi Pear Peach Strawberry] | input list --index"#,
247                result: None,
248            },
249            Example {
250                description: "Choose an item from a table using a column as display value",
251                example: r#"[[name price]; [Banana 12] [Kiwi 4] [Pear 7]] | input list -d name"#,
252                result: None,
253            },
254        ]
255    }
256}
257
258#[cfg(test)]
259mod test {
260    use super::*;
261
262    #[test]
263    fn test_examples() {
264        use crate::test_examples;
265
266        test_examples(InputList {})
267    }
268}