diff --git a/CHANGELOG.md b/CHANGELOG.md index dee4fdb..e3d45e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Interactive mode in `zoxide` no longer throws an error if `fzf` exits gracefully. - Canonicalize to regular paths instead of UNC paths on Windows. - `zoxide init` now uses PWD hooks by default for better performance. +- `$_ZO_ECHO` now only works when set to `1`. ### Fixed diff --git a/src/subcommand/init.rs b/src/subcommand/init.rs deleted file mode 100644 index 9262fe9..0000000 --- a/src/subcommand/init.rs +++ /dev/null @@ -1,413 +0,0 @@ -use anyhow::{anyhow, Result}; -use clap::arg_enum; -use structopt::StructOpt; -use uuid::Uuid; - -use std::borrow::Cow; -use std::io::{self, Write}; - -#[derive(Debug, StructOpt)] -#[structopt(about = "Generates shell configuration")] -pub struct Init { - #[structopt(possible_values = &Shell::variants(), case_insensitive = true)] - shell: Shell, - - #[structopt( - long, - help = "Changes the name of the 'z' command", - default_value = "z" - )] - z_cmd: String, - - #[structopt( - long, - help = "Prevents zoxide from defining any commands other than 'z'" - )] - no_define_aliases: bool, - - #[structopt( - long, - help = "Chooses event on which an entry is added to the database", - possible_values = &Hook::variants(), - default_value = "pwd", - case_insensitive = true - )] - hook: Hook, -} - -impl Init { - pub fn run(&self) -> Result<()> { - let config = match self.shell { - Shell::bash => BASH_CONFIG, - Shell::fish => FISH_CONFIG, - Shell::posix => POSIX_CONFIG, - Shell::zsh => ZSH_CONFIG, - }; - - let stdout = io::stdout(); - let mut handle = stdout.lock(); - - let z = config.z; - writeln!(handle, "{}", z(&self.z_cmd)).unwrap(); - - if !self.no_define_aliases { - let alias = config.alias; - writeln!(handle, "{}", alias(&self.z_cmd)).unwrap(); - } - - match self.hook { - Hook::none => (), - Hook::prompt => writeln!(handle, "{}", config.hook.prompt).unwrap(), - Hook::pwd => { - let hook_pwd = config.hook.pwd; - writeln!(handle, "{}", hook_pwd()?).unwrap(); - } - } - - Ok(()) - } -} - -arg_enum! { - #[allow(non_camel_case_types)] - #[derive(Debug)] - enum Shell { - bash, - fish, - posix, - zsh, - } -} - -arg_enum! { - #[allow(non_camel_case_types)] - #[derive(Debug)] - enum Hook { - none, - prompt, - pwd, - } -} - -const BASH_CONFIG: ShellConfig = ShellConfig { - z: bash_z, - alias: bash_alias, - hook: HookConfig { - prompt: BASH_HOOK_PROMPT, - pwd: bash_hook_pwd, - }, -}; - -const FISH_CONFIG: ShellConfig = ShellConfig { - z: fish_z, - alias: fish_alias, - hook: HookConfig { - prompt: FISH_HOOK_PROMPT, - pwd: fish_hook_pwd, - }, -}; - -const POSIX_CONFIG: ShellConfig = ShellConfig { - z: posix_z, - alias: posix_alias, - hook: HookConfig { - prompt: POSIX_HOOK_PROMPT, - pwd: posix_hook_pwd, - }, -}; - -const ZSH_CONFIG: ShellConfig = ShellConfig { - z: zsh_z, - alias: zsh_alias, - hook: HookConfig { - prompt: ZSH_HOOK_PROMPT, - pwd: zsh_hook_pwd, - }, -}; - -struct ShellConfig { - z: fn(&str) -> String, - alias: fn(&str) -> String, - hook: HookConfig, -} - -struct HookConfig { - prompt: &'static str, - pwd: fn() -> Result>, -} - -fn fish_z(z_cmd: &str) -> String { - format!( - r#" -function _z_cd - cd $argv - or return $status - - commandline -f repaint - - if test -n "$_ZO_ECHO" - echo $PWD - end -end - -function {} - set argc (count $argv) - - if test $argc -eq 0 - _z_cd $HOME - or return $status - - else if test $argc -eq 1 -a $argv[1] = '-' - _z_cd - - or return $status - - else - # FIXME: use string-collect from fish 3.1.0 once it has wider adoption - set -l IFS '' - set -l result (zoxide query $argv) - - if test -d $result; and string length -q -- $result - _z_cd $result - or return $status - else if test -n "$result" - echo $result - end - end -end -"#, - z_cmd - ) -} - -fn posix_z(z_cmd: &str) -> String { - format!( - r#" -_z_cd() {{ - cd "$@" || return "$?" - - if [ -n "$_ZO_ECHO" ]; then - echo "$PWD" - fi -}} - -{}() {{ - if [ "$#" -eq 0 ]; then - _z_cd ~ || return "$?" - elif [ "$#" -eq 1 ] && [ "$1" = '-' ]; then - if [ -n "$OLDPWD" ]; then - _z_cd "$OLDPWD" || return "$?" - else - echo 'zoxide: $OLDPWD is not set' - return 1 - fi - else - result="$(zoxide query "$@")" || return "$?" - if [ -d "$result" ]; then - _z_cd "$result" || return "$?" - elif [ -n "$result" ]; then - echo "$result" - fi - fi -}} -"#, - z_cmd - ) -} - -use posix_z as bash_z; -use posix_z as zsh_z; - -fn fish_alias(z_cmd: &str) -> String { - format!( - r#" -abbr -a zi '{} -i' - -abbr -a za 'zoxide add' - -abbr -a zq 'zoxide query' -abbr -a zqi 'zoxide query -i' - -abbr -a zr 'zoxide remove' -abbr -a zri 'zoxide remove -i' -"#, - z_cmd - ) -} - -fn posix_alias(z_cmd: &str) -> String { - format!( - r#" -alias zi='{} -i' - -alias za='zoxide add' - -alias zq='zoxide query' -alias zqi='zoxide query -i' - -alias zr='zoxide remove' -alias zri='zoxide remove -i' -"#, - z_cmd - ) -} - -use posix_alias as bash_alias; -use posix_alias as zsh_alias; - -const BASH_HOOK_PROMPT: &str = r#" -_zoxide_hook() { - zoxide add -} - -case "$PROMPT_COMMAND" in - *_zoxide_hook*) ;; - *) PROMPT_COMMAND="_zoxide_hook${PROMPT_COMMAND:+;${PROMPT_COMMAND}}" ;; -esac -"#; - -const FISH_HOOK_PROMPT: &str = r#" -function _zoxide_hook --on-event fish_prompt - zoxide add -end -"#; - -const POSIX_HOOK_PROMPT: &str = r#" -_zoxide_hook() { - zoxide add -} - -case "$PS1" in - *\$\(_zoxide_hook\)*) ;; - *) PS1="\$(_zoxide_hook)${PS1}" ;; -esac -"#; - -const ZSH_HOOK_PROMPT: &str = r#" -_zoxide_hook() { - zoxide add -} - -[[ -n "${precmd_functions[(r)_zoxide_hook]}" ]] || { - precmd_functions+=(_zoxide_hook) -} -"#; - -const fn bash_hook_pwd() -> Result> { - const HOOK_PWD: &str = r#" -_zoxide_hook() { - if [ -z "${_ZO_PWD}" ]; then - _ZO_PWD="${PWD}" - elif [ "${_ZO_PWD}" != "${PWD}" ]; then - _ZO_PWD="${PWD}" - zoxide add - fi -} - -case "$PROMPT_COMMAND" in - *_zoxide_hook*) ;; - *) PROMPT_COMMAND="_zoxide_hook${PROMPT_COMMAND:+;${PROMPT_COMMAND}}" ;; -esac -"#; - - Ok(Cow::Borrowed(HOOK_PWD)) -} - -const fn fish_hook_pwd() -> Result> { - const HOOK_PWD: &str = r#" -function _zoxide_hook --on-variable PWD - zoxide add -end -"#; - - Ok(Cow::Borrowed(HOOK_PWD)) -} - -fn posix_hook_pwd() -> Result> { - let mut tmp_path = std::env::temp_dir(); - tmp_path.push("zoxide"); - - let tmp_path_str = tmp_path - .to_str() - .ok_or_else(|| anyhow!("invalid Unicode in zoxide tmp path"))?; - - let pwd_path = tmp_path.join(format!("pwd-{}", Uuid::new_v4())); - - let pwd_path_str = pwd_path - .to_str() - .ok_or_else(|| anyhow!("invalid Unicode in zoxide pwd path"))?; - - let hook_pwd = format!( - r#" -# PWD hooks in POSIX use a temporary file, located at `$_ZO_PWD_PATH`, to track -# changes in the current directory. These files are removed upon restart, -# but they should ideally also be cleaned up once the shell exits using traps. -# -# This can be done as follows: -# -# trap '_zoxide_cleanup' EXIT HUP KILL TERM -# trap '_zoxide_cleanup; trap - INT; kill -s INT "$$"' INT -# trap '_zoxide_cleanup; trap - QUIT; kill -s QUIT "$$"' QUIT -# -# By default, traps are not set up because they override all previous traps. -# It is therefore up to the user to add traps to their shell configuration. - -_ZO_TMP_PATH={} -_ZO_PWD_PATH={} - -_zoxide_cleanup() {{ - rm -f "$_ZO_PWD_PATH" -}} - -_zoxide_setpwd() {{ - mkdir -p "$_ZO_TMP_PATH" - echo "$PWD" > "$_ZO_PWD_PATH" -}} - -_zoxide_setpwd - -_zoxide_hook() {{ - _ZO_OLDPWD="$(cat "$_ZO_PWD_PATH")" - if [ -z "$_ZO_OLDPWD" ] || [ "$_ZO_OLDPWD" != "$PWD" ]; then - _zoxide_setpwd && zoxide add > /dev/null - fi -}} - -case "$PS1" in - *\$\(_zoxide_hook\)*) ;; - *) PS1="\$(_zoxide_hook)${{PS1}}" ;; -esac"#, - posix_quote(tmp_path_str), - posix_quote(pwd_path_str), - ); - - Ok(Cow::Owned(hook_pwd)) -} - -const fn zsh_hook_pwd() -> Result> { - const HOOK_PWD: &str = r#" -_zoxide_hook() { - zoxide add -} - -chpwd_functions=(${chpwd_functions[@]} "_zoxide_hook") -"#; - - Ok(Cow::Borrowed(HOOK_PWD)) -} - -fn posix_quote(string: &str) -> String { - let mut quoted = String::with_capacity(string.len() + 2); - quoted.push('\''); - - for ch in string.chars() { - match ch { - '\\' => quoted.push_str(r"\\"), - '\'' => quoted.push_str(r"'\''"), - _ => quoted.push(ch), - } - } - - quoted.push('\''); - - quoted -} diff --git a/src/subcommand/init/mod.rs b/src/subcommand/init/mod.rs new file mode 100644 index 0000000..48318a7 --- /dev/null +++ b/src/subcommand/init/mod.rs @@ -0,0 +1,90 @@ +mod shell; + +use anyhow::Result; +use clap::arg_enum; +use structopt::StructOpt; + +use std::io::{self, Write}; + +#[derive(Debug, StructOpt)] +#[structopt(about = "Generates shell configuration")] +pub struct Init { + #[structopt(possible_values = &Shell::variants(), case_insensitive = true)] + shell: Shell, + + #[structopt( + long, + help = "Changes the name of the 'z' command", + default_value = "z" + )] + z_cmd: String, + + #[structopt( + long, + help = "Prevents zoxide from defining any commands other than 'z'" + )] + no_define_aliases: bool, + + #[structopt( + long, + help = "Chooses event on which an entry is added to the database", + possible_values = &Hook::variants(), + default_value = "pwd", + case_insensitive = true + )] + hook: Hook, +} + +impl Init { + pub fn run(&self) -> Result<()> { + let config = match self.shell { + Shell::bash => shell::bash::CONFIG, + Shell::fish => shell::fish::CONFIG, + Shell::posix => shell::posix::CONFIG, + Shell::zsh => shell::zsh::CONFIG, + }; + + let stdout = io::stdout(); + let mut handle = stdout.lock(); + + let z = config.z; + writeln!(handle, "{}", z(&self.z_cmd)).unwrap(); + + if !self.no_define_aliases { + let alias = config.alias; + writeln!(handle, "{}", alias(&self.z_cmd)).unwrap(); + } + + match self.hook { + Hook::none => (), + Hook::prompt => writeln!(handle, "{}", config.hook.prompt).unwrap(), + Hook::pwd => { + let hook_pwd = config.hook.pwd; + writeln!(handle, "{}", hook_pwd()?).unwrap(); + } + } + + Ok(()) + } +} + +arg_enum! { + #[allow(non_camel_case_types)] + #[derive(Debug)] + enum Shell { + bash, + fish, + posix, + zsh, + } +} + +arg_enum! { + #[allow(non_camel_case_types)] + #[derive(Debug)] + enum Hook { + none, + prompt, + pwd, + } +} diff --git a/src/subcommand/init/shell/bash.rs b/src/subcommand/init/shell/bash.rs new file mode 100644 index 0000000..69c551a --- /dev/null +++ b/src/subcommand/init/shell/bash.rs @@ -0,0 +1,45 @@ +use super::{posix, HookConfig, ShellConfig}; + +use anyhow::Result; + +use std::borrow::Cow; + +pub const CONFIG: ShellConfig = ShellConfig { + z: posix::CONFIG.z, + alias: posix::CONFIG.alias, + hook: HookConfig { + prompt: HOOK_PROMPT, + pwd: hook_pwd, + }, +}; + +const HOOK_PROMPT: &str = r#" +_zoxide_hook() { + zoxide add +} + +case "$PROMPT_COMMAND" in + *_zoxide_hook*) ;; + *) PROMPT_COMMAND="_zoxide_hook${PROMPT_COMMAND:+;${PROMPT_COMMAND}}" ;; +esac +"#; + +const fn hook_pwd() -> Result> { + const HOOK_PWD: &str = r#" +_zoxide_hook() { + if [ -z "${_ZO_PWD}" ]; then + _ZO_PWD="${PWD}" + elif [ "${_ZO_PWD}" != "${PWD}" ]; then + _ZO_PWD="${PWD}" + zoxide add + fi +} + +case "$PROMPT_COMMAND" in + *_zoxide_hook*) ;; + *) PROMPT_COMMAND="_zoxide_hook${PROMPT_COMMAND:+;${PROMPT_COMMAND}}" ;; +esac +"#; + + Ok(Cow::Borrowed(HOOK_PWD)) +} diff --git a/src/subcommand/init/shell/fish.rs b/src/subcommand/init/shell/fish.rs new file mode 100644 index 0000000..0b7e383 --- /dev/null +++ b/src/subcommand/init/shell/fish.rs @@ -0,0 +1,90 @@ +use super::{ShellConfig, HookConfig}; + +use anyhow::Result; + +use std::borrow::Cow; + +pub const CONFIG: ShellConfig = ShellConfig { + z, + alias, + hook: HookConfig { + prompt: HOOK_PROMPT, + pwd: hook_pwd, + }, +}; + +fn z(z_cmd: &str) -> String { + format!( + r#" +function _z_cd + cd $argv + or return $status + + commandline -f repaint + + if test "$_ZO_ECHO" = "1" + echo $PWD + end +end + +function {} + set argc (count $argv) + + if test $argc -eq 0 + _z_cd $HOME + or return $status + + else if test $argc -eq 1 -a $argv[1] = '-' + _z_cd - + or return $status + + else + # FIXME: use string-collect from fish 3.1.0 once it has wider adoption + set -l IFS '' + set -l result (zoxide query $argv) + + if test -d $result; and string length -q -- $result + _z_cd $result + or return $status + else if test -n "$result" + echo $result + end + end +end +"#, + z_cmd + ) +} + +fn alias(z_cmd: &str) -> String { + format!( + r#" +abbr -a zi '{} -i' + +abbr -a za 'zoxide add' + +abbr -a zq 'zoxide query' +abbr -a zqi 'zoxide query -i' + +abbr -a zr 'zoxide remove' +abbr -a zri 'zoxide remove -i' +"#, + z_cmd + ) +} + +const HOOK_PROMPT: &str = r#" +function _zoxide_hook --on-event fish_prompt + zoxide add +end +"#; + +const fn hook_pwd() -> Result> { + const HOOK_PWD: &str = r#" +function _zoxide_hook --on-variable PWD + zoxide add +end +"#; + + Ok(Cow::Borrowed(HOOK_PWD)) +} diff --git a/src/subcommand/init/shell/mod.rs b/src/subcommand/init/shell/mod.rs new file mode 100644 index 0000000..01172e7 --- /dev/null +++ b/src/subcommand/init/shell/mod.rs @@ -0,0 +1,19 @@ +pub mod bash; +pub mod fish; +pub mod posix; +pub mod zsh; + +use anyhow::Result; + +use std::borrow::Cow; + +pub struct ShellConfig { + pub z: fn(&str) -> String, + pub alias: fn(&str) -> String, + pub hook: HookConfig, +} + +pub struct HookConfig { + pub prompt: &'static str, + pub pwd: fn() -> Result>, +} diff --git a/src/subcommand/init/shell/posix.rs b/src/subcommand/init/shell/posix.rs new file mode 100644 index 0000000..0e7d6f0 --- /dev/null +++ b/src/subcommand/init/shell/posix.rs @@ -0,0 +1,155 @@ +use super::{HookConfig, ShellConfig}; + +use anyhow::{anyhow, Result}; +use uuid::Uuid; + +use std::borrow::Cow; + +pub const CONFIG: ShellConfig = ShellConfig { + z, + alias, + hook: HookConfig { + prompt: HOOK_PROMPT, + pwd: hook_pwd, + }, +}; + +fn z(z_cmd: &str) -> String { + format!( + r#" +_z_cd() {{ + cd "$@" || return "$?" + + if [ "$_ZO_ECHO" = "1" ]; then + echo "$PWD" + fi +}} + +{}() {{ + if [ "$#" -eq 0 ]; then + _z_cd ~ || return "$?" + elif [ "$#" -eq 1 ] && [ "$1" = '-' ]; then + if [ -n "$OLDPWD" ]; then + _z_cd "$OLDPWD" || return "$?" + else + echo 'zoxide: $OLDPWD is not set' + return 1 + fi + else + result="$(zoxide query "$@")" || return "$?" + if [ -d "$result" ]; then + _z_cd "$result" || return "$?" + elif [ -n "$result" ]; then + echo "$result" + fi + fi +}} +"#, + z_cmd + ) +} + +fn alias(z_cmd: &str) -> String { + format!( + r#" +alias zi='{} -i' + +alias za='zoxide add' + +alias zq='zoxide query' +alias zqi='zoxide query -i' + +alias zr='zoxide remove' +alias zri='zoxide remove -i' +"#, + z_cmd + ) +} + +const HOOK_PROMPT: &str = r#" +_zoxide_hook() { + zoxide add +} + +case "$PS1" in + *\$\(_zoxide_hook\)*) ;; + *) PS1="\$(_zoxide_hook)${PS1}" ;; +esac +"#; + +fn hook_pwd() -> Result> { + let mut tmp_path = std::env::temp_dir(); + tmp_path.push("zoxide"); + + let tmp_path_str = tmp_path + .to_str() + .ok_or_else(|| anyhow!("invalid Unicode in zoxide tmp path"))?; + + let pwd_path = tmp_path.join(format!("pwd-{}", Uuid::new_v4())); + + let pwd_path_str = pwd_path + .to_str() + .ok_or_else(|| anyhow!("invalid Unicode in zoxide pwd path"))?; + + let hook_pwd = format!( + r#" +# PWD hooks in POSIX use a temporary file, located at `$_ZO_PWD_PATH`, to track +# changes in the current directory. These files are removed upon restart, +# but they should ideally also be cleaned up once the shell exits using traps. +# +# This can be done as follows: +# +# trap '_zoxide_cleanup' EXIT HUP KILL TERM +# trap '_zoxide_cleanup; trap - INT; kill -s INT "$$"' INT +# trap '_zoxide_cleanup; trap - QUIT; kill -s QUIT "$$"' QUIT +# +# By default, traps are not set up because they override all previous traps. +# It is therefore up to the user to add traps to their shell configuration. + +_ZO_TMP_PATH={} +_ZO_PWD_PATH={} + +_zoxide_cleanup() {{ + rm -f "$_ZO_PWD_PATH" +}} + +_zoxide_setpwd() {{ + mkdir -p "$_ZO_TMP_PATH" + echo "$PWD" > "$_ZO_PWD_PATH" +}} + +_zoxide_setpwd + +_zoxide_hook() {{ + _ZO_OLDPWD="$(cat "$_ZO_PWD_PATH")" + if [ -z "$_ZO_OLDPWD" ] || [ "$_ZO_OLDPWD" != "$PWD" ]; then + _zoxide_setpwd && zoxide add > /dev/null + fi +}} + +case "$PS1" in + *\$\(_zoxide_hook\)*) ;; + *) PS1="\$(_zoxide_hook)${{PS1}}" ;; +esac"#, + quote(tmp_path_str), + quote(pwd_path_str), + ); + + Ok(Cow::Owned(hook_pwd)) +} + +fn quote(string: &str) -> String { + let mut quoted = String::with_capacity(string.len() + 2); + + quoted.push('\''); + for ch in string.chars() { + match ch { + '\\' => quoted.push_str(r"\\"), + '\'' => quoted.push_str(r"'\''"), + _ => quoted.push(ch), + } + } + quoted.push('\''); + + quoted +} diff --git a/src/subcommand/init/shell/zsh.rs b/src/subcommand/init/shell/zsh.rs new file mode 100644 index 0000000..623d13b --- /dev/null +++ b/src/subcommand/init/shell/zsh.rs @@ -0,0 +1,36 @@ +use super::{posix, HookConfig, ShellConfig}; + +use anyhow::Result; + +use std::borrow::Cow; + +pub const CONFIG: ShellConfig = ShellConfig { + z: posix::CONFIG.z, + alias: posix::CONFIG.alias, + hook: HookConfig { + prompt: HOOK_PROMPT, + pwd: hook_pwd, + }, +}; + +const HOOK_PROMPT: &str = r#" +_zoxide_hook() { + zoxide add +} + +[[ -n "${precmd_functions[(r)_zoxide_hook]}" ]] || { + precmd_functions+=(_zoxide_hook) +} +"#; + +const fn hook_pwd() -> Result> { + const HOOK_PWD: &str = r#" +_zoxide_hook() { + zoxide add +} + +chpwd_functions=(${chpwd_functions[@]} "_zoxide_hook") +"#; + + Ok(Cow::Borrowed(HOOK_PWD)) +}