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 {
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
255struct 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 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}