mirror of
https://github.com/Llewellynvdm/zoxide.git
synced 2024-12-28 20:12:38 +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::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());
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
|
49
src/main.rs
49
src/main.rs
@ -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 {
|
||||
|
@ -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
|
||||
|
34
src/util.rs
34
src/util.rs
@ -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)?;
|
||||
|
Loading…
Reference in New Issue
Block a user