diff --git a/README.md b/README.md index e268950..7a6053e 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,22 @@ some limited ability to make the manipulations dynamic. ![shadowenv in action](https://burkelibbey.s3.amazonaws.com/shadowenv.gif) -In order to use shadowenv, add a line to your shell profile (`.zshrc`, `.bash_profile`, or -`config.fish`) reading: +In order to use shadowenv, add initialization to your shell profile: ```bash -eval "$(shadowenv init bash)" # for bash -eval "$(shadowenv init zsh)" # for zsh -shadowenv init fish | source # for fish +# For bash (.bash_profile) +eval "$(shadowenv init bash)" + +# For zsh (.zshrc) +eval "$(shadowenv init zsh)" + +# For fish (config.fish) +shadowenv init fish | source + +# For nushell - first, generate the initialization script (run this in your terminal): +shadowenv init nushell | save -f ~/.shadowenv.nu +# Then add this line to the end of your config file (find it by running $nu.config-path): +source ~/.shadowenv.nu ``` With this code loaded, upon entering a directory containing a `.shadowenv.d` directory, diff --git a/sh/shadowenv.nu.in b/sh/shadowenv.nu.in new file mode 100644 index 0000000..b06321c --- /dev/null +++ b/sh/shadowenv.nu.in @@ -0,0 +1,43 @@ +def --env __shadowenv_hook [] { + mut flags = [--nushell] + if ($env.__shadowenv_force_run? | default false) { + $flags = [...$flags --force] + hide-env __shadowenv_force_run + } + # Get shadowenv changes as JSON (we can't use `source` dynamically in Nushell) + let data = (^@SELF@ hook ...$flags | complete) + if $data.exit_code != 0 { + return + } + + let changes = ($data.stdout | from json) + + # Print activation message if present + if $changes.message? != null { + print -e $changes.message + } + + # Apply environment changes if exported field exists + # exported contains vars to set (value) or unset (null) + if $changes.exported? != null { + for entry in ($changes.exported | transpose key value) { + if $entry.value == null { + hide-env $entry.key + } else if $entry.key == "PATH" { + # Convert colon-separated string to list for Nushell's PATH + let separator = (if $nu.os-info.name == "windows" { ";" } else { ":" }) + $env.PATH = ($entry.value | split row $separator) + } else { + load-env {($entry.key): $entry.value} + } + } + } +} + +$env.config = ($env.config | upsert hooks {|config| + $config.hooks | upsert pre_prompt {|hooks| + $hooks.pre_prompt | append {|| __shadowenv_hook } + } +}) + +$env.__shadowenv_force_run = true diff --git a/src/cli.rs b/src/cli.rs index 8c3d56c..002498a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -78,6 +78,10 @@ pub struct FormatOptions { #[arg(long)] pub fish: bool, + /// Format variable assignments for nushell. + #[arg(long)] + pub nushell: bool, + /// Format variable assignments as JSON. #[arg(long)] pub json: bool, @@ -103,6 +107,9 @@ pub enum InitCmd { /// Prints a script which can be eval'd by fish to set up shadowenv. Fish, + + /// Prints a script which can be sourced by nushell to set up shadowenv. + Nushell, } /// Options shared by all init subcommands diff --git a/src/hook.rs b/src/hook.rs index 248ab70..ee88b66 100644 --- a/src/hook.rs +++ b/src/hook.rs @@ -15,6 +15,7 @@ use std::{borrow::Cow, collections::HashMap, env, path::PathBuf, result::Result, pub enum VariableOutputMode { Fish, + Nushell, Porcelain, Posix, Json, @@ -26,6 +27,8 @@ struct Modifications { schema: String, exported: HashMap>, unexported: HashMap>, // Legacy. Not used, just shows up empty in json + #[serde(skip_serializing_if = "Option::is_none")] + message: Option, } impl Modifications { @@ -34,8 +37,14 @@ impl Modifications { schema: "v2".to_string(), exported: exports, unexported: HashMap::new(), + message: None, } } + + fn with_message(mut self, message: Option) -> Modifications { + self.message = message; + self + } } pub fn run(cmd: HookCmd) -> Result<(), Error> { @@ -43,6 +52,8 @@ pub fn run(cmd: HookCmd) -> Result<(), Error> { VariableOutputMode::Porcelain } else if cmd.format.fish { VariableOutputMode::Fish + } else if cmd.format.nushell { + VariableOutputMode::Nushell } else if cmd.format.json { VariableOutputMode::Json } else if cmd.format.pretty_json { @@ -219,6 +230,17 @@ pub fn apply_env(shadowenv: &Shadowenv, mode: VariableOutputMode) -> Result<(), shadowenv.features(), ); } + VariableOutputMode::Nushell => { + // Nushell mode outputs JSON with an activation message + // (Nushell can't eval dynamic code like bash, so we use JSON) + let message = output::format_activation_message( + shadowenv.current_dirs(), + shadowenv.prev_dirs(), + shadowenv.features(), + ); + let modifs = Modifications::new(shadowenv.exports()?).with_message(message); + println!("{}", serde_json::to_string(&modifs).unwrap()); + } VariableOutputMode::Porcelain => { // three fields: : : // opcodes: 1: set, unexported (unused) @@ -386,4 +408,5 @@ mod tests { assert_eq!(shell_escape(input), expected, "Failed for input: {}", input); } } + } diff --git a/src/init.rs b/src/init.rs index b231597..fcfb064 100644 --- a/src/init.rs +++ b/src/init.rs @@ -21,6 +21,11 @@ pub fn run(cmd: InitCmd) { include_bytes!("../sh/shadowenv.fish.in"), true, // Fish doesn't use hookbook ), + Nushell => print_script( + pb, + include_bytes!("../sh/shadowenv.nu.in"), + true, // Nushell doesn't use hookbook + ), }; } diff --git a/src/output.rs b/src/output.rs index 30e1ded..3387cf4 100644 --- a/src/output.rs +++ b/src/output.rs @@ -33,17 +33,19 @@ pub fn format_hook_error(err: Error, shellpid: u32, silent: bool) -> Option, prev_dirs: HashSet, features: HashSet, -) { - if !should_print_activation() { - return; - } +) -> Option { let added_dirs: HashSet = current_dirs.difference(&prev_dirs).cloned().collect(); let removed_dirs: HashSet = prev_dirs.difference(¤t_dirs).cloned().collect(); + // Don't print message if nothing changed + if added_dirs.is_empty() && removed_dirs.is_empty() && features.is_empty() { + return None; + } + let feature_list = if !features.is_empty() { format!( " \x1b[1;38;5;245m{}", @@ -57,12 +59,25 @@ pub fn print_activation_to_tty( String::new() }; - eprintln!( + Some(format!( "\x1b[1;34m{}{}{}\x1b[0m", SHADOWENV, dir_diff(added_dirs, removed_dirs).unwrap_or_default(), feature_list - ); + )) +} + +pub fn print_activation_to_tty( + current_dirs: HashSet, + prev_dirs: HashSet, + features: HashSet, +) { + if !should_print_activation() { + return; + } + if let Some(message) = format_activation_message(current_dirs, prev_dirs, features) { + eprintln!("{}", message); + } } fn dir_diff(added_dirs: HashSet, removed_dirs: HashSet) -> Option {