diff --git a/docs/getting-started.md b/docs/getting-started.md index 27c3d0b4b5..d7a7d4e90a 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -411,6 +411,21 @@ don't have these two set differently (might throw `os.environ['PATH'] = xonsh.built_ins.XSH.env.get_detyped('PATH')` at the end of a config to make sure they match) +### Elvish + +Add following to your `rc.elv`: + +```elvish +var mise: = (eval &ns=[&] &on-end=$put~ (mise activate elvish | slurp)) +mise:activate +``` + +Optionally alias `mise` to `mise:mise` for seamless integration of `mise {activate,deactivate,shell}`: + +```elvish +fn mise {|@args| mise:mise $@args } +``` + ### Something else? Adding a new shell is not hard at all since very little shell code is diff --git a/src/shell/elvish.rs b/src/shell/elvish.rs new file mode 100644 index 0000000000..a0f3add259 --- /dev/null +++ b/src/shell/elvish.rs @@ -0,0 +1,129 @@ +use std::path::Path; + +use indoc::formatdoc; + +use crate::shell::Shell; + +#[derive(Default)] +pub struct Elvish {} + +impl Shell for Elvish { + fn activate(&self, exe: &Path, flags: String) -> String { + let exe = exe.to_string_lossy(); + + formatdoc! {r#" + var hook-enabled = $false + + fn hook-env {{ + if $hook-enabled {{ + eval ({exe} hook-env{flags} -s elvish | slurp) + }} + }} + + set after-chdir = (conj $after-chdir {{|_| hook-env }}) + set edit:before-readline = (conj $edit:before-readline $hook-env~) + + fn activate {{ + set-env MISE_SHELL elvish + set hook-enabled = $true + hook-env + }} + + fn deactivate {{ + set hook-enabled = $false + eval ({exe} deactivate | slurp) + }} + + fn mise {{|@a| + if (== (count $a) 0) {{ + {exe} + return + }} + + if (not (or (has-value $a -h) (has-value $a --help))) {{ + var command = $a[0] + if (==s $command shell) {{ + try {{ eval ({exe} $@a) }} catch {{ }} + return + }} elif (==s $command deactivate) {{ + deactivate + return + }} elif (==s $command activate) {{ + activate + return + }} + }} + {exe} $@a + }} + "#} + } + + fn deactivate(&self) -> String { + formatdoc! {r#" + unset-env MISE_SHELL + unset-env __MISE_DIFF + unset-env __MISE_WATCH + "#} + } + + fn set_env(&self, k: &str, v: &str) -> String { + let k = shell_escape::unix::escape(k.into()); + let v = shell_escape::unix::escape(v.into()); + let v = v.replace("\\n", "\n"); + format!("set-env {k} {v}\n") + } + + fn prepend_env(&self, k: &str, v: &str) -> String { + let k = shell_escape::unix::escape(k.into()); + let v = shell_escape::unix::escape(v.into()); + format!("set-env {k} {v}(get-env {k})\n") + } + + fn unset_env(&self, k: &str) -> String { + format!("unset-env {k}\n", k = shell_escape::unix::escape(k.into())) + } +} + +#[cfg(test)] +mod tests { + use insta::assert_snapshot; + use test_log::test; + + use crate::test::{replace_path, reset}; + + use super::*; + + #[test] + fn test_hook_init() { + reset(); + let elvish = Elvish::default(); + let exe = Path::new("/some/dir/mise"); + assert_snapshot!(elvish.activate(exe, " --status".into())); + } + + #[test] + fn test_set_env() { + reset(); + assert_snapshot!(Elvish::default().set_env("FOO", "1")); + } + + #[test] + fn test_prepend_env() { + reset(); + let sh = Elvish::default(); + assert_snapshot!(replace_path(&sh.prepend_env("PATH", "/some/dir:/2/dir"))); + } + + #[test] + fn test_unset_env() { + reset(); + assert_snapshot!(Elvish::default().unset_env("FOO")); + } + + #[test] + fn test_deactivate() { + reset(); + let deactivate = Elvish::default().deactivate(); + assert_snapshot!(replace_path(&deactivate)); + } +} diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 8a8700f938..23e8673867 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -4,6 +4,7 @@ use std::path::Path; use crate::env; mod bash; +mod elvish; mod fish; mod nushell; mod xonsh; @@ -12,6 +13,7 @@ mod zsh; #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] pub enum ShellType { Bash, + Elvish, Fish, Nu, Xonsh, @@ -23,6 +25,8 @@ impl ShellType { let shell = env::var("MISE_SHELL").or(env::var("SHELL")).ok()?; if shell.ends_with("bash") { Some(ShellType::Bash) + } else if shell.ends_with("elvish") { + Some(ShellType::Elvish) } else if shell.ends_with("fish") { Some(ShellType::Fish) } else if shell.ends_with("nu") { @@ -41,6 +45,7 @@ impl Display for ShellType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Bash => write!(f, "bash"), + Self::Elvish => write!(f, "elvish"), Self::Fish => write!(f, "fish"), Self::Nu => write!(f, "nu"), Self::Xonsh => write!(f, "xonsh"), @@ -60,6 +65,7 @@ pub trait Shell { pub fn get_shell(shell: Option) -> Option> { match shell.or_else(ShellType::load) { Some(ShellType::Bash) => Some(Box::::default()), + Some(ShellType::Elvish) => Some(Box::::default()), Some(ShellType::Fish) => Some(Box::::default()), Some(ShellType::Nu) => Some(Box::::default()), Some(ShellType::Xonsh) => Some(Box::::default()), diff --git a/src/shell/snapshots/mise__shell__elvish__tests__deactivate.snap b/src/shell/snapshots/mise__shell__elvish__tests__deactivate.snap new file mode 100644 index 0000000000..6ec0eb6b3a --- /dev/null +++ b/src/shell/snapshots/mise__shell__elvish__tests__deactivate.snap @@ -0,0 +1,7 @@ +--- +source: src/shell/elvish.rs +expression: replace_path(&deactivate) +--- +unset-env MISE_SHELL +unset-env __MISE_DIFF +unset-env __MISE_WATCH diff --git a/src/shell/snapshots/mise__shell__elvish__tests__hook_init.snap b/src/shell/snapshots/mise__shell__elvish__tests__hook_init.snap new file mode 100644 index 0000000000..1610ae27a9 --- /dev/null +++ b/src/shell/snapshots/mise__shell__elvish__tests__hook_init.snap @@ -0,0 +1,47 @@ +--- +source: src/shell/elvish.rs +expression: "elvish.activate(exe, \" --status\".into())" +--- +var hook-enabled = $false + +fn hook-env { + if $hook-enabled { + eval (/some/dir/mise hook-env --status -s elvish | slurp) + } +} + +set after-chdir = (conj $after-chdir {|_| hook-env }) +set edit:before-readline = (conj $edit:before-readline $hook-env~) + +fn activate { + set-env MISE_SHELL elvish + set hook-enabled = $true + hook-env +} + +fn deactivate { + set hook-enabled = $false + eval (/some/dir/mise deactivate | slurp) +} + +fn mise {|@a| + if (== (count $a) 0) { + /some/dir/mise + return + } + + if (not (or (has-value $a -h) (has-value $a --help))) { + var command = $a[0] + if (==s $command shell) { + try { eval (/some/dir/mise $@a) } catch { } + return + } elif (==s $command deactivate) { + deactivate + return + } elif (==s $command activate) { + activate + return + } + } + /some/dir/mise $@a +} diff --git a/src/shell/snapshots/mise__shell__elvish__tests__prepend_env.snap b/src/shell/snapshots/mise__shell__elvish__tests__prepend_env.snap new file mode 100644 index 0000000000..306919713a --- /dev/null +++ b/src/shell/snapshots/mise__shell__elvish__tests__prepend_env.snap @@ -0,0 +1,5 @@ +--- +source: src/shell/elvish.rs +expression: "replace_path(&sh.prepend_env(\"PATH\", \"/some/dir:/2/dir\"))" +--- +set-env PATH '/some/dir:/2/dir'(get-env PATH) diff --git a/src/shell/snapshots/mise__shell__elvish__tests__set_env.snap b/src/shell/snapshots/mise__shell__elvish__tests__set_env.snap new file mode 100644 index 0000000000..1859c5e3a2 --- /dev/null +++ b/src/shell/snapshots/mise__shell__elvish__tests__set_env.snap @@ -0,0 +1,5 @@ +--- +source: src/shell/elvish.rs +expression: "Elvish::default().set_env(\"FOO\", \"1\")" +--- +set-env FOO 1 diff --git a/src/shell/snapshots/mise__shell__elvish__tests__unset_env.snap b/src/shell/snapshots/mise__shell__elvish__tests__unset_env.snap new file mode 100644 index 0000000000..4fa7c2e3ba --- /dev/null +++ b/src/shell/snapshots/mise__shell__elvish__tests__unset_env.snap @@ -0,0 +1,5 @@ +--- +source: src/shell/elvish.rs +expression: "Elvish::default().unset_env(\"FOO\")" +--- +unset-env FOO