Skip to main content

nu_command/filesystem/
start.rs

1use itertools::Itertools;
2use nu_engine::{command_prelude::*, env_to_strings};
3use nu_protocol::{ShellError, shell_error::generic::GenericError};
4use std::{
5    ffi::{OsStr, OsString},
6    process::Stdio,
7};
8
9#[derive(Clone)]
10pub struct Start;
11
12impl Command for Start {
13    fn name(&self) -> &str {
14        "start"
15    }
16
17    fn description(&self) -> &str {
18        "Open a folder, file, or website in the default application or viewer."
19    }
20
21    fn search_terms(&self) -> Vec<&str> {
22        vec!["load", "folder", "directory", "run", "open"]
23    }
24
25    fn signature(&self) -> nu_protocol::Signature {
26        Signature::build("start")
27            .input_output_types(vec![(Type::Nothing, Type::Any)])
28            .required("path", SyntaxShape::String, "Path or URL to open.")
29            .category(Category::FileSystem)
30    }
31
32    fn run(
33        &self,
34        engine_state: &EngineState,
35        stack: &mut Stack,
36        call: &Call,
37        _input: PipelineData,
38    ) -> Result<PipelineData, ShellError> {
39        let path = call.req::<Spanned<String>>(engine_state, stack, 0)?;
40        let path = Spanned {
41            item: nu_utils::strip_ansi_string_unlikely(path.item),
42            span: path.span,
43        };
44        let path_no_whitespace = path.item.trim_end_matches(|x| matches!(x, '\x09'..='\x0d'));
45        // Attempt to parse the input as a URL
46        if let Ok(url) = url::Url::parse(path_no_whitespace) {
47            open_path(url.as_str(), engine_state, stack, path.span)?;
48            return Ok(PipelineData::empty());
49        }
50        // If it's not a URL, treat it as a file path
51        let cwd = engine_state.cwd(Some(stack))?;
52        let full_path = nu_path::expand_path_with(path_no_whitespace, &cwd, true);
53
54        // Check if the path exists or if it's a valid file/directory
55        if full_path.exists() {
56            open_path(full_path, engine_state, stack, path.span)?;
57            return Ok(PipelineData::empty());
58        }
59        // If neither file nor URL, return an error
60        Err(ShellError::Generic(
61            GenericError::new(
62                format!("Cannot find file or URL: {}", &path.item),
63                "",
64                path.span,
65            )
66            .with_help("Ensure the path or URL is correct and try again."),
67        ))
68    }
69    fn examples(&self) -> Vec<nu_protocol::Example<'_>> {
70        vec![
71            Example {
72                description: "Open a text file with the default text editor.",
73                example: "start file.txt",
74                result: None,
75            },
76            Example {
77                description: "Open an image with the default image viewer.",
78                example: "start file.jpg",
79                result: None,
80            },
81            Example {
82                description: "Open the current directory with the default file manager.",
83                example: "start .",
84                result: None,
85            },
86            Example {
87                description: "Open a PDF with the default PDF viewer.",
88                example: "start file.pdf",
89                result: None,
90            },
91            Example {
92                description: "Open a website with the default browser.",
93                example: "start https://www.nushell.sh",
94                result: None,
95            },
96            Example {
97                description: "Open an application-registered protocol URL.",
98                example: "start obsidian://open?vault=Test",
99                result: None,
100            },
101        ]
102    }
103}
104
105fn open_path(
106    path: impl AsRef<OsStr>,
107    engine_state: &EngineState,
108    stack: &Stack,
109    span: Span,
110) -> Result<(), ShellError> {
111    try_commands(open::commands(path), engine_state, stack, span)
112}
113
114fn try_commands(
115    commands: Vec<std::process::Command>,
116    engine_state: &EngineState,
117    stack: &Stack,
118    span: Span,
119) -> Result<(), ShellError> {
120    let env_vars_str = env_to_strings(engine_state, stack)?;
121    let mut last_err = None;
122
123    for mut cmd in commands {
124        let status = cmd
125            .envs(&env_vars_str)
126            .stdin(Stdio::null())
127            .stdout(Stdio::null())
128            .stderr(Stdio::null())
129            .status();
130
131        match status {
132            Ok(status) if status.success() => return Ok(()),
133            Ok(status) => {
134                last_err = Some(format!(
135                    "Command `{}` failed with exit code: {}",
136                    format_command(&cmd),
137                    status.code().unwrap_or(-1)
138                ));
139            }
140            Err(err) => {
141                last_err = Some(format!(
142                    "Command `{}` failed with error: {}",
143                    format_command(&cmd),
144                    err
145                ));
146            }
147        }
148    }
149
150    Err(ShellError::ExternalCommand {
151        label: "Failed to start the specified path or URL".to_string(),
152        help: format!(
153            "Try a different path or install the appropriate application.\n{}",
154            last_err.unwrap_or_default()
155        ),
156        span,
157    })
158}
159
160fn format_command(command: &std::process::Command) -> String {
161    let parts_iter = std::iter::once(command.get_program()).chain(command.get_args());
162    Itertools::intersperse(parts_iter, OsStr::new(" "))
163        .collect::<OsString>()
164        .to_string_lossy()
165        .into_owned()
166}