nu_command/platform/ansi/
link.rs

1use 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}