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::error::AppError;
use crate::types::Timestamp;
use crate::types::{Rank, Timestamp};
use failure::ResultExt;
use fs2::FileExt;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::{self, BufReader, BufWriter};
use std::io::{Read, Write};
use std::path::Path;
use fs2::FileExt;
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct DB {
@ -21,7 +22,8 @@ impl DB {
pub fn open<P: AsRef<Path>>(path: P) -> Result<DB, failure::Error> {
match File::open(path) {
Ok(file) => {
file.lock_shared().with_context(|_| AppError::FileLockError)?;
file.lock_shared()
.with_context(|_| AppError::FileLockError)?;
let rd = BufReader::new(file);
let db = DB::read_from(rd).with_context(|_| AppError::DBReadError)?;
Ok(db)
@ -36,7 +38,8 @@ impl DB {
pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<(), failure::Error> {
if self.modified {
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);
self.write_into(wr)
.with_context(|_| AppError::DBWriteError)?;
@ -64,15 +67,27 @@ impl DB {
None => self.dirs.push(Dir {
path: path_str.to_owned(),
last_accessed: now,
rank: 1,
rank: 1.0,
}),
Some(dir) => {
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;
Ok(())
}
@ -85,7 +100,7 @@ impl DB {
.iter()
.enumerate()
.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() {
return Some(dir.to_owned());

View File

@ -49,13 +49,13 @@ impl Dir {
let duration = now - self.last_accessed;
if duration < HOUR {
self.rank * 4
self.rank * 4.0
} else if duration < DAY {
self.rank * 2
self.rank * 2.0
} else if duration < WEEK {
self.rank / 2
self.rank / 2.0
} else {
self.rank / 4
self.rank / 4.0
}
}
}

View File

@ -29,4 +29,7 @@ pub enum AppError {
GetCurrentDirError,
#[fail(display = "could not access path")]
PathAccessError,
#[fail(display = "could not decode ${} in env", 0)]
EnvError(String),
}

View File

@ -5,45 +5,50 @@ mod error;
mod types;
mod util;
use crate::config::get_zo_data;
use crate::db::DB;
use crate::error::AppError;
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, Arg, SubCommand};
use failure::ResultExt;
use std::env;
use std::path::Path;
fn zoxide_query(
db: &mut DB,
keywords: &[String],
now: Timestamp,
) -> Result<Option<String>, failure::Error> {
if let [path] = keywords {
if Path::new(&path).is_dir() {
return Ok(Some(path.to_owned()));
fn zoxide_query(db: &mut DB, mut keywords: Vec<String>, now: Timestamp) -> Option<String> {
if let [path] = keywords.as_slice() {
if Path::new(path).is_dir() {
return Some(path.to_owned());
}
}
if let Some(dir) = db.query(keywords, now) {
return Ok(Some(dir.path));
for keyword in &mut keywords {
keyword.make_ascii_lowercase();
}
Ok(None)
if let Some(dir) = db.query(&keywords, now) {
return Some(dir.path);
}
None
}
fn zoxide_query_interactive(
db: &mut DB,
keywords: &[String],
mut keywords: Vec<String>,
now: Timestamp,
) -> Result<Option<String>, failure::Error> {
db.remove_invalid();
for keyword in &mut keywords {
keyword.make_ascii_lowercase();
}
let dirs = db
.dirs
.iter()
.filter(|dir| dir.is_match(keywords))
.filter(|dir| dir.is_match(&keywords))
.cloned()
.collect();
@ -85,17 +90,25 @@ fn zoxide_app() -> App<'static, 'static> {
fn zoxide() -> Result<(), failure::Error> {
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)?;
if let Some(matches) = matches.subcommand_matches("query") {
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") {
zoxide_query_interactive(&mut db, &keywords, now)
zoxide_query_interactive(&mut db, keywords, now)
} else {
zoxide_query(&mut db, &keywords, now)
Ok(zoxide_query(&mut db, keywords, now))
}?;
if let Some(path) = path_opt {

View File

@ -1,3 +1,3 @@
// 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

View File

@ -1,10 +1,7 @@
use crate::config::DB_PATH;
use crate::dir::Dir;
use crate::error::AppError;
use crate::types::Timestamp;
use failure::ResultExt;
use std::env;
use std::ffi::OsString;
use std::io::{Read, Write};
use std::process::{Command, Stdio};
use std::time::SystemTime;
@ -18,22 +15,6 @@ pub fn get_current_time() -> Result<Timestamp, failure::Error> {
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> {
let fzf = Command::new("fzf")
.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);
}
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() {
// ensure that frecency fits in 4 characters
let mut frecency = dir.rank;
if frecency < 0 {
frecency = 0;
} else if frecency > 9999 {
frecency = 9999;
}
let frecency = if dir.rank > 9999.0 {
9999
} else if dir.rank > 0.0 {
dir.rank as i32
} else {
0
};
writeln!(fzf_stdin, "{:>4} {}", frecency, dir.path)
.with_context(|_| AppError::FzfIoError)?;