diff --git a/CHANGELOG.md b/CHANGELOG.md index 33ceacb..22b5b01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,16 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Manpages for each subcommand. +- Manpages for all subcommands. - Default prompt for Nushell. ### Changed +- `zoxide remove -i` now accepts multiple selections. +- `zoxide add` no longer accepts zero parameters. - `$_ZO_EXCLUDE_DIRS` now defaults to `"$HOME"`. ### Fixed - `cd -` on fish shells. +- `__zoxide_hook` no longer changes value of `$?` within `$PROMPT_COMMAND` on bash. ## [0.6.0] - 2021-04-09 @@ -122,7 +125,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- `fish` no longer `cd`s to the user's home when no match is found. +- fish no longer `cd`s to the user's home when no match is found. ## [0.3.1] - 2020-04-03 @@ -162,7 +165,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Incorrect exit codes in `z` command on `fish`. +- Incorrect exit codes in `z` command on fish. ### Removed @@ -175,7 +178,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `$_ZO_ECHO` to echo match before `cd`ing. - Minimal `ranger` plugin. - PWD hook to only update the database when the current directory is changed. -- Support for the `bash` shell. +- Support for bash. - `migrate` subcommand to allow users to migrate from `z`. ### Fixed @@ -189,11 +192,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `init` subcommand to remove dependency on shell plugin managers. - Support for `z -` command to go to previous directory. - `Cargo.lock` for more reproducible builds. -- Support for the `fish` shell. +- Support for the fish shell. ### Fixed -- `_zoxide_precmd` overriding other precmd hooks on `zsh`. +- `_zoxide_precmd` overriding other precmd hooks on zsh. ## [0.1.1] - 2020-03-08 @@ -215,7 +218,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - GitHub Actions pipeline to build and upload releases. -- Support for the `zsh` shell. +- Support for zsh. [0.6.0]: https://github.com/ajeetdsouza/zoxide/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/ajeetdsouza/zoxide/compare/v0.4.3...v0.5.0 diff --git a/Cargo.lock b/Cargo.lock index f757eb4..06a2451 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,11 +86,10 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "bincode" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d175dfa69e619905c4c3cdb7c3c203fa3bdd5d51184e3afdb2742c0280493772" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" dependencies = [ - "byteorder", "serde", ] @@ -125,9 +124,9 @@ dependencies = [ [[package]] name = "byteorder" -version = "1.3.4" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cfg-if" @@ -282,9 +281,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lexical-core" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" dependencies = [ "arrayvec", "bitflags", @@ -511,9 +510,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041" dependencies = [ "bitflags", ] @@ -554,9 +553,9 @@ checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" [[package]] name = "seq-macro" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5b3bd665f328d73d7079123bfbd14a6edd74187667b5c6f340adfc65ea9d25a" +checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99" [[package]] name = "serde" @@ -592,9 +591,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb" +checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index b0d6333..9fc26ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,6 @@ clap = "3.0.0-beta.2" dirs-next = "2.0.0" dunce = "1.0.1" glob = "0.3.0" -once_cell = "1.4.1" ordered-float = "2.0.0" serde = { version = "1.0.116", features = ["derive"] } tempfile = "3.1.0" @@ -27,6 +26,7 @@ rand = "0.7.3" [dev-dependencies] assert_cmd = "1.0.1" +once_cell = "1.4.1" seq-macro = "0.2.1" [features] diff --git a/src/cmd/add.rs b/src/cmd/add.rs index effa870..1b34cc3 100644 --- a/src/cmd/add.rs +++ b/src/cmd/add.rs @@ -3,7 +3,7 @@ use crate::config; use crate::db::DatabaseFile; use crate::util; -use anyhow::Result; +use anyhow::{bail, Result}; use clap::Clap; use std::path::PathBuf; @@ -11,20 +11,15 @@ use std::path::PathBuf; /// Add a new directory or increment its rank #[derive(Clap, Debug)] pub struct Add { - path: Option, + path: PathBuf, } impl Cmd for Add { fn run(&self) -> Result<()> { - let path = match &self.path { - Some(path) => { - if config::zo_resolve_symlinks() { - util::canonicalize(&path) - } else { - util::resolve_path(&path) - } - } - None => util::current_dir(), + let path = if config::zo_resolve_symlinks() { + util::canonicalize(&self.path) + } else { + util::resolve_path(&self.path) }?; if config::zo_exclude_dirs()? @@ -34,6 +29,9 @@ impl Cmd for Add { return Ok(()); } + if !path.is_dir() { + bail!("not a directory: {}", path.display()); + } let path = util::path_to_str(&path)?; let now = util::current_time()?; diff --git a/src/cmd/import.rs b/src/cmd/import.rs index 44402d3..959db74 100644 --- a/src/cmd/import.rs +++ b/src/cmd/import.rs @@ -30,7 +30,7 @@ impl Cmd for Import { let mut db = DatabaseFile::new(data_dir); let mut db = db.open()?; if !self.merge && !db.dirs.is_empty() { - bail!("current database is not empty, specify --merge to continue anyway") + bail!("current database is not empty, specify --merge to continue anyway"); } let resolve_symlinks = config::zo_resolve_symlinks(); diff --git a/src/cmd/init.rs b/src/cmd/init.rs index f6f43d1..63ea435 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -6,13 +6,11 @@ use crate::shell::{self, Hook, Opts}; use anyhow::{Context, Result}; use askama::Template; use clap::{ArgEnum, Clap}; -use once_cell::sync::OnceCell; use std::io::{self, Write}; /// Generate shell configuration #[derive(Clap, Debug)] -#[clap(after_help(env_help()))] pub struct Init { #[clap(arg_enum)] shell: Shell, @@ -59,7 +57,7 @@ impl Cmd for Init { Shell::Zsh => shell::Zsh(opts).render(), } .context("could not render template")?; - writeln!(io::stdout(), "{}", source).handle_err("stdout") + writeln!(io::stdout(), "{}", source).wrap_write("stdout") } } @@ -74,26 +72,3 @@ enum Shell { Xonsh, Zsh, } - -fn env_help() -> &'static str { - static ENV_HELP: OnceCell = OnceCell::new(); - ENV_HELP.get_or_init(|| { - #[cfg(unix)] - const PATH_SPLIT_SEPARATOR: u8 = b':'; - #[cfg(windows)] - const PATH_SPLIT_SEPARATOR: u8 = b';'; - - format!( - "\ -ENVIRONMENT VARIABLES: - _ZO_DATA_DIR Path for zoxide data files - [current: {data_dir}] - _ZO_ECHO Prints the matched directory before navigating to it when set to 1 - _ZO_EXCLUDE_DIRS List of directory globs to be excluded, separated by '{path_split_separator}' - _ZO_FZF_OPTS Custom flags to pass to fzf - _ZO_MAXAGE Maximum total age after which entries start getting deleted - _ZO_RESOLVE_SYMLINKS Resolve symlinks when storing paths", - data_dir=config::zo_data_dir().unwrap_or_else(|_| "none".into()).display(), - path_split_separator=PATH_SPLIT_SEPARATOR as char) - }) -} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 7be662b..3b24f73 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -13,6 +13,14 @@ pub use init::Init; pub use query::Query; pub use remove::Remove; +const ENV_HELP: &str = "ENVIRONMENT VARIABLES: + _ZO_DATA_DIR Path for zoxide data files + _ZO_ECHO Prints the matched directory before navigating to it when set to 1 + _ZO_EXCLUDE_DIRS List of directory globs to be excluded + _ZO_FZF_OPTS Custom flags to pass to fzf + _ZO_MAXAGE Maximum total age after which entries start getting deleted + _ZO_RESOLVE_SYMLINKS Resolve symlinks when storing paths"; + pub trait Cmd { fn run(&self) -> Result<()>; } @@ -21,6 +29,8 @@ pub trait Cmd { #[clap( about, author, + after_help = ENV_HELP, + global_setting(AppSettings::ColoredHelp), global_setting(AppSettings::DisableHelpSubcommand), global_setting(AppSettings::GlobalVersion), global_setting(AppSettings::VersionlessSubcommands), diff --git a/src/cmd/query.rs b/src/cmd/query.rs index d3d40e7..bfbcea6 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -47,10 +47,9 @@ impl Cmd for Query { .filter(|dir| Some(dir.path.as_ref()) != self.exclude.as_deref()); if self.interactive { - let mut fzf = Fzf::new()?; - let handle = fzf.stdin(); + let mut fzf = Fzf::new(false)?; for dir in matches { - writeln!(handle, "{}", dir.display_score(now)).handle_err("fzf")?; + writeln!(fzf.stdin(), "{}", dir.display_score(now)).wrap_write("fzf")?; } let selection = fzf.wait_select()?; @@ -76,9 +75,9 @@ impl Cmd for Query { } else { writeln!(handle, "{}", dir.display()) } - .handle_err("stdout")?; + .wrap_write("stdout")?; } - handle.flush().handle_err("stdout")?; + handle.flush().wrap_write("stdout")?; } else { let dir = matches.next().context("no match found")?; if self.score { @@ -86,7 +85,7 @@ impl Cmd for Query { } else { writeln!(io::stdout(), "{}", dir.display()) } - .handle_err("stdout")?; + .wrap_write("stdout")?; } Ok(()) diff --git a/src/cmd/remove.rs b/src/cmd/remove.rs index ed89765..5b03866 100644 --- a/src/cmd/remove.rs +++ b/src/cmd/remove.rs @@ -5,7 +5,7 @@ use crate::error::WriteErrorHandler; use crate::fzf::Fzf; use crate::util; -use anyhow::{bail, Context, Result}; +use anyhow::{bail, Result}; use clap::Clap; use std::io::Write; @@ -30,31 +30,45 @@ impl Cmd for Remove { let mut db = db.open()?; let selection; - let path = match &self.interactive { + match &self.interactive { Some(keywords) => { let query = Query::new(keywords); let now = util::current_time()?; - - let mut fzf = Fzf::new()?; - let handle = fzf.stdin(); let resolve_symlinks = config::zo_resolve_symlinks(); + + let mut fzf = Fzf::new(true)?; for dir in db.iter_matches(&query, now, resolve_symlinks) { - writeln!(handle, "{}", dir.display_score(now)).handle_err("fzf")?; + writeln!(fzf.stdin(), "{}", dir.display_score(now)).wrap_write("fzf")?; } selection = fzf.wait_select()?; - selection - .get(5..selection.len().saturating_sub(1)) - .context("fzf returned invalid output")? - } - None => self.path.as_ref().unwrap(), - }; + let paths = selection.lines().filter_map(|line| line.get(5..)); + let mut not_found = Vec::new(); + for path in paths { + if !db.remove(&path) { + not_found.push(path); + } + } - if !db.remove(path) { - let path = util::resolve_path(&path)?; - let path = util::path_to_str(&path)?; - if !db.remove(path) { - bail!("path not found in database: {}", &path) + if !not_found.is_empty() { + let mut err = "path not found in database:".to_string(); + for path in not_found { + err.push_str("\n "); + err.push_str(path.as_ref()); + } + bail!(err); + } + } + None => { + // unwrap is safe here because path is required_unless_present = "interactive" + let path = self.path.as_ref().unwrap(); + if !db.remove(path) { + let path_abs = util::resolve_path(&path)?; + let path_abs = util::path_to_str(&path_abs)?; + if path_abs != path && !db.remove(path) { + bail!("path not found in database:\n {}", &path) + } + } } } diff --git a/src/error.rs b/src/error.rs index c56e44a..5948a37 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,11 +16,11 @@ impl Display for SilentExit { } pub trait WriteErrorHandler { - fn handle_err(self, device: &str) -> Result<()>; + fn wrap_write(self, device: &str) -> Result<()>; } impl WriteErrorHandler for io::Result<()> { - fn handle_err(self, device: &str) -> Result<()> { + fn wrap_write(self, device: &str) -> Result<()> { match self { Err(e) if e.kind() == io::ErrorKind::BrokenPipe => bail!(SilentExit { code: 0 }), result => result.with_context(|| format!("could not write to {}", device)), diff --git a/src/fzf.rs b/src/fzf.rs index b308b23..9f692c3 100644 --- a/src/fzf.rs +++ b/src/fzf.rs @@ -10,13 +10,15 @@ pub struct Fzf { } impl Fzf { - pub fn new() -> Result { + pub fn new(multiple: bool) -> Result { let mut command = Command::new("fzf"); + if multiple { + command.arg("-m"); + } command .arg("-n2..") .stdin(Stdio::piped()) .stdout(Stdio::piped()); - if let Some(fzf_opts) = config::zo_fzf_opts() { command.env("FZF_DEFAULT_OPTS", fzf_opts); } @@ -27,6 +29,7 @@ impl Fzf { } pub fn stdin(&mut self) -> &mut ChildStdin { + // unwrap is safe here because command.stdin() has been piped self.child.stdin.as_mut().unwrap() } diff --git a/src/util.rs b/src/util.rs index 7ac3d59..293b576 100644 --- a/src/util.rs +++ b/src/util.rs @@ -147,9 +147,5 @@ pub fn resolve_path>(path: &P) -> Result { } } - let result = stack.iter().collect::(); - if !result.is_dir() { - bail!("could not resolve path: {}", result.display()); - } - Ok(result) + Ok(stack.iter().collect()) } diff --git a/templates/elvish.txt b/templates/elvish.txt index 68d2593..7db8174 100644 --- a/templates/elvish.txt +++ b/templates/elvish.txt @@ -32,9 +32,9 @@ if (not (and (builtin:has-env __zoxide_hooked) (builtin:eq (builtin:get-env __zo {%- when Hook::None %} {{ not_configured }} {%- when Hook::Prompt %} - edit:before-readline = [$@edit:before-readline []{ zoxide add $pwd }] + edit:before-readline = [$@edit:before-readline []{ zoxide add -- $pwd }] {%- when Hook::Pwd %} - after-chdir = [$@after-chdir [_]{ zoxide add $pwd }] + after-chdir = [$@after-chdir [_]{ zoxide add -- $pwd }] {%- endmatch %} }