Multi-select for zoxide remove -i (#192)

This commit is contained in:
Ajeet D'Souza 2021-04-29 01:31:00 +05:30 committed by GitHub
parent 0eb4418fd6
commit 027ae1df47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 90 additions and 93 deletions

View File

@ -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

25
Cargo.lock generated
View File

@ -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",

View File

@ -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]

View File

@ -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<PathBuf>,
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()?;

View File

@ -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();

View File

@ -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<String> = 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)
})
}

View File

@ -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),

View File

@ -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(())

View File

@ -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)
}
}
}
}

View File

@ -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)),

View File

@ -10,13 +10,15 @@ pub struct Fzf {
}
impl Fzf {
pub fn new() -> Result<Self> {
pub fn new(multiple: bool) -> Result<Self> {
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()
}

View File

@ -147,9 +147,5 @@ pub fn resolve_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
}
}
let result = stack.iter().collect::<PathBuf>();
if !result.is_dir() {
bail!("could not resolve path: {}", result.display());
}
Ok(result)
Ok(stack.iter().collect())
}

View File

@ -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 %}
}