nu_command/platform/ansi/
link.rs1use nu_engine::command_prelude::*;
2
3#[derive(Clone)]
4pub struct AnsiLink;
5
6impl Command for AnsiLink {
7 fn name(&self) -> &str {
8 "ansi link"
9 }
10
11 fn signature(&self) -> Signature {
12 Signature::build("ansi link")
13 .input_output_types(vec![
14 (Type::String, Type::String),
15 (
16 Type::List(Box::new(Type::String)),
17 Type::List(Box::new(Type::String)),
18 ),
19 (Type::table(), Type::table()),
20 (Type::record(), Type::record()),
21 ])
22 .named(
23 "text",
24 SyntaxShape::String,
25 "Link text. Uses uri as text if absent. In case of
26 tables, records and lists applies this text to all elements",
27 Some('t'),
28 )
29 .rest(
30 "cell path",
31 SyntaxShape::CellPath,
32 "For a data structure input, add links to all strings at the given cell paths.",
33 )
34 .allow_variants_without_examples(true)
35 .category(Category::Platform)
36 }
37
38 fn description(&self) -> &str {
39 "Add a link (using OSC 8 escape sequence) to the given string."
40 }
41
42 fn run(
43 &self,
44 engine_state: &EngineState,
45 stack: &mut Stack,
46 call: &Call,
47 input: PipelineData,
48 ) -> Result<PipelineData, ShellError> {
49 operate(engine_state, stack, call, input)
50 }
51
52 fn examples(&self) -> Vec<Example> {
53 vec![
54 Example {
55 description: "Create a link to open some file",
56 example: "'file:///file.txt' | ansi link --text 'Open Me!'",
57 result: Some(Value::string(
58 "\u{1b}]8;;file:///file.txt\u{1b}\\Open Me!\u{1b}]8;;\u{1b}\\",
59 Span::unknown(),
60 )),
61 },
62 Example {
63 description: "Create a link without text",
64 example: "'https://www.nushell.sh/' | ansi link",
65 result: Some(Value::string(
66 "\u{1b}]8;;https://www.nushell.sh/\u{1b}\\https://www.nushell.sh/\u{1b}]8;;\u{1b}\\",
67 Span::unknown(),
68 )),
69 },
70 Example {
71 description: "Format a table column into links",
72 example: "[[url text]; [https://example.com Text]] | ansi link url",
73 result: None,
74 },
75 ]
76 }
77}
78
79fn operate(
80 engine_state: &EngineState,
81 stack: &mut Stack,
82 call: &Call,
83 input: PipelineData,
84) -> Result<PipelineData, ShellError> {
85 let text: Option<Spanned<String>> = call.get_flag(engine_state, stack, "text")?;
86 let text = text.map(|e| e.item);
87 let column_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
88
89 let command_span = call.head;
90
91 if column_paths.is_empty() {
92 input.map(
93 move |v| process_value(&v, text.as_deref()),
94 engine_state.signals(),
95 )
96 } else {
97 input.map(
98 move |v| process_each_path(v, &column_paths, text.as_deref(), command_span),
99 engine_state.signals(),
100 )
101 }
102}
103
104fn process_each_path(
105 mut value: Value,
106 column_paths: &[CellPath],
107 text: Option<&str>,
108 command_span: Span,
109) -> Value {
110 for path in column_paths {
111 let ret = value.update_cell_path(&path.members, Box::new(|v| process_value(v, text)));
112 if let Err(error) = ret {
113 return Value::error(error, command_span);
114 }
115 }
116 value
117}
118
119fn process_value(value: &Value, text: Option<&str>) -> Value {
120 let span = value.span();
121 match value {
122 Value::String { val, .. } => {
123 let text = text.unwrap_or(val.as_str());
124 let result = add_osc_link(text, val.as_str());
125 Value::string(result, span)
126 }
127 other => {
128 let got = format!("value is {}, not string", other.get_type());
129
130 Value::error(
131 ShellError::TypeMismatch {
132 err_message: got,
133 span: other.span(),
134 },
135 other.span(),
136 )
137 }
138 }
139}
140
141fn add_osc_link(text: &str, link: &str) -> String {
142 format!("\u{1b}]8;;{link}\u{1b}\\{text}\u{1b}]8;;\u{1b}\\")
143}
144
145#[cfg(test)]
146mod tests {
147 use super::AnsiLink;
148
149 #[test]
150 fn examples_work_as_expected() {
151 use crate::test_examples;
152
153 test_examples(AnsiLink {})
154 }
155}