Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions sh/shadowenv.nu.in
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
23 changes: 23 additions & 0 deletions src/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use std::{borrow::Cow, collections::HashMap, env, path::PathBuf, result::Result,

pub enum VariableOutputMode {
Fish,
Nushell,
Porcelain,
Posix,
Json,
Expand All @@ -26,6 +27,8 @@ struct Modifications {
schema: String,
exported: HashMap<String, Option<String>>,
unexported: HashMap<String, Option<String>>, // Legacy. Not used, just shows up empty in json
#[serde(skip_serializing_if = "Option::is_none")]
message: Option<String>,
}

impl Modifications {
Expand All @@ -34,15 +37,23 @@ impl Modifications {
schema: "v2".to_string(),
exported: exports,
unexported: HashMap::new(),
message: None,
}
}

fn with_message(mut self, message: Option<String>) -> Modifications {
self.message = message;
self
}
}

pub fn run(cmd: HookCmd) -> Result<(), Error> {
let mode = if cmd.format.porcelain {
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 {
Expand Down Expand Up @@ -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: <operation> : <name> : <value>
// opcodes: 1: set, unexported (unused)
Expand Down Expand Up @@ -386,4 +408,5 @@ mod tests {
assert_eq!(shell_escape(input), expected, "Failed for input: {}", input);
}
}

}
5 changes: 5 additions & 0 deletions src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
),
};
}

Expand Down
29 changes: 22 additions & 7 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,19 @@ pub fn format_hook_error(err: Error, shellpid: u32, silent: bool) -> Option<Stri
Some(format!("{} \x1b[1;31mfailure: {}\x1b[0m", SHADOWENV, err))
}

pub fn print_activation_to_tty(
pub fn format_activation_message(
current_dirs: HashSet<PathBuf>,
prev_dirs: HashSet<PathBuf>,
features: HashSet<Feature>,
) {
if !should_print_activation() {
return;
}
) -> Option<String> {
let added_dirs: HashSet<PathBuf> = current_dirs.difference(&prev_dirs).cloned().collect();
let removed_dirs: HashSet<PathBuf> = prev_dirs.difference(&current_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{}",
Expand All @@ -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<PathBuf>,
prev_dirs: HashSet<PathBuf>,
features: HashSet<Feature>,
) {
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<PathBuf>, removed_dirs: HashSet<PathBuf>) -> Option<String> {
Expand Down
Loading