anomius fd684a204c
non-HTTP(s) URLs now works with start (#14370)
# Description
this PR should close #14315
This PR enhances the start command in Nushell to handle both files and
URLs more effectively, including support for custom URL schemes.
Previously, the start command only reliably opened HTTP and HTTPS URLs,
and custom schemes like Spotify and Obsidian which were not handled
earlier.

1. **Custom URL Schemes Support:**
- Added support for opening custom URL schemes

2. **Detailed Error Messages:**
- Improved error reporting for failed external commands.

- Captures and displays error output from the system to aid in
debugging.

**Example**

**Opening a custom URL scheme (e.g., Spotify):**

```bash
start spotify:track:4PTG3Z6ehGkBFwjybzWkR8?si=f9b4cdfc1aa14831
```

Opens the specified track in the Spotify application.

**User-Facing Changes**

- **New Feature:** The start command now supports opening URLs with
custom schemes
2025-01-23 17:14:31 -08:00

165 lines
5.2 KiB
Rust

use itertools::Itertools;
use nu_engine::{command_prelude::*, env_to_strings};
use nu_protocol::ShellError;
use std::{
ffi::{OsStr, OsString},
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 or URL 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'));
// Attempt to parse the input as a URL
if let Ok(url) = url::Url::parse(path_no_whitespace) {
open_path(url.as_str(), engine_state, stack, path.span)?;
return Ok(PipelineData::Empty);
}
// If it's not a URL, treat it as a file path
let cwd = engine_state.cwd(Some(stack))?;
let full_path = cwd.join(path_no_whitespace);
// Check if the path exists or if it's a valid file/directory
if full_path.exists() {
open_path(full_path, engine_state, stack, path.span)?;
return Ok(PipelineData::Empty);
}
// If neither file nor URL, return an error
Err(ShellError::GenericError {
error: format!("Cannot find file or URL: {}", &path.item),
msg: "".into(),
span: Some(path.span),
help: Some("Ensure the path or URL is correct and try again.".into()),
inner: vec![],
})
}
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 the default browser",
example: "start https://www.nushell.sh",
result: None,
},
Example {
description: "Open an application-registered protocol URL",
example: "start obsidian://open?vault=Test",
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 mut last_err = None;
for mut cmd in commands {
let status = cmd
.envs(&env_vars_str)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
match status {
Ok(status) if status.success() => return Ok(()),
Ok(status) => {
last_err = Some(format!(
"Command `{}` failed with exit code: {}",
format_command(&cmd),
status.code().unwrap_or(-1)
));
}
Err(err) => {
last_err = Some(format!(
"Command `{}` failed with error: {}",
format_command(&cmd),
err
));
}
}
}
Err(ShellError::ExternalCommand {
label: "Failed to start the specified path or URL".to_string(),
help: format!(
"Try a different path or install the appropriate application.\n{}",
last_err.unwrap_or_default()
),
span,
})
}
fn format_command(command: &std::process::Command) -> String {
let parts_iter = std::iter::once(command.get_program()).chain(command.get_args());
Itertools::intersperse(parts_iter, OsStr::new(" "))
.collect::<OsString>()
.to_string_lossy()
.into_owned()
}