Remove deleted entries if not accessed in the last 90 days
This commit is contained in:
parent
97dc08347d
commit
a4fcb39c8b
|
|
@ -8,6 +8,7 @@ zoxide is a smarter replacement for your cd command. It keeps track of the
|
||||||
directories you use most frequently, and uses a ranking algorithm to navigate
|
directories you use most frequently, and uses a ranking algorithm to navigate
|
||||||
to the best match.
|
to the best match.
|
||||||
.SH USAGE
|
.SH USAGE
|
||||||
|
.nf
|
||||||
\fBz\fR \fIfoo\fR # cd into highest ranked directory matching foo
|
\fBz\fR \fIfoo\fR # cd into highest ranked directory matching foo
|
||||||
\fBz\fR \fIfoo bar\fR # cd into highest ranked directory matching foo and bar
|
\fBz\fR \fIfoo bar\fR # cd into highest ranked directory matching foo and bar
|
||||||
.sp
|
.sp
|
||||||
|
|
@ -17,6 +18,7 @@ to the best match.
|
||||||
\fBz\fR \fI-\fR # cd into previous directory
|
\fBz\fR \fI-\fR # cd into previous directory
|
||||||
.sp
|
.sp
|
||||||
\fBzi\fR \fIfoo\fR # cd with interactive selection (using fzf)
|
\fBzi\fR \fIfoo\fR # cd with interactive selection (using fzf)
|
||||||
|
.fi
|
||||||
.SH SUBCOMMANDS
|
.SH SUBCOMMANDS
|
||||||
.TP
|
.TP
|
||||||
\fBzoxide-add\fR(1)
|
\fBzoxide-add\fR(1)
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ pub struct Query {
|
||||||
pub score: bool,
|
pub score: bool,
|
||||||
|
|
||||||
/// Exclude a path from results
|
/// Exclude a path from results
|
||||||
#[clap(long, hidden = true)]
|
#[clap(long, value_name = "path")]
|
||||||
pub exclude: Option<String>,
|
pub exclude: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::app::{Query, Run};
|
use crate::app::{Query, Run};
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::db::{DatabaseFile, Matcher};
|
use crate::db::DatabaseFile;
|
||||||
use crate::error::BrokenPipeHandler;
|
use crate::error::BrokenPipeHandler;
|
||||||
use crate::fzf::Fzf;
|
use crate::fzf::Fzf;
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
@ -16,19 +16,18 @@ impl Run for Query {
|
||||||
let mut db = db.open()?;
|
let mut db = db.open()?;
|
||||||
let now = util::current_time()?;
|
let now = util::current_time()?;
|
||||||
|
|
||||||
let mut matcher = Matcher::new().with_keywords(&self.keywords);
|
let mut stream = db.stream(now).with_keywords(&self.keywords);
|
||||||
if !self.all {
|
if !self.all {
|
||||||
let resolve_symlinks = config::zo_resolve_symlinks();
|
let resolve_symlinks = config::zo_resolve_symlinks();
|
||||||
matcher = matcher.with_exists(resolve_symlinks);
|
stream = stream.with_exists(resolve_symlinks);
|
||||||
|
}
|
||||||
|
if let Some(path) = &self.exclude {
|
||||||
|
stream = stream.with_exclude(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut matches = db
|
|
||||||
.iter(&matcher, now)
|
|
||||||
.filter(|dir| Some(dir.path.as_ref()) != self.exclude.as_deref());
|
|
||||||
|
|
||||||
if self.interactive {
|
if self.interactive {
|
||||||
let mut fzf = Fzf::new(false)?;
|
let mut fzf = Fzf::new(false)?;
|
||||||
for dir in matches {
|
while let Some(dir) = stream.next() {
|
||||||
writeln!(fzf.stdin(), "{}", dir.display_score(now)).pipe_exit("fzf")?;
|
writeln!(fzf.stdin(), "{}", dir.display_score(now)).pipe_exit("fzf")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,7 +48,7 @@ impl Run for Query {
|
||||||
let stdout = stdout.lock();
|
let stdout = stdout.lock();
|
||||||
let mut handle = BufWriter::new(stdout);
|
let mut handle = BufWriter::new(stdout);
|
||||||
|
|
||||||
for dir in matches {
|
while let Some(dir) = stream.next() {
|
||||||
if self.score {
|
if self.score {
|
||||||
writeln!(handle, "{}", dir.display_score(now))
|
writeln!(handle, "{}", dir.display_score(now))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -59,7 +58,7 @@ impl Run for Query {
|
||||||
}
|
}
|
||||||
handle.flush().pipe_exit("stdout")?;
|
handle.flush().pipe_exit("stdout")?;
|
||||||
} else {
|
} else {
|
||||||
let dir = matches.next().context("no match found")?;
|
let dir = stream.next().context("no match found")?;
|
||||||
if self.score {
|
if self.score {
|
||||||
writeln!(io::stdout(), "{}", dir.display_score(now))
|
writeln!(io::stdout(), "{}", dir.display_score(now))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::app::{Remove, Run};
|
use crate::app::{Remove, Run};
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::db::{DatabaseFile, Matcher};
|
use crate::db::DatabaseFile;
|
||||||
use crate::error::BrokenPipeHandler;
|
use crate::error::BrokenPipeHandler;
|
||||||
use crate::fzf::Fzf;
|
use crate::fzf::Fzf;
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
@ -18,11 +18,11 @@ impl Run for Remove {
|
||||||
let selection;
|
let selection;
|
||||||
match &self.interactive {
|
match &self.interactive {
|
||||||
Some(keywords) => {
|
Some(keywords) => {
|
||||||
let matcher = Matcher::new().with_keywords(keywords);
|
|
||||||
let now = util::current_time()?;
|
let now = util::current_time()?;
|
||||||
|
let mut stream = db.stream(now).with_keywords(keywords);
|
||||||
|
|
||||||
let mut fzf = Fzf::new(true)?;
|
let mut fzf = Fzf::new(true)?;
|
||||||
for dir in db.iter(&matcher, now) {
|
while let Some(dir) = stream.next() {
|
||||||
writeln!(fzf.stdin(), "{}", dir.display_score(now)).pipe_exit("fzf")?;
|
writeln!(fzf.stdin(), "{}", dir.display_score(now)).pipe_exit("fzf")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,24 @@
|
||||||
mod dir;
|
mod dir;
|
||||||
mod query;
|
mod stream;
|
||||||
|
|
||||||
pub use dir::{Dir, DirList, Epoch, Rank};
|
pub use dir::{Dir, DirList, Epoch, Rank};
|
||||||
pub use query::Matcher;
|
pub use stream::Stream;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use ordered_float::OrderedFloat;
|
|
||||||
use tempfile::{NamedTempFile, PersistError};
|
use tempfile::{NamedTempFile, PersistError};
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::cmp::Reverse;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub struct Database<'a> {
|
pub struct Database<'file> {
|
||||||
pub dirs: DirList<'a>,
|
pub dirs: DirList<'file>,
|
||||||
pub modified: bool,
|
pub modified: bool,
|
||||||
data_dir: &'a Path,
|
data_dir: &'file PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Database<'a> {
|
impl<'file> Database<'file> {
|
||||||
pub fn save(&mut self) -> Result<()> {
|
pub fn save(&mut self) -> Result<()> {
|
||||||
if !self.modified {
|
if !self.modified {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
@ -72,12 +70,9 @@ impl<'a> Database<'a> {
|
||||||
self.modified = true;
|
self.modified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter<'i>(&'i mut self, m: &'i Matcher, now: Epoch) -> impl Iterator<Item = &'i Dir> {
|
// Streaming iterator for directories.
|
||||||
self.dirs
|
pub fn stream(&mut self, now: Epoch) -> Stream<'_, 'file> {
|
||||||
.sort_unstable_by_key(|dir| Reverse(OrderedFloat(dir.score(now))));
|
Stream::new(self, now)
|
||||||
self.dirs
|
|
||||||
.iter()
|
|
||||||
.filter(move |dir| m.matches(dir.path.as_ref()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes the directory with `path` from the store.
|
/// Removes the directory with `path` from the store.
|
||||||
|
|
@ -159,15 +154,15 @@ fn persist<P: AsRef<Path>>(file: NamedTempFile, path: P) -> Result<(), PersistEr
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DatabaseFile {
|
pub struct DatabaseFile {
|
||||||
data_dir: PathBuf,
|
|
||||||
buffer: Vec<u8>,
|
buffer: Vec<u8>,
|
||||||
|
data_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DatabaseFile {
|
impl DatabaseFile {
|
||||||
pub fn new<P: Into<PathBuf>>(data_dir: P) -> DatabaseFile {
|
pub fn new<P: Into<PathBuf>>(data_dir: P) -> Self {
|
||||||
DatabaseFile {
|
DatabaseFile {
|
||||||
data_dir: data_dir.into(),
|
|
||||||
buffer: Vec::new(),
|
buffer: Vec::new(),
|
||||||
|
data_dir: data_dir.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,89 @@
|
||||||
|
use super::{Database, Dir, Epoch};
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
||||||
|
use ordered_float::OrderedFloat;
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::iter::Rev;
|
||||||
|
use std::ops::Range;
|
||||||
use std::path;
|
use std::path;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
pub struct Stream<'db, 'file> {
|
||||||
pub struct Matcher {
|
db: &'db mut Database<'file>,
|
||||||
|
idxs: Rev<Range<usize>>,
|
||||||
|
|
||||||
keywords: Vec<String>,
|
keywords: Vec<String>,
|
||||||
|
|
||||||
check_exists: bool,
|
check_exists: bool,
|
||||||
|
expire_below: Epoch,
|
||||||
resolve_symlinks: bool,
|
resolve_symlinks: bool,
|
||||||
|
|
||||||
|
exclude_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Matcher {
|
impl<'db, 'file> Stream<'db, 'file> {
|
||||||
pub fn new() -> Matcher {
|
pub fn new(db: &'db mut Database<'file>, now: Epoch) -> Self {
|
||||||
Matcher::default()
|
// Iterate in descending order of score.
|
||||||
|
db.dirs
|
||||||
|
.sort_unstable_by_key(|dir| OrderedFloat(dir.score(now)));
|
||||||
|
let idxs = (0..db.dirs.len()).rev();
|
||||||
|
|
||||||
|
// If a directory is deleted and hasn't been used for 90 days, delete
|
||||||
|
// it from the database.
|
||||||
|
let expire_below = now.saturating_sub(90 * 24 * 60 * 60);
|
||||||
|
|
||||||
|
Stream {
|
||||||
|
db,
|
||||||
|
idxs,
|
||||||
|
keywords: Vec::new(),
|
||||||
|
check_exists: false,
|
||||||
|
expire_below,
|
||||||
|
resolve_symlinks: false,
|
||||||
|
exclude_path: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_exists(mut self, resolve_symlinks: bool) -> Matcher {
|
pub fn with_exclude<S: Into<String>>(mut self, path: S) -> Self {
|
||||||
|
self.exclude_path = Some(path.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_exists(mut self, resolve_symlinks: bool) -> Self {
|
||||||
self.check_exists = true;
|
self.check_exists = true;
|
||||||
self.resolve_symlinks = resolve_symlinks;
|
self.resolve_symlinks = resolve_symlinks;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_keywords<S: AsRef<str>>(mut self, keywords: &[S]) -> Matcher {
|
pub fn with_keywords<S: AsRef<str>>(mut self, keywords: &[S]) -> Self {
|
||||||
self.keywords = keywords.iter().map(util::to_lowercase).collect();
|
self.keywords = keywords.iter().map(util::to_lowercase).collect();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn matches<S: AsRef<str>>(&self, path: S) -> bool {
|
pub fn next(&mut self) -> Option<&Dir<'file>> {
|
||||||
self.matches_keywords(&path) && self.matches_exists(path)
|
while let Some(idx) = self.idxs.next() {
|
||||||
|
let dir = &self.db.dirs[idx];
|
||||||
|
|
||||||
|
if !self.matches_keywords(&dir.path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.matches_exists(&dir.path) {
|
||||||
|
if dir.last_accessed < self.expire_below {
|
||||||
|
self.db.dirs.swap_remove(idx);
|
||||||
|
self.db.modified = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if Some(dir.path.as_ref()) == self.exclude_path.as_deref() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir = &self.db.dirs[idx];
|
||||||
|
return Some(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn matches_exists<S: AsRef<str>>(&self, path: S) -> bool {
|
fn matches_exists<S: AsRef<str>>(&self, path: S) -> bool {
|
||||||
|
|
@ -77,7 +133,9 @@ impl Matcher {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::Matcher;
|
use super::Database;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn query() {
|
fn query() {
|
||||||
|
|
@ -103,9 +161,16 @@ mod tests {
|
||||||
(&["/foo/", "/bar"], "/foo/baz/bar", true),
|
(&["/foo/", "/bar"], "/foo/baz/bar", true),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let mut db = Database {
|
||||||
|
dirs: Vec::new().into(),
|
||||||
|
modified: false,
|
||||||
|
data_dir: &PathBuf::new(),
|
||||||
|
};
|
||||||
|
let now = 0;
|
||||||
|
|
||||||
for &(keywords, path, is_match) in CASES {
|
for &(keywords, path, is_match) in CASES {
|
||||||
let matcher = Matcher::new().with_keywords(keywords);
|
let stream = db.stream(now).with_keywords(keywords);
|
||||||
assert_eq!(is_match, matcher.matches(path))
|
assert_eq!(is_match, stream.matches_keywords(path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue