mirror of
https://github.com/Llewellynvdm/zoxide.git
synced 2024-12-29 12:32:40 +00:00
Add aging algorithm
This commit is contained in:
parent
641446d291
commit
784ed10aad
@ -1 +1,36 @@
|
|||||||
pub const DB_PATH: &str = "_ZO_DBPATH";
|
use crate::error::AppError;
|
||||||
|
use crate::types::Rank;
|
||||||
|
use failure::{bail, ResultExt};
|
||||||
|
use std::env;
|
||||||
|
use std::ffi::OsString;
|
||||||
|
|
||||||
|
pub const ZO_DATA: &str = "_ZO_DATA";
|
||||||
|
pub const ZO_MAXAGE: &str = "_ZO_MAXAGE";
|
||||||
|
|
||||||
|
pub fn get_zo_data() -> Result<OsString, failure::Error> {
|
||||||
|
let path = match env::var_os(ZO_DATA) {
|
||||||
|
Some(path) => path,
|
||||||
|
None => {
|
||||||
|
let mut path = dirs::home_dir().ok_or_else(|| AppError::GetHomeDirError)?;
|
||||||
|
path.push(".zo");
|
||||||
|
path.into_os_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_zo_maxage() -> Result<Rank, failure::Error> {
|
||||||
|
if let Some(maxage_osstr) = env::var_os(ZO_MAXAGE) {
|
||||||
|
match maxage_osstr.to_str() {
|
||||||
|
Some(maxage_str) => {
|
||||||
|
let maxage = maxage_str
|
||||||
|
.parse::<Rank>()
|
||||||
|
.with_context(|_| AppError::EnvError(ZO_MAXAGE.to_owned()))?;
|
||||||
|
Ok(maxage)
|
||||||
|
}
|
||||||
|
None => bail!(AppError::EnvError(ZO_MAXAGE.to_owned())),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(5000.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
29
src/db.rs
29
src/db.rs
@ -1,13 +1,14 @@
|
|||||||
|
use crate::config::get_zo_maxage;
|
||||||
use crate::dir::Dir;
|
use crate::dir::Dir;
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use crate::types::Timestamp;
|
use crate::types::{Rank, Timestamp};
|
||||||
use failure::ResultExt;
|
use failure::ResultExt;
|
||||||
|
use fs2::FileExt;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{self, BufReader, BufWriter};
|
use std::io::{self, BufReader, BufWriter};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use fs2::FileExt;
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||||
pub struct DB {
|
pub struct DB {
|
||||||
@ -21,7 +22,8 @@ impl DB {
|
|||||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<DB, failure::Error> {
|
pub fn open<P: AsRef<Path>>(path: P) -> Result<DB, failure::Error> {
|
||||||
match File::open(path) {
|
match File::open(path) {
|
||||||
Ok(file) => {
|
Ok(file) => {
|
||||||
file.lock_shared().with_context(|_| AppError::FileLockError)?;
|
file.lock_shared()
|
||||||
|
.with_context(|_| AppError::FileLockError)?;
|
||||||
let rd = BufReader::new(file);
|
let rd = BufReader::new(file);
|
||||||
let db = DB::read_from(rd).with_context(|_| AppError::DBReadError)?;
|
let db = DB::read_from(rd).with_context(|_| AppError::DBReadError)?;
|
||||||
Ok(db)
|
Ok(db)
|
||||||
@ -36,7 +38,8 @@ impl DB {
|
|||||||
pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<(), failure::Error> {
|
pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<(), failure::Error> {
|
||||||
if self.modified {
|
if self.modified {
|
||||||
let file = File::create(path).with_context(|_| AppError::FileOpenError)?;
|
let file = File::create(path).with_context(|_| AppError::FileOpenError)?;
|
||||||
file.lock_exclusive().with_context(|_| AppError::FileLockError)?;
|
file.lock_exclusive()
|
||||||
|
.with_context(|_| AppError::FileLockError)?;
|
||||||
let wr = BufWriter::new(file);
|
let wr = BufWriter::new(file);
|
||||||
self.write_into(wr)
|
self.write_into(wr)
|
||||||
.with_context(|_| AppError::DBWriteError)?;
|
.with_context(|_| AppError::DBWriteError)?;
|
||||||
@ -64,15 +67,27 @@ impl DB {
|
|||||||
None => self.dirs.push(Dir {
|
None => self.dirs.push(Dir {
|
||||||
path: path_str.to_owned(),
|
path: path_str.to_owned(),
|
||||||
last_accessed: now,
|
last_accessed: now,
|
||||||
rank: 1,
|
rank: 1.0,
|
||||||
}),
|
}),
|
||||||
Some(dir) => {
|
Some(dir) => {
|
||||||
dir.last_accessed = now;
|
dir.last_accessed = now;
|
||||||
dir.rank += 1;
|
dir.rank += 1.0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let max_age = get_zo_maxage()?;
|
||||||
|
let sum_age = self.dirs.iter().map(|dir| dir.rank).sum::<Rank>();
|
||||||
|
|
||||||
|
if sum_age > max_age {
|
||||||
|
let factor = max_age / sum_age;
|
||||||
|
for dir in &mut self.dirs {
|
||||||
|
dir.rank *= factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dirs.retain(|dir| dir.rank >= 1.0);
|
||||||
self.modified = true;
|
self.modified = true;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +100,7 @@ impl DB {
|
|||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(_, dir)| dir.is_match(keywords))
|
.filter(|(_, dir)| dir.is_match(keywords))
|
||||||
.max_by_key(|(_, dir)| dir.get_frecency(now))?;
|
.max_by_key(|(_, dir)| dir.get_frecency(now) as i64)?;
|
||||||
|
|
||||||
if dir.is_dir() {
|
if dir.is_dir() {
|
||||||
return Some(dir.to_owned());
|
return Some(dir.to_owned());
|
||||||
|
@ -49,13 +49,13 @@ impl Dir {
|
|||||||
|
|
||||||
let duration = now - self.last_accessed;
|
let duration = now - self.last_accessed;
|
||||||
if duration < HOUR {
|
if duration < HOUR {
|
||||||
self.rank * 4
|
self.rank * 4.0
|
||||||
} else if duration < DAY {
|
} else if duration < DAY {
|
||||||
self.rank * 2
|
self.rank * 2.0
|
||||||
} else if duration < WEEK {
|
} else if duration < WEEK {
|
||||||
self.rank / 2
|
self.rank / 2.0
|
||||||
} else {
|
} else {
|
||||||
self.rank / 4
|
self.rank / 4.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,4 +29,7 @@ pub enum AppError {
|
|||||||
GetCurrentDirError,
|
GetCurrentDirError,
|
||||||
#[fail(display = "could not access path")]
|
#[fail(display = "could not access path")]
|
||||||
PathAccessError,
|
PathAccessError,
|
||||||
|
|
||||||
|
#[fail(display = "could not decode ${} in env", 0)]
|
||||||
|
EnvError(String),
|
||||||
}
|
}
|
||||||
|
49
src/main.rs
49
src/main.rs
@ -5,45 +5,50 @@ mod error;
|
|||||||
mod types;
|
mod types;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
use crate::config::get_zo_data;
|
||||||
use crate::db::DB;
|
use crate::db::DB;
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use crate::types::Timestamp;
|
use crate::types::Timestamp;
|
||||||
use crate::util::{fzf_helper, get_current_time, get_db_path, process_query};
|
use crate::util::{fzf_helper, get_current_time};
|
||||||
use clap::{app_from_crate, crate_authors, crate_description, crate_name, crate_version};
|
use clap::{app_from_crate, crate_authors, crate_description, crate_name, crate_version};
|
||||||
use clap::{App, Arg, SubCommand};
|
use clap::{App, Arg, SubCommand};
|
||||||
use failure::ResultExt;
|
use failure::ResultExt;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
fn zoxide_query(
|
fn zoxide_query(db: &mut DB, mut keywords: Vec<String>, now: Timestamp) -> Option<String> {
|
||||||
db: &mut DB,
|
if let [path] = keywords.as_slice() {
|
||||||
keywords: &[String],
|
if Path::new(path).is_dir() {
|
||||||
now: Timestamp,
|
return Some(path.to_owned());
|
||||||
) -> Result<Option<String>, failure::Error> {
|
|
||||||
if let [path] = keywords {
|
|
||||||
if Path::new(&path).is_dir() {
|
|
||||||
return Ok(Some(path.to_owned()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(dir) = db.query(keywords, now) {
|
for keyword in &mut keywords {
|
||||||
return Ok(Some(dir.path));
|
keyword.make_ascii_lowercase();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
if let Some(dir) = db.query(&keywords, now) {
|
||||||
|
return Some(dir.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn zoxide_query_interactive(
|
fn zoxide_query_interactive(
|
||||||
db: &mut DB,
|
db: &mut DB,
|
||||||
keywords: &[String],
|
mut keywords: Vec<String>,
|
||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
) -> Result<Option<String>, failure::Error> {
|
) -> Result<Option<String>, failure::Error> {
|
||||||
db.remove_invalid();
|
db.remove_invalid();
|
||||||
|
|
||||||
|
for keyword in &mut keywords {
|
||||||
|
keyword.make_ascii_lowercase();
|
||||||
|
}
|
||||||
|
|
||||||
let dirs = db
|
let dirs = db
|
||||||
.dirs
|
.dirs
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|dir| dir.is_match(keywords))
|
.filter(|dir| dir.is_match(&keywords))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@ -85,17 +90,25 @@ fn zoxide_app() -> App<'static, 'static> {
|
|||||||
fn zoxide() -> Result<(), failure::Error> {
|
fn zoxide() -> Result<(), failure::Error> {
|
||||||
let matches = zoxide_app().get_matches();
|
let matches = zoxide_app().get_matches();
|
||||||
|
|
||||||
let db_path = get_db_path()?;
|
let db_path = get_zo_data()?;
|
||||||
let mut db = DB::open(&db_path)?;
|
let mut db = DB::open(&db_path)?;
|
||||||
|
|
||||||
if let Some(matches) = matches.subcommand_matches("query") {
|
if let Some(matches) = matches.subcommand_matches("query") {
|
||||||
let now = get_current_time()?;
|
let now = get_current_time()?;
|
||||||
let keywords = process_query(matches.values_of("KEYWORD").unwrap_or_default());
|
|
||||||
|
let keywords = matches
|
||||||
|
.values_of_os("KEYWORD")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.map(|keyword| match keyword.to_str() {
|
||||||
|
Some(keyword) => Ok(keyword.to_owned()),
|
||||||
|
None => Err(AppError::UnicodeError),
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<String>, _>>()?;
|
||||||
|
|
||||||
let path_opt = if matches.is_present("interactive") {
|
let path_opt = if matches.is_present("interactive") {
|
||||||
zoxide_query_interactive(&mut db, &keywords, now)
|
zoxide_query_interactive(&mut db, keywords, now)
|
||||||
} else {
|
} else {
|
||||||
zoxide_query(&mut db, &keywords, now)
|
Ok(zoxide_query(&mut db, keywords, now))
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
if let Some(path) = path_opt {
|
if let Some(path) = path_opt {
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
// TODO: convert these to newtypes
|
// TODO: convert these to newtypes
|
||||||
pub use i32 as Rank;
|
pub use f64 as Rank;
|
||||||
pub use i64 as Timestamp; // use a signed integer so subtraction can be performed on it
|
pub use i64 as Timestamp; // use a signed integer so subtraction can be performed on it
|
||||||
|
34
src/util.rs
34
src/util.rs
@ -1,10 +1,7 @@
|
|||||||
use crate::config::DB_PATH;
|
|
||||||
use crate::dir::Dir;
|
use crate::dir::Dir;
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use crate::types::Timestamp;
|
use crate::types::Timestamp;
|
||||||
use failure::ResultExt;
|
use failure::ResultExt;
|
||||||
use std::env;
|
|
||||||
use std::ffi::OsString;
|
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
@ -18,22 +15,6 @@ pub fn get_current_time() -> Result<Timestamp, failure::Error> {
|
|||||||
Ok(current_time as Timestamp)
|
Ok(current_time as Timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_db_path() -> Result<OsString, failure::Error> {
|
|
||||||
let path = match env::var_os(DB_PATH) {
|
|
||||||
Some(path) => path,
|
|
||||||
None => {
|
|
||||||
let mut path = dirs::home_dir().ok_or_else(|| AppError::GetHomeDirError)?;
|
|
||||||
path.push(".zo");
|
|
||||||
path.into_os_string()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn process_query<'a, I: Iterator<Item = &'a str>>(keywords: I) -> Vec<String> {
|
|
||||||
keywords.map(|keyword| keyword.to_ascii_lowercase()).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fzf_helper(now: Timestamp, mut dirs: Vec<Dir>) -> Result<Option<String>, failure::Error> {
|
pub fn fzf_helper(now: Timestamp, mut dirs: Vec<Dir>) -> Result<Option<String>, failure::Error> {
|
||||||
let fzf = Command::new("fzf")
|
let fzf = Command::new("fzf")
|
||||||
.arg("-n2..")
|
.arg("-n2..")
|
||||||
@ -48,16 +29,17 @@ pub fn fzf_helper(now: Timestamp, mut dirs: Vec<Dir>) -> Result<Option<String>,
|
|||||||
dir.rank = dir.get_frecency(now);
|
dir.rank = dir.get_frecency(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
dirs.sort_by_key(|dir| std::cmp::Reverse(dir.rank));
|
dirs.sort_by_key(|dir| std::cmp::Reverse(dir.rank as i64));
|
||||||
|
|
||||||
for dir in dirs.iter() {
|
for dir in dirs.iter() {
|
||||||
// ensure that frecency fits in 4 characters
|
// ensure that frecency fits in 4 characters
|
||||||
let mut frecency = dir.rank;
|
let frecency = if dir.rank > 9999.0 {
|
||||||
if frecency < 0 {
|
9999
|
||||||
frecency = 0;
|
} else if dir.rank > 0.0 {
|
||||||
} else if frecency > 9999 {
|
dir.rank as i32
|
||||||
frecency = 9999;
|
} else {
|
||||||
}
|
0
|
||||||
|
};
|
||||||
|
|
||||||
writeln!(fzf_stdin, "{:>4} {}", frecency, dir.path)
|
writeln!(fzf_stdin, "{:>4} {}", frecency, dir.path)
|
||||||
.with_context(|_| AppError::FzfIoError)?;
|
.with_context(|_| AppError::FzfIoError)?;
|
||||||
|
Loading…
Reference in New Issue
Block a user