diff --git a/crates/nu-command/src/commands/char_.rs b/crates/nu-command/src/commands/char_.rs index 5c03737ad1..08140277e9 100644 --- a/crates/nu-command/src/commands/char_.rs +++ b/crates/nu-command/src/commands/char_.rs @@ -1,11 +1,108 @@ use crate::prelude::*; use nu_engine::{FromValue, WholeStreamCommand}; use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; +use nu_protocol::{Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value}; use nu_source::Tagged; +use indexmap::indexmap; +use indexmap::map::IndexMap; +use lazy_static::lazy_static; + pub struct Char; +struct CharArgs { + name: Option>, + rest: Vec, + list: bool, + unicode: bool, +} + +lazy_static! { + static ref CHAR_MAP: IndexMap<&'static str, String> = indexmap! { + // These are some regular characters that either can't used or + // it's just easier to use them like this. + "newline" => '\n'.to_string(), + "enter" => '\n'.to_string(), + "nl" => '\n'.to_string(), + "tab" => '\t'.to_string(), + "sp" => ' '.to_string(), + "space" => ' '.to_string(), + "pipe" => '|'.to_string(), + "left_brace" => '{'.to_string(), + "lbrace" => '{'.to_string(), + "right_brace" => '}'.to_string(), + "rbrace" => '}'.to_string(), + "left_paren" => '('.to_string(), + "lparen" => '('.to_string(), + "right_paren" => ')'.to_string(), + "rparen" => ')'.to_string(), + "left_bracket" => '['.to_string(), + "lbracket" => '['.to_string(), + "right_bracket" => ']'.to_string(), + "rbracket" => ']'.to_string(), + "sep" => std::path::MAIN_SEPARATOR.to_string(), + "separator" => std::path::MAIN_SEPARATOR.to_string(), + + // Unicode names came from https://www.compart.com/en/unicode + // Private Use Area (U+E000-U+F8FF) + // Unicode can't be mixed with Ansi or it will break width calculation + "branch" => '\u{e0a0}'.to_string(), //  + "segment" => '\u{e0b0}'.to_string(), //  + + "identical_to" => '\u{2261}'.to_string(), // ≡ + "hamburger" => '\u{2261}'.to_string(), // ≡ + "not_identical_to" => '\u{2262}'.to_string(), // ≢ + "branch_untracked" => '\u{2262}'.to_string(), // ≢ + "strictly_equivalent_to" => '\u{2263}'.to_string(), // ≣ + "branch_identical" => '\u{2263}'.to_string(), // ≣ + + "upwards_arrow" => '\u{2191}'.to_string(), // ↑ + "branch_ahead" => '\u{2191}'.to_string(), // ↑ + "downwards_arrow" => '\u{2193}'.to_string(), // ↓ + "branch_behind" => '\u{2193}'.to_string(), // ↓ + "up_down_arrow" => '\u{2195}'.to_string(), // ↕ + "branch_ahead_behind" => '\u{2195}'.to_string(), // ↕ + + "black_right_pointing_triangle" => '\u{25b6}'.to_string(), // ▶ + "prompt" => '\u{25b6}'.to_string(), // ▶ + "vector_or_cross_product" => '\u{2a2f}'.to_string(), // ⨯ + "failed" => '\u{2a2f}'.to_string(), // ⨯ + "high_voltage_sign" => '\u{26a1}'.to_string(), // ⚡ + "elevated" => '\u{26a1}'.to_string(), // ⚡ + "tilde" => '~'.to_string(), // ~ + "twiddle" => '~'.to_string(), // ~ + "squiggly" => '~'.to_string(), // ~ + "home" => '~'.to_string(), // ~ + "hash" => '#'.to_string(), // # + "hashtag" => '#'.to_string(), // # + "pound_sign" => '#'.to_string(), // # + "sharp" => '#'.to_string(), // # + "root" => '#'.to_string(), // # + + // Weather symbols + "sun" => "☀️".to_string(), + "sunny" => "☀️".to_string(), + "sunrise" => "☀️".to_string(), + "moon" => "🌛".to_string(), + "cloudy" => "☁️".to_string(), + "cloud" => "☁️".to_string(), + "clouds" => "☁️".to_string(), + "rainy" => "🌦️".to_string(), + "rain" => "🌦️".to_string(), + "foggy" => "🌫️".to_string(), + "fog" => "🌫️".to_string(), + "mist" => '\u{2591}'.to_string(), + "haze" => '\u{2591}'.to_string(), + "snowy" => "❄️".to_string(), + "snow" => "❄️".to_string(), + "thunderstorm" => "🌩️".to_string(), + "thunder" => "🌩️".to_string(), + + "bel" => '\x07'.to_string(), // Terminal Bell + "backspace" => '\x08'.to_string(), // Backspace + }; +} + impl WholeStreamCommand for Char { fn name(&self) -> &str { "char" @@ -13,17 +110,18 @@ impl WholeStreamCommand for Char { fn signature(&self) -> Signature { Signature::build("char") - .required( + .optional( "character", SyntaxShape::Any, "the name of the character to output", ) .rest(SyntaxShape::String, "multiple Unicode bytes") + .switch("list", "List all supported character names", Some('l')) .switch("unicode", "Unicode string i.e. 1f378", Some('u')) } fn usage(&self) -> &str { - "Output special characters (eg. 'newline')." + "Output special characters (e.g., 'newline')." } fn examples(&self) -> Vec { @@ -57,62 +155,86 @@ impl WholeStreamCommand for Char { ] } - fn run_with_actions(&self, args: CommandArgs) -> Result { + fn run(&self, args: CommandArgs) -> Result { + let args_tag = args.call_info.name_tag.clone(); let args = args.evaluate_once()?; + let args = CharArgs { + name: args.opt(0)?, + rest: args.rest(1)?, + list: args.has_flag("list"), + unicode: args.has_flag("unicode"), + }; - let name: Tagged = args.req(0)?; - let rest: Vec = args.rest(1)?; - let unicode = args.has_flag("unicode"); - - if unicode { - if !rest.is_empty() { - // Setup a new buffer to put all the Unicode bytes in - let mut multi_byte = String::new(); - // Get the first byte - let decoded_char = string_to_unicode_char(&name.item, &name.tag); - match decoded_char { - Ok(ch) => multi_byte.push(ch), - Err(e) => return Err(e), - } - // Get the rest of the bytes - for byte_part in rest { - let byte_part: Tagged = FromValue::from_value(&byte_part)?; - let decoded_char = string_to_unicode_char(&byte_part, &byte_part.tag); + if args.list { + Ok(CHAR_MAP + .iter() + .map(move |(name, s)| { + let mut dict = TaggedDictBuilder::with_capacity(&args_tag, 2); + dict.insert_untagged("name", UntaggedValue::string(*name)); + dict.insert_untagged("character", UntaggedValue::string(s)); + let unicode_parts: Vec = + s.chars().map(|c| format!("{:x}", c as u32)).collect(); + dict.insert_untagged("unicode", UntaggedValue::string(unicode_parts.join(" "))); + dict.into_value() + }) + .to_output_stream()) + } else if let Some(name) = args.name { + if args.unicode { + if !args.rest.is_empty() { + // Setup a new buffer to put all the Unicode bytes in + let mut multi_byte = String::new(); + // Get the first byte + let decoded_char = string_to_unicode_char(&name.item, &name.tag); match decoded_char { Ok(ch) => multi_byte.push(ch), Err(e) => return Err(e), } + // Get the rest of the bytes + for byte_part in args.rest { + let byte_part: Tagged = FromValue::from_value(&byte_part)?; + let decoded_char = string_to_unicode_char(&byte_part, &byte_part.tag); + match decoded_char { + Ok(ch) => multi_byte.push(ch), + Err(e) => return Err(e), + } + } + Ok(OutputStream::one( + UntaggedValue::string(multi_byte).into_value(name.tag), + )) + } else { + let decoded_char = string_to_unicode_char(&name.item, &name.tag); + if let Ok(ch) = decoded_char { + Ok(OutputStream::one( + UntaggedValue::string(ch).into_value(name.tag()), + )) + } else { + Err(ShellError::labeled_error( + "error decoding Unicode character", + "error decoding Unicode character", + name.tag(), + )) + } } - Ok(ActionStream::one(ReturnSuccess::value( - UntaggedValue::string(multi_byte).into_value(name.tag), - ))) } else { - let decoded_char = string_to_unicode_char(&name.item, &name.tag); - if let Ok(ch) = decoded_char { - Ok(ActionStream::one(ReturnSuccess::value( - UntaggedValue::string(ch).into_value(name.tag()), - ))) + let special_character = str_to_character(&name.item); + if let Some(output) = special_character { + Ok(OutputStream::one( + UntaggedValue::string(output).into_value(name.tag()), + )) } else { Err(ShellError::labeled_error( - "error decoding Unicode character", - "error decoding Unicode character", + "error finding named character", + "error finding named character", name.tag(), )) } } } else { - let special_character = str_to_character(&name.item); - if let Some(output) = special_character { - Ok(ActionStream::one(ReturnSuccess::value( - UntaggedValue::string(output).into_value(name.tag()), - ))) - } else { - Err(ShellError::labeled_error( - "error finding named character", - "error finding named character", - name.tag(), - )) - } + Err(ShellError::labeled_error( + "char requires the name of the character", + "missing name of the character", + &args_tag, + )) } } } @@ -134,55 +256,7 @@ fn string_to_unicode_char(s: &str, t: &Tag) -> Result { } fn str_to_character(s: &str) -> Option { - match s { - // These are some regular characters that either can't used or - // it's just easier to use them like this. - "newline" | "enter" | "nl" => Some("\n".into()), - "tab" => Some("\t".into()), - "sp" | "space" => Some(" ".into()), - "pipe" => Some('|'.to_string()), - "left_brace" | "lbrace" => Some('{'.to_string()), - "right_brace" | "rbrace" => Some('}'.to_string()), - "left_paren" | "lparen" => Some('('.to_string()), - "right_paren" | "rparen" => Some(')'.to_string()), - "left_bracket" | "lbracket" => Some('['.to_string()), - "right_bracket" | "rbracket" => Some(']'.to_string()), - - // Unicode names came from https://www.compart.com/en/unicode - // Private Use Area (U+E000-U+F8FF) - // Unicode can't be mixed with Ansi or it will break width calculation - "branch" => Some('\u{e0a0}'.to_string()), //  - "segment" => Some('\u{e0b0}'.to_string()), //  - - "identical_to" | "hamburger" => Some('\u{2261}'.to_string()), // ≡ - "not_identical_to" | "branch_untracked" => Some('\u{2262}'.to_string()), // ≢ - "strictly_equivalent_to" | "branch_identical" => Some('\u{2263}'.to_string()), // ≣ - - "upwards_arrow" | "branch_ahead" => Some('\u{2191}'.to_string()), // ↑ - "downwards_arrow" | "branch_behind" => Some('\u{2193}'.to_string()), // ↓ - "up_down_arrow" | "branch_ahead_behind" => Some('\u{2195}'.to_string()), // ↕ - - "black_right_pointing_triangle" | "prompt" => Some('\u{25b6}'.to_string()), // ▶ - "vector_or_cross_product" | "failed" => Some('\u{2a2f}'.to_string()), // ⨯ - "high_voltage_sign" | "elevated" => Some('\u{26a1}'.to_string()), // ⚡ - "tilde" | "twiddle" | "squiggly" | "home" => Some("~".into()), // ~ - "hash" | "hashtag" | "pound_sign" | "sharp" | "root" => Some("#".into()), // # - - // Weather symbols - "sun" | "sunny" | "sunrise" => Some("☀️".to_string()), - "moon" => Some("🌛".to_string()), - "cloudy" | "cloud" | "clouds" => Some("☁️".to_string()), - "rainy" | "rain" => Some("🌦️".to_string()), - "foggy" | "fog" => Some("🌫️".to_string()), - "mist" | "haze" => Some("\u{2591}".to_string()), - "snowy" | "snow" => Some("❄️".to_string()), - "thunderstorm" | "thunder" => Some("🌩️".to_string()), - - "bel" => Some('\x07'.to_string()), // Terminal Bell - "backspace" => Some('\x08'.to_string()), // Backspace - - _ => None, - } + CHAR_MAP.get(s).map(|s| s.into()) } #[cfg(test)]