diff --git a/Cargo.lock b/Cargo.lock index cf05fa954..ea9de5c2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3272,6 +3272,8 @@ name = "nu-ansi-term" version = "0.32.1" dependencies = [ "doc-comment", + "itertools", + "overload", "regex 1.5.4", "serde 1.0.126", "serde_json", @@ -4294,6 +4296,12 @@ dependencies = [ "num-traits 0.2.14", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking" version = "2.0.0" diff --git a/crates/nu-ansi-term/Cargo.toml b/crates/nu-ansi-term/Cargo.toml index 453f8e3b2..591b10697 100644 --- a/crates/nu-ansi-term/Cargo.toml +++ b/crates/nu-ansi-term/Cargo.toml @@ -18,10 +18,15 @@ doctest = false [features] derive_serde_style = ["serde"] -[dependencies.serde] -version = "1.0.90" -features = ["derive"] -optional = true +[dependencies] +overload = "0.1.1" +serde = { version = "1.0.90", features = ["derive"], optional = true } +itertools = "0.10.0" + +# [dependencies.serde] +# version = "1.0.90" +# features = ["derive"] +# optional = true [target.'cfg(target_os="windows")'.dependencies.winapi] version = "0.3.4" diff --git a/crates/nu-ansi-term/examples/gradient_colors.rs b/crates/nu-ansi-term/examples/gradient_colors.rs new file mode 100644 index 000000000..1c9583865 --- /dev/null +++ b/crates/nu-ansi-term/examples/gradient_colors.rs @@ -0,0 +1,37 @@ +use nu_ansi_term::{build_all_gradient_text, Color, Gradient, Rgb, TargetGround}; + +fn main() { + let text = "lorem ipsum quia dolor sit amet, consectetur, adipisci velit"; + + // a gradient from hex colors + let start = Rgb::from_hex(0x40c9ff); + let end = Rgb::from_hex(0xe81cff); + let grad0 = Gradient::new(start, end); + + // a gradient from color::rgb() + let start = Color::Rgb(64, 201, 255); + let end = Color::Rgb(232, 28, 255); + let gradient = Gradient::from_color_rgb(start, end); + + // a slightly different gradient + let start2 = Color::Rgb(128, 64, 255); + let end2 = Color::Rgb(0, 28, 255); + let gradient2 = Gradient::from_color_rgb(start2, end2); + + // reverse the gradient + let gradient3 = gradient.reverse(); + + let build_fg = gradient.build(text, TargetGround::Foreground); + println!("{}", build_fg); + let build_bg = gradient.build(text, TargetGround::Background); + println!("{}", build_bg); + let bgt = build_all_gradient_text(text, gradient, gradient2); + println!("{}", bgt); + let bgt2 = build_all_gradient_text(text, gradient, gradient3); + println!("{}", bgt2); + + println!( + "{}", + grad0.build("nushell is awesome", TargetGround::Foreground) + ); +} diff --git a/crates/nu-ansi-term/src/gradient.rs b/crates/nu-ansi-term/src/gradient.rs new file mode 100644 index 000000000..a0d94c8cd --- /dev/null +++ b/crates/nu-ansi-term/src/gradient.rs @@ -0,0 +1,105 @@ +use crate::{rgb::Rgb, Color}; + +/// Linear color gradient between two color stops +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Gradient { + /// Start Color of Gradient + pub start: Rgb, + + /// End Color of Gradient + pub end: Rgb, +} + +impl Gradient { + /// Creates a new [Gradient] with two [Rgb] colors, `start` and `end` + #[inline] + pub const fn new(start: Rgb, end: Rgb) -> Self { + Self { start, end } + } + pub const fn from_color_rgb(start: Color, end: Color) -> Self { + let start_grad = match start { + Color::Rgb(r, g, b) => Rgb { r, g, b }, + _ => Rgb { r: 0, g: 0, b: 0 }, + }; + let end_grad = match end { + Color::Rgb(r, g, b) => Rgb { r, g, b }, + _ => Rgb { r: 0, g: 0, b: 0 }, + }; + + Self { + start: start_grad, + end: end_grad, + } + } + + /// Computes the [Rgb] color between `start` and `end` for `t` + pub fn at(&self, t: f32) -> Rgb { + self.start.lerp(self.end, t) + } + + /// Returns the reverse of `self` + #[inline] + pub const fn reverse(&self) -> Self { + Self::new(self.end, self.start) + } + + #[allow(dead_code)] + pub fn build(&self, text: &str, target: TargetGround) -> String { + let delta = 1.0 / text.len() as f32; + let mut result = text.char_indices().fold(String::new(), |mut acc, (i, c)| { + let temp = format!( + "\x1B[{}m{}", + self.at(i as f32 * delta).ansi_color_code(target), + c + ); + acc.push_str(&temp); + acc + }); + + result.push_str("\x1B[0m"); + result + } +} + +#[allow(dead_code)] +pub fn build_all_gradient_text(text: &str, foreground: Gradient, background: Gradient) -> String { + let delta = 1.0 / text.len() as f32; + let mut result = text.char_indices().fold(String::new(), |mut acc, (i, c)| { + let step = i as f32 * delta; + let temp = format!( + "\x1B[{};{}m{}", + foreground + .at(step) + .ansi_color_code(TargetGround::Foreground), + background + .at(step) + .ansi_color_code(TargetGround::Background), + c + ); + acc.push_str(&temp); + acc + }); + + result.push_str("\x1B[0m"); + result +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TargetGround { + Foreground, + Background, +} + +impl TargetGround { + #[inline] + pub const fn code(&self) -> u8 { + match self { + Self::Foreground => 30, + Self::Background => 40, + } + } +} + +pub trait ANSIColorCode { + fn ansi_color_code(&self, target: TargetGround) -> String; +} diff --git a/crates/nu-ansi-term/src/lib.rs b/crates/nu-ansi-term/src/lib.rs index e19d6b1e9..aa3648da9 100644 --- a/crates/nu-ansi-term/src/lib.rs +++ b/crates/nu-ansi-term/src/lib.rs @@ -121,13 +121,13 @@ //! `Fixed` colors instead, but there’s nothing to be gained by doing so //! either. //! -//! You can also access full 24-bit color by using the `Color::RGB` variant, +//! You can also access full 24-bit color by using the `Color::Rgb` variant, //! which takes separate `u8` arguments for red, green, and blue: //! //! ``` -//! use nu_ansi_term::Color::RGB; +//! use nu_ansi_term::Color::Rgb; //! -//! RGB(70, 130, 180).paint("Steel blue"); +//! Rgb(70, 130, 180).paint("Steel blue"); //! ``` //! //! ## Combining successive colored strings @@ -233,7 +233,7 @@ #![crate_type = "rlib"] #![crate_type = "dylib"] #![warn(missing_copy_implementations)] -#![warn(missing_docs)] +// #![warn(missing_docs)] #![warn(trivial_casts, trivial_numeric_casts)] // #![warn(unused_extern_crates, unused_qualifications)] @@ -265,3 +265,9 @@ mod util; pub use util::*; mod debug; + +pub mod gradient; +pub use gradient::*; + +mod rgb; +pub use rgb::*; diff --git a/crates/nu-ansi-term/src/rgb.rs b/crates/nu-ansi-term/src/rgb.rs new file mode 100644 index 000000000..19475c36b --- /dev/null +++ b/crates/nu-ansi-term/src/rgb.rs @@ -0,0 +1,173 @@ +// Code liberally borrowed from here +// https://github.com/navierr/coloriz +use std::ops; +use std::u32; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Rgb { + /// Red + pub r: u8, + /// Green + pub g: u8, + /// Blue + pub b: u8, +} + +impl Rgb { + /// Creates a new [Rgb] color + #[inline] + pub const fn new(r: u8, g: u8, b: u8) -> Self { + Self { r, g, b } + } + + /// Creates a new [Rgb] color with a hex code + #[inline] + pub const fn from_hex(hex: u32) -> Self { + Self::new((hex >> 16) as u8, (hex >> 8) as u8, hex as u8) + } + + pub fn from_hex_string(hex: String) -> Self { + if hex.chars().count() == 8 && hex.starts_with("0x") { + // eprintln!("hex:{:?}", hex); + let (_, value_string) = hex.split_at(2); + // eprintln!("value_string:{:?}", value_string); + let int_val = u64::from_str_radix(value_string, 16); + match int_val { + Ok(num) => Self::new( + ((num & 0xff0000) >> 16) as u8, + ((num & 0xff00) >> 8) as u8, + (num & 0xff) as u8, + ), + // Don't fail, just make the color black + // Should we fail? + _ => Self::new(0, 0, 0), + } + } else { + // Don't fail, just make the color black. + // Should we fail? + Self::new(0, 0, 0) + } + } + + /// Creates a new [Rgb] color with three [f32] values + pub fn from_f32(r: f32, g: f32, b: f32) -> Self { + Self::new( + (r.clamp(0.0, 1.0) * 255.0) as u8, + (g.clamp(0.0, 1.0) * 255.0) as u8, + (b.clamp(0.0, 1.0) * 255.0) as u8, + ) + } + + /// Creates a grayscale [Rgb] color + #[inline] + pub const fn gray(x: u8) -> Self { + Self::new(x, x, x) + } + + /// Creates a grayscale [Rgb] color with a [f32] value + pub fn gray_f32(x: f32) -> Self { + Self::from_f32(x, x, x) + } + + /// Creates a new [Rgb] color from a [HSL] color + // pub fn from_hsl(hsl: HSL) -> Self { + // if hsl.s == 0.0 { + // return Self::gray_f32(hsl.l); + // } + + // let q = if hsl.l < 0.5 { + // hsl.l * (1.0 + hsl.s) + // } else { + // hsl.l + hsl.s - hsl.l * hsl.s + // }; + // let p = 2.0 * hsl.l - q; + // let h2c = |t: f32| { + // let t = t.clamp(0.0, 1.0); + // if 6.0 * t < 1.0 { + // p + 6.0 * (q - p) * t + // } else if t < 0.5 { + // q + // } else if 1.0 < 1.5 * t { + // p + 6.0 * (q - p) * (1.0 / 1.5 - t) + // } else { + // p + // } + // }; + + // Self::from_f32(h2c(hsl.h + 1.0 / 3.0), h2c(hsl.h), h2c(hsl.h - 1.0 / 3.0)) + // } + + /// Computes the linear interpolation between `self` and `other` for `t` + pub fn lerp(&self, other: Self, t: f32) -> Self { + let t = t.clamp(0.0, 1.0); + self * (1.0 - t) + other * t + } +} + +impl From<(u8, u8, u8)> for Rgb { + fn from((r, g, b): (u8, u8, u8)) -> Self { + Self::new(r, g, b) + } +} + +impl From<(f32, f32, f32)> for Rgb { + fn from((r, g, b): (f32, f32, f32)) -> Self { + Self::from_f32(r, g, b) + } +} + +use crate::ANSIColorCode; +use crate::TargetGround; +impl ANSIColorCode for Rgb { + fn ansi_color_code(&self, target: TargetGround) -> String { + format!("{};2;{};{};{}", target.code() + 8, self.r, self.g, self.b) + } +} + +overload::overload!( + (lhs: ?Rgb) + (rhs: ?Rgb) -> Rgb { + Rgb::new( + lhs.r.saturating_add(rhs.r), + lhs.g.saturating_add(rhs.g), + lhs.b.saturating_add(rhs.b) + ) + } +); + +overload::overload!( + (lhs: ?Rgb) - (rhs: ?Rgb) -> Rgb { + Rgb::new( + lhs.r.saturating_sub(rhs.r), + lhs.g.saturating_sub(rhs.g), + lhs.b.saturating_sub(rhs.b) + ) + } +); + +overload::overload!( + (lhs: ?Rgb) * (rhs: ?f32) -> Rgb { + Rgb::new( + (lhs.r as f32 * rhs.clamp(0.0, 1.0)) as u8, + (lhs.g as f32 * rhs.clamp(0.0, 1.0)) as u8, + (lhs.b as f32 * rhs.clamp(0.0, 1.0)) as u8 + ) + } +); + +overload::overload!( + (lhs: ?f32) * (rhs: ?Rgb) -> Rgb { + Rgb::new( + (rhs.r as f32 * lhs.clamp(0.0, 1.0)) as u8, + (rhs.g as f32 * lhs.clamp(0.0, 1.0)) as u8, + (rhs.b as f32 * lhs.clamp(0.0, 1.0)) as u8 + ) + } +); + +overload::overload!( + -(rgb: ?Rgb) -> Rgb { + Rgb::new( + 255 - rgb.r, + 255 - rgb.g, + 255 - rgb.b) + } +); diff --git a/crates/nu-ansi-term/src/style.rs b/crates/nu-ansi-term/src/style.rs index 05fb00cbd..5348f0ae8 100644 --- a/crates/nu-ansi-term/src/style.rs +++ b/crates/nu-ansi-term/src/style.rs @@ -364,7 +364,7 @@ pub enum Color { /// [cc]: https://upload.wikimedia.org/wikipedia/commons/1/15/Xterm_256color_chart.svg Fixed(u8), - /// A 24-bit RGB color, as specified by ISO-8613-3. + /// A 24-bit Rgb color, as specified by ISO-8613-3. Rgb(u8, u8, u8), } @@ -546,7 +546,7 @@ impl Color { /// ``` /// use nu_ansi_term::Color; /// - /// let style = Color::RGB(31, 31, 31).on(Color::White); + /// let style = Color::Rgb(31, 31, 31).on(Color::White); /// println!("{}", style.paint("eyyyy")); /// ``` pub fn on(self, background: Color) -> Style { @@ -584,13 +584,13 @@ mod serde_json_tests { let colors = &[ Color::Red, Color::Blue, - Color::RGB(123, 123, 123), + Color::Rgb(123, 123, 123), Color::Fixed(255), ]; assert_eq!( serde_json::to_string(&colors).unwrap(), - String::from("[\"Red\",\"Blue\",{\"RGB\":[123,123,123]},{\"Fixed\":255}]") + String::from("[\"Red\",\"Blue\",{\"Rgb\":[123,123,123]},{\"Fixed\":255}]") ); } @@ -599,7 +599,7 @@ mod serde_json_tests { let colors = &[ Color::Red, Color::Blue, - Color::RGB(123, 123, 123), + Color::Rgb(123, 123, 123), Color::Fixed(255), ]; diff --git a/crates/nu-command/src/commands.rs b/crates/nu-command/src/commands.rs index 73120a6c9..f910d5ef8 100644 --- a/crates/nu-command/src/commands.rs +++ b/crates/nu-command/src/commands.rs @@ -146,6 +146,7 @@ pub(crate) use autoview::Autoview; pub(crate) use cd::Cd; pub(crate) use ansi::Ansi; +pub(crate) use ansi::AnsiGradient; pub(crate) use ansi::AnsiStrip; pub(crate) use append::Command as Append; pub(crate) use autoenv::Autoenv; diff --git a/crates/nu-command/src/commands/ansi/command.rs b/crates/nu-command/src/commands/ansi/command.rs index 88fdbc7a1..e37fbaa43 100644 --- a/crates/nu-command/src/commands/ansi/command.rs +++ b/crates/nu-command/src/commands/ansi/command.rs @@ -331,7 +331,7 @@ pub fn str_to_ansi(s: &str) -> Option { // OSC escape (Operating system command) "osc" | "escape_right" => Some("\x1b]".to_string()), - // Ansi RGB - Needs to be 32;2;r;g;b or 48;2;r;g;b + // 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()), diff --git a/crates/nu-command/src/commands/ansi/gradient.rs b/crates/nu-command/src/commands/ansi/gradient.rs new file mode 100644 index 000000000..0d71d636f --- /dev/null +++ b/crates/nu-command/src/commands/ansi/gradient.rs @@ -0,0 +1,326 @@ +use crate::prelude::*; +use nu_ansi_term::{build_all_gradient_text, gradient::TargetGround, Gradient, Rgb}; +use nu_engine::WholeStreamCommand; +use nu_errors::ShellError; +use nu_protocol::ShellTypeName; +use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; +use nu_source::Tag; + +pub struct SubCommand; + +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "ansi gradient" + } + + fn signature(&self) -> Signature { + Signature::build("ansi gradient") + .named( + "fgstart", + SyntaxShape::String, + "foreground gradient start color in hex (0x123456)", + Some('a'), + ) + .named( + "fgend", + SyntaxShape::String, + "foreground gradient end color in hex", + Some('b'), + ) + .named( + "bgstart", + SyntaxShape::String, + "background gradient start color in hex", + Some('c'), + ) + .named( + "bgend", + SyntaxShape::String, + "background gradient end color in hex", + Some('d'), + ) + .rest( + SyntaxShape::ColumnPath, + "optionally, draw gradients using text from column paths", + ) + } + + fn usage(&self) -> &str { + "draw text with a provided start and end code making a gradient" + } + + fn run(&self, args: CommandArgs) -> Result { + operate(args) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "draw text in a gradient with foreground start and end colors", + example: + "echo 'Hello, Nushell! This is a gradient.' | ansi gradient --fgstart 0x40c9ff --fgend 0xe81cff", + result: None, + }, + Example { + description: "draw text in a gradient with foreground start and end colors and background start and end colors", + example: + "echo 'Hello, Nushell! This is a gradient.' | ansi gradient --fgstart 0x40c9ff --fgend 0xe81cff --bgstart 0xe81cff --bgend 0x40c9ff", + result: None, + }, + Example { + description: "draw text in a gradient by specifying foreground start color - end color is assumed to be black", + example: + "echo 'Hello, Nushell! This is a gradient.' | ansi gradient --fgstart 0x40c9ff", + result: None, + }, + Example { + description: "draw text in a gradient by specifying foreground end color - start color is assumed to be black", + example: + "echo 'Hello, Nushell! This is a gradient.' | ansi gradient --fgend 0xe81cff", + result: None, + }, + ] + } +} + +fn operate(args: CommandArgs) -> Result { + let args = args.evaluate_once()?; + let fgstart: Option = args.get_flag("fgstart")?; + let fgend: Option = args.get_flag("fgend")?; + let bgstart: Option = args.get_flag("bgstart")?; + let bgend: Option = args.get_flag("bgend")?; + let column_paths: Vec<_> = args.rest(0)?; + + let fgs_hex = fgstart.map(|color| Rgb::from_hex_string(color.convert_to_string())); + let fge_hex = fgend.map(|color| Rgb::from_hex_string(color.convert_to_string())); + let bgs_hex = bgstart.map(|color| Rgb::from_hex_string(color.convert_to_string())); + let bge_hex = bgend.map(|color| Rgb::from_hex_string(color.convert_to_string())); + + let result: Vec = args + .input + .map(move |v| { + if column_paths.is_empty() { + action(&v, v.tag(), fgs_hex, fge_hex, bgs_hex, bge_hex) + } else { + let mut ret = v; + + for path in &column_paths { + ret = ret.swap_data_by_column_path( + path, + Box::new(move |old| { + action(old, old.tag(), fgs_hex, fge_hex, bgs_hex, bge_hex) + }), + )?; + } + + Ok(ret) + } + }) + .collect::, _>>()?; + + Ok(OutputStream::from_stream(result.into_iter())) +} + +fn action( + input: &Value, + tag: impl Into, + fg_start: Option, + fg_end: Option, + bg_start: Option, + bg_end: Option, +) -> Result { + let tag = tag.into(); + + match &input.value { + UntaggedValue::Primitive(Primitive::String(astring)) => { + match (fg_start, fg_end, bg_start, bg_end) { + (None, None, None, None) => { + // Error - no colors + Err(ShellError::labeled_error( + "please supply color parameters", + "please supply foreground and/or background color parameters", + Tag::unknown(), + )) + } + (None, None, None, Some(bg_end)) => { + // Error - missing bg_start, so assume black + let bg_start = Rgb::new(0, 0, 0); + let gradient = Gradient::new(bg_start, bg_end); + let gradient_string = gradient.build(astring, TargetGround::Background); + Ok(UntaggedValue::string(gradient_string).into_value(tag)) + } + (None, None, Some(bg_start), None) => { + // Error - missing bg_end, so assume black + let bg_end = Rgb::new(0, 0, 0); + let gradient = Gradient::new(bg_start, bg_end); + let gradient_string = gradient.build(astring, TargetGround::Background); + Ok(UntaggedValue::string(gradient_string).into_value(tag)) + } + (None, None, Some(bg_start), Some(bg_end)) => { + // Background Only + let gradient = Gradient::new(bg_start, bg_end); + let gradient_string = gradient.build(astring, TargetGround::Background); + Ok(UntaggedValue::string(gradient_string).into_value(tag)) + } + (None, Some(fg_end), None, None) => { + // Error - missing fg_start, so assume black + let fg_start = Rgb::new(0, 0, 0); + let gradient = Gradient::new(fg_start, fg_end); + let gradient_string = gradient.build(astring, TargetGround::Foreground); + Ok(UntaggedValue::string(gradient_string).into_value(tag)) + } + (None, Some(fg_end), None, Some(bg_end)) => { + // missin fg_start and bg_start, so assume black + let fg_start = Rgb::new(0, 0, 0); + let bg_start = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = + build_all_gradient_text(astring, fg_gradient, bg_gradient); + Ok(UntaggedValue::string(gradient_string).into_value(tag)) + } + (None, Some(fg_end), Some(bg_start), None) => { + // Error - missing fg_start and bg_end + let fg_start = Rgb::new(0, 0, 0); + let bg_end = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = + build_all_gradient_text(astring, fg_gradient, bg_gradient); + Ok(UntaggedValue::string(gradient_string).into_value(tag)) + } + (None, Some(fg_end), Some(bg_start), Some(bg_end)) => { + // Error - missing fg_start, so assume black + let fg_start = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = + build_all_gradient_text(astring, fg_gradient, bg_gradient); + Ok(UntaggedValue::string(gradient_string).into_value(tag)) + } + (Some(fg_start), None, None, None) => { + // Error - missing fg_end, so assume black + let fg_end = Rgb::new(0, 0, 0); + let gradient = Gradient::new(fg_start, fg_end); + let gradient_string = gradient.build(astring, TargetGround::Foreground); + Ok(UntaggedValue::string(gradient_string).into_value(tag)) + } + (Some(fg_start), None, None, Some(bg_end)) => { + // Error - missing fg_end, bg_start, so assume black + let fg_end = Rgb::new(0, 0, 0); + let bg_start = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = + build_all_gradient_text(astring, fg_gradient, bg_gradient); + Ok(UntaggedValue::string(gradient_string).into_value(tag)) + } + (Some(fg_start), None, Some(bg_start), None) => { + // Error - missing fg_end, bg_end, so assume black + let fg_end = Rgb::new(0, 0, 0); + let bg_end = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = + build_all_gradient_text(astring, fg_gradient, bg_gradient); + Ok(UntaggedValue::string(gradient_string).into_value(tag)) + } + (Some(fg_start), None, Some(bg_start), Some(bg_end)) => { + // Error - missing fg_end, so assume black + let fg_end = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = + build_all_gradient_text(astring, fg_gradient, bg_gradient); + Ok(UntaggedValue::string(gradient_string).into_value(tag)) + } + (Some(fg_start), Some(fg_end), None, None) => { + // Foreground Only + let gradient = Gradient::new(fg_start, fg_end); + let gradient_string = gradient.build(astring, TargetGround::Foreground); + Ok(UntaggedValue::string(gradient_string).into_value(tag)) + } + (Some(fg_start), Some(fg_end), None, Some(bg_end)) => { + // Error - missing bg_start, so assume black + let bg_start = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = + build_all_gradient_text(astring, fg_gradient, bg_gradient); + Ok(UntaggedValue::string(gradient_string).into_value(tag)) + } + (Some(fg_start), Some(fg_end), Some(bg_start), None) => { + // Error - missing bg_end, so assume black + let bg_end = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = + build_all_gradient_text(astring, fg_gradient, bg_gradient); + Ok(UntaggedValue::string(gradient_string).into_value(tag)) + } + (Some(fg_start), Some(fg_end), Some(bg_start), Some(bg_end)) => { + // Foreground and Background Gradient + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = + build_all_gradient_text(astring, fg_gradient, bg_gradient); + Ok(UntaggedValue::string(gradient_string).into_value(tag)) + } + } + } + other => { + let got = format!("got {}", other.type_name()); + + Err(ShellError::labeled_error( + "value is not string", + got, + tag.span, + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::ShellError; + use super::{action, SubCommand}; + use nu_ansi_term::Rgb; + use nu_protocol::UntaggedValue; + use nu_source::Tag; + + #[test] + fn examples_work_as_expected() -> Result<(), ShellError> { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + // #[test] + // fn test_stripping() { + // let input_string = + // UntaggedValue::string("\u{1b}[3;93;41mHello\u{1b}[0m \u{1b}[1;32mNu \u{1b}[1;35mWorld") + // .into_untagged_value(); + // let expected = UntaggedValue::string("Hello Nu World").into_untagged_value(); + + // let actual = action(&input_string, Tag::unknown()).unwrap(); + // assert_eq!(actual, expected); + // } + + #[test] + fn test_fg_gradient() { + let input_string = UntaggedValue::string("Hello, World!").into_untagged_value(); + let expected = UntaggedValue::string("\u{1b}[38;2;64;201;255mH\u{1b}[38;2;76;187;254me\u{1b}[38;2;89;174;254ml\u{1b}[38;2;102;160;254ml\u{1b}[38;2;115;147;254mo\u{1b}[38;2;128;133;254m,\u{1b}[38;2;141;120;254m \u{1b}[38;2;153;107;254mW\u{1b}[38;2;166;94;254mo\u{1b}[38;2;179;80;254mr\u{1b}[38;2;192;67;254ml\u{1b}[38;2;205;53;254md\u{1b}[38;2;218;40;254m!\u{1b}[0m").into_untagged_value(); + let fg_start = Rgb::from_hex_string("0x40c9ff".to_string()); + let fg_end = Rgb::from_hex_string("0xe81cff".to_string()); + let actual = action( + &input_string, + Tag::unknown(), + Some(fg_start), + Some(fg_end), + None, + None, + ) + .unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-command/src/commands/ansi/mod.rs b/crates/nu-command/src/commands/ansi/mod.rs index 274777ad8..e7bbad5b7 100644 --- a/crates/nu-command/src/commands/ansi/mod.rs +++ b/crates/nu-command/src/commands/ansi/mod.rs @@ -1,5 +1,7 @@ mod command; +mod gradient; mod strip; pub use command::Command as Ansi; +pub use gradient::SubCommand as AnsiGradient; pub use strip::SubCommand as AnsiStrip; diff --git a/crates/nu-command/src/commands/default_context.rs b/crates/nu-command/src/commands/default_context.rs index 2d1c6b822..3830e5456 100644 --- a/crates/nu-command/src/commands/default_context.rs +++ b/crates/nu-command/src/commands/default_context.rs @@ -111,6 +111,7 @@ pub fn create_default_context(interactive: bool) -> Result