diff --git a/Cargo.lock b/Cargo.lock index 10e357aad6..e164431b7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -587,13 +587,23 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.4.3" +version = "4.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ae8ba90b9d8b007efe66e55e48fb936272f5ca00349b5b0e89877520d35ea7" +checksum = "b5a2d6eec27fce550d708b2be5d798797e5a55b246b323ef36924a0001996352" dependencies = [ "clap 4.5.0", ] +[[package]] +name = "clap_complete_nushell" +version = "4.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe32110e006bccf720f8c9af3fee1ba7db290c724eab61544e1d3295be3a40e" +dependencies = [ + "clap 4.5.0", + "clap_complete", +] + [[package]] name = "clap_derive" version = "4.5.0" @@ -3985,6 +3995,7 @@ dependencies = [ "async-trait", "clap 4.5.0", "clap_complete", + "clap_complete_nushell", "colored", "futures", "itertools 0.12.0", diff --git a/Cargo.toml b/Cargo.toml index 9106164c72..2b64dffaab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ bytes = "1.5" cached = "0.48.1" chrono = { version = "0.4.34", features = ["serde"] } clap = { version = "4.5", features = ["derive", "env"] } -clap_complete = "4.3" +clap_complete = "4.5" colored = "2.0.0" config = "0.14.0" console = "0.15.8" diff --git a/crates/spfs/src/bootstrap.rs b/crates/spfs/src/bootstrap.rs index 5836fcc41c..fb74c20246 100644 --- a/crates/spfs/src/bootstrap.rs +++ b/crates/spfs/src/bootstrap.rs @@ -186,6 +186,16 @@ pub fn build_interactive_shell_command( ], vars: vec![shell_message], }), + Shell::Nushell(nu) => Ok(Command { + executable: nu.into(), + args: vec![ + "--env-config".into(), + rt.config.nu_env_file.as_os_str().to_owned(), + "--config".into(), + rt.config.nu_config_file.as_os_str().to_owned(), + ], + vars: vec![shell_message], + }), #[cfg(windows)] Shell::Powershell(ps1) => Ok(Command { executable: ps1.into(), @@ -221,6 +231,26 @@ where let startup_file = match shell.kind() { ShellKind::Bash => &runtime.config.sh_startup_file, ShellKind::Tcsh => &runtime.config.csh_startup_file, + ShellKind::Nushell => { + let mut cmd = command.into(); + for arg in args.into_iter().map(Into::into) { + cmd.push(" "); + cmd.push(arg); + } + let args = vec![ + "--env-config".into(), + runtime.config.nu_env_file.as_os_str().to_owned(), + "--config".into(), + runtime.config.nu_config_file.as_os_str().to_owned(), + "-c".into(), + cmd, + ]; + return Ok(Command { + executable: shell.executable().into(), + args, + vars: vec![], + }); + } ShellKind::Powershell => { let mut cmd = command.into(); for arg in args.into_iter().map(Into::into) { @@ -244,7 +274,6 @@ where let mut shell_args = vec![startup_file.into(), command.into()]; shell_args.extend(args.into_iter().map(Into::into)); - Ok(Command { executable: shell.executable().into(), args: shell_args, @@ -385,6 +414,7 @@ pub enum ShellKind { Bash, Tcsh, Powershell, + Nushell, } impl AsRef for ShellKind { @@ -392,6 +422,7 @@ impl AsRef for ShellKind { match self { Self::Bash => "bash", Self::Tcsh => "tcsh", + Self::Nushell => "nu", Self::Powershell => "powershell.exe", } } @@ -404,6 +435,7 @@ pub enum Shell { Bash(PathBuf), #[cfg(unix)] Tcsh(PathBuf), + Nushell(PathBuf), #[cfg(windows)] Powershell(PathBuf), } @@ -415,6 +447,7 @@ impl Shell { Self::Bash(_) => ShellKind::Bash, #[cfg(unix)] Self::Tcsh(_) => ShellKind::Tcsh, + Self::Nushell(_) => ShellKind::Nushell, #[cfg(windows)] Self::Powershell(_) => ShellKind::Powershell, } @@ -427,6 +460,7 @@ impl Shell { Self::Bash(p) => p, #[cfg(unix)] Self::Tcsh(p) => p, + Self::Nushell(p) => p, #[cfg(windows)] Self::Powershell(p) => p, } @@ -442,6 +476,7 @@ impl Shell { Some(n) if n == ShellKind::Bash.as_ref() => Ok(Self::Bash(path.to_owned())), #[cfg(unix)] Some(n) if n == ShellKind::Tcsh.as_ref() => Ok(Self::Tcsh(path.to_owned())), + Some(n) if n == ShellKind::Nushell.as_ref() => Ok(Self::Nushell(path.to_owned())), #[cfg(windows)] Some(n) if n == ShellKind::Powershell.as_ref() => Ok(Self::Powershell(path.to_owned())), Some(_) => Err(Error::new(format!("Unsupported shell: {path:?}"))), @@ -477,7 +512,12 @@ impl Shell { } } - for kind in &[ShellKind::Bash, ShellKind::Tcsh, ShellKind::Powershell] { + for kind in &[ + ShellKind::Bash, + ShellKind::Tcsh, + ShellKind::Powershell, + ShellKind::Nushell, + ] { if let Some(path) = which(kind) { if let Ok(shell) = Shell::from_path(path) { return Ok(shell); diff --git a/crates/spfs/src/runtime/config_nu.rs b/crates/spfs/src/runtime/config_nu.rs new file mode 100644 index 0000000000..bedb90757d --- /dev/null +++ b/crates/spfs/src/runtime/config_nu.rs @@ -0,0 +1,17 @@ +// Copyright (c) Contributors to the SPK project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/spkenv/spk +// Warning Nushell version >=0.97 + +pub fn source(_tmpdir: Option<&T>) -> String +where + T: AsRef, +{ + r#" + $env.config = { + show_banner: false, + } + $env.SPFS_SHELL_MESSAGE? | print + "# + .to_string() +} diff --git a/crates/spfs/src/runtime/env_nu.rs b/crates/spfs/src/runtime/env_nu.rs new file mode 100644 index 0000000000..16b1f98d0c --- /dev/null +++ b/crates/spfs/src/runtime/env_nu.rs @@ -0,0 +1,78 @@ +// Copyright (c) Contributors to the SPK project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/spkenv/spk +// Warning Nushell version >=0.97 + +pub fn source(_tmpdir: Option<&T>) -> String +where + T: AsRef, +{ + r#" + def create_left_prompt [] { + let dir = match (do --ignore-shell-errors { $env.PWD | path relative-to $nu.home-path }) { + null => $env.PWD + '' => '~' + $relative_pwd => ([~ $relative_pwd] | path join) + } + + let path_color = (if (is-admin) { ansi red_bold } else { ansi green_bold }) + let separator_color = (if (is-admin) { ansi light_red_bold } else { ansi light_green_bold }) + let path_segment = $"($path_color)($dir)(ansi reset)" + + $path_segment | str replace --all (char path_sep) $"($separator_color)(char path_sep)($path_color)" + } + + def create_right_prompt [] { + # create a right prompt in magenta with green separators and am/pm underlined + let time_segment = ([ + (ansi reset) + (ansi magenta) + (date now | format date '%x %X') # try to respect user's locale + ] | str join | str replace --regex --all "([/:])" $"(ansi green)${1}(ansi magenta)" | + str replace --regex --all "([AP]M)" $"(ansi magenta_underline)${1}") + + let last_exit_code = if ($env.LAST_EXIT_CODE != 0) {([ + (ansi rb) + ($env.LAST_EXIT_CODE) + ] | str join) + } else { "" } + + ([$last_exit_code, (char space), $time_segment] | str join) + } + + # Use nushell functions to define your right and left prompt + $env.PROMPT_COMMAND = {|| create_left_prompt } + # FIXME: This default is not implemented in rust code as of 2023-09-08. + $env.PROMPT_COMMAND_RIGHT = {|| create_right_prompt } + + # The prompt indicators are environmental variables that represent + # the state of the prompt + $env.PROMPT_INDICATOR = {|| "> " } + $env.PROMPT_INDICATOR_VI_INSERT = {|| ": " } + $env.PROMPT_INDICATOR_VI_NORMAL = {|| "> " } + $env.PROMPT_MULTILINE_INDICATOR = {|| "::: " } + + + $env.ENV_CONVERSIONS = { + "PATH": { + from_string: { |s| $s | split row (char esep) | path expand --no-symlink } + to_string: { |v| $v | path expand --no-symlink | str join (char esep) } + } + "Path": { + from_string: { |s| $s | split row (char esep) | path expand --no-symlink } + to_string: { |v| $v | path expand --no-symlink | str join (char esep) } + } + } + + let $spfs_startup_dir = if $nu.os-info.name == "windows" { + "C:/spfs/etc/spfs/startup.d" + } else if $nu.os-info.name == "linux" { + "/spfs/etc/spfs/startup.d" + } else { + exit 1 + } + + $env.NU_VENDOR_AUTOLOAD_DIR = ($spfs_startup_dir) + "# + .to_string() +} diff --git a/crates/spfs/src/runtime/mod.rs b/crates/spfs/src/runtime/mod.rs index 0ee10a9bac..d15c649ef2 100644 --- a/crates/spfs/src/runtime/mod.rs +++ b/crates/spfs/src/runtime/mod.rs @@ -4,6 +4,8 @@ //! Handles the setup and initialization of runtime environments +mod config_nu; +mod env_nu; #[cfg(unix)] pub mod overlayfs; #[cfg(unix)] diff --git a/crates/spfs/src/runtime/storage.rs b/crates/spfs/src/runtime/storage.rs index 002fe1f038..163ce87b7b 100644 --- a/crates/spfs/src/runtime/storage.rs +++ b/crates/spfs/src/runtime/storage.rs @@ -26,6 +26,7 @@ use tokio::io::AsyncReadExt; #[cfg(windows)] use super::startup_ps; +use super::{config_nu, env_nu}; #[cfg(unix)] use super::{startup_csh, startup_sh}; use crate::encoding::Digest; @@ -379,6 +380,9 @@ pub struct Config { pub sh_startup_file: PathBuf, /// The location of the startup script for csh-based shells pub csh_startup_file: PathBuf, + /// The location of the startup script for nushell-based shells + pub nu_env_file: PathBuf, + pub nu_config_file: PathBuf, /// The location of the expect utility script used for csh-based shell environments /// \[DEPRECATED\] This field still exists for spk/spfs interop but is unused #[serde(skip_deserializing, default = "Config::default_csh_expect_file")] @@ -420,6 +424,8 @@ impl Config { const SH_STARTUP_FILE: &'static str = "startup.sh"; const CSH_STARTUP_FILE: &'static str = ".cshrc"; const PS_STARTUP_FILE: &'static str = "startup.ps1"; + const NU_ENV_FILE: &'static str = "env.nu"; + const NU_CONFIG_FILE: &'static str = "config.nu"; const DEV_NULL: &'static str = "/dev/null"; /// Return a dummy value for the legacy csh_expect_file field. @@ -440,6 +446,8 @@ impl Config { csh_startup_file: root.join(Self::CSH_STARTUP_FILE), csh_expect_file: Self::default_csh_expect_file(), ps_startup_file: temp_dir().join(Self::PS_STARTUP_FILE), + nu_env_file: root.join(Self::NU_ENV_FILE), + nu_config_file: root.join(Self::NU_CONFIG_FILE), runtime_dir: Some(root), tmpfs_size, mount_namespace: None, @@ -1035,6 +1043,16 @@ impl Runtime { startup_csh::source(tmpdir_value_for_child_process), ) .map_err(|err| Error::RuntimeWriteError(self.config.csh_startup_file.clone(), err))?; + std::fs::write( + &self.config.nu_env_file, + env_nu::source(tmpdir_value_for_child_process), + ) + .map_err(|err| Error::RuntimeWriteError(self.config.nu_env_file.clone(), err))?; + std::fs::write( + &self.config.nu_config_file, + config_nu::source(tmpdir_value_for_child_process), + ) + .map_err(|err| Error::RuntimeWriteError(self.config.nu_config_file.clone(), err))?; #[cfg(windows)] std::fs::write( &self.config.ps_startup_file, diff --git a/crates/spk-build/src/build/binary.rs b/crates/spk-build/src/build/binary.rs index 2b1719cde1..8b043d42e5 100644 --- a/crates/spk-build/src/build/binary.rs +++ b/crates/spk-build/src/build/binary.rs @@ -688,23 +688,29 @@ where let mut startup_file_csh = startup_dir.join(format!("spk_{}.csh", package.name())); let mut startup_file_sh = startup_dir.join(format!("spk_{}.sh", package.name())); + let mut startup_file_nu = startup_dir.join(format!("spk_{}.nu", package.name())); let mut csh_file = std::fs::File::create(&startup_file_csh) .map_err(|err| Error::FileOpenError(startup_file_csh.to_owned(), err))?; let mut sh_file = std::fs::File::create(&startup_file_sh) .map_err(|err| Error::FileOpenError(startup_file_sh.to_owned(), err))?; - + let mut nu_file = std::fs::File::create(&startup_file_nu) + .map_err(|err| Error::FileOpenError(startup_file_nu.to_owned(), err))?; for op in ops { if let Some(priority) = op.priority() { let original_startup_file_sh_name = startup_file_sh.clone(); let original_startup_file_csh_name = startup_file_csh.clone(); + let original_startup_file_nu_name = startup_file_nu.clone(); startup_file_sh.set_file_name(format!("{priority:02}_spk_{}.sh", package.name())); startup_file_csh.set_file_name(format!("{priority:02}_spk_{}.csh", package.name())); + startup_file_nu.set_file_name(format!("{priority:02}_spk_{}.nu", package.name())); std::fs::rename(original_startup_file_sh_name, &startup_file_sh) .map_err(|err| Error::FileWriteError(startup_file_sh.to_owned(), err))?; std::fs::rename(original_startup_file_csh_name, &startup_file_csh) .map_err(|err| Error::FileWriteError(startup_file_csh.to_owned(), err))?; + std::fs::rename(original_startup_file_nu_name, &startup_file_nu) + .map_err(|err| Error::FileWriteError(startup_file_nu.to_owned(), err))?; continue; } @@ -715,6 +721,9 @@ where sh_file .write_fmt(format_args!("{}\n", op.bash_source())) .map_err(|err| Error::FileWriteError(startup_file_sh.to_owned(), err))?; + nu_file + .write_fmt(format_args!("{}\n", op.nushell_source())) + .map_err(|err| Error::FileWriteError(startup_file_nu.to_owned(), err))?; } Ok(()) } diff --git a/crates/spk-cli/group1/Cargo.toml b/crates/spk-cli/group1/Cargo.toml index 822834a9fb..57efdb0941 100644 --- a/crates/spk-cli/group1/Cargo.toml +++ b/crates/spk-cli/group1/Cargo.toml @@ -34,6 +34,7 @@ spk-storage = { workspace = true } strip-ansi-escapes = { version = "0.1.1" } tokio = { workspace = true, features = ["rt"] } tracing = { workspace = true } +clap_complete_nushell ={ version = "4.5"} [dev-dependencies] rstest = { workspace = true } diff --git a/crates/spk-cli/group1/src/cmd_completion.rs b/crates/spk-cli/group1/src/cmd_completion.rs index 84caf2751d..ef8b51828c 100644 --- a/crates/spk-cli/group1/src/cmd_completion.rs +++ b/crates/spk-cli/group1/src/cmd_completion.rs @@ -4,29 +4,74 @@ //! Generate shell completions for "spk" +use std::fmt; use std::io::Write; -use clap::{value_parser, Command, Parser}; +use clap::{value_parser, Command, Parser, ValueEnum}; use clap_complete; -use clap_complete::Shell; +use clap_complete::{Generator, Shell}; +use clap_complete_nushell::Nushell; use miette::Result; use spk_cli_common::CommandArgs; +#[derive(Clone, Debug, ValueEnum)] +enum ShellCompletion { + /// Bourne Again SHell (bash) + Bash, + /// Friendly Interactive SHell (fish) + Fish, + /// PowerShell + Zsh, + /// Nushell + Nushell, +} +impl fmt::Display for ShellCompletion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + ShellCompletion::Bash => "bash", + ShellCompletion::Fish => "fish", + ShellCompletion::Zsh => "zsh", + ShellCompletion::Nushell => "nu", + }; + write!(f, "{}", s) + } +} +impl Generator for ShellCompletion { + /// Generate the file name for the completion script. + fn file_name(&self, name: &str) -> String { + match self { + ShellCompletion::Bash => Shell::Bash.file_name(name), + ShellCompletion::Fish => Shell::Fish.file_name(name), + ShellCompletion::Zsh => Shell::Zsh.file_name(name), + ShellCompletion::Nushell => Nushell.file_name(name), + } + } + + /// Generate the completion script for the shell. + fn generate(&self, cmd: &clap::Command, buf: &mut dyn std::io::Write) { + match self { + ShellCompletion::Bash => Shell::Bash.generate(cmd, buf), + ShellCompletion::Fish => Shell::Fish.generate(cmd, buf), + ShellCompletion::Zsh => Shell::Zsh.generate(cmd, buf), + ShellCompletion::Nushell => Nushell.generate(cmd, buf), + } + } +} + /// Generate shell completions for "spk" #[derive(Parser, Clone, Debug)] #[command(author, about, long_about)] pub struct Completion { /// Shell syntax to emit - #[arg(default_value_t = Shell::Bash, value_parser = value_parser!(Shell))] - pub shell: Shell, + #[arg(default_value_t = ShellCompletion::Bash, value_parser = value_parser!(ShellCompletion))] + shell: ShellCompletion, } impl Completion { pub fn run(&self, mut cmd: Command) -> Result { let mut buf = vec![]; - clap_complete::generate(self.shell, &mut cmd, "spk", &mut buf); + clap_complete::generate(self.shell.clone(), &mut cmd, "spk", &mut buf); std::io::stdout().write_all(&buf).unwrap_or(()); - Ok(0) } } @@ -34,10 +79,10 @@ impl Completion { impl CommandArgs for Completion { fn get_positional_args(&self) -> Vec { let args: Vec = vec![match self.shell { - Shell::Bash => "bash".to_string(), - Shell::Fish => "fish".to_string(), - Shell::Zsh => "zsh".to_string(), - _ => todo!(), // Shell is non-exhaustive. + ShellCompletion::Bash => "bash".to_string(), + ShellCompletion::Fish => "fish".to_string(), + ShellCompletion::Zsh => "zsh".to_string(), + ShellCompletion::Nushell => "nu".to_string(), }]; args diff --git a/crates/spk-schema/src/environ.rs b/crates/spk-schema/src/environ.rs index 74515e25cd..67690e7d8d 100644 --- a/crates/spk-schema/src/environ.rs +++ b/crates/spk-schema/src/environ.rs @@ -63,6 +63,7 @@ impl EnvOp { spfs::ShellKind::Bash => self.bash_source(), spfs::ShellKind::Tcsh => self.tcsh_source(), spfs::ShellKind::Powershell => self.powershell_source(), + spfs::ShellKind::Nushell => self.nushell_source(), } } @@ -148,6 +149,17 @@ impl EnvOp { pub fn powershell_source(&self) -> String { todo!() } + + /// Construct the nushell source representation for this operation + pub fn nushell_source(&self) -> String { + match self { + Self::Append(op) => op.nu_source(), + Self::Comment(op) => op.nu_source(), + Self::Prepend(op) => op.nu_source(), + Self::Priority(op) => op.nu_source(), + Self::Set(op) => op.nu_source(), + } + } } impl<'de> Deserialize<'de> for EnvOp { @@ -358,6 +370,12 @@ impl AppendEnv { ] .join("\n") } + pub fn nu_source(&self) -> String { + format!( + "$env.{} = (\"{}\" | append $env.{}?)", + self.append, self.append, self.value + ) + } } /// Adds a comment to the generated environment script @@ -376,6 +394,10 @@ impl EnvComment { // Both bash and tcsh source use the same comment syntax self.bash_source() } + pub fn nu_source(&self) -> String { + // Nushell use the same comment syntax as bash + self.bash_source() + } } /// Assigns a priority to the generated environment script @@ -398,6 +420,9 @@ impl EnvPriority { pub fn priority(&self) -> u8 { self.priority } + pub fn nu_source(&self) -> String { + String::from("") + } } /// Operates on an environment variable by prepending to the beginning @@ -447,6 +472,12 @@ impl PrependEnv { ] .join("\n") } + pub fn nu_source(&self) -> String { + format!( + "$env.{} = ($env.{}? | prepend \"{}\")", + self.prepend, self.prepend, self.value + ) + } } /// Operates on an environment variable by setting it to a value @@ -465,4 +496,7 @@ impl SetEnv { pub fn tcsh_source(&self) -> String { format!("setenv {} \"{}\"", self.set, self.value) } + pub fn nu_source(&self) -> String { + format!("$env.{} = \"{}\"", self.set, self.value) + } } diff --git a/cspell.json b/cspell.json index 975da1b413..d2e789e392 100644 --- a/cspell.json +++ b/cspell.json @@ -30,6 +30,7 @@ "ASWF", "atexit", "autogen", + "AUTOLOAD", "automounter", "autopoint", "autoreconf", @@ -201,6 +202,7 @@ "errexit", "Errno", "esac", + "esep", "ESRCH", "euid", "evenmoredata", @@ -449,6 +451,8 @@ "NQCYJ", "NTSTATUS", "numpy", + "nushell", + "Nushell", "nvcc", "NVPTX", "NZFA",