Add alias backwards compatibility for v3 databases

This commit is contained in:
Tahaa-Dev 2026-07-15 21:45:29 +03:00 committed by Taha Mahmoud
parent 336d621969
commit 18d238a463
11 changed files with 114 additions and 49 deletions

View File

@ -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<Cow<'a, str>>,
}
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<Epoch>,
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())
}
}

View File

@ -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<u8>,
#[borrows(bytes)]
#[covariant]
pub dirs: Vec<Dir<'this>>,
pub dirs: Vec<DirV4<'this>>,
dirty: bool,
}
impl Database {
const VERSION: u32 = 3;
const PREV_VERSION: u32 = 3;
const VERSION: u32 = 4;
pub fn open() -> Result<Self> {
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<Vec<u8>> {
fn serialize(dirs: &[DirV4<'_>]) -> Result<Vec<u8>> {
(|| -> 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<Vec<Dir<'_>>> {
fn deserialize(bytes: &[u8]) -> Result<Vec<DirV4<'_>>> {
// 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::<Vec<DirV3>>(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
)
}
};

View File

@ -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];

View File

@ -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<impl Iterator<Item = Result<Dir<'static>, ImportError>>>;
fn dirs(&self) -> Result<impl Iterator<Item = Result<DirV4<'static>, ImportError>>>;
}
/// A single record that failed to import.

View File

@ -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<impl Iterator<Item = Result<Dir<'static>, ImportError>>> {
fn dirs(&self) -> Result<impl Iterator<Item = Result<DirV4<'static>, 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<Dir<'static>, ImportError> {
fn parse_line(&self, line: &[u8]) -> Result<DirV4<'static>, 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<Dir<'static>, ImportError>;
type Item = Result<DirV4<'static>, ImportError>;
fn next(&mut self) -> Option<Self::Item> {
loop {

View File

@ -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<impl Iterator<Item = Result<Dir<'static>, ImportError>>> {
fn dirs(&self) -> Result<impl Iterator<Item = Result<DirV4<'static>, 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<R: BufRead> Iter<R> {
ImportError { path: Some(self.path.clone()), line_num: self.line_num, source }
}
fn parse_line(&self, line: &[u8]) -> Result<Dir<'static>, ImportError> {
fn parse_line(&self, line: &[u8]) -> Result<DirV4<'static>, ImportError> {
let line =
str::from_utf8(line).map_err(|e| self.err(anyhow!(e).context("invalid utf-8")))?;
@ -52,12 +52,17 @@ impl<R: BufRead> Iter<R> {
// 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<R: BufRead> Iterator for Iter<R> {
type Item = Result<Dir<'static>, ImportError>;
type Item = Result<DirV4<'static>, ImportError>;
fn next(&mut self) -> Option<Self::Item> {
loop {

View File

@ -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<impl Iterator<Item = Result<Dir<'static>, ImportError>>> {
fn dirs(&self) -> Result<impl Iterator<Item = Result<DirV4<'static>, ImportError>>> {
let path = data_path()?;
let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?;
let reader = BufReader::new(file);

View File

@ -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<impl Iterator<Item = Result<Dir<'static>, ImportError>>> {
fn dirs(&self) -> Result<impl Iterator<Item = Result<DirV4<'static>, 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<R: BufRead> Iter<R> {
ImportError { path: Some(self.path.clone()), line_num: self.line_num, source }
}
fn parse_line(&self, line: &[u8]) -> Result<Dir<'static>, ImportError> {
fn parse_line(&self, line: &[u8]) -> Result<DirV4<'static>, 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<R: BufRead> Iter<R> {
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<R: BufRead> Iterator for Iter<R> {
type Item = Result<Dir<'static>, ImportError>;
type Item = Result<DirV4<'static>, ImportError>;
fn next(&mut self) -> Option<Self::Item> {
loop {

View File

@ -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<impl Iterator<Item = Result<Dir<'static>, ImportError>>> {
fn dirs(&self) -> Result<impl Iterator<Item = Result<DirV4<'static>, ImportError>>> {
let path = data_path()?;
let err = match File::open(&path) {
Ok(file) => return Ok(z::Iter::new(BufReader::new(file), path)),

View File

@ -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<impl Iterator<Item = Result<Dir<'static>, ImportError>>> {
fn dirs(&self) -> Result<impl Iterator<Item = Result<DirV4<'static>, ImportError>>> {
let path = data_path()?;
let file = File::open(&path).with_context(|| format!("could not read {path:?}"))?;
let reader = BufReader::new(file);

View File

@ -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<Option<String>> {
pub fn write(&mut self, dir: &DirV4, now: Epoch) -> Result<Option<String>> {
let handle = self.0.stdin.as_mut().unwrap();
match write!(handle, "{}\0", dir.display().with_score(now).with_separator('\t')) {
Ok(()) => Ok(None),