diff --git a/Cargo.lock b/Cargo.lock index f076743662..f3ce874047 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,6 +472,27 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" +[[package]] +name = "bzip2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.9+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad3b39a260062fca31f7b0b12f207e8f2590a67d32ec7d59c20484b07ea7285e" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cache-padded" version = "1.1.1" @@ -2671,6 +2692,7 @@ dependencies = [ "regex", "roxmltree", "rusqlite", + "rust-embed", "rustyline", "serde 1.0.114", "serde-hjson 0.9.1", @@ -2696,6 +2718,7 @@ dependencies = [ "users", "uuid", "which", + "zip", ] [[package]] @@ -3893,6 +3916,38 @@ dependencies = [ "crossbeam-utils 0.7.2", ] +[[package]] +name = "rust-embed" +version = "5.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213acf1bc5a6dfcd70b62db1e9a7d06325c0e73439c312fcb8599d456d9686ee" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "5.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7903c2cf599db8f310b392332f38367ca4acc84420fa1aee3536299f433c10d5" +dependencies = [ + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97655158074ccb2d2cfb1ccb4c956ef0f4054e43a2c1e71146d4991e6961e105" +dependencies = [ + "walkdir", +] + [[package]] name = "rust-ini" version = "0.13.0" @@ -5100,7 +5155,9 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58287c28d78507f5f91f2a4cf1e8310e2c76fd4c6932f93ac60fd1ceb402db7d" dependencies = [ + "bzip2", "crc32fast", "flate2", "podio", + "time", ] diff --git a/assets/228_themes.zip b/assets/228_themes.zip new file mode 100644 index 0000000000..eca629d50c Binary files /dev/null and b/assets/228_themes.zip differ diff --git a/assets/syntaxes.bin b/assets/syntaxes.bin deleted file mode 100644 index dee29367ce..0000000000 Binary files a/assets/syntaxes.bin and /dev/null differ diff --git a/assets/themes.bin b/assets/themes.bin deleted file mode 100644 index 23ad913280..0000000000 Binary files a/assets/themes.bin and /dev/null differ diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 9740c677ce..6d7e34f455 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -67,6 +67,7 @@ query_interface = "0.3.5" rand = "0.7" regex = "1" roxmltree = "0.13.0" +rust-embed = "5.6.0" rustyline = "6.2.0" serde = {version = "1.0.114", features = ["derive"]} serde-hjson = "0.9.1" @@ -88,6 +89,7 @@ umask = "1.0.0" unicode-xid = "0.2.1" uuid_crate = {package = "uuid", version = "0.8.1", features = ["v4"], optional = true} which = {version = "4.0.1", optional = true} +zip = "0.5.6" clipboard = {version = "0.5", optional = true} encoding_rs = "0.8.23" diff --git a/crates/nu-cli/src/commands/to_html.rs b/crates/nu-cli/src/commands/to_html.rs index 4581cb6c23..7ecb41bba0 100644 --- a/crates/nu-cli/src/commands/to_html.rs +++ b/crates/nu-cli/src/commands/to_html.rs @@ -6,7 +6,79 @@ use nu_errors::ShellError; use nu_protocol::{Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; use nu_source::{AnchorLocation, Tagged}; use regex::Regex; +use rust_embed::RustEmbed; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; use std::collections::HashMap; +use std::error::Error; +use std::io::Read; + +#[derive(Serialize, Deserialize, Debug)] +pub struct HtmlThemes { + themes: Vec, +} + +#[allow(non_snake_case)] +#[derive(Serialize, Deserialize, Debug)] +pub struct HtmlTheme { + name: String, + black: String, + red: String, + green: String, + yellow: String, + blue: String, + purple: String, + cyan: String, + white: String, + brightBlack: String, + brightRed: String, + brightGreen: String, + brightYellow: String, + brightBlue: String, + brightPurple: String, + brightCyan: String, + brightWhite: String, + background: String, + foreground: String, +} + +impl Default for HtmlThemes { + fn default() -> Self { + HtmlThemes { + themes: vec![HtmlTheme::default()], + } + } +} + +impl Default for HtmlTheme { + fn default() -> Self { + HtmlTheme { + name: "nu_default".to_string(), + black: "black".to_string(), + red: "red".to_string(), + green: "green".to_string(), + yellow: "#717100".to_string(), + blue: "blue".to_string(), + purple: "#c800c8".to_string(), + cyan: "#037979".to_string(), + white: "white".to_string(), + brightBlack: "black".to_string(), + brightRed: "red".to_string(), + brightGreen: "green".to_string(), + brightYellow: "#717100".to_string(), + brightBlue: "blue".to_string(), + brightPurple: "#c800c8".to_string(), + brightCyan: "#037979".to_string(), + brightWhite: "white".to_string(), + background: "white".to_string(), + foreground: "black".to_string(), + } + } +} + +#[derive(RustEmbed)] +#[folder = "../../assets/"] // TODO: Should assets be part of the crate versus the project? +struct Assets; pub struct ToHTML; @@ -17,6 +89,7 @@ pub struct ToHTMLArgs { dark: bool, partial: bool, theme: Option>, + list: bool, } #[async_trait] @@ -42,9 +115,10 @@ impl WholeStreamCommand for ToHTML { .named( "theme", SyntaxShape::String, - "the name of the theme to use (default, campbell, github, blulocolight)", + "the name of the theme to use (github, blulocolight, ...)", Some('t'), ) + .switch("list", "list the names of all available themes", Some('l')) } fn usage(&self) -> &str { @@ -60,183 +134,133 @@ impl WholeStreamCommand for ToHTML { } } -fn get_campbell_theme(is_dark: bool) -> HashMap<&'static str, String> { - // for reference here is Microsoft's Campbell Theme - // taken from here - // https://docs.microsoft.com/en-us/windows/terminal/customize-settings/color-schemes - let mut hm: HashMap<&str, String> = HashMap::new(); - - hm.insert("bold_black", "#767676".to_string()); - hm.insert("bold_red", "#E74856".to_string()); - hm.insert("bold_green", "#16C60C".to_string()); - hm.insert("bold_yellow", "#F9F1A5".to_string()); - hm.insert("bold_blue", "#3B78FF".to_string()); - hm.insert("bold_magenta", "#B4009E".to_string()); - hm.insert("bold_cyan", "#61D6D6".to_string()); - hm.insert("bold_white", "#F2F2F2".to_string()); - - hm.insert("black", "#0C0C0C".to_string()); - hm.insert("red", "#C50F1F".to_string()); - hm.insert("green", "#13A10E".to_string()); - hm.insert("yellow", "#C19C00".to_string()); - hm.insert("blue", "#0037DA".to_string()); - hm.insert("magenta", "#881798".to_string()); - hm.insert("cyan", "#3A96DD".to_string()); - hm.insert("white", "#CCCCCC".to_string()); - - // Try to make theme work with light or dark but - // flipping the foreground and background but leave - // the other colors the same. - if is_dark { - hm.insert("background", "#0C0C0C".to_string()); - hm.insert("foreground", "#CCCCCC".to_string()); - } else { - hm.insert("background", "#CCCCCC".to_string()); - hm.insert("foreground", "#0C0C0C".to_string()); - } - - hm -} - -fn get_default_theme(is_dark: bool) -> HashMap<&'static str, String> { - let mut hm: HashMap<&str, String> = HashMap::new(); - - // This theme has different colors for dark and light - // so we can't just swap the background colors. - if is_dark { - hm.insert("bold_black", "black".to_string()); - hm.insert("bold_red", "red".to_string()); - hm.insert("bold_green", "green".to_string()); - hm.insert("bold_yellow", "yellow".to_string()); - hm.insert("bold_blue", "blue".to_string()); - hm.insert("bold_magenta", "magenta".to_string()); - hm.insert("bold_cyan", "cyan".to_string()); - hm.insert("bold_white", "white".to_string()); - - hm.insert("black", "black".to_string()); - hm.insert("red", "red".to_string()); - hm.insert("green", "green".to_string()); - hm.insert("yellow", "yellow".to_string()); - hm.insert("blue", "blue".to_string()); - hm.insert("magenta", "magenta".to_string()); - hm.insert("cyan", "cyan".to_string()); - hm.insert("white", "white".to_string()); - - hm.insert("background", "black".to_string()); - hm.insert("foreground", "white".to_string()); - } else { - hm.insert("bold_black", "black".to_string()); - hm.insert("bold_red", "red".to_string()); - hm.insert("bold_green", "green".to_string()); - hm.insert("bold_yellow", "#717100".to_string()); - hm.insert("bold_blue", "blue".to_string()); - hm.insert("bold_magenta", "#c800c8".to_string()); - hm.insert("bold_cyan", "#037979".to_string()); - hm.insert("bold_white", "white".to_string()); - - hm.insert("black", "black".to_string()); - hm.insert("red", "red".to_string()); - hm.insert("green", "green".to_string()); - hm.insert("yellow", "#717100".to_string()); - hm.insert("blue", "blue".to_string()); - hm.insert("magenta", "#c800c8".to_string()); - hm.insert("cyan", "#037979".to_string()); - hm.insert("white", "white".to_string()); - - hm.insert("background", "white".to_string()); - hm.insert("foreground", "black".to_string()); - } - - hm -} - -fn get_github_theme(is_dark: bool) -> HashMap<&'static str, String> { - // Suggested by Jörn for use with demo site - // Taken from here https://github.com/mbadolato/iTerm2-Color-Schemes/blob/master/windowsterminal/Github.json - // This is a light theme named github, intended for a white background - // The next step will be to load these json themes if we ever get to that point - let mut hm: HashMap<&str, String> = HashMap::new(); - - hm.insert("bold_black", "#666666".to_string()); - hm.insert("bold_red", "#de0000".to_string()); - hm.insert("bold_green", "#87d5a2".to_string()); - hm.insert("bold_yellow", "#f1d007".to_string()); - hm.insert("bold_blue", "#2e6cba".to_string()); - hm.insert("bold_magenta", "#ffa29f".to_string()); - hm.insert("bold_cyan", "#1cfafe".to_string()); - hm.insert("bold_white", "#ffffff".to_string()); - - hm.insert("black", "#3e3e3e".to_string()); - hm.insert("red", "#970b16".to_string()); - hm.insert("green", "#07962a".to_string()); - hm.insert("yellow", "#f8eec7".to_string()); - hm.insert("blue", "#003e8a".to_string()); - hm.insert("magenta", "#e94691".to_string()); - hm.insert("cyan", "#89d1ec".to_string()); - hm.insert("white", "#ffffff".to_string()); - - // Try to make theme work with light or dark but - // flipping the foreground and background but leave - // the other colors the same. - if is_dark { - hm.insert("background", "#3e3e3e".to_string()); - hm.insert("foreground", "#f4f4f4".to_string()); - } else { - hm.insert("background", "#f4f4f4".to_string()); - hm.insert("foreground", "#3e3e3e".to_string()); - } - - hm -} - -fn get_blulocolight_theme(is_dark: bool) -> HashMap<&'static str, String> { - let mut hm: HashMap<&str, String> = HashMap::new(); - - hm.insert("bold_black", "#dedfe8".to_string()); - hm.insert("bold_red", "#fc4a6d".to_string()); - hm.insert("bold_green", "#34b354".to_string()); - hm.insert("bold_yellow", "#b89427".to_string()); - hm.insert("bold_blue", "#1085d9".to_string()); - hm.insert("bold_magenta", "#c00db3".to_string()); - hm.insert("bold_cyan", "#5b80ad".to_string()); - hm.insert("bold_white", "#1d1d22".to_string()); - - hm.insert("black", "#cbccd5".to_string()); - hm.insert("red", "#c90e42".to_string()); - hm.insert("green", "#21883a".to_string()); - hm.insert("yellow", "#d54d17".to_string()); - hm.insert("blue", "#1e44dd".to_string()); - hm.insert("magenta", "#6d1bed".to_string()); - hm.insert("cyan", "#1f4d7a".to_string()); - hm.insert("white", "#000000".to_string()); - - // Try to make theme work with light or dark but - // flipping the foreground and background but leave - // the other colors the same. - if is_dark { - hm.insert("background", "#2a2c33".to_string()); - hm.insert("foreground", "#f7f7f7".to_string()); - } else { - hm.insert("background", "#f7f7f7".to_string()); - hm.insert("foreground", "#2a2c33".to_string()); - } - - hm -} - -fn get_colors(is_dark: bool, theme: &Option>) -> HashMap<&'static str, String> { +fn get_theme_from_asset_file( + is_dark: bool, + theme: &Option>, + theme_tag: &Tag, +) -> Result, ShellError> { let theme_name = match theme { Some(s) => s.to_string(), - None => "default".to_string(), + None => "default".to_string(), // There is no theme named "default" so this will be HtmlTheme::default(), which is "nu_default". }; - match theme_name.as_ref() { - "default" => get_default_theme(is_dark), - "campbell" => get_campbell_theme(is_dark), - "github" => get_github_theme(is_dark), - "blulocolight" => get_blulocolight_theme(is_dark), - _ => get_default_theme(is_dark), + // 228 themes come from + // https://github.com/mbadolato/iTerm2-Color-Schemes/tree/master/windowsterminal + // we should find a hit on any name in there + let asset = get_asset_by_name_as_html_themes("228_themes.zip", "228_themes.json"); + + // If asset doesn't work, make sure to return the default theme + let asset = match asset { + Ok(a) => a, + _ => HtmlThemes::default(), + }; + + // Find the theme by theme name + let th = asset + .themes + .iter() + .find(|&n| n.name.to_lowercase() == *theme_name.to_lowercase().as_str()); // case insensitive search + + // If no theme is found by the name provided, ensure we return the default theme + let default_theme = HtmlTheme::default(); + let th = match th { + Some(t) => t, + None => &default_theme, + }; + + // this just means no theme was passed in + if th.name.to_lowercase().eq(&"nu_default".to_string()) + // this means there was a theme passed in + && theme.is_some() + { + return Err(ShellError::labeled_error( + "Error finding theme name", + "Error finding theme name", + theme_tag.span, + )); } + + Ok(convert_html_theme_to_hash_map(is_dark, th)) +} + +fn get_asset_by_name_as_html_themes( + zip_name: &str, + json_name: &str, +) -> Result> { + match Assets::get(zip_name) { + Some(content) => { + let asset: Vec = match content { + Cow::Borrowed(bytes) => bytes.into(), + Cow::Owned(bytes) => bytes, + }; + let reader = std::io::Cursor::new(asset); + let mut archive = zip::ZipArchive::new(reader)?; + let mut zip_file = archive.by_name(json_name)?; + let mut contents = String::new(); + zip_file.read_to_string(&mut contents)?; + Ok(serde_json::from_str(&contents)?) + } + None => { + let th = HtmlThemes::default(); + Ok(th) + } + } +} + +fn convert_html_theme_to_hash_map( + is_dark: bool, + theme: &HtmlTheme, +) -> HashMap<&'static str, String> { + let mut hm: HashMap<&str, String> = HashMap::new(); + + hm.insert("bold_black", theme.brightBlack[..].to_string()); + hm.insert("bold_red", theme.brightRed[..].to_string()); + hm.insert("bold_green", theme.brightGreen[..].to_string()); + hm.insert("bold_yellow", theme.brightYellow[..].to_string()); + hm.insert("bold_blue", theme.brightBlue[..].to_string()); + hm.insert("bold_magenta", theme.brightPurple[..].to_string()); + hm.insert("bold_cyan", theme.brightCyan[..].to_string()); + hm.insert("bold_white", theme.brightWhite[..].to_string()); + + hm.insert("black", theme.black[..].to_string()); + hm.insert("red", theme.red[..].to_string()); + hm.insert("green", theme.green[..].to_string()); + hm.insert("yellow", theme.yellow[..].to_string()); + hm.insert("blue", theme.blue[..].to_string()); + hm.insert("magenta", theme.purple[..].to_string()); + hm.insert("cyan", theme.cyan[..].to_string()); + hm.insert("white", theme.white[..].to_string()); + + // Try to make theme work with light or dark but + // flipping the foreground and background but leave + // the other colors the same. + if is_dark { + hm.insert("background", theme.black[..].to_string()); + hm.insert("foreground", theme.white[..].to_string()); + } else { + hm.insert("background", theme.white[..].to_string()); + hm.insert("foreground", theme.black[..].to_string()); + } + + hm +} + +fn get_list_of_theme_names() -> Vec { + let asset = get_asset_by_name_as_html_themes("228_themes.zip", "228_themes.json"); + + // If asset doesn't work, make sure to return the default theme + let html_themes = match asset { + Ok(a) => a, + _ => HtmlThemes::default(), + }; + + let theme_names: Vec = html_themes + .themes + .iter() + .map(|n| n.name[..].to_string()) + .collect(); + + theme_names } async fn to_html( @@ -252,6 +276,7 @@ async fn to_html( dark, partial, theme, + list, }, input, ) = args.process(®istry).await?; @@ -261,66 +286,100 @@ async fn to_html( .filter(|headers| !headers.is_empty() && (headers.len() > 1 || headers[0] != "")); let mut output_string = String::new(); let mut regex_hm: HashMap = HashMap::new(); - let color_hm = get_colors(dark, &theme); - // change the color of the page - if !partial { - output_string.push_str(&format!( - r"", - color_hm - .get("background") - .expect("Error getting background color"), - color_hm - .get("foreground") - .expect("Error getting foreground color") - )); + if list { + // Get the list of theme names + let theme_names = get_list_of_theme_names(); + + // Put that list into the output string + for s in theme_names.iter() { + output_string.push_str(&format!("{}\n", s)); + } + + output_string.push_str("\nScreenshots of themes can be found here:\n"); + output_string.push_str("https://github.com/mbadolato/iTerm2-Color-Schemes\n"); + + // Short circuit and return the output_string + Ok(OutputStream::one(ReturnSuccess::value( + UntaggedValue::string(output_string).into_value(name_tag), + ))) } else { - output_string.push_str(&format!( - "
", - color_hm - .get("background") - .expect("Error getting background color"), - color_hm - .get("foreground") - .expect("Error getting foreground color") - )); - } + let theme_tag = match &theme { + Some(v) => &v.tag, + None => &name_tag, + }; - let inner_value = match input.len() { - 0 => String::default(), - 1 => match headers { - Some(headers) => html_table(input, headers), - None => { - let value = &input[0]; - html_value(value) + let color_hm = get_theme_from_asset_file(dark, &theme, &theme_tag); + let color_hm = match color_hm { + Ok(c) => c, + _ => { + return Err(ShellError::labeled_error( + "Error finding theme name", + "Error finding theme name", + theme_tag.span, + )) } - }, - _ => match headers { - Some(headers) => html_table(input, headers), - None => html_list(input), - }, - }; + }; - output_string.push_str(&inner_value); + // change the color of the page + if !partial { + output_string.push_str(&format!( + r"", + color_hm + .get("background") + .expect("Error getting background color"), + color_hm + .get("foreground") + .expect("Error getting foreground color") + )); + } else { + output_string.push_str(&format!( + "
", + color_hm + .get("background") + .expect("Error getting background color"), + color_hm + .get("foreground") + .expect("Error getting foreground color") + )); + } - if !partial { - output_string.push_str(""); - } else { - output_string.push_str("
") + let inner_value = match input.len() { + 0 => String::default(), + 1 => match headers { + Some(headers) => html_table(input, headers), + None => { + let value = &input[0]; + html_value(value) + } + }, + _ => match headers { + Some(headers) => html_table(input, headers), + None => html_list(input), + }, + }; + + output_string.push_str(&inner_value); + + if !partial { + output_string.push_str(""); + } else { + output_string.push_str("
") + } + + // Check to see if we want to remove all color or change ansi to html colors + if html_color { + setup_html_color_regexes(&mut regex_hm, &color_hm); + output_string = run_regexes(®ex_hm, &output_string); + } else if no_color { + setup_no_color_regexes(&mut regex_hm); + output_string = run_regexes(®ex_hm, &output_string); + } + + Ok(OutputStream::one(ReturnSuccess::value( + UntaggedValue::string(output_string).into_value(name_tag), + ))) } - - // Check to see if we want to remove all color or change ansi to html colors - if html_color { - setup_html_color_regexes(&mut regex_hm, dark, &theme); - output_string = run_regexes(®ex_hm, &output_string); - } else if no_color { - setup_no_color_regexes(&mut regex_hm); - output_string = run_regexes(®ex_hm, &output_string); - } - - Ok(OutputStream::one(ReturnSuccess::value( - UntaggedValue::string(output_string).into_value(name_tag), - ))) } fn html_list(list: Vec) -> String { @@ -438,11 +497,8 @@ fn html_value(value: &Value) -> String { fn setup_html_color_regexes( hash: &mut HashMap, - is_dark: bool, - theme: &Option>, + color_hm: &HashMap<&str, String>, ) { - let color_hm = get_colors(is_dark, theme); - // All the bold colors hash.insert( 0, diff --git a/crates/nu-cli/tests/format_conversions/html.rs b/crates/nu-cli/tests/format_conversions/html.rs index f21c5ccb82..6094cb83c5 100644 --- a/crates/nu-cli/tests/format_conversions/html.rs +++ b/crates/nu-cli/tests/format_conversions/html.rs @@ -60,21 +60,6 @@ fn test_cd_html_color_flag_dark_false() { ); } -#[test] -fn test_cd_html_color_flag_dark_true() { - let actual = nu!( - cwd: ".", pipeline( - r#" - cd --help | to html --html_color --dark - "# - ) - ); - assert_eq!( - actual.out, - r"Change to a new path.

Usage:
> cd (directory) {flags}

Parameters:
(directory) the directory to change to

Flags:
-h, --help: Display this help message

Examples:
Change to a new directory called 'dirname'
> cd dirname

Change to your home directory
>
cd

Change to your home directory (alternate version)
>
cd
~

Change to the previous directory
>
cd
-

" - ); -} - #[test] fn test_no_color_flag() { let actual = nu!( @@ -90,21 +75,6 @@ fn test_no_color_flag() { ); } -#[test] -fn test_html_color_where_flag_dark_true() { - let actual = nu!( - cwd: ".", pipeline( - r#" - where --help | to html --html_color --dark - "# - ) - ); - assert_eq!( - actual.out, - r"Filter table to match the condition.

Usage:
> where <condition> {flags}

Parameters:
<condition> the condition that must match

Flags:
-h, --help: Display this help message

Examples:
List all files in the current directory with sizes greater than 2kb
> ls | where size > 2kb

List only the files in the current directory
>
ls
| where type == File

List all files with names that contain "Car"
>
ls
| where name =~ "Car"

List all files that were modified in the last two months
>
ls
| where modified <= 2M

" - ); -} - #[test] fn test_html_color_where_flag_dark_false() { let actual = nu!(