diff --git a/CHANGELOG.md b/CHANGELOG.md index 6884b5d..21c099a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Interactive mode for removing entries (`zoxide remove -i`). +- Aliases for interactive `query` and `remove` (`zqi` and `zri` respectively) + +### Changed + +- `zoxide remove` now throws an error if there was no match in the database. +- Interactive mode in `zoxide` no longer throws an error if `fzf` exits gracefully. + ## [0.3.1] - 2020-04-03 ### Added diff --git a/src/db.rs b/src/db.rs index 33e84bb..df33498 100644 --- a/src/db.rs +++ b/src/db.rs @@ -239,7 +239,13 @@ impl DB { } } - pub fn query_all<'a>(&'a mut self, keywords: &'a [String]) -> impl Iterator { + pub fn query_many<'a>(&'a mut self, keywords: &'a [String]) -> impl Iterator { + self.query_all() + .iter() + .filter(move |dir| dir.is_match(keywords)) + } + + pub fn query_all(&mut self) -> &[Dir] { let orig_len = self.data.dirs.len(); self.data.dirs.retain(Dir::is_dir); @@ -247,28 +253,34 @@ impl DB { self.modified = true; } - self.data - .dirs - .iter() - .filter(move |dir| dir.is_match(keywords)) + self.data.dirs.as_slice() } pub fn remove>(&mut self, path: P) -> Result<()> { - let path_canonicalized; - let path_abs = match path.as_ref().canonicalize() { - Ok(path_abs) => { - path_canonicalized = path_abs; - &path_canonicalized - } - Err(_) => path.as_ref(), - }; + if let Ok(path_abs) = path.as_ref().canonicalize() { + self.remove_exact(path_abs) + .or_else(|_| self.remove_exact(path)) + } else { + self.remove_exact(path) + } + } - if let Some(idx) = self.data.dirs.iter().position(|dir| dir.path == path_abs) { + pub fn remove_exact>(&mut self, path: P) -> Result<()> { + if let Some(idx) = self + .data + .dirs + .iter() + .position(|dir| dir.path == path.as_ref()) + { self.data.dirs.swap_remove(idx); self.modified = true; + Ok(()) + } else { + bail!( + "could not find path in database: {}", + path.as_ref().display() + ) } - - Ok(()) } fn get_path_tmp(&self) -> PathBuf { diff --git a/src/subcommand/init.rs b/src/subcommand/init.rs index d51d16e..e629bb5 100644 --- a/src/subcommand/init.rs +++ b/src/subcommand/init.rs @@ -219,9 +219,14 @@ 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 ) @@ -231,9 +236,14 @@ 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 ) diff --git a/src/subcommand/query.rs b/src/subcommand/query.rs index ac4c5bc..30d04a2 100644 --- a/src/subcommand/query.rs +++ b/src/subcommand/query.rs @@ -70,7 +70,7 @@ impl Query { .collect::>(); let mut db = util::get_db()?; - let dirs = db.query_all(&keywords); + let dirs = db.query_many(&keywords); util::fzf_helper(now, dirs) } } diff --git a/src/subcommand/remove.rs b/src/subcommand/remove.rs index 5ba579d..d5f08e9 100644 --- a/src/subcommand/remove.rs +++ b/src/subcommand/remove.rs @@ -8,11 +8,29 @@ use std::path::PathBuf; #[derive(Debug, StructOpt)] #[structopt(about = "Remove a directory")] pub struct Remove { - path: PathBuf, + #[structopt(required_unless("interactive"))] + path: Option, + #[structopt(short, long, help = "Opens an interactive selection menu using fzf")] + interactive: bool, } impl Remove { pub fn run(&self) -> Result<()> { - util::get_db()?.remove(&self.path) + if self.interactive { + let mut db = util::get_db()?; + let dirs = db.query_all(); + let now = util::get_current_time()?; + + if let Some(path_bytes) = util::fzf_helper(now, dirs)? { + let path = util::bytes_to_path(&path_bytes)?; + db.remove_exact(path)?; + } + + Ok(()) + } else { + // structopt guarantees that unwrap is safe here + let path = self.path.as_ref().unwrap(); + util::get_db()?.remove(path) + } } } diff --git a/src/util.rs b/src/util.rs index b05fd67..89d7a51 100644 --- a/src/util.rs +++ b/src/util.rs @@ -12,15 +12,35 @@ use std::process::{Command, Stdio}; use std::time::SystemTime; #[cfg(unix)] -pub fn path_to_bytes>(path: &P) -> Option<&[u8]> { +pub fn path_to_bytes>(path: &P) -> Result<&[u8]> { use std::os::unix::ffi::OsStrExt; - Some(path.as_ref().as_os_str().as_bytes()) + Ok(path.as_ref().as_os_str().as_bytes()) } #[cfg(not(unix))] -pub fn path_to_bytes>(path: &P) -> Option<&[u8]> { - Some(path.as_ref().to_str()?.as_bytes()) +pub fn path_to_bytes>(path: &P) -> Result<&[u8]> { + match path.as_ref().to_str() { + Some(path_str) => Ok(path_str.as_bytes()), + None => bail!("invalid Unicode in path"), + } +} + +#[cfg(unix)] +pub fn bytes_to_path(bytes: &[u8]) -> Result<&Path> { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + Ok(Path::new(OsStr::from_bytes(bytes))) +} + +#[cfg(not(unix))] +pub fn bytes_to_path(bytes: &[u8]) -> Result<&Path> { + use std::str; + + str::from_utf8(bytes) + .map(Path::new) + .context("invalid Unicode in path") } pub fn get_db() -> Result { @@ -75,7 +95,7 @@ where for &(dir, frecency) in dir_frecencies.iter() { // ensure that frecency fits in 4 characters - if let Some(path_bytes) = path_to_bytes(&dir.path) { + if let Ok(path_bytes) = path_to_bytes(&dir.path) { (|| { write!(fzf_stdin, "{:>4} ", frecency)?; fzf_stdin.write_all(path_bytes)?;