nu_command/filesystem/
start.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
use itertools::Itertools;
use nu_engine::{command_prelude::*, env_to_strings};
use nu_path::canonicalize_with;
use std::{
    ffi::{OsStr, OsString},
    path::Path,
    process::Stdio,
};

#[derive(Clone)]
pub struct Start;

impl Command for Start {
    fn name(&self) -> &str {
        "start"
    }

    fn description(&self) -> &str {
        "Open a folder, file or website in the default application or viewer."
    }

    fn search_terms(&self) -> Vec<&str> {
        vec!["load", "folder", "directory", "run", "open"]
    }

    fn signature(&self) -> nu_protocol::Signature {
        Signature::build("start")
            .input_output_types(vec![(Type::Nothing, Type::Any)])
            .required("path", SyntaxShape::String, "Path to open.")
            .category(Category::FileSystem)
    }

    fn run(
        &self,
        engine_state: &EngineState,
        stack: &mut Stack,
        call: &Call,
        _input: PipelineData,
    ) -> Result<PipelineData, ShellError> {
        let path = call.req::<Spanned<String>>(engine_state, stack, 0)?;
        let path = Spanned {
            item: nu_utils::strip_ansi_string_unlikely(path.item),
            span: path.span,
        };
        let path_no_whitespace = &path.item.trim_end_matches(|x| matches!(x, '\x09'..='\x0d'));
        // only check if file exists in current current directory
        let file_path = Path::new(path_no_whitespace);
        if file_path.exists() {
            open_path(path_no_whitespace, engine_state, stack, path.span)?;
        } else if file_path.starts_with("https://") || file_path.starts_with("http://") {
            let url = url::Url::parse(&path.item).map_err(|_| ShellError::GenericError {
                error: format!("Cannot parse url: {}", &path.item),
                msg: "".to_string(),
                span: Some(path.span),
                help: Some("cannot parse".to_string()),
                inner: vec![],
            })?;
            open_path(url.as_str(), engine_state, stack, path.span)?;
        } else {
            // try to distinguish between file not found and opening url without prefix
            let cwd = engine_state.cwd(Some(stack))?;
            if let Ok(canon_path) = canonicalize_with(path_no_whitespace, cwd) {
                open_path(canon_path, engine_state, stack, path.span)?;
            } else {
                // open crate does not allow opening URL without prefix
                let path_with_prefix = Path::new("https://").join(&path.item);
                let common_domains = ["com", "net", "org", "edu", "sh"];
                if let Some(url) = path_with_prefix.to_str() {
                    let url = url::Url::parse(url).map_err(|_| ShellError::GenericError {
                        error: format!("Cannot parse url: {}", &path.item),
                        msg: "".into(),
                        span: Some(path.span),
                        help: Some("cannot parse".into()),
                        inner: vec![],
                    })?;
                    if let Some(domain) = url.host() {
                        let domain = domain.to_string();
                        let ext = Path::new(&domain).extension().and_then(|s| s.to_str());
                        if let Some(url_ext) = ext {
                            if common_domains.contains(&url_ext) {
                                open_path(url.as_str(), engine_state, stack, path.span)?;
                            }
                        }
                    }
                    return Err(ShellError::GenericError {
                        error: format!("Cannot find file or url: {}", &path.item),
                        msg: "".into(),
                        span: Some(path.span),
                        help: Some("Use prefix https:// to disambiguate URLs from files".into()),
                        inner: vec![],
                    });
                }
            };
        }
        Ok(PipelineData::Empty)
    }

    fn examples(&self) -> Vec<nu_protocol::Example> {
        vec![
            Example {
                description: "Open a text file with the default text editor",
                example: "start file.txt",
                result: None,
            },
            Example {
                description: "Open an image with the default image viewer",
                example: "start file.jpg",
                result: None,
            },
            Example {
                description: "Open the current directory with the default file manager",
                example: "start .",
                result: None,
            },
            Example {
                description: "Open a pdf with the default pdf viewer",
                example: "start file.pdf",
                result: None,
            },
            Example {
                description: "Open a website with default browser",
                example: "start https://www.nushell.sh",
                result: None,
            },
        ]
    }
}

fn open_path(
    path: impl AsRef<OsStr>,
    engine_state: &EngineState,
    stack: &Stack,
    span: Span,
) -> Result<(), ShellError> {
    try_commands(open::commands(path), engine_state, stack, span)
}

fn try_commands(
    commands: Vec<std::process::Command>,
    engine_state: &EngineState,
    stack: &Stack,
    span: Span,
) -> Result<(), ShellError> {
    let env_vars_str = env_to_strings(engine_state, stack)?;
    let cmd_run_result = commands.into_iter().map(|mut cmd| {
        let status = cmd
            .envs(&env_vars_str)
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status();
        match status {
            Ok(status) if status.success() => Ok(()),
            Ok(status) => Err(format!(
                "\nCommand `{}` failed with {}",
                format_command(&cmd),
                status
            )),
            Err(err) => Err(format!(
                "\nCommand `{}` failed with {}",
                format_command(&cmd),
                err
            )),
        }
    });

    for one_result in cmd_run_result {
        if let Err(err_msg) = one_result {
            return Err(ShellError::ExternalCommand {
                label: "No command found to start with this path".to_string(),
                help: "Try different path or install appropriate command\n".to_string() + &err_msg,
                span,
            });
        } else if one_result.is_ok() {
            break;
        }
    }
    Ok(())
}

fn format_command(command: &std::process::Command) -> String {
    let parts_iter = std::iter::repeat(command.get_program())
        .take(1)
        .chain(command.get_args());
    Itertools::intersperse(parts_iter, " ".as_ref())
        .collect::<OsString>()
        .to_string_lossy()
        .into_owned()
}