nu_command/platform/input/
list.rs1use 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 {
133 let multi_select = MultiSelect::new(); 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(); 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(); 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}