From 9a94b3c6563a08bac19cfc7e4f9ac04f821ae65d Mon Sep 17 00:00:00 2001 From: Elton Leander Pinto Date: Fri, 8 May 2020 14:19:48 -0400 Subject: [PATCH] `start` command in nushell (#1727) * mvp for start command * modified the signature of the start command * parse filenames * working model for macos is done * refactored to read from pipes * start command works well on macos; manual testing reveals need of --args flag support * implemented start error; color printing of warning and errors * ran clippy and fixed warnings * fix a clippy lint that was caught in pipeline * fix dead code clippy lint for windows * add cfg annotation to import --- Cargo.lock | 14 +++ crates/nu_plugin_start/Cargo.toml | 22 ++++ crates/nu_plugin_start/build.rs | 3 + crates/nu_plugin_start/src/lib.rs | 4 + crates/nu_plugin_start/src/main.rs | 9 ++ crates/nu_plugin_start/src/nu/mod.rs | 25 ++++ crates/nu_plugin_start/src/start.rs | 181 +++++++++++++++++++++++++++ 7 files changed, 258 insertions(+) create mode 100644 crates/nu_plugin_start/Cargo.toml create mode 100644 crates/nu_plugin_start/build.rs create mode 100644 crates/nu_plugin_start/src/lib.rs create mode 100644 crates/nu_plugin_start/src/main.rs create mode 100644 crates/nu_plugin_start/src/nu/mod.rs create mode 100644 crates/nu_plugin_start/src/start.rs diff --git a/Cargo.lock b/Cargo.lock index e21eea5ce..e1fc3ee18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2427,6 +2427,20 @@ dependencies = [ "nu-source", ] +[[package]] +name = "nu_plugin_start" +version = "0.1.0" +dependencies = [ + "ansi_term 0.12.1", + "nu-build", + "nu-errors", + "nu-plugin", + "nu-protocol", + "nu-source", + "open", + "url", +] + [[package]] name = "nu_plugin_str" version = "0.13.0" diff --git a/crates/nu_plugin_start/Cargo.toml b/crates/nu_plugin_start/Cargo.toml new file mode 100644 index 000000000..767bafa01 --- /dev/null +++ b/crates/nu_plugin_start/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "nu_plugin_start" +version = "0.1.0" +authors = ["The Nu Project Contributors"] +edition = "2018" +description = "A plugin to open files/URLs directly from Nushell" +license = "MIT" + +[lib] +doctest = false + +[dependencies] +nu-plugin = { path = "../nu-plugin", version = "0.13.0" } +nu-protocol = { path = "../nu-protocol", version = "0.13.0" } +nu-source = { path = "../nu-source", version = "0.13.0" } +nu-errors = { path = "../nu-errors", version = "0.13.0" } +ansi_term = "0.12.1" +url = "2.1.1" +open = "1.4.0" + +[build-dependencies] +nu-build = { version = "0.13.0", path = "../nu-build" } diff --git a/crates/nu_plugin_start/build.rs b/crates/nu_plugin_start/build.rs new file mode 100644 index 000000000..b7511cfc6 --- /dev/null +++ b/crates/nu_plugin_start/build.rs @@ -0,0 +1,3 @@ +fn main() -> Result<(), Box> { + nu_build::build() +} diff --git a/crates/nu_plugin_start/src/lib.rs b/crates/nu_plugin_start/src/lib.rs new file mode 100644 index 000000000..de341633d --- /dev/null +++ b/crates/nu_plugin_start/src/lib.rs @@ -0,0 +1,4 @@ +mod nu; +mod start; + +pub use start::Start; diff --git a/crates/nu_plugin_start/src/main.rs b/crates/nu_plugin_start/src/main.rs new file mode 100644 index 000000000..85e416a29 --- /dev/null +++ b/crates/nu_plugin_start/src/main.rs @@ -0,0 +1,9 @@ +use nu_plugin::serve_plugin; +use nu_plugin_start::Start; + +fn main() { + serve_plugin(&mut Start { + filenames: vec![], + application: None, + }); +} diff --git a/crates/nu_plugin_start/src/nu/mod.rs b/crates/nu_plugin_start/src/nu/mod.rs new file mode 100644 index 000000000..6db74d66e --- /dev/null +++ b/crates/nu_plugin_start/src/nu/mod.rs @@ -0,0 +1,25 @@ +use nu_errors::ShellError; +use nu_plugin::Plugin; +use nu_protocol::{CallInfo, Signature, SyntaxShape, Value}; + +use crate::start::Start; + +impl Plugin for Start { + fn config(&mut self) -> Result { + Ok(Signature::build("start") + .desc("Opens each file/directory/URL using the default application") + .rest(SyntaxShape::String, "files/urls/directories to open") + .named( + "application", + SyntaxShape::String, + "Specifies the application used for opening the files/directories/urls", + Some('a'), + )) + } + fn sink(&mut self, call_info: CallInfo, input: Vec) { + self.parse(call_info, input); + if let Err(e) = self.exec() { + println!("{}", e); + } + } +} diff --git a/crates/nu_plugin_start/src/start.rs b/crates/nu_plugin_start/src/start.rs new file mode 100644 index 000000000..705702c5a --- /dev/null +++ b/crates/nu_plugin_start/src/start.rs @@ -0,0 +1,181 @@ +use ansi_term::Color; +use nu_protocol::{CallInfo, Value}; +use std::error::Error; +use std::fmt; +use std::path::Path; + +#[cfg(not(target_os = "windows"))] +use std::process::{Command, Stdio}; + +pub struct Start { + pub filenames: Vec, + pub application: Option, +} + +#[derive(Debug)] +pub struct StartError { + msg: String, +} + +impl StartError { + fn new(msg: &str) -> StartError { + StartError { + msg: msg.to_owned(), + } + } +} + +impl fmt::Display for StartError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}: {}", + Color::Red.bold().paint("start error"), + self.msg + ) + } +} + +impl Error for StartError {} + +impl Start { + pub fn parse(&mut self, call_info: CallInfo, input: Vec) { + input.iter().for_each(|val| { + if val.is_some() { + self.parse_value(val); + } + }); + self.parse_filenames(&call_info); + self.parse_application(&call_info); + } + + fn add_filename(&mut self, filename: String) { + if Path::new(&filename).exists() || url::Url::parse(&filename).is_ok() { + self.filenames.push(filename); + } else { + print_warning(format!( + "The file '{}' does not exist", + Color::White.bold().paint(filename) + )); + } + } + + fn parse_filenames(&mut self, call_info: &CallInfo) { + let candidates = match &call_info.args.positional { + Some(values) => values + .iter() + .map(|val| val.as_string()) + .collect::, _>>() + .unwrap_or_else(|_| vec![]), + None => vec![], + }; + + for candidate in candidates { + self.add_filename(candidate); + } + } + + fn parse_application(&mut self, call_info: &CallInfo) { + self.application = if let Some(app) = call_info.args.get("application") { + match app.as_string() { + Ok(name) => Some(name), + Err(_) => None, + } + } else { + None + }; + } + + pub fn parse_value(&mut self, input: &Value) { + if let Ok(filename) = input.as_string() { + self.add_filename(filename); + } else { + print_warning(format!("Could not convert '{:?}' to string", input)); + } + } + + #[cfg(target_os = "macos")] + pub fn exec(&mut self) -> Result<(), StartError> { + let mut args = vec![]; + args.append(&mut self.filenames); + + if let Some(app_name) = &self.application { + args.append(&mut vec![String::from("-a"), app_name.to_string()]); + } + exec_cmd("open", &args) + } + + #[cfg(target_os = "windows")] + pub fn exec(&mut self) -> Result<(), StartError> { + if let Some(app_name) = &self.application { + for file in &self.filenames { + match open::with(file, app_name) { + Ok(_) => continue, + Err(_) => { + return Err(StartError::new( + "Failed to open file with specified application", + )) + } + } + } + } else { + for file in &self.filenames { + match open::that(file) { + Ok(_) => continue, + Err(_) => { + return Err(StartError::new( + "Failed to open file with default application", + )) + } + } + } + } + Ok(()) + } + + #[cfg(not(any(target_os = "windows", target_os = "macos")))] + pub fn exec(&mut self) -> Result<(), StartError> { + let mut args = vec![]; + args.append(&mut self.filenames); + + if let Some(app_name) = &self.application { + exec_cmd(&app_name, &args) + } else { + for cmd in &["xdg-open", "gnome-open", "kde-open", "wslview"] { + if exec_cmd(cmd, &args).is_err() { + continue; + } + } + Err(StartError::new( + "Failed to open file(s) with xdg-open. gnome-open, kde-open, and wslview", + )) + } + } +} + +fn print_warning(msg: String) { + println!("{}: {}", Color::Yellow.bold().paint("warning"), msg); +} + +#[cfg(not(target_os = "windows"))] +fn exec_cmd(cmd: &str, args: &[String]) -> Result<(), StartError> { + if args.is_empty() { + return Err(StartError::new("No file(s) or application provided")); + } + let status = match Command::new(cmd) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .args(args) + .status() + { + Ok(exit_code) => exit_code, + Err(_) => return Err(StartError::new("Failed to run native open syscall")), + }; + if status.success() { + Ok(()) + } else { + Err(StartError::new( + "Failed to run start. Hint: The file(s)/application may not exist", + )) + } +}