diff --git a/src/db/dir.rs b/src/db/dir.rs index ec4e2b0..d0d37a4 100644 --- a/src/db/dir.rs +++ b/src/db/dir.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::util::{DAY, HOUR, WEEK}; #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Dir<'a> { +pub struct DirV4<'a> { #[serde(borrow)] pub path: Cow<'a, str>, pub rank: Rank, @@ -15,12 +15,25 @@ pub struct Dir<'a> { pub aliases: Vec>, } -impl Dir<'_> { - pub fn display(&self) -> DirDisplay<'_> { - DirDisplay::new(self) +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DirV3<'a> { + #[serde(borrow)] + pub path: Cow<'a, str>, + pub rank: Rank, + pub last_accessed: Epoch, +} + +pub trait Dir<'a> { + fn path(&self) -> &str; + fn score(&self, now: Epoch) -> Rank; +} + +impl Dir<'_> for DirV4<'_> { + fn path(&self) -> &str { + &self.path } - pub fn score(&self, now: Epoch) -> Rank { + fn score(&self, now: Epoch) -> Rank { // The older the entry, the lesser its importance. let duration = now.saturating_sub(self.last_accessed); if duration < HOUR { @@ -35,14 +48,40 @@ impl Dir<'_> { } } -pub struct DirDisplay<'a> { - dir: &'a Dir<'a>, +impl DirV4<'_> { + pub fn display(&self) -> DirDisplay<'_, Self> { + DirDisplay::new(self) + } +} + +impl Dir<'_> for DirV3<'_> { + fn path(&self) -> &str { + &self.path + } + + fn score(&self, now: Epoch) -> Rank { + // The older the entry, the lesser its importance. + let duration = now.saturating_sub(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 + } + } +} + +pub struct DirDisplay<'a, T: Dir<'a>> { + dir: &'a T, now: Option, separator: char, } -impl<'a> DirDisplay<'a> { - fn new(dir: &'a Dir) -> Self { +impl<'a, T: Dir<'a>> DirDisplay<'a, T> { + fn new(dir: &'a T) -> Self { Self { dir, separator: ' ', now: None } } @@ -57,13 +96,13 @@ impl<'a> DirDisplay<'a> { } } -impl Display for DirDisplay<'_> { +impl<'a, T: Dir<'a>> Display for DirDisplay<'a, T> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { if let Some(now) = self.now { let score = self.dir.score(now).clamp(0.0, 9999.0); write!(f, "{score:>6.1}{}", self.separator)?; } - write!(f, "{}", self.dir.path) + write!(f, "{}", self.dir.path()) } } diff --git a/src/db/mod.rs b/src/db/mod.rs index c650bee..dab0cf0 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -8,7 +8,8 @@ use anyhow::{Context, Result, bail}; use bincode::Options; use ouroboros::self_referencing; -pub use crate::db::dir::{Dir, Epoch, Rank}; +use crate::db::dir::{Dir, DirV3}; +pub use crate::db::dir::{DirV4, Epoch, Rank}; pub use crate::db::stream::{Stream, StreamOptions}; use crate::{config, util}; @@ -18,12 +19,13 @@ pub struct Database { bytes: Vec, #[borrows(bytes)] #[covariant] - pub dirs: Vec>, + pub dirs: Vec>, dirty: bool, } impl Database { - const VERSION: u32 = 3; + const PREV_VERSION: u32 = 3; + const VERSION: u32 = 4; pub fn open() -> Result { let data_dir = config::data_dir()?; @@ -82,7 +84,7 @@ impl Database { None => { let aliases = if let Some(alias) = alias { vec![alias.into().into()] } else { Vec::new() }; - dirs.push(Dir { + dirs.push(DirV4 { path: path.into().into(), rank: by.max(0.0), last_accessed: now, @@ -107,7 +109,7 @@ impl Database { self.with_dirs_mut(|dirs| { let aliases = if let Some(alias) = alias { vec![alias.into().into()] } else { Vec::new() }; - dirs.push(Dir { path: path.into().into(), rank, last_accessed: now, aliases }) + dirs.push(DirV4 { path: path.into().into(), rank, last_accessed: now, aliases }) }); self.with_dirty_mut(|dirty| *dirty = true); } @@ -132,7 +134,7 @@ impl Database { None => { let aliases = if let Some(alias) = alias { vec![alias.into().into()] } else { Vec::new() }; - dirs.push(Dir { + dirs.push(DirV4 { path: path.into().into(), rank: by.max(0.0), last_accessed: now, @@ -215,7 +217,7 @@ impl Database { pub fn sort_by_score(&mut self, now: Epoch) { self.with_dirs_mut(|dirs| { - dirs.sort_unstable_by(|dir1: &Dir, dir2: &Dir| { + dirs.sort_unstable_by(|dir1: &DirV4, dir2: &DirV4| { dir1.score(now).total_cmp(&dir2.score(now)) }) }); @@ -226,11 +228,11 @@ impl Database { *self.borrow_dirty() } - pub fn dirs(&self) -> &[Dir<'_>] { + pub fn dirs(&self) -> &[DirV4<'_>] { self.borrow_dirs() } - fn serialize(dirs: &[Dir<'_>]) -> Result> { + fn serialize(dirs: &[DirV4<'_>]) -> Result> { (|| -> bincode::Result<_> { // Preallocate buffer with combined size of sections. let buffer_size = @@ -246,7 +248,7 @@ impl Database { .context("could not serialize database") } - fn deserialize(bytes: &[u8]) -> Result>> { + fn deserialize(bytes: &[u8]) -> Result>> { // Assume a maximum size for the database. This prevents bincode from throwing // strange errors when it encounters invalid data. const MAX_SIZE: u64 = 32 << 20; // 32 MiB @@ -265,8 +267,27 @@ impl Database { Self::VERSION => { deserializer.deserialize(bytes_dirs).context("could not deserialize database")? } + Self::PREV_VERSION => { + let old_dirs = deserializer + .deserialize::>(bytes_dirs) + .context("could not deserialize v3 database")?; + + old_dirs + .into_iter() + .map(|dir: DirV3| DirV4 { + path: dir.path, + rank: dir.rank, + last_accessed: dir.last_accessed, + aliases: Vec::new(), + }) + .collect() + } version => { - bail!("unsupported version (got {version}, supports {})", Self::VERSION) + bail!( + "unsupported version (got {version}, supports {}, {})", + Self::VERSION, + Self::PREV_VERSION + ) } }; diff --git a/src/db/stream.rs b/src/db/stream.rs index b2a2447..62d2efc 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -6,7 +6,7 @@ use std::{fs, path}; use glob::Pattern; -use crate::db::{Database, Dir, Epoch}; +use crate::db::{Database, DirV4, Epoch}; use crate::util::{self, MONTH}; pub struct Stream<'a> { @@ -22,7 +22,7 @@ impl<'a> Stream<'a> { Stream { db, idxs, options } } - pub fn next(&mut self) -> Option<&Dir<'_>> { + pub fn next(&mut self) -> Option<&DirV4<'_>> { while let Some(idx) = self.idxs.next() { let dir = &self.db.dirs()[idx]; diff --git a/src/import.rs b/src/import.rs index 2e468c8..cd976d3 100644 --- a/src/import.rs +++ b/src/import.rs @@ -18,7 +18,7 @@ use std::path::PathBuf; use anyhow::Result; use crate::config; -use crate::db::{Database, Dir}; +use crate::db::{Database, DirV4}; pub(crate) trait Importer { /// Yields directory entries to be imported. @@ -26,7 +26,7 @@ pub(crate) trait Importer { /// The outer `Result` reports failure to fetch the input (e.g. missing /// file, subprocess errored). The per-item `Result` reports a malformed /// row, which doesn't necessarily abort the whole import. - fn dirs(&self) -> Result, ImportError>>>; + fn dirs(&self) -> Result, ImportError>>>; } /// A single record that failed to import. diff --git a/src/import/atuin.rs b/src/import/atuin.rs index 8535ec9..7596013 100644 --- a/src/import/atuin.rs +++ b/src/import/atuin.rs @@ -5,14 +5,14 @@ use std::str; use anyhow::{Context, Result, anyhow}; -use crate::db::{Dir, Epoch}; +use crate::db::{DirV4, Epoch}; use crate::import::{ImportError, Importer}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct Atuin {} impl Importer for Atuin { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { // atuin renders `{time}` as `YYYY-MM-DD HH:MM:SS` in UTC. let mut child = Command::new("atuin") .args(["history", "list", "--format={time}\t{directory}", "--print0"]) @@ -46,7 +46,7 @@ impl Iter { ImportError { path: None, line_num: self.line_num, source } } - fn parse_line(&self, line: &[u8]) -> Result, ImportError> { + fn parse_line(&self, line: &[u8]) -> Result, ImportError> { let line = str::from_utf8(line).map_err(|e| self.err(anyhow!(e).context("invalid utf-8")))?; @@ -60,7 +60,7 @@ impl Iter { .assume_utc() .unix_timestamp(); - let dir = Dir { + let dir = DirV4 { path: Cow::Owned(path.to_string()), rank: 1.0, last_accessed: timestamp as Epoch, @@ -71,7 +71,7 @@ impl Iter { } impl Iterator for Iter { - type Item = Result, ImportError>; + type Item = Result, ImportError>; fn next(&mut self) -> Option { loop { diff --git a/src/import/autojump.rs b/src/import/autojump.rs index 2ff95f0..06ba392 100644 --- a/src/import/autojump.rs +++ b/src/import/autojump.rs @@ -6,14 +6,14 @@ use std::{env, str}; use anyhow::{Context, Result, anyhow}; -use crate::db::Dir; +use crate::db::DirV4; use crate::import::{ImportError, Importer}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct Autojump {} impl Importer for Autojump { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { let path = data_path()?; let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; let reader = BufReader::new(file); @@ -37,7 +37,7 @@ impl Iter { ImportError { path: Some(self.path.clone()), line_num: self.line_num, source } } - fn parse_line(&self, line: &[u8]) -> Result, ImportError> { + fn parse_line(&self, line: &[u8]) -> Result, ImportError> { let line = str::from_utf8(line).map_err(|e| self.err(anyhow!(e).context("invalid utf-8")))?; @@ -52,12 +52,17 @@ impl Iter { // take a while to normalize. let rank = sigmoid(rank); - Ok(Dir { path: Cow::Owned(path.to_string()), rank, last_accessed: 0, aliases: Vec::new() }) + Ok(DirV4 { + path: Cow::Owned(path.to_string()), + rank, + last_accessed: 0, + aliases: Vec::new(), + }) } } impl Iterator for Iter { - type Item = Result, ImportError>; + type Item = Result, ImportError>; fn next(&mut self) -> Option { loop { diff --git a/src/import/fasd.rs b/src/import/fasd.rs index ab0788d..1dba739 100644 --- a/src/import/fasd.rs +++ b/src/import/fasd.rs @@ -5,14 +5,14 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use crate::db::Dir; +use crate::db::DirV4; use crate::import::{ImportError, Importer, z}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct Fasd {} impl Importer for Fasd { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { let path = data_path()?; let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; let reader = BufReader::new(file); diff --git a/src/import/z.rs b/src/import/z.rs index de24238..d43bcaf 100644 --- a/src/import/z.rs +++ b/src/import/z.rs @@ -6,14 +6,14 @@ use std::{env, str}; use anyhow::{Context, Result, anyhow}; -use crate::db::Dir; +use crate::db::DirV4; use crate::import::{ImportError, Importer}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct Z {} impl Importer for Z { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { let path = data_path()?; let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; let reader = BufReader::new(file); @@ -37,7 +37,7 @@ impl Iter { ImportError { path: Some(self.path.clone()), line_num: self.line_num, source } } - fn parse_line(&self, line: &[u8]) -> Result, ImportError> { + fn parse_line(&self, line: &[u8]) -> Result, ImportError> { let line = str::from_utf8(line).map_err(|e| self.err(anyhow!(e).context("invalid utf-8")))?; let err = || self.err(anyhow!("invalid entry: {line}")); @@ -54,12 +54,12 @@ impl Iter { let path = split.next().ok_or_else(err)?; - Ok(Dir { path: Cow::Owned(path.to_string()), rank, last_accessed, aliases: Vec::new() }) + Ok(DirV4 { path: Cow::Owned(path.to_string()), rank, last_accessed, aliases: Vec::new() }) } } impl Iterator for Iter { - type Item = Result, ImportError>; + type Item = Result, ImportError>; fn next(&mut self) -> Option { loop { diff --git a/src/import/z_lua.rs b/src/import/z_lua.rs index 0ea4584..364c9db 100644 --- a/src/import/z_lua.rs +++ b/src/import/z_lua.rs @@ -6,14 +6,14 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use crate::db::Dir; +use crate::db::DirV4; use crate::import::{ImportError, Importer, z}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct ZLua {} impl Importer for ZLua { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { let path = data_path()?; let err = match File::open(&path) { Ok(file) => return Ok(z::Iter::new(BufReader::new(file), path)), diff --git a/src/import/zsh_z.rs b/src/import/zsh_z.rs index 652faf8..7d3474c 100644 --- a/src/import/zsh_z.rs +++ b/src/import/zsh_z.rs @@ -5,14 +5,14 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use crate::db::Dir; +use crate::db::DirV4; use crate::import::{ImportError, Importer, z}; #[derive(clap::Args, Clone, Debug)] pub(crate) struct ZshZ {} impl Importer for ZshZ { - fn dirs(&self) -> Result, ImportError>>> { + fn dirs(&self) -> Result, ImportError>>> { let path = data_path()?; let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?; let reader = BufReader::new(file); diff --git a/src/util.rs b/src/util.rs index 4c0a27c..d26d4a8 100644 --- a/src/util.rs +++ b/src/util.rs @@ -10,7 +10,7 @@ use std::{env, mem}; use anyhow::anyhow; use anyhow::{Context, Result, bail}; -use crate::db::{Dir, Epoch}; +use crate::db::{DirV4, Epoch}; use crate::error::SilentExit; pub const SECOND: Epoch = 1; @@ -121,7 +121,7 @@ impl Fzf { pub struct FzfChild(Child); impl FzfChild { - pub fn write(&mut self, dir: &Dir, now: Epoch) -> Result> { + pub fn write(&mut self, dir: &DirV4, now: Epoch) -> Result> { let handle = self.0.stdin.as_mut().unwrap(); match write!(handle, "{}\0", dir.display().with_score(now).with_separator('\t')) { Ok(()) => Ok(None),