Add aging algorithm

This commit is contained in:
Ajeet D'Souza 2020-03-06 23:13:32 +05:30
parent 641446d291
commit 784ed10aad
7 changed files with 105 additions and 57 deletions

View File

@ -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)
}
}

View File

@ -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());

View File

@ -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
} }
} }
} }

View File

@ -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),
} }

View File

@ -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 {

View File

@ -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

View File

@ -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)?;