use nu_engine::CallExt; use nu_protocol::{ ast::{Call, CellPath}, engine::Command, engine::EngineState, engine::Stack, Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, }; #[derive(Clone)] pub struct SubCommand; impl Command for SubCommand { fn name(&self) -> &str { "ansi link" } fn signature(&self) -> Signature { Signature::build("ansi link") .input_output_types(vec![ (Type::String, Type::String), ( Type::List(Box::new(Type::String)), Type::List(Box::new(Type::String)), ), (Type::Table(vec![]), Type::Table(vec![])), (Type::Record(vec![]), Type::Record(vec![])), ]) .named( "text", SyntaxShape::String, "Link text. Uses uri as text if absent. In case of tables, records and lists applies this text to all elements", Some('t'), ) .rest( "cell path", SyntaxShape::CellPath, "for a data structure input, add links to all strings at the given cell paths", ) .allow_variants_without_examples(true) .category(Category::Platform) } fn usage(&self) -> &str { "Add a link (using OSC 8 escape sequence) to the given string." } fn run( &self, engine_state: &EngineState, stack: &mut Stack, call: &Call, input: PipelineData, ) -> Result { operate(engine_state, stack, call, input) } fn examples(&self) -> Vec { vec![ Example { description: "Create a link to open some file", example: "'file:///file.txt' | ansi link --text 'Open Me!'", result: Some(Value::string( "\u{1b}]8;;file:///file.txt\u{1b}\\Open Me!\u{1b}]8;;\u{1b}\\", Span::unknown(), )), }, Example { description: "Create a link without text", example: "'https://www.nushell.sh/' | ansi link", result: Some(Value::string( "\u{1b}]8;;https://www.nushell.sh/\u{1b}\\https://www.nushell.sh/\u{1b}]8;;\u{1b}\\", Span::unknown(), )), }, Example { description: "Format a table column into links", example: "[[url text]; [https://example.com Text]] | ansi link url", result: None, }, ] } } fn operate( engine_state: &EngineState, stack: &mut Stack, call: &Call, input: PipelineData, ) -> Result { let text: Option> = call.get_flag(engine_state, stack, "text")?; let text = text.map(|e| e.item); let column_paths: Vec = call.rest(engine_state, stack, 0)?; let command_span = call.head; if column_paths.is_empty() { input.map( move |v| process_value(&v, &text), engine_state.ctrlc.clone(), ) } else { input.map( move |v| process_each_path(v, &column_paths, &text, command_span), engine_state.ctrlc.clone(), ) } } fn process_each_path( mut value: Value, column_paths: &Vec, text: &Option, command_span: Span, ) -> Value { for path in column_paths { let ret = value.update_cell_path(&path.members, Box::new(|v| process_value(v, text))); if let Err(error) = ret { return Value::Error { error: Box::new(error), span: command_span, }; } } value } fn process_value(value: &Value, text: &Option) -> Value { match value { Value::String { val, span } => { let text = text.as_deref().unwrap_or(val.as_str()); let result = add_osc_link(text, val.as_str()); Value::string(result, *span) } other => { let got = format!("value is {}, not string", other.get_type()); Value::Error { error: Box::new(ShellError::TypeMismatch { err_message: got, span: other.span(), }), span: other.span(), } } } } fn add_osc_link(text: &str, link: &str) -> String { format!("\u{1b}]8;;{link}\u{1b}\\{text}\u{1b}]8;;\u{1b}\\") } #[cfg(test)] mod tests { use super::SubCommand; #[test] fn examples_work_as_expected() { use crate::test_examples; test_examples(SubCommand {}) } }