From 639062f131318332d3b47fe9704f04b75528010f Mon Sep 17 00:00:00 2001 From: Jeff Melton Date: Mon, 25 May 2026 13:36:25 -0500 Subject: [PATCH] Add Murex shell support Adds the Murex shell (https://murex.rocks) to the set of shells supported by 'zoxide init'. Changes: - templates/murex.txt: new init script template implementing PWD/Prompt hooks via 'event onPrompt name=command-completion', __zoxide_z/zi helpers with empty-PARAMS handling, and clean return-1 on no-match queries. - src/shell.rs: wires the template + two tests: * murex_murex (functional, runs the rendered init under murex -c), parameterized across all opts combinations and gated by the nix-dev feature so it only runs in the project's nix-shell * murex_template_has_direct_path_handling (string assertions on the rendered template, runs always) - src/cmd/cmd.rs: adds Murex to the InitShell enum - src/cmd/init.rs: imports Murex and adds the match arm - shell.nix: pins Murex to v7.2.1001 via a surgical overrideAttrs on the existing pkgs.murex (only this single package changes; the nixpkgs pin itself stays put). v7.0 introduced the 'command-completion' onPrompt interrupt that the prompt/PWD hooks depend on, so the version bundled with the current nixpkgs pin (v6.4.2063) is too old to source the rendered init. Completion stubs in contrib/completions/ are not touched directly; they regenerate via build.rs when the InitShell enum changes. Closes #1085 --- contrib/completions/_zoxide | 2 +- contrib/completions/zoxide.bash | 2 +- contrib/completions/zoxide.nu | 2 +- contrib/completions/zoxide.ts | 1 + shell.nix | 11 +++ src/cmd/cmd.rs | 1 + src/cmd/init.rs | 3 +- src/shell.rs | 31 ++++++++ templates/murex.txt | 132 ++++++++++++++++++++++++++++++++ 9 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 templates/murex.txt diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index 2f4c9f0..460bbae 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -178,7 +178,7 @@ _arguments "${_arguments_options[@]}" : \ '--help[Print help]' \ '-V[Print version]' \ '--version[Print version]' \ -':shell:(bash elvish fish nushell posix powershell tcsh xonsh zsh)' \ +':shell:(bash elvish fish murex nushell posix powershell tcsh xonsh zsh)' \ && ret=0 ;; (query) diff --git a/contrib/completions/zoxide.bash b/contrib/completions/zoxide.bash index 1e6fdcf..0eda72b 100644 --- a/contrib/completions/zoxide.bash +++ b/contrib/completions/zoxide.bash @@ -275,7 +275,7 @@ _zoxide() { return 0 ;; zoxide__subcmd__init) - opts="-h -V --no-cmd --cmd --hook --help --version bash elvish fish nushell posix powershell tcsh xonsh zsh" + opts="-h -V --no-cmd --cmd --hook --help --version bash elvish fish murex nushell posix powershell tcsh xonsh zsh" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/contrib/completions/zoxide.nu b/contrib/completions/zoxide.nu index 4d07049..95adc57 100644 --- a/contrib/completions/zoxide.nu +++ b/contrib/completions/zoxide.nu @@ -93,7 +93,7 @@ module completions { ] def "nu-complete zoxide init shell" [] { - [ "bash" "elvish" "fish" "nushell" "posix" "powershell" "tcsh" "xonsh" "zsh" ] + [ "bash" "elvish" "fish" "murex" "nushell" "posix" "powershell" "tcsh" "xonsh" "zsh" ] } def "nu-complete zoxide init hook" [] { diff --git a/contrib/completions/zoxide.ts b/contrib/completions/zoxide.ts index 207da2f..704a953 100644 --- a/contrib/completions/zoxide.ts +++ b/contrib/completions/zoxide.ts @@ -285,6 +285,7 @@ const completion: Fig.Spec = { "bash", "elvish", "fish", + "murex", "nushell", "posix", "powershell", diff --git a/shell.nix b/shell.nix index 05f270c..2ae745d 100644 --- a/shell.nix +++ b/shell.nix @@ -25,6 +25,17 @@ in pkgs.mkShell { pkgs.elvish pkgs.fish pkgs.ksh + (pkgs.murex.overrideAttrs (old: rec { + version = "7.2.1001"; + src = pkgs.fetchFromGitHub { + owner = "lmorg"; + repo = "murex"; + rev = "v${version}"; + hash = "sha256-Ua5KEtT1HXRCqW4MwB0dYCd03DBrliEfgiSmcp+vZS8="; + }; + vendorHash = "sha256-MaBBi2Qi7s9lfRWmnYkyr7PtwzC7ZL0jmyUXzISOXVg="; + doCheck = false; + })) pkgs.nushell pkgs.powershell pkgs.tcsh diff --git a/src/cmd/cmd.rs b/src/cmd/cmd.rs index 0de2ee5..512790e 100644 --- a/src/cmd/cmd.rs +++ b/src/cmd/cmd.rs @@ -156,6 +156,7 @@ pub enum InitShell { Bash, Elvish, Fish, + Murex, Nushell, #[clap(alias = "ksh")] Posix, diff --git a/src/cmd/init.rs b/src/cmd/init.rs index 980513e..c67f1e2 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -6,7 +6,7 @@ use askama::Template; use crate::cmd::{Init, InitShell, Run}; use crate::config; use crate::error::BrokenPipeHandler; -use crate::shell::{Bash, Elvish, Fish, Nushell, Opts, Posix, Powershell, Tcsh, Xonsh, Zsh}; +use crate::shell::{Bash, Elvish, Fish, Murex, Nushell, Opts, Posix, Powershell, Tcsh, Xonsh, Zsh}; impl Run for Init { fn run(&self) -> Result<()> { @@ -19,6 +19,7 @@ impl Run for Init { InitShell::Bash => Bash(opts).render(), InitShell::Elvish => Elvish(opts).render(), InitShell::Fish => Fish(opts).render(), + InitShell::Murex => Murex(opts).render(), InitShell::Nushell => Nushell(opts).render(), InitShell::Posix => Posix(opts).render(), InitShell::Powershell => Powershell(opts).render(), diff --git a/src/shell.rs b/src/shell.rs index 8812b1c..f85c781 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -26,6 +26,7 @@ macro_rules! make_template { make_template!(Bash, "bash.txt"); make_template!(Elvish, "elvish.txt"); make_template!(Fish, "fish.txt"); +make_template!(Murex, "murex.txt"); make_template!(Nushell, "nushell.txt"); make_template!(Posix, "posix.txt"); make_template!(Powershell, "powershell.txt"); @@ -159,6 +160,36 @@ mod tests { .stderr(""); } + #[apply(opts)] + fn murex_murex(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { + let opts = Opts { cmd, hook, echo, resolve_symlinks }; + let source = Murex(&opts).render().unwrap(); + Command::new("murex").args(["-c", &source, "--quiet"]).assert().success(); + } + + #[test] + fn murex_template_has_direct_path_handling() { + let opts = + Opts { cmd: Some("z"), hook: InitHook::None, echo: false, resolve_symlinks: false }; + let source = Murex(&opts).render().unwrap(); + + // Ensure murex z handles: "-- path" and tries direct cd on single-arg + assert!( + source.contains("if { $__zoxide_argc == 2 && $PARAMS[0] == \"--\" }"), + "murex template should handle literal path with --" + ); + assert!( + source.contains("fexec function __zoxide_cd $PARAMS[0]"), + "murex template should attempt cd directly on single-arg" + ); + + // Ensure __zoxide_zi exists (interactive-only) + assert!( + source.contains("fexec builtin function __zoxide_zi"), + "murex template should define __zoxide_zi" + ); + } + #[apply(opts)] fn nushell_nushell(cmd: Option<&str>, hook: InitHook, echo: bool, resolve_symlinks: bool) { let opts = Opts { cmd, hook, echo, resolve_symlinks }; diff --git a/templates/murex.txt b/templates/murex.txt new file mode 100644 index 0000000..a938272 --- /dev/null +++ b/templates/murex.txt @@ -0,0 +1,132 @@ +{%- let section = "# =============================================================================\n#" -%} +{%- let not_configured = "# -- not configured --" -%} + +{{ section }} +# Utility functions for zoxide. +# + +# cd + custom logic based on the value of _ZO_ECHO. +fexec builtin function __zoxide_cd { + fexec builtin cd $PARAMS[0] + {%- if echo %} + fexec builtin out $PWD + {%- endif %} +} + +{{ section }} +# Hook configuration for zoxide. +# + +{% match hook %} +{%- when InitHook::None -%} +{{ not_configured }} + +{%- when InitHook::Prompt -%} +# Initialize hook to add new entries to the database. +fexec builtin event onPrompt __zoxide_hook=command-completion { + exec zoxide add -- $PWD +} + +{%- when InitHook::Pwd -%} +# Emulate a PWD hook by tracking the last directory and updating on prompt. +fexec builtin out $PWD -> global: str __zoxide_oldpwd +fexec builtin event onPrompt __zoxide_hook=command-completion { + # Initialize global if missing (eg new session or cleared state) + if { !$__zoxide_oldpwd } then { fexec builtin out $PWD -> global: str __zoxide_oldpwd } + if { $__zoxide_oldpwd != $PWD } then { + fexec builtin out $PWD -> global: str __zoxide_oldpwd + exec zoxide add -- $PWD + } +} + +{%- endmatch %} + +{{ section }} +# When using zoxide with --no-cmd, alias these internal functions as desired. +# + +# Jump to a directory using only keywords. +fexec builtin function __zoxide_z { + __zoxide_argc = 0 + trypipe { @PARAMS -> count -> set int __zoxide_argc } + if { $__zoxide_argc == 0 } then { fexec function __zoxide_cd $HOME; fexec builtin return } + if { $PARAMS[0] == "-" } then { + fexec function __zoxide_cd - + fexec builtin return + } + # If a literal path is provided with "--", cd to it directly. + if { $__zoxide_argc == 2 && $PARAMS[0] == "--" } then { + fexec function __zoxide_cd $PARAMS[1] + fexec builtin return + } + # If a single argument is provided, try cd directly; if it fails, fall back to query. + if { $__zoxide_argc == 1 } then { + trypipe { + fexec function __zoxide_cd $PARAMS[0] + fexec builtin return + } + } + # Quiet query: capture result; suppress noise; return 1 on no match + fexec builtin out '' -> set: str __zoxide_result + trypipe { + exec zoxide query --exclude $PWD -- @PARAMS -> set: str __zoxide_result + } + if { $__zoxide_result } then { + fexec function __zoxide_cd $__zoxide_result + } else { + fexec builtin return 1 + } +} + +# Jump to a directory using interactive search. +fexec builtin function __zoxide_zi { + # Interactive query; return 1 when no selection + fexec builtin out '' -> set: str __zoxide_result + # Branch on arity: expanding `-- @PARAMS` with empty @PARAMS errors + # before zoxide can launch fzf, so omit it in the no-args case. + __zoxide_argc = 0 + trypipe { @PARAMS -> count -> set int __zoxide_argc } + if { $__zoxide_argc == 0 } then { + trypipe { + exec zoxide query --interactive -> set: str __zoxide_result + } + } else { + trypipe { + exec zoxide query --interactive -- @PARAMS -> set: str __zoxide_result + } + } + if { $__zoxide_result } then { + fexec function __zoxide_cd $__zoxide_result + } else { + fexec builtin return 1 + } +} + +{{ section }} +# Commands for zoxide. Disable these using --no-cmd. +# + +{%- match cmd %} +{%- when Some with (cmd) %} + +# Wrapper checks arity using $PARAMS (JSON) since expanding @PARAMS in a function +# body errors when the array is empty. Forwarding logic stays in __zoxide_z/zi. +function {{cmd}} { + tout json "$PARAMS" -> count -> set int __zoxide_z_argc + if { $__zoxide_z_argc == 0 } then { __zoxide_z } else { __zoxide_z @PARAMS } +} +function {{cmd}}i { + tout json "$PARAMS" -> count -> set int __zoxide_zi_argc + if { $__zoxide_zi_argc == 0 } then { __zoxide_zi } else { __zoxide_zi @PARAMS } +} + +{%- when None %} + +{{ not_configured }} + +{%- endmatch %} + +{{ section }} +# To initialize zoxide, add this to your shell configuration file (usually ~/.murex_profile): +# +# zoxide init murex -> source