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
|
||||
to the best match.
|
||||
.SH USAGE
|
||||
.nf
|
||||
\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
|
||||
.sp
|
||||
|
|
@ -17,6 +18,7 @@ to the best match.
|
|||
\fBz\fR \fI-\fR # cd into previous directory
|
||||
.sp
|
||||
\fBzi\fR \fIfoo\fR # cd with interactive selection (using fzf)
|
||||
.fi
|
||||
.SH SUBCOMMANDS
|
||||
.TP
|
||||
\fBzoxide-add\fR(1)
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ pub struct Query {
|
|||
pub score: bool,
|
||||
|
||||
/// Exclude a path from results
|
||||
#[clap(long, hidden = true)]
|
||||
#[clap(long, value_name = "path")]
|
||||
pub exclude: Option<String>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::app::{Query, Run};
|
||||
use crate::config;
|
||||
use crate::db::{DatabaseFile, Matcher};
|
||||
use crate::db::DatabaseFile;
|
||||
use crate::error::BrokenPipeHandler;
|
||||
use crate::fzf::Fzf;
|
||||
use crate::util;
|
||||
|
|
@ -16,19 +16,18 @@ impl Run for Query {
|
|||
let mut db = db.open()?;
|
||||
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 {
|
||||
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 {
|
||||
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")?;
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +48,7 @@ impl Run for Query {
|
|||
let stdout = stdout.lock();
|
||||
let mut handle = BufWriter::new(stdout);
|
||||
|
||||
for dir in matches {
|
||||
while let Some(dir) = stream.next() {
|
||||
if self.score {
|
||||
writeln!(handle, "{}", dir.display_score(now))
|
||||
} else {
|
||||
|
|
@ -59,7 +58,7 @@ impl Run for Query {
|
|||
}
|
||||
handle.flush().pipe_exit("stdout")?;
|
||||
} else {
|
||||
let dir = matches.next().context("no match found")?;
|
||||
let dir = stream.next().context("no match found")?;
|
||||
if self.score {
|
||||
writeln!(io::stdout(), "{}", dir.display_score(now))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::app::{Remove, Run};
|
||||
use crate::config;
|
||||
use crate::db::{DatabaseFile, Matcher};
|
||||
use crate::db::DatabaseFile;
|
||||
use crate::error::BrokenPipeHandler;
|
||||
use crate::fzf::Fzf;
|
||||
use crate::util;
|
||||
|
|
@ -18,11 +18,11 @@ impl Run for Remove {
|
|||
let selection;
|
||||
match &self.interactive {
|
||||
Some(keywords) => {
|
||||
let matcher = Matcher::new().with_keywords(keywords);
|
||||
let now = util::current_time()?;
|
||||
let mut stream = db.stream(now).with_keywords(keywords);
|
||||
|
||||
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")?;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,24 @@
|
|||
mod dir;
|
||||
mod query;
|
||||
mod stream;
|
||||
|
||||
pub use dir::{Dir, DirList, Epoch, Rank};
|
||||
pub use query::Matcher;
|
||||
pub use stream::Stream;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use ordered_float::OrderedFloat;
|
||||
use tempfile::{NamedTempFile, PersistError};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::cmp::Reverse;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub struct Database<'a> {
|
||||
pub dirs: DirList<'a>,
|
||||
pub struct Database<'file> {
|
||||
pub dirs: DirList<'file>,
|
||||
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<()> {
|
||||
if !self.modified {
|
||||
return Ok(());
|
||||
|
|
@ -72,12 +70,9 @@ impl<'a> Database<'a> {
|
|||
self.modified = true;
|
||||
}
|
||||
|
||||
pub fn iter<'i>(&'i mut self, m: &'i Matcher, now: Epoch) -> impl Iterator<Item = &'i Dir> {
|
||||
self.dirs
|
||||
.sort_unstable_by_key(|dir| Reverse(OrderedFloat(dir.score(now))));
|
||||
self.dirs
|
||||
.iter()
|
||||
.filter(move |dir| m.matches(dir.path.as_ref()))
|
||||
// Streaming iterator for directories.
|
||||
pub fn stream(&mut self, now: Epoch) -> Stream<'_, 'file> {
|
||||
Stream::new(self, now)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
data_dir: PathBuf,
|
||||
buffer: Vec<u8>,
|
||||
data_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl DatabaseFile {
|
||||
pub fn new<P: Into<PathBuf>>(data_dir: P) -> DatabaseFile {
|
||||
pub fn new<P: Into<PathBuf>>(data_dir: P) -> Self {
|
||||
DatabaseFile {
|
||||
data_dir: data_dir.into(),
|
||||
buffer: Vec::new(),
|
||||
data_dir: data_dir.into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +1,89 @@
|
|||
use super::{Database, Dir, Epoch};
|
||||
use crate::util;
|
||||
|
||||
use ordered_float::OrderedFloat;
|
||||
|
||||
use std::fs;
|
||||
use std::iter::Rev;
|
||||
use std::ops::Range;
|
||||
use std::path;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Matcher {
|
||||
pub struct Stream<'db, 'file> {
|
||||
db: &'db mut Database<'file>,
|
||||
idxs: Rev<Range<usize>>,
|
||||
|
||||
keywords: Vec<String>,
|
||||
|
||||
check_exists: bool,
|
||||
expire_below: Epoch,
|
||||
resolve_symlinks: bool,
|
||||
|
||||
exclude_path: Option<String>,
|
||||
}
|
||||
|
||||
impl Matcher {
|
||||
pub fn new() -> Matcher {
|
||||
Matcher::default()
|
||||
impl<'db, 'file> Stream<'db, 'file> {
|
||||
pub fn new(db: &'db mut Database<'file>, now: Epoch) -> Self {
|
||||
// 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.resolve_symlinks = resolve_symlinks;
|
||||
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
|
||||
}
|
||||
|
||||
pub fn matches<S: AsRef<str>>(&self, path: S) -> bool {
|
||||
self.matches_keywords(&path) && self.matches_exists(path)
|
||||
pub fn next(&mut self) -> Option<&Dir<'file>> {
|
||||
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 {
|
||||
|
|
@ -77,7 +133,9 @@ impl Matcher {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Matcher;
|
||||
use super::Database;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn query() {
|
||||
|
|
@ -103,9 +161,16 @@ mod tests {
|
|||
(&["/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 {
|
||||
let matcher = Matcher::new().with_keywords(keywords);
|
||||
assert_eq!(is_match, matcher.matches(path))
|
||||
let stream = db.stream(now).with_keywords(keywords);
|
||||
assert_eq!(is_match, stream.matches_keywords(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue