diff --git a/src/config.rs b/src/config.rs index 2d91116..1190517 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,8 +3,8 @@ use crate::db::Rank; use anyhow::{bail, Context, Result}; use std::env; -use std::fs; use std::ffi::OsString; +use std::fs; use std::path::PathBuf; pub fn zo_data_dir() -> Result { diff --git a/src/db.rs b/src/db.rs index 4d95671..b65f4f2 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,4 +1,5 @@ use anyhow::{bail, Context, Result}; +use float_ord::FloatOrd; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -149,6 +150,10 @@ impl Db { Ok(()) } + pub fn matches<'a>(&'a mut self, now: Epoch, keywords: &[String]) -> DbMatches<'a> { + DbMatches::new(self, now, keywords) + } + fn get_path>(data_dir: P) -> PathBuf { data_dir.as_ref().join("db.zo") } @@ -167,6 +172,49 @@ impl Drop for Db { } } +/// Streaming iterator for matching entries +pub struct DbMatches<'a> { + db: &'a mut Db, + idxs: std::iter::Rev>, + keywords: Vec, +} + +impl<'a> DbMatches<'a> { + pub fn new(db: &'a mut Db, now: Epoch, keywords: &[String]) -> DbMatches<'a> { + db.dirs + .sort_unstable_by_key(|dir| FloatOrd(dir.get_frecency(now))); + + let idxs = (0..db.dirs.len()).rev(); + let keywords = keywords + .iter() + .map(|keyword| keyword.to_lowercase()) + .collect(); + + DbMatches { db, idxs, keywords } + } + + pub fn next(&mut self) -> Option<&Dir> { + for idx in &mut self.idxs { + let dir = &self.db.dirs[idx]; + + if !dir.is_match(&self.keywords) { + continue; + } + + if !dir.is_valid() { + self.db.dirs.swap_remove(idx); + self.db.modified = true; + continue; + } + + let dir = &self.db.dirs[idx]; + return Some(dir); + } + + None + } +} + pub type Rank = f64; pub type Epoch = i64; // use a signed integer so subtraction can be performed on it diff --git a/src/subcommand/add.rs b/src/subcommand/add.rs index 894f533..1b9537c 100644 --- a/src/subcommand/add.rs +++ b/src/subcommand/add.rs @@ -8,8 +8,9 @@ use structopt::StructOpt; use std::env; use std::path::{Path, PathBuf}; +/// Add a new directory or increment its rank #[derive(Debug, StructOpt)] -#[structopt(about = "Add a new directory or increment its rank")] +#[structopt()] pub struct Add { path: Option, } diff --git a/src/subcommand/import.rs b/src/subcommand/import.rs index fafb5aa..fd224dd 100644 --- a/src/subcommand/import.rs +++ b/src/subcommand/import.rs @@ -7,12 +7,14 @@ use structopt::StructOpt; use std::fs; use std::path::{Path, PathBuf}; +/// Import from z database #[derive(Debug, StructOpt)] -#[structopt(about = "Import from z database")] +#[structopt()] pub struct Import { path: PathBuf, - #[structopt(long, help = "Merge entries into existing database")] + /// Merge entries into existing database + #[structopt(long)] merge: bool, } diff --git a/src/subcommand/init/mod.rs b/src/subcommand/init/mod.rs index cf04cbd..29d8d52 100644 --- a/src/subcommand/init/mod.rs +++ b/src/subcommand/init/mod.rs @@ -6,30 +6,24 @@ use structopt::StructOpt; use std::io::{self, Write}; +/// Generates shell configuration #[derive(Debug, StructOpt)] -#[structopt(about = "Generates shell configuration")] +#[structopt()] pub struct Init { #[structopt(possible_values = &Shell::variants(), case_insensitive = true)] shell: Shell, - #[structopt( - long, - help = "Renames the 'z' command and corresponding aliases", - alias = "z-cmd", - default_value = "z" - )] + /// Renames the 'z' command and corresponding aliases + #[structopt(long, alias = "z-cmd", default_value = "z")] cmd: String, - #[structopt( - long, - alias = "no-define-aliases", - help = "Prevents zoxide from defining any commands other than 'z'" - )] + /// Prevents zoxide from defining any commands other than 'z' + #[structopt(long, alias = "no-define-aliases")] no_aliases: bool, + /// Chooses event on which an entry is added to the database #[structopt( long, - help = "Chooses event on which an entry is added to the database", possible_values = &Hook::variants(), default_value = "pwd", case_insensitive = true diff --git a/src/subcommand/query.rs b/src/subcommand/query.rs index c51229b..da77f12 100644 --- a/src/subcommand/query.rs +++ b/src/subcommand/query.rs @@ -2,100 +2,95 @@ use crate::fzf::Fzf; use crate::util; use anyhow::{bail, Result}; -use float_ord::FloatOrd; use structopt::StructOpt; +use std::io::{self, Write}; use std::path::Path; +/// Search for a directory #[derive(Debug, StructOpt)] -#[structopt(about = "Search for a directory")] +#[structopt()] pub struct Query { keywords: Vec, - #[structopt(short, long, help = "Opens an interactive selection menu using fzf")] + + /// Opens an interactive selection menu using fzf + #[structopt(short, long, conflicts_with = "list")] interactive: bool, + + /// List all matching directories + #[structopt(short, long, conflicts_with = "interactive")] + list: bool, } impl Query { pub fn run(&self) -> Result<()> { - let path_opt = if self.interactive { - query_interactive(&self.keywords)? - } else { - query(&self.keywords)? - }; + if self.list { + return query_list(&self.keywords); + } - match path_opt { - Some(path) => println!("{}", path), - None => bail!("no match found"), - }; + if self.interactive { + return query_interactive(&self.keywords); + } - Ok(()) + // if the input is already a valid path, simply return it + if let [path] = self.keywords.as_slice() { + if Path::new(path).is_dir() { + println!("{}", path); + return Ok(()); + } + } + + query(&self.keywords) } } -fn query(keywords: &[String]) -> Result> { - // if the input is already a valid path, simply return it - if let [path] = keywords { - if Path::new(path).is_dir() { - return Ok(Some(path.to_string())); - } - } - +fn query(keywords: &[String]) -> Result<()> { let mut db = util::get_db()?; let now = util::get_current_time()?; - let keywords = keywords - .iter() - .map(|keyword| keyword.to_lowercase()) - .collect::>(); + let mut matches = db.matches(now, keywords); - db.dirs - .sort_unstable_by_key(|dir| FloatOrd(dir.get_frecency(now))); - - for idx in (0..db.dirs.len()).rev() { - let dir = &db.dirs[idx]; - if !dir.is_match(&keywords) { - continue; - } - - if !dir.is_valid() { - db.dirs.swap_remove(idx); - db.modified = true; - continue; - } - - let path = &dir.path; - return Ok(Some(path.to_string())); + match matches.next() { + Some(dir) => println!("{}", dir.path), + None => bail!("no match found"), } - Ok(None) + Ok(()) } -fn query_interactive(keywords: &[String]) -> Result> { - let keywords = keywords - .iter() - .map(|keyword| keyword.to_lowercase()) - .collect::>(); - +fn query_interactive(keywords: &[String]) -> Result<()> { let mut db = util::get_db()?; let now = util::get_current_time()?; let mut fzf = Fzf::new()?; - for idx in (0..db.dirs.len()).rev() { - let dir = &db.dirs[idx]; - - if !dir.is_match(&keywords) { - continue; - } - - if !dir.is_valid() { - db.dirs.swap_remove(idx); - db.modified = true; - continue; - } + let mut matches = db.matches(now, keywords); + while let Some(dir) = matches.next() { fzf.write_dir(dir, now); } - fzf.wait_selection() + match fzf.wait_selection()? { + Some(path) => println!("{}", path), + None => bail!("no match found"), + }; + + Ok(()) +} + +fn query_list(keywords: &[String]) -> Result<()> { + let mut db = util::get_db()?; + let now = util::get_current_time()?; + + let mut matches = db.matches(now, keywords); + + let stdout = io::stdout(); + let mut handle = stdout.lock(); + + while let Some(dir) = matches.next() { + let path = &dir.path; + writeln!(handle, "{}", path).unwrap(); + } + + Ok(()) } diff --git a/src/subcommand/remove.rs b/src/subcommand/remove.rs index 43a97cb..35dcbc2 100644 --- a/src/subcommand/remove.rs +++ b/src/subcommand/remove.rs @@ -3,8 +3,9 @@ use crate::util::{canonicalize, get_db, path_to_str}; use anyhow::{bail, Result}; use structopt::StructOpt; +/// Remove a directory #[derive(Debug, StructOpt)] -#[structopt(about = "Remove a directory")] +#[structopt()] pub struct Remove { path: String, }