From 208a6a9eb8179cdd600bc3137188fc417099e36a Mon Sep 17 00:00:00 2001 From: Ajeet D'Souza <98ajeet@gmail.com> Date: Sat, 16 May 2020 17:31:11 +0530 Subject: [PATCH] Convert paths to String --- Cargo.lock | 59 +++----- Cargo.toml | 3 - src/config.rs | 10 +- src/db.rs | 223 +++++++++-------------------- src/dir.rs | 106 -------------- src/fzf.rs | 88 ++++++++++++ src/main.rs | 4 +- src/subcommand/add.rs | 74 ++++++++-- src/subcommand/import.rs | 76 +++++++++- src/subcommand/init/shell/posix.rs | 4 +- src/subcommand/query.rs | 139 +++++++++--------- src/subcommand/remove.rs | 86 +++++++++-- src/util.rs | 124 +--------------- 13 files changed, 459 insertions(+), 537 deletions(-) delete mode 100644 src/dir.rs create mode 100644 src/fzf.rs diff --git a/Cargo.lock b/Cargo.lock index f5ac3fe..dedf602 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,16 +67,6 @@ dependencies = [ "constant_time_eq 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "bstr" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-automata 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "byteorder" version = "1.3.4" @@ -182,11 +172,6 @@ name = "libc" version = "0.2.70" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "memchr" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "ppv-lite86" version = "0.2.6" @@ -198,9 +183,9 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro-error-attr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.21 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.22 (registry+https://github.com/rust-lang/crates.io-index)", "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -209,16 +194,16 @@ name = "proc-macro-error-attr" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.21 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.22 (registry+https://github.com/rust-lang/crates.io-index)", "syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "proc-macro2" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -229,7 +214,7 @@ name = "quote" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -284,14 +269,6 @@ dependencies = [ "rust-argon2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "regex-automata" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "rust-argon2" version = "0.7.0" @@ -316,9 +293,9 @@ name = "serde_derive" version = "1.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.21 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.22 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -343,17 +320,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro-error 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.21 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.22 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "syn" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -363,9 +340,9 @@ name = "syn-mid" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.21 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.22 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -439,7 +416,6 @@ version = "0.4.0" dependencies = [ "anyhow 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", "bincode 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "bstr 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "dunce 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -460,7 +436,6 @@ dependencies = [ "checksum bincode 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5753e2a71534719bf3f4e57006c3a4f0d2c672a4b676eec84161f763eca87dbf" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" -"checksum bstr 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "31accafdb70df7871592c058eca3985b71104e15ac32f64706022c58867da931" "checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" @@ -475,11 +450,10 @@ dependencies = [ "checksum hermit-abi 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "61565ff7aaace3525556587bd2dc31d4a07071957be715e63ce7b1eccf51a8f4" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)" = "3baa92041a6fec78c687fa0cc2b3fae8884f743d672cf551bed1d6dac6988d0f" -"checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" "checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" "checksum proc-macro-error 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678" "checksum proc-macro-error-attr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53" -"checksum proc-macro2 1.0.12 (registry+https://github.com/rust-lang/crates.io-index)" = "8872cf6f48eee44265156c111456a700ab3483686b3f96df4cf5481c89157319" +"checksum proc-macro2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "53f5ffe53a6b28e37c9c1ce74893477864d64f74778a93a4beb43c8fa167f639" "checksum quote 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "42934bc9c8ab0d3b273a16d8551c8f0fcff46be73276ca083ec2414c15c4ba5e" "checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" "checksum rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" @@ -487,14 +461,13 @@ dependencies = [ "checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" "checksum redox_users 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" -"checksum regex-automata 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" "checksum rust-argon2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" "checksum serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)" = "99e7b308464d16b56eba9964e4972a3eee817760ab60d88c3f86e1fecb08204c" "checksum serde_derive 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)" = "818fbf6bfa9a42d3bfcaca148547aa00c7b915bec71d1757aa2d44ca68771984" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum structopt 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "863246aaf5ddd0d6928dfeb1a9ca65f505599e4e1b399935ef7e75107516b4ef" "checksum structopt-derive 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "d239ca4b13aee7a2142e6795cbd69e457665ff8037aed33b3effdc430d2f927a" -"checksum syn 1.0.21 (registry+https://github.com/rust-lang/crates.io-index)" = "4696caa4048ac7ce2bcd2e484b3cef88c1004e41b8e945a277e2c25dc0b72060" +"checksum syn 1.0.22 (registry+https://github.com/rust-lang/crates.io-index)" = "1425de3c33b0941002740a420b1a906a350b88d08b82b2c8a01035a3f9447bac" "checksum syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" diff --git a/Cargo.toml b/Cargo.toml index e47927a..c142831 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,9 +23,6 @@ serde = { version = "1.0.106", features = ["derive"] } structopt = "0.3.12" uuid = { version = "0.8.1", features = ["v4"] } -[target.'cfg(unix)'.dependencies] -bstr = "0.2.12" - [profile.release] codegen-units = 1 lto = "fat" diff --git a/src/config.rs b/src/config.rs index fa71d19..cb52977 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use crate::dir::Rank; +use crate::db::Rank; use anyhow::{bail, Context, Result}; @@ -36,13 +36,13 @@ pub fn zo_maxage() -> Result { match env::var_os("_ZO_MAXAGE") { Some(maxage_osstr) => match maxage_osstr.to_str() { Some(maxage_str) => { - let maxage = maxage_str - .parse::() - .context("unable to parse _ZO_MAXAGE as integer")?; + let maxage = maxage_str.parse::().with_context(|| { + format!("unable to parse _ZO_MAXAGE as integer: {}", maxage_str) + })?; Ok(maxage as Rank) } - None => bail!("invalid Unicode in _ZO_MAXAGE"), + None => bail!("invalid utf-8 sequence in _ZO_MAXAGE"), }, None => Ok(1000.0), } diff --git a/src/db.rs b/src/db.rs index ffbc941..9950983 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,15 +1,11 @@ -use crate::dir::{Dir, Epoch, Rank}; - use anyhow::{bail, Context, Result}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use std::fs::{self, File, OpenOptions}; -use std::io::{self, BufRead, BufReader, Write}; +use std::fs::{self, OpenOptions}; +use std::io::{self, Write}; use std::path::{Path, PathBuf}; -pub use i32 as DBVersion; - #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] struct DbVersion(u32); @@ -157,158 +153,6 @@ impl Db { Ok(()) } - pub fn import>(&mut self, path: P, merge: bool) -> Result<()> { - if !self.dirs.is_empty() && !merge { - bail!( - "To prevent conflicts, you can only import from z with an empty zoxide database!\n\ - If you wish to merge the two, specify the `--merge` flag." - ); - } - - let z_db_file = File::open(path).context("could not open z database file")?; - let reader = BufReader::new(z_db_file); - - for (idx, read_line) in reader.lines().enumerate() { - let line_number = idx + 1; - let line = if let Ok(line) = read_line { - line - } else { - eprintln!( - "could not read entry at line {}: {:?}", - line_number, read_line - ); - continue; - }; - - let split_line = line.rsplitn(3, '|').collect::>(); - - match split_line.as_slice() { - [epoch_str, rank_str, path_str] => { - let epoch = match epoch_str.parse::() { - Ok(epoch) => epoch, - Err(e) => { - eprintln!( - "invalid epoch '{}' at line {}: {}", - epoch_str, line_number, e - ); - continue; - } - }; - let rank = match rank_str.parse::() { - Ok(rank) => rank, - Err(e) => { - eprintln!("invalid rank '{}' at line {}: {}", rank_str, line_number, e); - continue; - } - }; - let path_abs = match dunce::canonicalize(path_str) { - Ok(path) => path, - Err(e) => { - eprintln!("invalid path '{}' at line {}: {}", path_str, line_number, e); - continue; - } - }; - - if merge { - // If the path exists in the database, add the ranks and set the epoch to - // the largest of the parsed epoch and the already present epoch. - if let Some(dir) = self.dirs.iter_mut().find(|dir| dir.path == path_abs) { - dir.rank += rank; - dir.last_accessed = Epoch::max(epoch, dir.last_accessed); - - continue; - }; - } - - self.dirs.push(Dir { - path: path_abs, - rank, - last_accessed: epoch, - }); - } - [] | [""] => {} // ignore blank lines - line => { - eprintln!("invalid entry at line {}: {:?}", line_number, line); - continue; - } - }; - } - - self.modified = true; - - Ok(()) - } - - pub fn add>(&mut self, path: P, max_age: Rank, now: Epoch) -> Result<()> { - let path_abs = dunce::canonicalize(&path) - .with_context(|| format!("could not access directory: {}", path.as_ref().display()))?; - - match self.dirs.iter_mut().find(|dir| dir.path == path_abs) { - None => self.dirs.push(Dir { - path: path_abs, - last_accessed: now, - rank: 1.0, - }), - Some(dir) => { - dir.last_accessed = now; - dir.rank += 1.0; - } - }; - - let sum_age = self.dirs.iter().map(|dir| dir.rank).sum::(); - - if sum_age > max_age { - let factor = 0.9 * 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(()) - } - - pub fn query_many<'a>(&'a mut self, keywords: &'a [String]) -> impl Iterator { - self.query_all() - .iter() - .filter(move |dir| dir.is_match(keywords)) - } - - pub fn query_all(&mut self) -> &[Dir] { - let orig_len = self.dirs.len(); - self.dirs.retain(Dir::is_valid); - - if orig_len != self.dirs.len() { - self.modified = true; - } - - self.dirs.as_slice() - } - - pub fn remove>(&mut self, path: P) -> Result<()> { - if let Ok(path_abs) = dunce::canonicalize(&path) { - self.remove_exact(path_abs) - .or_else(|_| self.remove_exact(path)) - } else { - self.remove_exact(path) - } - } - - pub fn remove_exact>(&mut self, path: P) -> Result<()> { - if let Some(idx) = self.dirs.iter().position(|dir| dir.path == path.as_ref()) { - self.dirs.swap_remove(idx); - self.modified = true; - Ok(()) - } else { - bail!( - "could not find path in database: {}", - path.as_ref().display() - ) - } - } - fn get_path>(data_dir: P) -> PathBuf { data_dir.as_ref().join("db.zo") } @@ -326,3 +170,66 @@ impl Drop for Db { } } } + +pub type Rank = f64; +pub type Epoch = i64; // use a signed integer so subtraction can be performed on it + +#[derive(Debug, Deserialize, Serialize)] +pub struct Dir { + pub path: String, + pub rank: Rank, + pub last_accessed: Epoch, +} + +impl Dir { + pub fn is_valid(&self) -> bool { + self.rank.is_finite() && self.rank >= 1.0 && Path::new(&self.path).is_dir() + } + + pub fn is_match(&self, query: &[String]) -> bool { + let path_lower = self.path.to_lowercase(); + + if let Some(query_name) = query + .last() + .and_then(|query_last| Path::new(query_last).file_name()) + { + if let Some(dir_name) = Path::new(&path_lower).file_name() { + // unwrap is safe here because we've already handled invalid UTF-8 + let dir_name_str = dir_name.to_str().unwrap(); + let query_name_str = query_name.to_str().unwrap(); + + if !dir_name_str.contains(query_name_str) { + return false; + } + } + } + + let mut subpath = path_lower.as_str(); + + for subquery in query.iter() { + match subpath.find(subquery) { + Some(idx) => subpath = &subpath[idx + subquery.len()..], + None => return false, + } + } + + true + } + + pub fn get_frecency(&self, now: Epoch) -> Rank { + const HOUR: Epoch = 60 * 60; + const DAY: Epoch = 24 * HOUR; + const WEEK: Epoch = 7 * DAY; + + let duration = now - self.last_accessed; + if duration < HOUR { + self.rank * 4.0 + } else if duration < DAY { + self.rank * 2.0 + } else if duration < WEEK { + self.rank * 0.5 + } else { + self.rank * 0.25 + } + } +} diff --git a/src/dir.rs b/src/dir.rs deleted file mode 100644 index dc03447..0000000 --- a/src/dir.rs +++ /dev/null @@ -1,106 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use std::path::{Path, PathBuf}; - -pub use f64 as Rank; -pub use i64 as Epoch; // use a signed integer so subtraction can be performed on it - -#[derive(Debug, Deserialize, Serialize)] -pub struct Dir { - pub path: PathBuf, - pub rank: Rank, - pub last_accessed: Epoch, -} - -impl Dir { - pub fn is_valid(&self) -> bool { - self.rank.is_finite() && self.rank >= 1.0 && self.path.is_dir() - } - - #[cfg(unix)] - pub fn is_match(&self, query: &[String]) -> bool { - use bstr::ByteSlice; - - use std::ffi::OsStr; - use std::os::unix::ffi::OsStrExt; - - let path_lower = self.path.as_os_str().as_bytes().to_lowercase(); - - if let Some(query_name) = query - .last() - .and_then(|query_last| Path::new(query_last).file_name()) - { - if let Some(dir_name) = Path::new(OsStr::from_bytes(&path_lower)).file_name() { - let dir_name_bytes = dir_name.as_bytes(); - let query_name_bytes = query_name.as_bytes(); - - if !dir_name_bytes.contains_str(query_name_bytes) { - return false; - } - } - } - - let mut subpath = path_lower.as_slice(); - - for subquery in query.iter() { - let subquery_bytes = subquery.as_bytes(); - match subpath.find(subquery_bytes) { - Some(idx) => subpath = &subpath[idx + subquery_bytes.len()..], - None => return false, - } - } - - true - } - - #[cfg(not(unix))] - pub fn is_match(&self, query: &[String]) -> bool { - let path_lower = match self.path.to_str() { - Some(path_str) => path_str.to_lowercase(), - None => return false, // silently ignore invalid UTF-8 - }; - - let mut subpath = path_lower.as_str(); - - if let Some(query_name) = query - .last() - .and_then(|query_last| Path::new(query_last).file_name()) - { - if let Some(dir_name) = Path::new(&path_lower).file_name() { - // unwrap is safe here because we've already handled invalid UTF-8 - let dir_name_str = dir_name.to_str().unwrap(); - let query_name_str = query_name.to_str().unwrap(); - - if !dir_name_str.contains(query_name_str) { - return false; - } - } - } - - for subquery in query.iter() { - match subpath.find(subquery) { - Some(idx) => subpath = &subpath[idx + subquery.len()..], - None => return false, - } - } - - true - } - - pub fn get_frecency(&self, now: Epoch) -> Rank { - const HOUR: Epoch = 60 * 60; - const DAY: Epoch = 24 * HOUR; - const WEEK: Epoch = 7 * DAY; - - let duration = now - self.last_accessed; - if duration < HOUR { - self.rank * 4.0 - } else if duration < DAY { - self.rank * 2.0 - } else if duration < WEEK { - self.rank / 2.0 - } else { - self.rank / 4.0 - } - } -} diff --git a/src/fzf.rs b/src/fzf.rs new file mode 100644 index 0000000..c39a06b --- /dev/null +++ b/src/fzf.rs @@ -0,0 +1,88 @@ +use crate::db::{Dir, Epoch}; +use crate::error::SilentExit; + +use anyhow::{bail, Context, Result}; + +use std::io::Write; +use std::process::{Child, Command, Stdio}; +use std::str; + +pub struct Fzf { + child: Child, + lines: Vec, +} + +impl Fzf { + pub fn new() -> Result { + let child = Command::new("fzf") + .args(&["-n2..", "--no-sort"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .context("could not launch fzf")?; + + Ok(Fzf { + child, + lines: Vec::new(), + }) + } + + pub fn write_dir(&mut self, dir: &Dir, now: Epoch) { + let frecency = dir.get_frecency(now); + + let frecency_scaled = if frecency > 9999.0 { + 9999 + } else if frecency > 0.0 { + frecency as u32 + } else { + 0 + }; + + self.lines + .push(format!("{:>4} {}", frecency_scaled, dir.path)); + } + + pub fn wait_selection(mut self) -> Result> { + // unwrap() here is safe since we have captured `stdin` + let stdin = self.child.stdin.as_mut().unwrap(); + + self.lines.sort_unstable_by(|line1, line2| line2.cmp(line1)); + + for line in self.lines.iter() { + writeln!(stdin, "{}", line).context("could not write into fzf stdin")?; + } + + let output = self + .child + .wait_with_output() + .context("wait failed on fzf")?; + + match output.status.code() { + // normal exit + Some(0) => { + let path_bytes = output + .stdout + .get(12..output.stdout.len() - 1) + .context("fzf returned invalid output")?; + + let path_str = + str::from_utf8(path_bytes).context("invalid utf-8 sequence in fzf output")?; + + Ok(Some(path_str.to_string())) + } + + // no match + Some(1) => Ok(None), + + // error + Some(2) => bail!("fzf returned an error"), + + // terminated by a signal + Some(code @ 130) => bail!(SilentExit { code }), + Some(128..=254) | None => bail!("fzf was terminated"), + + // unknown + _ => bail!("fzf returned an unknown error"), + } + } +} diff --git a/src/main.rs b/src/main.rs index c5a4502..3650524 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,9 @@ +#![forbid(unsafe_code)] + mod config; mod db; -mod dir; mod error; +mod fzf; mod subcommand; mod util; diff --git a/src/subcommand/add.rs b/src/subcommand/add.rs index da46d38..37ce51f 100644 --- a/src/subcommand/add.rs +++ b/src/subcommand/add.rs @@ -1,16 +1,16 @@ use crate::config; +use crate::db::{Dir, Rank}; use crate::util; use anyhow::{Context, Result}; use structopt::StructOpt; use std::env; -use std::path::PathBuf; #[derive(Debug, StructOpt)] #[structopt(about = "Add a new directory or increment its rank")] pub struct Add { - path: Option, + path: Option, } impl Add { @@ -20,20 +20,66 @@ impl Add { Some(path) => path, None => { current_dir = env::current_dir().context("unable to fetch current directory")?; - ¤t_dir + current_dir.to_str().with_context(|| { + format!("invalid utf-8 sequence in path: {}", current_dir.display()) + })? } }; - let excluded_dirs = config::zo_exclude_dirs(); - if excluded_dirs.contains(path) { - return Ok(()); - } - - let mut db = util::get_db()?; - - let maxage = config::zo_maxage()?; - let now = util::get_current_time()?; - - db.add(path, maxage, now) + add(path) } } + +fn add(path: &str) -> Result<()> { + let path_abs = dunce::canonicalize(path) + .with_context(|| format!("could not resolve directory: {}", path))?; + + let exclude_dirs = config::zo_exclude_dirs(); + if exclude_dirs + .iter() + .any(|excluded_path| excluded_path == &path_abs) + { + return Ok(()); + } + + let path_abs_str = path_abs + .to_str() + .with_context(|| format!("invalid utf-8 sequence in path: {}", path_abs.display()))?; + + let mut db = util::get_db()?; + let now = util::get_current_time()?; + + let maxage = config::zo_maxage()?; + + match db.dirs.iter_mut().find(|dir| dir.path == path_abs_str) { + None => db.dirs.push(Dir { + path: path_abs_str.to_string(), + last_accessed: now, + rank: 1.0, + }), + Some(dir) => { + dir.last_accessed = now; + dir.rank += 1.0; + } + }; + + let sum_age = db.dirs.iter().map(|dir| dir.rank).sum::(); + + if sum_age > maxage { + let factor = 0.9 * maxage / sum_age; + for dir in &mut db.dirs { + dir.rank *= factor; + } + + for idx in (0..db.dirs.len()).rev() { + let dir = &db.dirs[idx]; + if dir.rank < 1.0 { + db.dirs.swap_remove(idx); + } + } + } + + db.modified = true; + + Ok(()) +} diff --git a/src/subcommand/import.rs b/src/subcommand/import.rs index 3e10191..a3f6f27 100644 --- a/src/subcommand/import.rs +++ b/src/subcommand/import.rs @@ -1,9 +1,11 @@ +use crate::db::{Db, Dir}; use crate::util; -use anyhow::Result; +use anyhow::{bail, Context, Result}; use structopt::StructOpt; -use std::path::PathBuf; +use std::fs; +use std::path::{Path, PathBuf}; #[derive(Debug, StructOpt)] #[structopt(about = "Import from z database")] @@ -16,6 +18,74 @@ pub struct Import { impl Import { pub fn run(&self) -> Result<()> { - util::get_db()?.import(&self.path, self.merge) + import(&self.path, self.merge) } } + +fn import>(path: P, merge: bool) -> Result<()> { + let mut db = util::get_db()?; + + if !db.dirs.is_empty() && !merge { + bail!( + "To prevent conflicts, you can only import from z with an empty zoxide database!\n\ + If you wish to merge the two, specify the `--merge` flag." + ); + } + + let buffer = fs::read_to_string(&path) + .with_context(|| format!("could not read z database: {}", path.as_ref().display()))?; + + for (idx, line) in buffer.lines().enumerate() { + if let Err(e) = import_line(&mut db, line) { + let line_num = idx + 1; + eprintln!("Error on line {}: {}", line_num, e); + } + } + + db.modified = true; + println!("Completed import."); + + Ok(()) +} + +fn import_line(db: &mut Db, line: &str) -> Result<()> { + let mut split_line = line.rsplitn(3, '|'); + + let (path_str, epoch_str, rank_str) = (|| { + let epoch_str = split_line.next()?; + let rank_str = split_line.next()?; + let path_str = split_line.next()?; + Some((path_str, epoch_str, rank_str)) + })() + .context("invalid entry")?; + + let epoch = epoch_str + .parse::() + .with_context(|| format!("invalid epoch: {}", epoch_str))?; + + let rank = rank_str + .parse::() + .with_context(|| format!("invalid rank: {}", rank_str))?; + + let path_abs = dunce::canonicalize(path_str) + .with_context(|| format!("could not resolve path: {}", path_str))?; + + let path_abs_str = path_abs + .to_str() + .with_context(|| format!("invalid utf-8 sequence in path: {}", path_abs.display()))?; + + // If the path exists in the database, add the ranks and set the epoch to + // the largest of the parsed epoch and the already present epoch. + if let Some(dir) = db.dirs.iter_mut().find(|dir| dir.path == path_abs_str) { + dir.rank += rank; + dir.last_accessed = epoch.max(dir.last_accessed); + } else { + db.dirs.push(Dir { + path: path_abs_str.to_string(), + rank, + last_accessed: epoch, + }); + } + + Ok(()) +} diff --git a/src/subcommand/init/shell/posix.rs b/src/subcommand/init/shell/posix.rs index 5c19f7f..7a8f90c 100644 --- a/src/subcommand/init/shell/posix.rs +++ b/src/subcommand/init/shell/posix.rs @@ -83,13 +83,13 @@ fn hook_pwd() -> Result> { let tmp_path_str = tmp_path .to_str() - .context("invalid Unicode in zoxide tmp path")?; + .context("invalid utf-8 sequence in zoxide tmp path")?; let pwd_path = tmp_path.join(format!("pwd-{}", Uuid::new_v4())); let pwd_path_str = pwd_path .to_str() - .context("invalid Unicode in zoxide pwd path")?; + .context("invalid utf-8 sequence in zoxide pwd path")?; let hook_pwd = format!( r#" diff --git a/src/subcommand/query.rs b/src/subcommand/query.rs index 49e7366..bf4cb5c 100644 --- a/src/subcommand/query.rs +++ b/src/subcommand/query.rs @@ -1,11 +1,10 @@ -use crate::db::Db; +use crate::fzf::Fzf; use crate::util; use anyhow::{bail, Result}; use float_ord::FloatOrd; use structopt::StructOpt; -use std::io::{self, Write}; use std::path::Path; #[derive(Debug, StructOpt)] @@ -19,76 +18,86 @@ pub struct Query { impl Query { pub fn run(&self) -> Result<()> { let path_opt = if self.interactive { - self.query_interactive()? + query_interactive(&self.keywords)? } else { - let mut db = util::get_db()?; - self.query(&mut db)? + query(&self.keywords)? }; match path_opt { - Some(path) => { - let stdout = io::stdout(); - let mut handle = stdout.lock(); - handle.write_all(&path).unwrap(); - handle.write_all(b"\n").unwrap(); - } + Some(path) => println!("{}", path), None => bail!("no match found"), }; Ok(()) } - - fn query(&self, db: &mut Db) -> Result>> { - // if the input is already a valid path, simply return it - if let [path] = self.keywords.as_slice() { - if Path::new(path).is_dir() { - return Ok(Some(path.as_bytes().to_vec())); - } - } - - let now = util::get_current_time()?; - - let keywords = self - .keywords - .iter() - .map(|keyword| keyword.to_lowercase()) - .collect::>(); - - db.dirs - .sort_unstable_by_key(|dir| FloatOrd(dir.get_frecency(now))); - - // Iterating in reverse order ensures that the directory indices do not - // change as we remove them. - for idx in (0..db.dirs.len()).rev() { - let dir = &db.dirs[idx]; - if !dir.is_match(&keywords) { - continue; - } - - if !dir.is_valid() { - db.dirs.swap_remove(idx); - db.modified = true; - continue; - } - - let path = util::path_to_bytes(&dir.path)?.to_vec(); - return Ok(Some(path)); - } - - Ok(None) - } - - fn query_interactive(&self) -> Result>> { - let now = util::get_current_time()?; - - let keywords = self - .keywords - .iter() - .map(|keyword| keyword.to_lowercase()) - .collect::>(); - - let mut db = util::get_db()?; - let dirs = db.query_many(&keywords); - util::fzf_helper(now, dirs) - } +} + +fn query(keywords: &[String]) -> Result> { + // if the input is already a valid path, simply return it + if let [path] = keywords { + if Path::new(path).is_dir() { + return Ok(Some(path.to_string())); + } + } + + let mut db = util::get_db()?; + let now = util::get_current_time()?; + + let keywords = keywords + .iter() + .map(|keyword| keyword.to_lowercase()) + .collect::>(); + + db.dirs + .sort_unstable_by_key(|dir| FloatOrd(dir.get_frecency(now))); + + // Iterating in reverse order ensures that the directory indices do not + // change as we remove them. + for idx in (0..db.dirs.len()).rev() { + let dir = &db.dirs[idx]; + if !dir.is_match(&keywords) { + continue; + } + + if !dir.is_valid() { + db.dirs.swap_remove(idx); + db.modified = true; + continue; + } + + let path = &dir.path; + return Ok(Some(path.to_string())); + } + + Ok(None) +} + +fn query_interactive(keywords: &[String]) -> Result> { + let keywords = keywords + .iter() + .map(|keyword| keyword.to_lowercase()) + .collect::>(); + + let mut db = util::get_db()?; + let now = util::get_current_time()?; + + let mut fzf = Fzf::new()?; + + for idx in (0..db.dirs.len()).rev() { + let dir = &db.dirs[idx]; + + if !dir.is_match(&keywords) { + continue; + } + + if !dir.is_valid() { + db.dirs.swap_remove(idx); + db.modified = true; + continue; + } + + fzf.write_dir(&dir, now); + } + + fzf.wait_selection() } diff --git a/src/subcommand/remove.rs b/src/subcommand/remove.rs index 7b20f6f..6d087c1 100644 --- a/src/subcommand/remove.rs +++ b/src/subcommand/remove.rs @@ -1,6 +1,7 @@ +use crate::fzf::Fzf; use crate::util; -use anyhow::Result; +use anyhow::{bail, Context, Result}; use structopt::StructOpt; #[derive(Debug, StructOpt)] @@ -14,28 +15,83 @@ pub struct Remove { impl Remove { pub fn run(&self) -> Result<()> { if self.interactive { - let mut db = util::get_db()?; - let dirs = db.query_many(&self.query); - let now = util::get_current_time()?; - - if let Some(path_bytes) = util::fzf_helper(now, dirs)? { - let path = util::bytes_to_path(&path_bytes)?; - db.remove_exact(path)?; - } - - Ok(()) + remove_interactive(&self.query) } else { - match self.query.as_slice() { - [path] => util::get_db()?.remove(path), - _ => clap::Error::with_description( + if let &[path] = &self.query.as_slice() { + remove(&path) + } else { + clap::Error::with_description( &format!( "remove requires 1 value in non-interactive mode, but {} were provided", self.query.len() ), clap::ErrorKind::WrongNumberOfValues, ) - .exit(), + .exit(); } } } } + +fn remove(path: &str) -> Result<()> { + let mut db = util::get_db()?; + + if let Some(idx) = db.dirs.iter().position(|dir| &dir.path == path) { + db.dirs.swap_remove(idx); + db.modified = true; + return Ok(()); + } + + let path_abs = + dunce::canonicalize(path).with_context(|| format!("could not resolve path: {}", path))?; + + let path_abs_str = path_abs + .to_str() + .with_context(|| format!("invalid utf-8 sequence in path: {}", path_abs.display()))?; + + if let Some(idx) = db.dirs.iter().position(|dir| dir.path == path_abs_str) { + db.dirs.swap_remove(idx); + db.modified = true; + return Ok(()); + } + + bail!("could not find path in database: {}", path) +} + +fn remove_interactive(keywords: &[String]) -> Result<()> { + let mut db = util::get_db()?; + let now = util::get_current_time()?; + + let keywords = keywords + .iter() + .map(|keyword| keyword.to_lowercase()) + .collect::>(); + + let mut fzf = Fzf::new()?; + + for idx in (0..db.dirs.len()).rev() { + let dir = &db.dirs[idx]; + + if !dir.is_match(&keywords) { + continue; + } + + if !dir.is_valid() { + db.dirs.swap_remove(idx); + db.modified = true; + continue; + } + + fzf.write_dir(&dir, now); + } + + if let Some(path) = fzf.wait_selection()? { + if let Some(idx) = db.dirs.iter().position(|dir| dir.path == path) { + db.dirs.swap_remove(idx); + db.modified = true; + return Ok(()); + } + } + + bail!("no match found"); +} diff --git a/src/util.rs b/src/util.rs index c760013..32e6216 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,48 +1,10 @@ use crate::config; -use crate::db::Db; -use crate::dir::{Dir, Epoch}; -use crate::error::SilentExit; +use crate::db::{Db, Epoch}; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result}; -use std::cmp::Reverse; -use std::io::{Read, Write}; -use std::path::Path; -use std::process::{Command, Stdio}; use std::time::SystemTime; -#[cfg(unix)] -pub fn path_to_bytes>(path: &P) -> Result<&[u8]> { - use std::os::unix::ffi::OsStrExt; - - Ok(path.as_ref().as_os_str().as_bytes()) -} - -#[cfg(not(unix))] -pub fn path_to_bytes>(path: &P) -> Result<&[u8]> { - match path.as_ref().to_str() { - Some(path_str) => Ok(path_str.as_bytes()), - None => bail!("invalid Unicode in path"), - } -} - -#[cfg(unix)] -pub fn bytes_to_path(bytes: &[u8]) -> Result<&Path> { - use std::ffi::OsStr; - use std::os::unix::ffi::OsStrExt; - - Ok(Path::new(OsStr::from_bytes(bytes))) -} - -#[cfg(not(unix))] -pub fn bytes_to_path(bytes: &[u8]) -> Result<&Path> { - use std::str; - - str::from_utf8(bytes) - .map(Path::new) - .context("invalid Unicode in path") -} - pub fn get_db() -> Result { let data_dir = config::zo_data_dir()?; Db::open(data_dir) @@ -56,85 +18,3 @@ pub fn get_current_time() -> Result { Ok(current_time as Epoch) } - -pub fn fzf_helper<'a, I>(now: Epoch, dirs: I) -> Result>> -where - I: IntoIterator, -{ - let mut fzf = Command::new("fzf") - .args(&["-n2..", "--no-sort"]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .context("could not launch fzf")?; - - let fzf_stdin = fzf - .stdin - .as_mut() - .context("could not connect to fzf stdin")?; - - let mut dir_frecencies = dirs - .into_iter() - .map(|dir| (dir, clamp(dir.get_frecency(now), 0.0, 9999.0) as i32)) - .collect::>(); - - dir_frecencies.sort_unstable_by_key(|&(dir, frecency)| Reverse((frecency, &dir.path))); - - for &(dir, frecency) in dir_frecencies.iter() { - if let Ok(path_bytes) = path_to_bytes(&dir.path) { - (|| { - write!(fzf_stdin, "{:>4} ", frecency)?; - fzf_stdin.write_all(path_bytes)?; - writeln!(fzf_stdin) - })() - .context("could not write into fzf stdin")?; - } - } - - let fzf_stdout = fzf - .stdout - .as_mut() - .context("could not connect to fzf stdout")?; - - let mut buffer = Vec::new(); - fzf_stdout - .read_to_end(&mut buffer) - .context("could not read from fzf stdout")?; - - let status = fzf.wait().context("wait failed on fzf")?; - match status.code() { - // normal exit - Some(0) => match buffer.get(12..buffer.len() - 1) { - Some(path) => Ok(Some(path.to_vec())), - None => bail!("fzf returned invalid output"), - }, - - // no match - Some(1) => Ok(None), - - // error - Some(2) => bail!("fzf returned an error"), - - // terminated by a signal - Some(code @ 130) => bail!(SilentExit { code }), - Some(128..=254) | None => bail!("fzf was terminated"), - - // unknown - _ => bail!("fzf returned an unknown error"), - } -} - -// FIXME: replace with f64::clamp once it is stable -#[must_use = "method returns a new number and does not mutate the original value"] -#[inline] -pub fn clamp(val: f64, min: f64, max: f64) -> f64 { - assert!(min <= max); - - if val > max { - max - } else if val > min { - val - } else { - min - } -}