nushell/crates/nu-command/src/platform/input/list.rs
Darren Schroeder 88acc11501
add input_output type to input list to return string (#9557)
# Description

This PR fixes a bug that @amtoine found. It adds an input_output type so
that `input list`'s signature supports `string` as an output.

## Before

![image](https://github.com/nushell/nushell/assets/343840/7ee2b672-9976-4c69-a9a2-686ddbd3a60d)
```
Signatures:
  list<any> | input list <string?> -> list<any>
```

## After

![image](https://github.com/nushell/nushell/assets/343840/f86747cf-a134-4bb0-b89c-2e28f590f3c3)
```
Signatures:
  list<any> | input list <string?> -> list<any>
  list<string> | input list <string?> -> <string>
```

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A
clippy::needless_collect -A clippy::result_large_err` to check that
you're using the standard code style
- `cargo test --workspace` to check that all tests pass
- `cargo run -- crates/nu-std/tests/run.nu` to run the tests for the
standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
2023-06-29 08:27:25 -05:00

287 lines
10 KiB
Rust

use dialoguer::{console::Term, Select};
use dialoguer::{FuzzySelect, MultiSelect};
use nu_ansi_term::Color;
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Type,
Value,
};
use std::fmt::{Display, Formatter};
enum InteractMode {
Single(Option<usize>),
Multi(Option<Vec<usize>>),
}
#[derive(Clone)]
struct Options {
name: String,
value: Value,
}
impl Display for Options {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
}
#[derive(Clone)]
pub struct InputList;
const INTERACT_ERROR: &str = "Interact error, could not process options";
impl Command for InputList {
fn name(&self) -> &str {
"input list"
}
fn signature(&self) -> Signature {
Signature::build("input list")
.input_output_types(vec![
(
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::Any)),
),
(Type::List(Box::new(Type::String)), Type::String),
])
.optional("prompt", SyntaxShape::String, "the prompt to display")
.switch(
"multi",
"Use multiple results, you can press a to toggle all options on/off",
Some('m'),
)
.switch("fuzzy", "Use a fuzzy select.", Some('f'))
.allow_variants_without_examples(true)
.category(Category::Platform)
}
fn usage(&self) -> &str {
"Interactive list selection."
}
fn extra_usage(&self) -> &str {
"Abort with esc or q."
}
fn search_terms(&self) -> Vec<&str> {
vec!["prompt", "ask", "menu"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let prompt: Option<String> = call.opt(engine_state, stack, 0)?;
let options: Vec<Options> = match input {
PipelineData::Value(Value::Range { .. }, ..)
| PipelineData::Value(Value::List { .. }, ..)
| PipelineData::ListStream { .. }
| PipelineData::Value(Value::Record { .. }, ..) => {
let mut lentable = Vec::<usize>::new();
let rows = input.into_iter().collect::<Vec<_>>();
rows.iter().for_each(|row| {
if let Ok(record) = row.as_record() {
let columns = record.1.len();
for (i, (col, val)) in record.0.iter().zip(record.1.iter()).enumerate() {
if i == columns - 1 {
break;
}
if let Ok(val) = val.as_string() {
let len = nu_utils::strip_ansi_likely(&val).len()
+ nu_utils::strip_ansi_likely(col).len();
if let Some(max_len) = lentable.get(i) {
lentable[i] = (*max_len).max(len);
} else {
lentable.push(len);
}
}
}
}
});
rows.into_iter()
.map_while(move |x| {
if let Ok(val) = x.as_string() {
Some(Options {
name: val,
value: x,
})
} else if let Ok(record) = x.as_record() {
let mut options = Vec::new();
let columns = record.1.len();
for (i, (col, val)) in record.0.iter().zip(record.1.iter()).enumerate()
{
if let Ok(val) = val.as_string() {
let len = nu_utils::strip_ansi_likely(&val).len()
+ nu_utils::strip_ansi_likely(col).len();
options.push(format!(
" {}{}{}: {}{}",
Color::Cyan.prefix(),
col,
Color::Cyan.suffix(),
&val,
if i == columns - 1 {
String::from("")
} else {
format!(
"{} |",
" ".repeat(
lentable
.get(i)
.cloned()
.unwrap_or_default()
.saturating_sub(len)
)
)
}
));
}
}
Some(Options {
name: options.join(""),
value: x,
})
} else {
None
}
})
.collect()
}
_ => {
return Err(ShellError::TypeMismatch {
err_message: "expected a list or table".to_string(),
span: head,
})
}
};
let prompt = prompt.unwrap_or_default();
if options.is_empty() {
return Err(ShellError::TypeMismatch {
err_message: "expected a list or table, it can also be a problem with the an inner type of your list.".to_string(),
span: head,
});
}
if call.has_flag("multi") && call.has_flag("fuzzy") {
return Err(ShellError::TypeMismatch {
err_message: "Fuzzy search is not supported for multi select".to_string(),
span: head,
});
}
// could potentially be used to map the use theme colors at some point
// let theme = dialoguer::theme::ColorfulTheme {
// active_item_style: Style::new().fg(Color::Cyan).bold(),
// ..Default::default()
// };
let ans: InteractMode = if call.has_flag("multi") {
let mut multi_select = MultiSelect::new(); //::with_theme(&theme);
InteractMode::Multi(
if !prompt.is_empty() {
multi_select.with_prompt(&prompt)
} else {
&mut multi_select
}
.items(&options)
.report(false)
.interact_on_opt(&Term::stderr())
.map_err(|err| ShellError::IOError(format!("{}: {}", INTERACT_ERROR, err)))?,
)
} else if call.has_flag("fuzzy") {
let mut fuzzy_select = FuzzySelect::new(); //::with_theme(&theme);
InteractMode::Single(
if !prompt.is_empty() {
fuzzy_select.with_prompt(&prompt)
} else {
&mut fuzzy_select
}
.items(&options)
.default(0)
.report(false)
.interact_on_opt(&Term::stderr())
.map_err(|err| ShellError::IOError(format!("{}: {}", INTERACT_ERROR, err)))?,
)
} else {
let mut select = Select::new(); //::with_theme(&theme);
InteractMode::Single(
if !prompt.is_empty() {
select.with_prompt(&prompt)
} else {
&mut select
}
.items(&options)
.default(0)
.report(false)
.interact_on_opt(&Term::stderr())
.map_err(|err| ShellError::IOError(format!("{}: {}", INTERACT_ERROR, err)))?,
)
};
Ok(match ans {
InteractMode::Multi(res) => match res {
Some(opts) => Value::List {
vals: opts.iter().map(|s| options[*s].value.clone()).collect(),
span: head,
},
None => Value::List {
vals: vec![],
span: head,
},
},
InteractMode::Single(res) => match res {
Some(opt) => options[opt].value.clone(),
None => Value::String {
val: "".to_string(),
span: head,
},
},
}
.into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Return a single value from a list",
example: r#"[1 2 3 4 5] | input list 'Rate it'"#,
result: None,
},
Example {
description: "Return multiple values from a list",
example: r#"[Banana Kiwi Pear Peach Strawberry] | input list -m 'Add fruits to the basket'"#,
result: None,
},
Example {
description: "Return a single record from a table with fuzzy search",
example: r#"ls | input list -f 'Select the target'"#,
result: None,
},
]
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(InputList {})
}
}