diff --git a/crates/nu-command/src/platform/ansi/ansi_.rs b/crates/nu-command/src/platform/ansi/ansi_.rs index bd1612ead5..1e3cf77365 100644 --- a/crates/nu-command/src/platform/ansi/ansi_.rs +++ b/crates/nu-command/src/platform/ansi/ansi_.rs @@ -1,13 +1,16 @@ use nu_ansi_term::*; use nu_engine::CallExt; -use nu_protocol::engine::{EngineState, Stack}; -use nu_protocol::record; use nu_protocol::{ - ast::Call, engine::Command, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, - PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, + ast::Call, + engine::Command, + engine::StateWorkingSet, + engine::{EngineState, Stack}, + record, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, + ShellError, Signature, Span, SyntaxShape, Type, Value, }; use once_cell::sync::Lazy; use std::collections::HashMap; +use std::sync::{atomic::AtomicBool, Arc}; #[derive(Clone)] pub struct AnsiCommand; @@ -529,6 +532,10 @@ impl Command for AnsiCommand { .category(Category::Platform) } + fn is_const(&self) -> bool { + true + } + fn usage(&self) -> &str { "Output ANSI codes to change color and style of text." } @@ -632,9 +639,9 @@ Operating system commands: bg: '#ff0000' attr: b } - $"(ansi --escape $bold_blue_on_red)Hello Nu World(ansi reset)""#, + $"(ansi --escape $bold_blue_on_red)Hello, Nu World!(ansi reset)""#, result: Some(Value::test_string( - "\u{1b}[1;48;2;255;0;0;38;2;0;0;255mHello Nu World\u{1b}[0m", + "\u{1b}[1;48;2;255;0;0;38;2;0;0;255mHello, Nu World!\u{1b}[0m", )), }, ] @@ -655,9 +662,10 @@ Operating system commands: let escape: bool = call.has_flag(engine_state, stack, "escape")?; let osc: bool = call.has_flag(engine_state, stack, "osc")?; let use_ansi_coloring = engine_state.get_config().use_ansi_coloring; + let ctrlc = engine_state.ctrlc.clone(); if list { - return generate_ansi_code_list(engine_state, call.head, use_ansi_coloring); + return generate_ansi_code_list(ctrlc, call.head, use_ansi_coloring); } // The code can now be one of the ansi abbreviations like green_bold @@ -673,118 +681,152 @@ Operating system commands: } }; - let param_is_string = matches!(code, Value::String { .. }); - - if escape && osc { - return Err(ShellError::IncompatibleParameters { - left_message: "escape".into(), - left_span: call - .get_named_arg("escape") - .expect("Unexpected missing argument") - .span, - right_message: "osc".into(), - right_span: call - .get_named_arg("osc") - .expect("Unexpected missing argument") - .span, - }); - } - - let code_string = if param_is_string { - code.as_string().expect("error getting code as string") - } else { - "".to_string() - }; - - let param_is_valid_string = param_is_string && !code_string.is_empty(); - - if (escape || osc) && (param_is_valid_string) { - let code_vec: Vec = code_string.chars().collect(); - if code_vec[0] == '\\' { - let span = match call.get_flag_expr("escape") { - Some(expr) => expr.span, - None => call.head, - }; - - return Err(ShellError::TypeMismatch { - err_message: "no need for escape characters".into(), - span, - }); - } - } - - let output = if escape && param_is_valid_string { - format!("\x1b[{code_string}") - } else if osc && param_is_valid_string { - // Operating system command aka osc ESC ] <- note the right brace, not left brace for osc - // OCS's need to end with either: - // bel '\x07' char - // string terminator aka st '\\' char - format!("\x1b]{code_string}") - } else if param_is_valid_string { - // parse hex colors like #00FF00 - if code_string.starts_with('#') { - match nu_color_config::color_from_hex(&code_string) { - Ok(color) => match color { - Some(c) => c.prefix().to_string(), - None => Color::White.prefix().to_string(), - }, - Err(err) => { - return Err(ShellError::GenericError { - error: "error parsing hex color".into(), - msg: format!("{err}"), - span: Some(code.span()), - help: None, - inner: vec![], - }); - } - } - } else { - match str_to_ansi(&code_string) { - Some(c) => c, - None => { - return Err(ShellError::TypeMismatch { - err_message: String::from("Unknown ansi code"), - span: call - .positional_nth(0) - .expect("Unexpected missing argument") - .span, - }) - } - } - } - } else { - // This is a record that should look like - // { fg: "#ff0000" bg: "#00ff00" attr: bli } - let record = code.as_record()?; - // create a NuStyle to parse the information into - let mut nu_style = nu_color_config::NuStyle { - fg: None, - bg: None, - attr: None, - }; - // Iterate and populate NuStyle with real values - for (k, v) in record { - match k.as_str() { - "fg" => nu_style.fg = Some(v.as_string()?), - "bg" => nu_style.bg = Some(v.as_string()?), - "attr" => nu_style.attr = Some(v.as_string()?), - _ => { - return Err(ShellError::IncompatibleParametersSingle { - msg: format!("unknown ANSI format key: expected one of ['fg', 'bg', 'attr'], found '{k}'"), - span: code.span(), - }) - } - } - } - // Now create a nu_ansi_term::Style from the NuStyle - let style = nu_color_config::parse_nustyle(nu_style); - // Return the prefix string. The prefix is the Ansi String. The suffix would be 0m, reset/stop coloring. - style.prefix().to_string() - }; + let output = heavy_lifting(code, escape, osc, call)?; Ok(Value::string(output, call.head).into_pipeline_data()) } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + _input: PipelineData, + ) -> Result { + let list: bool = call.has_flag_const(working_set, "list")?; + let escape: bool = call.has_flag_const(working_set, "escape")?; + let osc: bool = call.has_flag_const(working_set, "osc")?; + let use_ansi_coloring = working_set.get_config().use_ansi_coloring; + let ctrlc = working_set.permanent().ctrlc.clone(); + + if list { + return generate_ansi_code_list(ctrlc, call.head, use_ansi_coloring); + } + + // The code can now be one of the ansi abbreviations like green_bold + // or it can be a record like this: { fg: "#ff0000" bg: "#00ff00" attr: bli } + // this record is defined in nu-color-config crate + let code: Value = match call.opt_const(working_set, 0)? { + Some(c) => c, + None => { + return Err(ShellError::MissingParameter { + param_name: "code".into(), + span: call.head, + }) + } + }; + + let output = heavy_lifting(code, escape, osc, call)?; + + Ok(Value::string(output, call.head).into_pipeline_data()) + } +} + +fn heavy_lifting(code: Value, escape: bool, osc: bool, call: &Call) -> Result { + let param_is_string = matches!(code, Value::String { .. }); + if escape && osc { + return Err(ShellError::IncompatibleParameters { + left_message: "escape".into(), + left_span: call + .get_named_arg("escape") + .expect("Unexpected missing argument") + .span, + right_message: "osc".into(), + right_span: call + .get_named_arg("osc") + .expect("Unexpected missing argument") + .span, + }); + } + let code_string = if param_is_string { + code.as_string().expect("error getting code as string") + } else { + "".to_string() + }; + let param_is_valid_string = param_is_string && !code_string.is_empty(); + if (escape || osc) && (param_is_valid_string) { + let code_vec: Vec = code_string.chars().collect(); + if code_vec[0] == '\\' { + let span = match call.get_flag_expr("escape") { + Some(expr) => expr.span, + None => call.head, + }; + + return Err(ShellError::TypeMismatch { + err_message: "no need for escape characters".into(), + span, + }); + } + } + let output = if escape && param_is_valid_string { + format!("\x1b[{code_string}") + } else if osc && param_is_valid_string { + // Operating system command aka osc ESC ] <- note the right brace, not left brace for osc + // OCS's need to end with either: + // bel '\x07' char + // string terminator aka st '\\' char + format!("\x1b]{code_string}") + } else if param_is_valid_string { + // parse hex colors like #00FF00 + if code_string.starts_with('#') { + match nu_color_config::color_from_hex(&code_string) { + Ok(color) => match color { + Some(c) => c.prefix().to_string(), + None => Color::White.prefix().to_string(), + }, + Err(err) => { + return Err(ShellError::GenericError { + error: "error parsing hex color".into(), + msg: format!("{err}"), + span: Some(code.span()), + help: None, + inner: vec![], + }); + } + } + } else { + match str_to_ansi(&code_string) { + Some(c) => c, + None => { + return Err(ShellError::TypeMismatch { + err_message: String::from("Unknown ansi code"), + span: call + .positional_nth(0) + .expect("Unexpected missing argument") + .span, + }) + } + } + } + } else { + // This is a record that should look like + // { fg: "#ff0000" bg: "#00ff00" attr: bli } + let record = code.as_record()?; + // create a NuStyle to parse the information into + let mut nu_style = nu_color_config::NuStyle { + fg: None, + bg: None, + attr: None, + }; + // Iterate and populate NuStyle with real values + for (k, v) in record { + match k.as_str() { + "fg" => nu_style.fg = Some(v.as_string()?), + "bg" => nu_style.bg = Some(v.as_string()?), + "attr" => nu_style.attr = Some(v.as_string()?), + _ => { + return Err(ShellError::IncompatibleParametersSingle { + msg: format!("unknown ANSI format key: expected one of ['fg', 'bg', 'attr'], found '{k}'"), + span: code.span(), + }) + } + } + } + // Now create a nu_ansi_term::Style from the NuStyle + let style = nu_color_config::parse_nustyle(nu_style); + // Return the prefix string. The prefix is the Ansi String. The suffix would be 0m, reset/stop coloring. + style.prefix().to_string() + }; + Ok(output) } pub fn str_to_ansi(s: &str) -> Option { @@ -792,7 +834,7 @@ pub fn str_to_ansi(s: &str) -> Option { } fn generate_ansi_code_list( - engine_state: &EngineState, + ctrlc: Option>, call_span: Span, use_ansi_coloring: bool, ) -> Result { @@ -827,7 +869,7 @@ fn generate_ansi_code_list( Value::record(record, call_span) }) - .into_pipeline_data(engine_state.ctrlc.clone())); + .into_pipeline_data(ctrlc)); } fn build_ansi_hashmap(v: &[AnsiCode]) -> HashMap<&str, &str> { diff --git a/crates/nu-command/tests/commands/move_/umv.rs b/crates/nu-command/tests/commands/move_/umv.rs index 95f9927e50..5b1e947e9f 100644 --- a/crates/nu-command/tests/commands/move_/umv.rs +++ b/crates/nu-command/tests/commands/move_/umv.rs @@ -283,7 +283,7 @@ fn errors_if_moving_to_itself() { "umv mydir mydir/mydir_2/" ); assert!(actual.err.contains("cannot move")); - assert!(actual.err.contains("to a subdirectory of")); + assert!(actual.err.contains("to a subdirectory")); }); } diff --git a/crates/nu-engine/src/call_ext.rs b/crates/nu-engine/src/call_ext.rs index 3e8c77ca0e..54b5e7881d 100644 --- a/crates/nu-engine/src/call_ext.rs +++ b/crates/nu-engine/src/call_ext.rs @@ -1,6 +1,7 @@ use nu_protocol::{ ast::Call, - engine::{EngineState, Stack}, + engine::{EngineState, Stack, StateWorkingSet}, + eval_const::eval_constant, FromValue, ShellError, Value, }; @@ -36,6 +37,12 @@ pub trait CallExt { pos: usize, ) -> Result, ShellError>; + fn opt_const( + &self, + working_set: &StateWorkingSet, + pos: usize, + ) -> Result, ShellError>; + fn req( &self, engine_state: &EngineState, @@ -126,6 +133,19 @@ impl CallExt for Call { } } + fn opt_const( + &self, + working_set: &StateWorkingSet, + pos: usize, + ) -> Result, ShellError> { + if let Some(expr) = self.positional_nth(pos) { + let result = eval_constant(working_set, expr)?; + FromValue::from_value(result).map(Some) + } else { + Ok(None) + } + } + fn req( &self, engine_state: &EngineState,