From e289630920307b2a475fd810425d8ad2bca72928 Mon Sep 17 00:00:00 2001 From: Matthew Auld <32310590+matthewauld@users.noreply.github.com> Date: Wed, 15 Dec 2021 18:06:35 -0500 Subject: [PATCH] Porting 'ansi' command from nushell to engine-q (#494) * Porting 'ansi' command from nushell to engine-q * Added StrCollect to example_test.rs to allow example tests to run * Run 'cargo fmt' to fix formatting * Update command.rs * Update command.rs * Update command.rs * Added a category Co-authored-by: JT <547158+jntrnr@users.noreply.github.com> --- crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/example_test.rs | 3 +- .../nu-command/src/platform/ansi/command.rs | 355 ++++++++++++++++++ crates/nu-command/src/platform/ansi/mod.rs | 3 + crates/nu-command/src/platform/mod.rs | 2 + 5 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 crates/nu-command/src/platform/ansi/command.rs create mode 100644 crates/nu-command/src/platform/ansi/mod.rs diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 671421e668..6ce9e5a247 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -129,6 +129,7 @@ pub fn create_default_context() -> EngineState { StrSubstring, StrTrim, StrUpcase, + Ansi }; // FileSystem diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs index 55df0f01e8..f2a7bc920f 100644 --- a/crates/nu-command/src/example_test.rs +++ b/crates/nu-command/src/example_test.rs @@ -7,7 +7,7 @@ use nu_protocol::{ use crate::To; -use super::{Date, From, Into, Math, Path, Random, Split, Str, Url}; +use super::{Date, From, Into, Math, Path, Random, Split, Str, StrCollect, Url}; pub fn test_examples(cmd: impl Command + 'static) { let examples = cmd.examples(); @@ -27,6 +27,7 @@ pub fn test_examples(cmd: impl Command + 'static) { working_set.add_decl(Box::new(Path)); working_set.add_decl(Box::new(Date)); working_set.add_decl(Box::new(Url)); + working_set.add_decl(Box::new(StrCollect)); use super::Echo; working_set.add_decl(Box::new(Echo)); diff --git a/crates/nu-command/src/platform/ansi/command.rs b/crates/nu-command/src/platform/ansi/command.rs new file mode 100644 index 0000000000..320abd7676 --- /dev/null +++ b/crates/nu-command/src/platform/ansi/command.rs @@ -0,0 +1,355 @@ +use nu_ansi_term::*; +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, engine::Command, Category, Example, IntoPipelineData, PipelineData, ShellError, + Signature, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct AnsiCommand; + +impl Command for AnsiCommand { + fn name(&self) -> &str { + "ansi" + } + + fn signature(&self) -> Signature { + Signature::build("ansi") + .required( + "code", + SyntaxShape::String, + "the name of the code to use like 'green' or 'reset' to reset the color", + ) + .switch( + "escape", // \x1b[ + "escape sequence without the escape character(s)", + Some('e'), + ) + .switch( + "osc", // \x1b] + "operating system command (ocs) escape sequence without the escape character(s)", + Some('o'), + ) + .category(Category::Platform) + } + + fn usage(&self) -> &str { + "Output ANSI codes to change color." + } + + fn extra_usage(&self) -> &str { + r#"For escape sequences: +Escape: '\x1b[' is not required for --escape parameter +Format: #(;#)m +Example: 1;31m for bold red or 2;37;41m for dimmed white fg with red bg +There can be multiple text formatting sequence numbers +separated by a ; and ending with an m where the # is of the +following values: + attributes + 0 reset / normal display + 1 bold or increased intensity + 2 faint or decreased intensity + 3 italic on (non-mono font) + 4 underline on + 5 slow blink on + 6 fast blink on + 7 reverse video on + 8 nondisplayed (invisible) on + 9 strike-through on + + foreground/bright colors background/bright colors + 30/90 black 40/100 black + 31/91 red 41/101 red + 32/92 green 42/102 green + 33/93 yellow 43/103 yellow + 34/94 blue 44/104 blue + 35/95 magenta 45/105 magenta + 36/96 cyan 46/106 cyan + 37/97 white 47/107 white + https://en.wikipedia.org/wiki/ANSI_escape_code + +OSC: '\x1b]' is not required for --osc parameter +Example: echo [(ansi -o '0') 'some title' (char bel)] | str collect +Format: # + 0 Set window title and icon name + 1 Set icon name + 2 Set window title + 4 Set/read color palette + 9 iTerm2 Grown notifications + 10 Set foreground color (x11 color spec) + 11 Set background color (x11 color spec) + ... others"# + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Change color to green", + example: r#"ansi green"#, + result: Some(Value::test_string("\u{1b}[32m")), + }, + Example { + description: "Reset the color", + example: r#"ansi reset"#, + result: Some(Value::test_string("\u{1b}[0m")), + }, + Example { + description: + "Use ansi to color text (rb = red bold, gb = green bold, pb = purple bold)", + example: r#"echo [(ansi rb) Hello " " (ansi gb) Nu " " (ansi pb) World] | str collect"#, + result: Some(Value::test_string( + "\u{1b}[1;31mHello \u{1b}[1;32mNu \u{1b}[1;35mWorld", + )), + }, + Example { + description: + "Use ansi to color text (rb = red bold, gb = green bold, pb = purple bold)", + example: r#"echo [(ansi -e '3;93;41m') Hello (ansi reset) " " (ansi gb) Nu " " (ansi pb) World] | str collect"#, + result: Some(Value::test_string( + "\u{1b}[3;93;41mHello\u{1b}[0m \u{1b}[1;32mNu \u{1b}[1;35mWorld", + )), + }, + ] + } + + fn run( + &self, + engine_state: &nu_protocol::engine::EngineState, + stack: &mut nu_protocol::engine::Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let escape: bool = call.has_flag("escape"); + let osc: bool = call.has_flag("osc"); + let code: String = call.req(engine_state, stack, 0)?; + 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, + }); + } + if escape || osc { + let code_vec: Vec = code.chars().collect(); + if code_vec[0] == '\\' { + return Err(ShellError::UnsupportedInput( + String::from("no need for escape characters"), + call.get_flag_expr("escape") + .expect("Unexpected missing argument") + .span, + )); + } + } + let output = if escape { + format!("\x1b[{}", code) + } else if osc { + //Operating system command aka osc ESC ] <- note the right brace, not left brace for osc + // OCS's need to end with a bell '\x07' char + format!("\x1b]{};", code) + } else { + match str_to_ansi(&code) { + Some(c) => c, + None => { + return Err(ShellError::UnsupportedInput( + String::from("Unknown ansi code"), + call.nth(0).expect("Unexpected missing argument").span, + )) + } + } + }; + Ok(Value::string(output, call.head).into_pipeline_data()) + } +} + +pub fn str_to_ansi(s: &str) -> Option { + match s { + "g" | "green" => Some(Color::Green.prefix().to_string()), + "gb" | "green_bold" => Some(Color::Green.bold().prefix().to_string()), + "gu" | "green_underline" => Some(Color::Green.underline().prefix().to_string()), + "gi" | "green_italic" => Some(Color::Green.italic().prefix().to_string()), + "gd" | "green_dimmed" => Some(Color::Green.dimmed().prefix().to_string()), + "gr" | "green_reverse" => Some(Color::Green.reverse().prefix().to_string()), + + "lg" | "light_green" => Some(Color::LightGreen.prefix().to_string()), + "lgb" | "light_green_bold" => Some(Color::LightGreen.bold().prefix().to_string()), + "lgu" | "light_green_underline" => Some(Color::LightGreen.underline().prefix().to_string()), + "lgi" | "light_green_italic" => Some(Color::LightGreen.italic().prefix().to_string()), + "lgd" | "light_green_dimmed" => Some(Color::LightGreen.dimmed().prefix().to_string()), + "lgr" | "light_green_reverse" => Some(Color::LightGreen.reverse().prefix().to_string()), + + "r" | "red" => Some(Color::Red.prefix().to_string()), + "rb" | "red_bold" => Some(Color::Red.bold().prefix().to_string()), + "ru" | "red_underline" => Some(Color::Red.underline().prefix().to_string()), + "ri" | "red_italic" => Some(Color::Red.italic().prefix().to_string()), + "rd" | "red_dimmed" => Some(Color::Red.dimmed().prefix().to_string()), + "rr" | "red_reverse" => Some(Color::Red.reverse().prefix().to_string()), + + "lr" | "light_red" => Some(Color::LightRed.prefix().to_string()), + "lrb" | "light_red_bold" => Some(Color::LightRed.bold().prefix().to_string()), + "lru" | "light_red_underline" => Some(Color::LightRed.underline().prefix().to_string()), + "lri" | "light_red_italic" => Some(Color::LightRed.italic().prefix().to_string()), + "lrd" | "light_red_dimmed" => Some(Color::LightRed.dimmed().prefix().to_string()), + "lrr" | "light_red_reverse" => Some(Color::LightRed.reverse().prefix().to_string()), + + "u" | "blue" => Some(Color::Blue.prefix().to_string()), + "ub" | "blue_bold" => Some(Color::Blue.bold().prefix().to_string()), + "uu" | "blue_underline" => Some(Color::Blue.underline().prefix().to_string()), + "ui" | "blue_italic" => Some(Color::Blue.italic().prefix().to_string()), + "ud" | "blue_dimmed" => Some(Color::Blue.dimmed().prefix().to_string()), + "ur" | "blue_reverse" => Some(Color::Blue.reverse().prefix().to_string()), + + "lu" | "light_blue" => Some(Color::LightBlue.prefix().to_string()), + "lub" | "light_blue_bold" => Some(Color::LightBlue.bold().prefix().to_string()), + "luu" | "light_blue_underline" => Some(Color::LightBlue.underline().prefix().to_string()), + "lui" | "light_blue_italic" => Some(Color::LightBlue.italic().prefix().to_string()), + "lud" | "light_blue_dimmed" => Some(Color::LightBlue.dimmed().prefix().to_string()), + "lur" | "light_blue_reverse" => Some(Color::LightBlue.reverse().prefix().to_string()), + + "b" | "black" => Some(Color::Black.prefix().to_string()), + "bb" | "black_bold" => Some(Color::Black.bold().prefix().to_string()), + "bu" | "black_underline" => Some(Color::Black.underline().prefix().to_string()), + "bi" | "black_italic" => Some(Color::Black.italic().prefix().to_string()), + "bd" | "black_dimmed" => Some(Color::Black.dimmed().prefix().to_string()), + "br" | "black_reverse" => Some(Color::Black.reverse().prefix().to_string()), + + "ligr" | "light_gray" => Some(Color::LightGray.prefix().to_string()), + "ligrb" | "light_gray_bold" => Some(Color::LightGray.bold().prefix().to_string()), + "ligru" | "light_gray_underline" => Some(Color::LightGray.underline().prefix().to_string()), + "ligri" | "light_gray_italic" => Some(Color::LightGray.italic().prefix().to_string()), + "ligrd" | "light_gray_dimmed" => Some(Color::LightGray.dimmed().prefix().to_string()), + "ligrr" | "light_gray_reverse" => Some(Color::LightGray.reverse().prefix().to_string()), + + "y" | "yellow" => Some(Color::Yellow.prefix().to_string()), + "yb" | "yellow_bold" => Some(Color::Yellow.bold().prefix().to_string()), + "yu" | "yellow_underline" => Some(Color::Yellow.underline().prefix().to_string()), + "yi" | "yellow_italic" => Some(Color::Yellow.italic().prefix().to_string()), + "yd" | "yellow_dimmed" => Some(Color::Yellow.dimmed().prefix().to_string()), + "yr" | "yellow_reverse" => Some(Color::Yellow.reverse().prefix().to_string()), + + "ly" | "light_yellow" => Some(Color::LightYellow.prefix().to_string()), + "lyb" | "light_yellow_bold" => Some(Color::LightYellow.bold().prefix().to_string()), + "lyu" | "light_yellow_underline" => { + Some(Color::LightYellow.underline().prefix().to_string()) + } + "lyi" | "light_yellow_italic" => Some(Color::LightYellow.italic().prefix().to_string()), + "lyd" | "light_yellow_dimmed" => Some(Color::LightYellow.dimmed().prefix().to_string()), + "lyr" | "light_yellow_reverse" => Some(Color::LightYellow.reverse().prefix().to_string()), + + "p" | "purple" => Some(Color::Purple.prefix().to_string()), + "pb" | "purple_bold" => Some(Color::Purple.bold().prefix().to_string()), + "pu" | "purple_underline" => Some(Color::Purple.underline().prefix().to_string()), + "pi" | "purple_italic" => Some(Color::Purple.italic().prefix().to_string()), + "pd" | "purple_dimmed" => Some(Color::Purple.dimmed().prefix().to_string()), + "pr" | "purple_reverse" => Some(Color::Purple.reverse().prefix().to_string()), + + "lp" | "light_purple" => Some(Color::LightPurple.prefix().to_string()), + "lpb" | "light_purple_bold" => Some(Color::LightPurple.bold().prefix().to_string()), + "lpu" | "light_purple_underline" => { + Some(Color::LightPurple.underline().prefix().to_string()) + } + "lpi" | "light_purple_italic" => Some(Color::LightPurple.italic().prefix().to_string()), + "lpd" | "light_purple_dimmed" => Some(Color::LightPurple.dimmed().prefix().to_string()), + "lpr" | "light_purple_reverse" => Some(Color::LightPurple.reverse().prefix().to_string()), + + "c" | "cyan" => Some(Color::Cyan.prefix().to_string()), + "cb" | "cyan_bold" => Some(Color::Cyan.bold().prefix().to_string()), + "cu" | "cyan_underline" => Some(Color::Cyan.underline().prefix().to_string()), + "ci" | "cyan_italic" => Some(Color::Cyan.italic().prefix().to_string()), + "cd" | "cyan_dimmed" => Some(Color::Cyan.dimmed().prefix().to_string()), + "cr" | "cyan_reverse" => Some(Color::Cyan.reverse().prefix().to_string()), + + "lc" | "light_cyan" => Some(Color::LightCyan.prefix().to_string()), + "lcb" | "light_cyan_bold" => Some(Color::LightCyan.bold().prefix().to_string()), + "lcu" | "light_cyan_underline" => Some(Color::LightCyan.underline().prefix().to_string()), + "lci" | "light_cyan_italic" => Some(Color::LightCyan.italic().prefix().to_string()), + "lcd" | "light_cyan_dimmed" => Some(Color::LightCyan.dimmed().prefix().to_string()), + "lcr" | "light_cyan_reverse" => Some(Color::LightCyan.reverse().prefix().to_string()), + + "w" | "white" => Some(Color::White.prefix().to_string()), + "wb" | "white_bold" => Some(Color::White.bold().prefix().to_string()), + "wu" | "white_underline" => Some(Color::White.underline().prefix().to_string()), + "wi" | "white_italic" => Some(Color::White.italic().prefix().to_string()), + "wd" | "white_dimmed" => Some(Color::White.dimmed().prefix().to_string()), + "wr" | "white_reverse" => Some(Color::White.reverse().prefix().to_string()), + + "dgr" | "dark_gray" => Some(Color::DarkGray.prefix().to_string()), + "dgrb" | "dark_gray_bold" => Some(Color::DarkGray.bold().prefix().to_string()), + "dgru" | "dark_gray_underline" => Some(Color::DarkGray.underline().prefix().to_string()), + "dgri" | "dark_gray_italic" => Some(Color::DarkGray.italic().prefix().to_string()), + "dgrd" | "dark_gray_dimmed" => Some(Color::DarkGray.dimmed().prefix().to_string()), + "dgrr" | "dark_gray_reverse" => Some(Color::DarkGray.reverse().prefix().to_string()), + + "reset" => Some("\x1b[0m".to_owned()), + + // Reference for ansi codes https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 + // Another good reference http://ascii-table.com/ansi-escape-sequences.php + + // For setting title like `echo [(char title) (pwd) (char bel)] | str collect` + "title" => Some("\x1b]2;".to_string()), // ESC]2; xterm sets window title using OSC syntax escapes + + // Ansi Erase Sequences + "clear_screen" => Some("\x1b[J".to_string()), // clears the screen + "clear_screen_from_cursor_to_end" => Some("\x1b[0J".to_string()), // clears from cursor until end of screen + "clear_screen_from_cursor_to_beginning" => Some("\x1b[1J".to_string()), // clears from cursor to beginning of screen + "cls" | "clear_entire_screen" => Some("\x1b[2J".to_string()), // clears the entire screen + "erase_line" => Some("\x1b[K".to_string()), // clears the current line + "erase_line_from_cursor_to_end" => Some("\x1b[0K".to_string()), // clears from cursor to end of line + "erase_line_from_cursor_to_beginning" => Some("\x1b[1K".to_string()), // clears from cursor to start of line + "erase_entire_line" => Some("\x1b[2K".to_string()), // clears entire line + + // Turn on/off cursor + "cursor_off" => Some("\x1b[?25l".to_string()), + "cursor_on" => Some("\x1b[?25h".to_string()), + + // Turn on/off blinking + "cursor_blink_off" => Some("\x1b[?12l".to_string()), + "cursor_blink_on" => Some("\x1b[?12h".to_string()), + + // Cursor position in ESC [ ;R where r = row and c = column + "cursor_position" => Some("\x1b[6n".to_string()), + + // Report Terminal Identity + "identity" => Some("\x1b[0c".to_string()), + + // Ansi escape only - CSI command + "csi" | "escape" | "escape_left" => Some("\x1b[".to_string()), + // OSC escape (Operating system command) + "osc" | "escape_right" => Some("\x1b]".to_string()), + // OSC string terminator + "string_terminator" | "st" | "str_term" => Some("\x1b\\".to_string()), + + // Ansi Rgb - Needs to be 32;2;r;g;b or 48;2;r;g;b + // assuming the rgb will be passed via command and no here + "rgb_fg" => Some("\x1b[38;2;".to_string()), + "rgb_bg" => Some("\x1b[48;2;".to_string()), + + // Ansi color index - Needs 38;5;idx or 48;5;idx where idx = 0 to 255 + "idx_fg" | "color_idx_fg" => Some("\x1b[38;5;".to_string()), + "idx_bg" | "color_idx_bg" => Some("\x1b[48;5;".to_string()), + + // Returns terminal size like "[;R" where r is rows and c is columns + // This should work assuming your terminal is not greater than 999x999 + "size" => Some("\x1b[s\x1b[999;999H\x1b[6n\x1b[u".to_string()), + + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::AnsiCommand; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + + test_examples(AnsiCommand {}) + } +} diff --git a/crates/nu-command/src/platform/ansi/mod.rs b/crates/nu-command/src/platform/ansi/mod.rs new file mode 100644 index 0000000000..ae64fe844b --- /dev/null +++ b/crates/nu-command/src/platform/ansi/mod.rs @@ -0,0 +1,3 @@ +mod command; + +pub use command::AnsiCommand as Ansi; diff --git a/crates/nu-command/src/platform/mod.rs b/crates/nu-command/src/platform/mod.rs index bb50c304f3..baea7b2a61 100644 --- a/crates/nu-command/src/platform/mod.rs +++ b/crates/nu-command/src/platform/mod.rs @@ -1,7 +1,9 @@ +mod ansi; mod clear; mod kill; mod sleep; +pub use ansi::Ansi; pub use clear::Clear; pub use kill::Kill; pub use sleep::Sleep;