* Allow querying the current path separator * Convert char command to engine-p * Wrap char args into struct * Add --list option to char command This lists all the available character names, along with the character and its unicode points.
274 lines
11 KiB
Rust
274 lines
11 KiB
Rust
use crate::prelude::*;
|
||
use nu_engine::{FromValue, WholeStreamCommand};
|
||
use nu_errors::ShellError;
|
||
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<Tagged<String>>,
|
||
rest: Vec<Value>,
|
||
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"
|
||
}
|
||
|
||
fn signature(&self) -> Signature {
|
||
Signature::build("char")
|
||
.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 (e.g., 'newline')."
|
||
}
|
||
|
||
fn examples(&self) -> Vec<Example> {
|
||
vec![
|
||
Example {
|
||
description: "Output newline",
|
||
example: r#"char newline"#,
|
||
result: Some(vec![Value::from("\n")]),
|
||
},
|
||
Example {
|
||
description: "Output prompt character, newline and a hamburger character",
|
||
example: r#"echo (char prompt) (char newline) (char hamburger)"#,
|
||
result: Some(vec![
|
||
UntaggedValue::string("\u{25b6}").into(),
|
||
UntaggedValue::string("\n").into(),
|
||
UntaggedValue::string("\u{2261}").into(),
|
||
]),
|
||
},
|
||
Example {
|
||
description: "Output Unicode character",
|
||
example: r#"char -u 1f378"#,
|
||
result: Some(vec![Value::from("\u{1f378}")]),
|
||
},
|
||
Example {
|
||
description: "Output multi-byte Unicode character",
|
||
example: r#"char -u 1F468 200D 1F466 200D 1F466"#,
|
||
result: Some(vec![Value::from(
|
||
"\u{1F468}\u{200D}\u{1F466}\u{200D}\u{1F466}",
|
||
)]),
|
||
},
|
||
]
|
||
}
|
||
|
||
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
|
||
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"),
|
||
};
|
||
|
||
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<String> =
|
||
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<String> = 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(),
|
||
))
|
||
}
|
||
}
|
||
} else {
|
||
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 finding named character",
|
||
"error finding named character",
|
||
name.tag(),
|
||
))
|
||
}
|
||
}
|
||
} else {
|
||
Err(ShellError::labeled_error(
|
||
"char requires the name of the character",
|
||
"missing name of the character",
|
||
&args_tag,
|
||
))
|
||
}
|
||
}
|
||
}
|
||
|
||
fn string_to_unicode_char(s: &str, t: &Tag) -> Result<char, ShellError> {
|
||
let decoded_char = u32::from_str_radix(s, 16)
|
||
.ok()
|
||
.and_then(std::char::from_u32);
|
||
|
||
if let Some(ch) = decoded_char {
|
||
Ok(ch)
|
||
} else {
|
||
Err(ShellError::labeled_error(
|
||
"error decoding Unicode character",
|
||
"error decoding Unicode character",
|
||
t,
|
||
))
|
||
}
|
||
}
|
||
|
||
fn str_to_character(s: &str) -> Option<String> {
|
||
CHAR_MAP.get(s).map(|s| s.into())
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::Char;
|
||
use super::ShellError;
|
||
|
||
#[test]
|
||
fn examples_work_as_expected() -> Result<(), ShellError> {
|
||
use crate::examples::test as test_examples;
|
||
|
||
test_examples(Char {})
|
||
}
|
||
}
|