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 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 let cwd = engine_state.cwd(Some(stack))?;
52 let full_path = nu_path::expand_path_with(path_no_whitespace, &cwd, true);
53
54 if full_path.exists() {
56 open_path(full_path, engine_state, stack, path.span)?;
57 return Ok(PipelineData::empty());
58 }
59 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}