Configurable built-in prompts (#2064)

* Add ansi, do, and prompt customization

* Fix test

* Cleanups
This commit is contained in:
Jonathan Turner 2020-06-27 10:37:31 +12:00 committed by GitHub
parent 6daec399e6
commit 05781607f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 340 additions and 11 deletions

View file

@ -269,6 +269,7 @@ pub fn create_default_context(
whole_stream_command(Debug),
whole_stream_command(Alias),
whole_stream_command(WithEnv),
whole_stream_command(Do),
// Statistics
whole_stream_command(Size),
whole_stream_command(Count),
@ -303,6 +304,7 @@ pub fn create_default_context(
whole_stream_command(StrToDatetime),
whole_stream_command(StrTrim),
whole_stream_command(BuildString),
whole_stream_command(Ansi),
// Column manipulation
whole_stream_command(Reject),
whole_stream_command(Select),
@ -588,6 +590,7 @@ pub async fn cli(
rl.set_helper(Some(crate::shell::Helper::new(context.clone())));
let config = config::config(Tag::unknown())?;
let use_starship = match config.get("use_starship") {
Some(b) => match b.as_bool() {
Ok(b) => b,
@ -596,7 +599,7 @@ pub async fn cli(
_ => false,
};
let edit_mode = config::config(Tag::unknown())?
let edit_mode = config
.get("edit_mode")
.map(|s| match s.value.expect_string() {
"vi" => EditMode::Vi,
@ -607,21 +610,21 @@ pub async fn cli(
rl.set_edit_mode(edit_mode);
let max_history_size = config::config(Tag::unknown())?
let max_history_size = config
.get("history_size")
.map(|i| i.value.expect_int())
.unwrap_or(100_000);
rl.set_max_history_size(max_history_size as usize);
let key_timeout = config::config(Tag::unknown())?
let key_timeout = config
.get("key_timeout")
.map(|s| s.value.expect_int())
.unwrap_or(1);
rl.set_keyseq_timeout(key_timeout as i32);
let completion_mode = config::config(Tag::unknown())?
let completion_mode = config
.get("completion_mode")
.map(|s| match s.value.expect_string() {
"list" => CompletionType::List,
@ -649,6 +652,48 @@ pub async fn cli(
_ => {}
};
starship::print::get_prompt(starship_context)
} else if let Some(prompt) = config.get("prompt") {
let prompt_line = prompt.as_string()?;
if let Ok(result) = nu_parser::lite_parse(&prompt_line, 0).map_err(ShellError::from)
{
let prompt_block = nu_parser::classify_block(&result, context.registry());
let env = context.get_env();
match run_block(
&prompt_block.block,
&mut context,
InputStream::empty(),
&Value::nothing(),
&IndexMap::new(),
&env,
)
.await
{
Ok(result) => {
context.clear_errors();
match result.collect_string(Tag::unknown()).await {
Ok(string_result) => string_result.item,
Err(_) => {
context.maybe_print_errors(Text::from(prompt_line));
context.clear_errors();
"Error running prompt> ".to_string()
}
}
}
Err(_) => {
context.maybe_print_errors(Text::from(prompt_line));
context.clear_errors();
"Error running prompt> ".to_string()
}
}
} else {
"Error parsing prompt> ".to_string()
}
} else {
format!(
"\x1b[32m{}{}\x1b[m> ",

View file

@ -5,6 +5,7 @@ mod from_delimited_data;
mod to_delimited_data;
pub(crate) mod alias;
pub(crate) mod ansi;
pub(crate) mod append;
pub(crate) mod args;
pub(crate) mod autoview;
@ -23,6 +24,7 @@ pub(crate) mod cp;
pub(crate) mod date;
pub(crate) mod debug;
pub(crate) mod default;
pub(crate) mod do_;
pub(crate) mod drop;
pub(crate) mod du;
pub(crate) mod each;
@ -135,6 +137,7 @@ pub(crate) use command::{
};
pub(crate) use alias::Alias;
pub(crate) use ansi::Ansi;
pub(crate) use append::Append;
pub(crate) use build_string::BuildString;
pub(crate) use cal::Cal;
@ -146,6 +149,7 @@ pub(crate) use cp::Cpy;
pub(crate) use date::Date;
pub(crate) use debug::Debug;
pub(crate) use default::Default;
pub(crate) use do_::Do;
pub(crate) use drop::Drop;
pub(crate) use du::Du;
pub(crate) use each::Each;

View file

@ -0,0 +1,96 @@
use crate::commands::WholeStreamCommand;
use crate::prelude::*;
use nu_errors::ShellError;
use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value};
pub struct Ansi;
#[derive(Deserialize)]
struct AnsiArgs {
color: Value,
}
#[async_trait]
impl WholeStreamCommand for Ansi {
fn name(&self) -> &str {
"ansi"
}
fn signature(&self) -> Signature {
Signature::build("ansi").required(
"color",
SyntaxShape::Any,
"the name of the color to use or 'reset' to reset the color",
)
}
fn usage(&self) -> &str {
"Output ANSI codes to change color"
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Change color to green",
example: r#"ansi green"#,
result: Some(vec![Value::from("\u{1b}[32m")]),
},
Example {
description: "Reset the color",
example: r#"ansi reset"#,
result: Some(vec![Value::from("\u{1b}[0m")]),
},
]
}
async fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
let (AnsiArgs { color }, _) = args.process(&registry).await?;
let color_string = color.as_string()?;
let ansi_code = str_to_ansi_color(color_string);
if let Some(output) = ansi_code {
Ok(OutputStream::one(ReturnSuccess::value(
UntaggedValue::string(output).into_value(color.tag()),
)))
} else {
Err(ShellError::labeled_error(
"Unknown color",
"unknown color",
color.tag(),
))
}
}
}
fn str_to_ansi_color(s: String) -> Option<String> {
match s.as_str() {
"g" | "green" => Some(ansi_term::Color::Green.prefix().to_string()),
"r" | "red" => Some(ansi_term::Color::Red.prefix().to_string()),
"u" | "blue" => Some(ansi_term::Color::Blue.prefix().to_string()),
"b" | "black" => Some(ansi_term::Color::Black.prefix().to_string()),
"y" | "yellow" => Some(ansi_term::Color::Yellow.prefix().to_string()),
"p" | "purple" => Some(ansi_term::Color::Purple.prefix().to_string()),
"c" | "cyan" => Some(ansi_term::Color::Cyan.prefix().to_string()),
"w" | "white" => Some(ansi_term::Color::White.prefix().to_string()),
"reset" => Some("\x1b[0m".to_owned()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::Ansi;
#[test]
fn examples_work_as_expected() {
use crate::examples::test as test_examples;
test_examples(Ansi {})
}
}

View file

@ -214,6 +214,9 @@ fn spawn(
if !is_last {
process.stdout(Stdio::piped());
trace!(target: "nu::run::external", "set up stdout pipe");
process.stderr(Stdio::piped());
trace!(target: "nu::run::external", "set up stderr pipe");
}
// open since we have some contents for stdin
@ -312,6 +315,20 @@ fn spawn(
return Err(());
};
let stderr = if let Some(stderr) = child.stderr.take() {
stderr
} else {
let _ = stdout_read_tx.send(Ok(Value {
value: UntaggedValue::Error(ShellError::labeled_error(
"Can't redirect the stderr for external command",
"can't redirect stderr",
&stdout_name_tag,
)),
tag: stdout_name_tag,
}));
return Err(());
};
let file = futures::io::AllowStdIo::new(stdout);
let stream = FramedRead::new(file, MaybeTextCodec);
@ -365,6 +382,64 @@ fn spawn(
}
}
}
let file = futures::io::AllowStdIo::new(stderr);
let err_stream = FramedRead::new(file, MaybeTextCodec);
for err_line in block_on_stream(err_stream) {
match err_line {
Ok(line) => match line {
StringOrBinary::String(s) => {
let result = stdout_read_tx.send(Ok(Value {
value: UntaggedValue::Error(
ShellError::untagged_runtime_error(s.clone()),
),
tag: stdout_name_tag.clone(),
}));
if result.is_err() {
break;
}
}
StringOrBinary::Binary(_) => {
let result = stdout_read_tx.send(Ok(Value {
value: UntaggedValue::Error(
ShellError::untagged_runtime_error(
"Binary in stderr output",
),
),
tag: stdout_name_tag.clone(),
}));
if result.is_err() {
break;
}
}
},
Err(e) => {
// If there's an exit status, it makes sense that we may error when
// trying to read from its stdout pipe (likely been closed). In that
// case, don't emit an error.
let should_error = match child.wait() {
Ok(exit_status) => !exit_status.success(),
Err(_) => true,
};
if should_error {
let _ = stdout_read_tx.send(Ok(Value {
value: UntaggedValue::Error(ShellError::labeled_error(
format!("Unable to read from stderr ({})", e),
"unable to read from stderr",
&stdout_name_tag,
)),
tag: stdout_name_tag.clone(),
}));
}
return Ok(());
}
}
}
}
// We can give an error when we see a non-zero exit code, but this is different

View file

@ -0,0 +1,109 @@
use crate::commands::classified::block::run_block;
use crate::commands::WholeStreamCommand;
use crate::prelude::*;
use nu_errors::ShellError;
use nu_protocol::{hir::Block, ReturnSuccess, Signature, SyntaxShape, Value};
pub struct Do;
#[derive(Deserialize, Debug)]
struct DoArgs {
block: Block,
ignore_errors: bool,
}
#[async_trait]
impl WholeStreamCommand for Do {
fn name(&self) -> &str {
"do"
}
fn signature(&self) -> Signature {
Signature::build("with-env")
.required("block", SyntaxShape::Block, "the block to run ")
.switch(
"ignore_errors",
"ignore errors as the block runs",
Some('i'),
)
}
fn usage(&self) -> &str {
"Runs a block, optionally ignoring errors"
}
async fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
do_(args, registry).await
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Run the block",
example: r#"do { echo hello }"#,
result: Some(vec![Value::from("hello")]),
},
Example {
description: "Run the block and ignore errors",
example: r#"do -i { thisisnotarealcommand }"#,
result: Some(vec![Value::nothing()]),
},
]
}
}
async fn do_(
raw_args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
let registry = registry.clone();
let mut context = Context::from_raw(&raw_args, &registry);
let scope = raw_args.call_info.scope.clone();
let (
DoArgs {
ignore_errors,
block,
},
input,
) = raw_args.process(&registry).await?;
let result = run_block(
&block,
&mut context,
input,
&scope.it,
&scope.vars,
&scope.env,
)
.await;
if ignore_errors {
match result {
Ok(mut stream) => {
let output = stream.drain_vec().await;
context.clear_errors();
Ok(futures::stream::iter(output).to_output_stream())
}
Err(_) => Ok(OutputStream::one(ReturnSuccess::value(Value::nothing()))),
}
} else {
result.map(|x| x.to_output_stream())
}
}
#[cfg(test)]
mod tests {
use super::Do;
#[test]
fn examples_work_as_expected() {
use crate::examples::test as test_examples;
test_examples(Do {})
}
}

View file

@ -74,7 +74,7 @@ mod tests {
Ok(int(10)),
Ok(int(10)),
Ok(int(10)),
Ok(table(&vec![int(10)])),
Ok(table(&[int(10)])),
Ok(int(10)),
],
},
@ -87,7 +87,7 @@ mod tests {
Ok(int(10)),
Ok(int(30)),
Ok(int(20)),
Ok(table(&vec![int(10), int(20), int(30)])),
Ok(table(&[int(10), int(20), int(30)])),
Ok(int(60)),
],
},
@ -100,7 +100,7 @@ mod tests {
Ok(int(10)),
Ok(decimal(26.5)),
Ok(decimal(26.5)),
Ok(table(&vec![decimal(26.5)])),
Ok(table(&[decimal(26.5)])),
Ok(decimal(63)),
],
},
@ -113,7 +113,7 @@ mod tests {
Ok(int(-14)),
Ok(int(10)),
Ok(int(-11)),
Ok(table(&vec![int(-14), int(-11), int(10)])),
Ok(table(&[int(-14), int(-11), int(10)])),
Ok(int(-15)),
],
},
@ -126,7 +126,7 @@ mod tests {
Ok(decimal(-13.5)),
Ok(int(10)),
Ok(decimal(-11.5)),
Ok(table(&vec![decimal(-13.5), decimal(-11.5), int(10)])),
Ok(table(&[decimal(-13.5), decimal(-11.5), int(10)])),
Ok(decimal(-15)),
],
},
@ -145,8 +145,8 @@ mod tests {
Ok(row!["col1".to_owned() => int(4), "col2".to_owned() => int(8)]),
Ok(row!["col1".to_owned() => decimal(2.5), "col2".to_owned() => decimal(6.5)]),
Ok(row![
"col1".to_owned() => table(&vec![int(1), int(2), int(3), int(4)]),
"col2".to_owned() => table(&vec![int(5), int(6), int(7), int(8)])
"col1".to_owned() => table(&[int(1), int(2), int(3), int(4)]),
"col2".to_owned() => table(&[int(5), int(6), int(7), int(8)])
]),
Ok(row!["col1".to_owned() => int(10), "col2".to_owned() => int(26)]),
],