From a909c60f05138e621bc09994fc994505dc8e2c73 Mon Sep 17 00:00:00 2001 From: Artemiy Date: Sun, 15 Jan 2023 18:23:37 +0300 Subject: [PATCH] Ansi link (#7751) --- crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/platform/ansi/link.rs | 162 ++++++++++++++++++++ crates/nu-command/src/platform/ansi/mod.rs | 2 + crates/nu-command/src/platform/mod.rs | 2 +- crates/nu-protocol/src/value/mod.rs | 4 +- 5 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 crates/nu-command/src/platform/ansi/link.rs diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 5eb73c711..f8e978a09 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -288,6 +288,7 @@ pub fn create_default_context() -> EngineState { Ansi, AnsiGradient, AnsiStrip, + AnsiLink, Clear, Du, KeybindingsDefault, diff --git a/crates/nu-command/src/platform/ansi/link.rs b/crates/nu-command/src/platform/ansi/link.rs new file mode 100644 index 000000000..362ce90f3 --- /dev/null +++ b/crates/nu-command/src/platform/ansi/link.rs @@ -0,0 +1,162 @@ +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", + ) + .vectorizes_over_list(true) + .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, &command_span), + 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, command_span)), + ); + if let Err(error) = ret { + return Value::Error { error }; + } + } + value +} + +fn process_value(value: &Value, text: &Option, command_span: &Span) -> 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: ShellError::TypeMismatch(got, other.span().unwrap_or(*command_span)), + } + } + } +} + +fn add_osc_link(text: &str, link: &str) -> String { + format!("\u{1b}]8;;{}\u{1b}\\{}\u{1b}]8;;\u{1b}\\", link, text) +} + +#[cfg(test)] +mod tests { + use super::SubCommand; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/platform/ansi/mod.rs b/crates/nu-command/src/platform/ansi/mod.rs index 4613686bc..a0b02d3d5 100644 --- a/crates/nu-command/src/platform/ansi/mod.rs +++ b/crates/nu-command/src/platform/ansi/mod.rs @@ -1,7 +1,9 @@ mod ansi_; mod gradient; +mod link; mod strip; pub use ansi_::AnsiCommand as Ansi; pub use gradient::SubCommand as AnsiGradient; +pub use link::SubCommand as AnsiLink; pub use strip::SubCommand as AnsiStrip; diff --git a/crates/nu-command/src/platform/mod.rs b/crates/nu-command/src/platform/mod.rs index 085f4f972..d1cf60141 100644 --- a/crates/nu-command/src/platform/mod.rs +++ b/crates/nu-command/src/platform/mod.rs @@ -8,7 +8,7 @@ mod reedline_commands; mod sleep; mod term_size; -pub use ansi::{Ansi, AnsiGradient, AnsiStrip}; +pub use ansi::{Ansi, AnsiGradient, AnsiLink, AnsiStrip}; pub use clear::Clear; pub use dir_info::{DirBuilder, DirInfo, FileInfo}; pub use du::Du; diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index d67470f0c..ee6229b81 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -1011,10 +1011,10 @@ impl Value { } /// Follow a given cell path into the value: for example accessing select elements in a stream or list - pub fn update_cell_path( + pub fn update_cell_path<'a>( &mut self, cell_path: &[PathMember], - callback: Box Value>, + callback: Box Value + 'a>, ) -> Result<(), ShellError> { let orig = self.clone();