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        let answer: InteractMode = if multi {
127            let multi_select = MultiSelect::with_theme(&NuTheme);
128
129            InteractMode::Multi(
130                if let Some(prompt) = prompt {
131                    multi_select.with_prompt(&prompt)
132                } else {
133                    multi_select
134                }
135                .items(&options)
136                .report(false)
137                .interact_on_opt(&Term::stderr())
138                .map_err(|dialoguer::Error::IO(err)| {
139                    IoError::new_with_additional_context(err, call.head, None, INTERACT_ERROR)
140                })?,
141            )
142        } else if fuzzy {
143            let fuzzy_select = FuzzySelect::with_theme(&NuTheme);
144
145            InteractMode::Single(
146                if let Some(prompt) = prompt {
147                    fuzzy_select.with_prompt(&prompt)
148                } else {
149                    fuzzy_select
150                }
151                .items(&options)
152                .default(0)
153                .report(false)
154                .interact_on_opt(&Term::stderr())
155                .map_err(|dialoguer::Error::IO(err)| {
156                    IoError::new_with_additional_context(err, call.head, None, INTERACT_ERROR)
157                })?,
158            )
159        } else {
160            let select = Select::with_theme(&NuTheme);
161            InteractMode::Single(
162                if let Some(prompt) = prompt {
163                    select.with_prompt(&prompt)
164                } else {
165                    select
166                }
167                .items(&options)
168                .default(0)
169                .report(false)
170                .interact_on_opt(&Term::stderr())
171                .map_err(|dialoguer::Error::IO(err)| {
172                    IoError::new_with_additional_context(err, call.head, None, INTERACT_ERROR)
173                })?,
174            )
175        };
176
177        Ok(match answer {
178            InteractMode::Multi(res) => {
179                if index {
180                    match res {
181                        Some(opts) => Value::list(
182                            opts.into_iter()
183                                .map(|s| Value::int(s as i64, head))
184                                .collect(),
185                            head,
186                        ),
187                        None => Value::nothing(head),
188                    }
189                } else {
190                    match res {
191                        Some(opts) => Value::list(
192                            opts.iter().map(|s| options[*s].value.clone()).collect(),
193                            head,
194                        ),
195                        None => Value::nothing(head),
196                    }
197                }
198            }
199            InteractMode::Single(res) => {
200                if index {
201                    match res {
202                        Some(opt) => Value::int(opt as i64, head),
203                        None => Value::nothing(head),
204                    }
205                } else {
206                    match res {
207                        Some(opt) => options[opt].value.clone(),
208                        None => Value::nothing(head),
209                    }
210                }
211            }
212        }
213        .into_pipeline_data())
214    }
215
216    fn examples(&self) -> Vec<Example<'_>> {
217        vec![
218            Example {
219                description: "Return a single value from a list",
220                example: r#"[1 2 3 4 5] | input list 'Rate it'"#,
221                result: None,
222            },
223            Example {
224                description: "Return multiple values from a list",
225                example: r#"[Banana Kiwi Pear Peach Strawberry] | input list --multi 'Add fruits to the basket'"#,
226                result: None,
227            },
228            Example {
229                description: "Return a single record from a table with fuzzy search",
230                example: r#"ls | input list --fuzzy 'Select the target'"#,
231                result: None,
232            },
233            Example {
234                description: "Choose an item from a range",
235                example: r#"1..10 | input list"#,
236                result: None,
237            },
238            Example {
239                description: "Return the index of a selected item",
240                example: r#"[Banana Kiwi Pear Peach Strawberry] | input list --index"#,
241                result: None,
242            },
243            Example {
244                description: "Choose an item from a table using a column as display value",
245                example: r#"[[name price]; [Banana 12] [Kiwi 4] [Pear 7]] | input list -d name"#,
246                result: None,
247            },
248        ]
249    }
250}
251
252use dialoguer::theme::{SimpleTheme, Theme};
253use nu_ansi_term::ansi::RESET;
254
255// could potentially be used to map the use theme colors at some point
256
257/// Theme for handling already colored items gracefully.
258struct NuTheme;
259
260impl Theme for NuTheme {
261    fn format_select_prompt_item(
262        &self,
263        f: &mut dyn std::fmt::Write,
264        text: &str,
265        active: bool,
266    ) -> std::fmt::Result {
267        SimpleTheme.format_select_prompt_item(f, text, active)?;
268        write!(f, "{RESET}")
269    }
270
271    fn format_multi_select_prompt_item(
272        &self,
273        f: &mut dyn std::fmt::Write,
274        text: &str,
275        checked: bool,
276        active: bool,
277    ) -> std::fmt::Result {
278        SimpleTheme.format_multi_select_prompt_item(f, text, checked, active)?;
279        write!(f, "{RESET}")
280    }
281
282    fn format_sort_prompt_item(
283        &self,
284        f: &mut dyn std::fmt::Write,
285        text: &str,
286        picked: bool,
287        active: bool,
288    ) -> std::fmt::Result {
289        SimpleTheme.format_sort_prompt_item(f, text, picked, active)?;
290        writeln!(f, "{RESET}")
291    }
292
293    fn format_fuzzy_select_prompt_item(
294        &self,
295        f: &mut dyn std::fmt::Write,
296        text: &str,
297        active: bool,
298        highlight_matches: bool,
299        matcher: &fuzzy_matcher::skim::SkimMatcherV2,
300        search_term: &str,
301    ) -> std::fmt::Result {
302        use fuzzy_matcher::FuzzyMatcher;
303        write!(f, "{} ", if active { ">" } else { " " })?;
304
305        if !highlight_matches {
306            return write!(f, "{text}{RESET}");
307        }
308        let Some((_score, indices)) = matcher.fuzzy_indices(text, search_term) else {
309            return write!(f, "{text}{RESET}");
310        };
311        let prefix = nu_ansi_term::Style::new()
312            .italic()
313            .underline()
314            .prefix()
315            .to_string();
316        // HACK: Reset italic and underline, from the `ansi` command, should be moved to `nu_ansi_term`
317        let suffix = "\x1b[23;24m";
318
319        for (idx, c) in text.chars().enumerate() {
320            if indices.contains(&idx) {
321                write!(f, "{prefix}{c}{suffix}")?;
322            } else {
323                write!(f, "{c}")?;
324            }
325        }
326        write!(f, "{RESET}")
327    }
328}
329
330#[cfg(test)]
331mod test {
332    use super::*;
333
334    #[test]
335    fn test_examples() {
336        use crate::test_examples;
337
338        test_examples(InputList {})
339    }
340}