diff --git a/src/config.rs b/src/config.rs index df4c5e6..e3b6b5c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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 { + 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 { + if let Some(maxage_osstr) = env::var_os(ZO_MAXAGE) { + match maxage_osstr.to_str() { + Some(maxage_str) => { + let maxage = maxage_str + .parse::() + .with_context(|_| AppError::EnvError(ZO_MAXAGE.to_owned()))?; + Ok(maxage) + } + None => bail!(AppError::EnvError(ZO_MAXAGE.to_owned())), + } + } else { + Ok(5000.0) + } +} diff --git a/src/db.rs b/src/db.rs index 1eff26d..6ef2eac 100644 --- a/src/db.rs +++ b/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>(path: P) -> Result { 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>(&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::(); + + 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()); diff --git a/src/dir.rs b/src/dir.rs index cf6696e..bfcac92 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -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 } } } diff --git a/src/error.rs b/src/error.rs index 4e9030e..572a973 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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), } diff --git a/src/main.rs b/src/main.rs index 3e277c9..972656f 100644 --- a/src/main.rs +++ b/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, 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, now: Timestamp) -> Option { + 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, now: Timestamp, ) -> Result, 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::, _>>()?; 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 { diff --git a/src/types.rs b/src/types.rs index e5ac0e5..de81a2b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -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 diff --git a/src/util.rs b/src/util.rs index cb71625..bdaf19a 100644 --- a/src/util.rs +++ b/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 { Ok(current_time as Timestamp) } -pub fn get_db_path() -> Result { - 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>(keywords: I) -> Vec { - keywords.map(|keyword| keyword.to_ascii_lowercase()).collect() -} - pub fn fzf_helper(now: Timestamp, mut dirs: Vec) -> Result, failure::Error> { let fzf = Command::new("fzf") .arg("-n2..") @@ -48,16 +29,17 @@ pub fn fzf_helper(now: Timestamp, mut dirs: Vec) -> Result, 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)?;