Use ouroboros
This commit is contained in:
parent
8da87f0881
commit
22e862b4a5
|
|
@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- `edit` subcommand to adjust the scores of entries.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Zsh: completions clashing with `zsh-autocomplete`.
|
||||
|
|
@ -20,6 +24,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Fzf: `<TAB>` now cycles through completions.
|
||||
- Fzf: enable colors in preview when possible on macOS / BSD.
|
||||
|
||||
### Removed
|
||||
|
||||
- `remove` subcommand: use `edit` instead.
|
||||
|
||||
## [0.8.3] - 2022-09-02
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "Inflector"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.19"
|
||||
|
|
@ -11,6 +17,12 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aliasable"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.66"
|
||||
|
|
@ -411,6 +423,29 @@ version = "6.3.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9"
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca"
|
||||
dependencies = [
|
||||
"aliasable",
|
||||
"ouroboros_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros_macro"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
version = "2.1.1"
|
||||
|
|
@ -806,6 +841,7 @@ dependencies = [
|
|||
"fastrand",
|
||||
"glob",
|
||||
"nix",
|
||||
"ouroboros",
|
||||
"rstest",
|
||||
"rstest_reuse",
|
||||
"serde",
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ nix = { version = "0.25.0", default-features = false, features = [
|
|||
"fs",
|
||||
"user",
|
||||
] }
|
||||
ouroboros = "0.15.5"
|
||||
rstest = { version = "0.15.0", default-features = false }
|
||||
rstest_reuse = "0.4.0"
|
||||
serde = { version = "1.0.116", features = ["derive"] }
|
||||
|
|
@ -51,6 +52,7 @@ dirs.workspace = true
|
|||
dunce.workspace = true
|
||||
fastrand.workspace = true
|
||||
glob.workspace = true
|
||||
ouroboros.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
|
|
|
|||
|
|
@ -134,8 +134,6 @@ _arguments "${_arguments_options[@]}" \
|
|||
;;
|
||||
(remove)
|
||||
_arguments "${_arguments_options[@]}" \
|
||||
'-i[Use interactive selection]' \
|
||||
'--interactive[Use interactive selection]' \
|
||||
'-h[Print help information]' \
|
||||
'--help[Print help information]' \
|
||||
'-V[Print version information]' \
|
||||
|
|
|
|||
|
|
@ -114,8 +114,6 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock {
|
|||
break
|
||||
}
|
||||
'zoxide;remove' {
|
||||
[CompletionResult]::new('-i', 'i', [CompletionResultType]::ParameterName, 'Use interactive selection')
|
||||
[CompletionResult]::new('--interactive', 'interactive', [CompletionResultType]::ParameterName, 'Use interactive selection')
|
||||
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information')
|
||||
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information')
|
||||
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ _zoxide() {
|
|||
return 0
|
||||
;;
|
||||
zoxide__remove)
|
||||
opts="-i -h -V --interactive --help --version [PATHS]..."
|
||||
opts="-h -V --help --version [PATHS]..."
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
|
|
|
|||
|
|
@ -101,8 +101,6 @@ set edit:completion:arg-completer[zoxide] = {|@words|
|
|||
cand --version 'Print version information'
|
||||
}
|
||||
&'zoxide;remove'= {
|
||||
cand -i 'Use interactive selection'
|
||||
cand --interactive 'Use interactive selection'
|
||||
cand -h 'Print help information'
|
||||
cand --help 'Print help information'
|
||||
cand -V 'Print version information'
|
||||
|
|
|
|||
|
|
@ -38,6 +38,5 @@ complete -c zoxide -n "__fish_seen_subcommand_from query" -s l -l list -d 'List
|
|||
complete -c zoxide -n "__fish_seen_subcommand_from query" -s s -l score -d 'Print score with results'
|
||||
complete -c zoxide -n "__fish_seen_subcommand_from query" -s h -l help -d 'Print help information'
|
||||
complete -c zoxide -n "__fish_seen_subcommand_from query" -s V -l version -d 'Print version information'
|
||||
complete -c zoxide -n "__fish_seen_subcommand_from remove" -s i -l interactive -d 'Use interactive selection'
|
||||
complete -c zoxide -n "__fish_seen_subcommand_from remove" -s h -l help -d 'Print help information'
|
||||
complete -c zoxide -n "__fish_seen_subcommand_from remove" -s V -l version -d 'Print version information'
|
||||
|
|
|
|||
|
|
@ -243,10 +243,6 @@ const completion: Fig.Spec = {
|
|||
name: "remove",
|
||||
description: "Remove a directory from the database",
|
||||
options: [
|
||||
{
|
||||
name: ["-i", "--interactive"],
|
||||
description: "Use interactive selection",
|
||||
},
|
||||
{
|
||||
name: ["-h", "--help"],
|
||||
description: "Print help information",
|
||||
|
|
|
|||
|
|
@ -141,9 +141,6 @@ pub struct Query {
|
|||
/// Remove a directory from the database
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Remove {
|
||||
/// Use interactive selection
|
||||
#[clap(long, short)]
|
||||
pub interactive: bool,
|
||||
#[clap(value_hint = ValueHint::DirPath)]
|
||||
pub paths: Vec<String>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,76 +4,61 @@ use std::process::Command;
|
|||
use anyhow::Result;
|
||||
|
||||
use crate::cmd::{Edit, EditCommand, Run};
|
||||
use crate::db::{Database, DatabaseFile, Epoch};
|
||||
use crate::{config, util};
|
||||
use crate::store::{Epoch, Store};
|
||||
use crate::util;
|
||||
|
||||
impl Run for Edit {
|
||||
fn run(&self) -> Result<()> {
|
||||
let now = util::current_time()?;
|
||||
|
||||
let data_dir = config::data_dir()?;
|
||||
let mut db = DatabaseFile::new(data_dir);
|
||||
let db = &mut db.open()?;
|
||||
let db = &mut Store::open()?;
|
||||
|
||||
match &self.cmd {
|
||||
Some(EditCommand::Decrement { path }) => {
|
||||
if let Some(dir) = db.dirs.iter_mut().find(|dir| &dir.path == path) {
|
||||
dir.rank = (dir.rank - 1.0).max(0.0);
|
||||
}
|
||||
db.modified = true;
|
||||
db.increment(path, -1.0, now);
|
||||
db.save()?;
|
||||
print_dirs(db, now);
|
||||
}
|
||||
Some(EditCommand::Delete { path }) => {
|
||||
if let Some(idx) = db.dirs.iter().position(|dir| &dir.path == path) {
|
||||
db.dirs.remove(idx);
|
||||
}
|
||||
db.modified = true;
|
||||
db.remove(path);
|
||||
db.save()?;
|
||||
print_dirs(db, now);
|
||||
}
|
||||
Some(EditCommand::Increment { path }) => {
|
||||
if let Some(dir) = db.dirs.iter_mut().find(|dir| &dir.path == path) {
|
||||
dir.rank += 1.0;
|
||||
}
|
||||
db.modified = true;
|
||||
db.increment(path, 1.0, now);
|
||||
db.save()?;
|
||||
print_dirs(db, now);
|
||||
}
|
||||
Some(EditCommand::Reload) => print_dirs(db, now),
|
||||
None => {
|
||||
db.dirs.sort_unstable_by(|dir1, dir2| dir2.score(now).total_cmp(&dir1.score(now)));
|
||||
db.modified = true;
|
||||
db.sort_by_score(now);
|
||||
db.save()?;
|
||||
|
||||
let mut fzf = Command::new("fzf");
|
||||
fzf.args([
|
||||
"--bind=\
|
||||
ctrl-r:reload(zoxide edit reload),\
|
||||
ctrl-w:reload(zoxide edit delete {2..}),\
|
||||
ctrl-a:reload(zoxide edit increment {2..}),\
|
||||
ctrl-d:reload(zoxide edit decrement {2..}),\
|
||||
double-click:ignore,\
|
||||
enter:abort",
|
||||
"--header=\
|
||||
ctrl-r:reload ctrl-w:delete
|
||||
ctrl-a:increment ctrl-d:decrement
|
||||
|
||||
SCORE PATH",
|
||||
//
|
||||
// Search mode
|
||||
"--delimiter=\\x00 ",
|
||||
"--nth=2",
|
||||
// Search result
|
||||
"--no-sort",
|
||||
// Interface
|
||||
"--bind=\
|
||||
ctrl-r:reload(zoxide edit reload),\
|
||||
ctrl-w:reload(zoxide edit delete {2..}),\
|
||||
ctrl-a:reload(zoxide edit increment {2..}),\
|
||||
ctrl-d:reload(zoxide edit decrement {2..}),\
|
||||
ctrl-z:ignore,\
|
||||
double-click:ignore,\
|
||||
enter:abort",
|
||||
"--cycle",
|
||||
"--keep-right",
|
||||
// Layout
|
||||
"--header=\
|
||||
ctrl-r:reload ctrl-w:delete
|
||||
ctrl-a:increment ctrl-d:decrement
|
||||
|
||||
SCORE PATH",
|
||||
"--info=inline",
|
||||
"--layout=reverse",
|
||||
// Key/Event bindings
|
||||
"--bind=ctrl-z:ignore",
|
||||
])
|
||||
.envs([("FZF_DEFAULT_COMMAND", "zoxide edit reload")]);
|
||||
|
||||
|
|
@ -86,9 +71,9 @@ SCORE PATH",
|
|||
}
|
||||
}
|
||||
|
||||
fn print_dirs(db: &Database, now: Epoch) {
|
||||
fn print_dirs(db: &Store, now: Epoch) {
|
||||
let stdout = &mut io::stdout().lock();
|
||||
for dir in db.dirs.iter() {
|
||||
for dir in db.dirs().iter().rev() {
|
||||
writeln!(stdout, "{:>5}\x00 {}", dir.score(now), &dir.path).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
use std::io::{self, Write};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use crate::cmd::{Remove, Run};
|
||||
use crate::config;
|
||||
use crate::db::DatabaseFile;
|
||||
use crate::util::{self, Fzf};
|
||||
use crate::{config, util};
|
||||
|
||||
impl Run for Remove {
|
||||
fn run(&self) -> Result<()> {
|
||||
|
|
@ -13,38 +10,13 @@ impl Run for Remove {
|
|||
let mut db = DatabaseFile::new(data_dir);
|
||||
let mut db = db.open()?;
|
||||
|
||||
if self.interactive {
|
||||
let keywords = &self.paths;
|
||||
let now = util::current_time()?;
|
||||
let mut stream = db.stream(now).with_keywords(keywords);
|
||||
|
||||
let mut fzf = Fzf::new(true)?;
|
||||
let stdin = fzf.stdin();
|
||||
|
||||
let selection = loop {
|
||||
let Some(dir) = stream.next() else { break fzf.select()? };
|
||||
match writeln!(stdin, "{}", dir.display_score(now)) {
|
||||
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => break fzf.select()?,
|
||||
result => result.context("could not write to fzf")?,
|
||||
}
|
||||
};
|
||||
|
||||
let paths = selection.lines().filter_map(|line| line.get(5..));
|
||||
for path in paths {
|
||||
if !db.remove(path) {
|
||||
for path in &self.paths {
|
||||
if !db.remove(path) {
|
||||
let path_abs = util::resolve_path(path)?;
|
||||
let path_abs = util::path_to_str(&path_abs)?;
|
||||
if path_abs == path || !db.remove(path_abs) {
|
||||
db.modified = false;
|
||||
bail!("path not found in database: {path}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for path in &self.paths {
|
||||
if !db.remove(path) {
|
||||
let path_abs = util::resolve_path(path)?;
|
||||
let path_abs = util::path_to_str(&path_abs)?;
|
||||
if path_abs == path || !db.remove(path_abs) {
|
||||
db.modified = false;
|
||||
bail!("path not found in database: {path}")
|
||||
}
|
||||
bail!("path not found in database: {path}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ mod config;
|
|||
mod db;
|
||||
mod error;
|
||||
mod shell;
|
||||
mod store;
|
||||
mod util;
|
||||
|
||||
use std::env;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
use std::{fs, io};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use bincode::Options;
|
||||
use ouroboros::self_referencing;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{config, util};
|
||||
|
||||
#[self_referencing]
|
||||
pub struct Store {
|
||||
path: PathBuf,
|
||||
bytes: Vec<u8>,
|
||||
#[borrows(bytes)]
|
||||
#[covariant]
|
||||
dirs: Vec<Dir<'this>>,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
const VERSION: u32 = 3;
|
||||
|
||||
pub fn open() -> Result<Self> {
|
||||
let data_dir = config::data_dir()?;
|
||||
let path = data_dir.join("db.zo");
|
||||
|
||||
match fs::read(&path) {
|
||||
Ok(bytes) => Self::try_new(path, bytes, |bytes| Self::deserialize(bytes), false),
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
// Create data directory, but don't create any file yet. The file will be created
|
||||
// later by [`Database::save`] if any data is modified.
|
||||
fs::create_dir_all(&data_dir)
|
||||
.with_context(|| format!("unable to create data directory: {}", data_dir.display()))?;
|
||||
Ok(Self::new(data_dir, Vec::new(), |_| Vec::new(), false))
|
||||
}
|
||||
Err(e) => Err(e).with_context(|| format!("could not read from database: {}", path.display())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&mut self) -> Result<()> {
|
||||
// Only write to disk if the database is modified.
|
||||
if !self.borrow_dirty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let bytes = Self::serialize(self.borrow_dirs())?;
|
||||
util::write(self.borrow_path(), &bytes).context("could not write to database")?;
|
||||
self.with_dirty_mut(|dirty| *dirty = false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Increments the rank of a directory, or creates it if it does not exist.
|
||||
pub fn increment(&mut self, path: impl AsRef<str> + Into<String>, by: Rank, now: Epoch) {
|
||||
self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) {
|
||||
Some(dir) => dir.rank = (dir.rank + by).max(0.0),
|
||||
None => dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now }),
|
||||
});
|
||||
self.with_dirty_mut(|dirty| *dirty = true);
|
||||
}
|
||||
|
||||
/// Increments the rank and updates the last_accessed of a directory, or
|
||||
/// creates it if it does not exist.
|
||||
pub fn increment_update(&mut self, path: impl AsRef<str> + Into<String>, by: Rank, now: Epoch) {
|
||||
self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) {
|
||||
Some(dir) => {
|
||||
dir.rank = (dir.rank + by).max(0.0);
|
||||
dir.last_accessed = now;
|
||||
}
|
||||
None => dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now }),
|
||||
});
|
||||
self.with_dirty_mut(|dirty| *dirty = true);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, path: impl AsRef<str>) -> bool {
|
||||
let deleted = self.with_dirs_mut(|dirs| match dirs.iter().position(|dir| dir.path == path.as_ref()) {
|
||||
Some(idx) => {
|
||||
dirs.swap_remove(idx);
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
});
|
||||
self.with_dirty_mut(|dirty| *dirty |= deleted);
|
||||
deleted
|
||||
}
|
||||
|
||||
pub fn age(&mut self, max_age: Rank) {
|
||||
let mut dirty = false;
|
||||
self.with_dirs_mut(|dirs| {
|
||||
let total_age = dirs.iter().map(|dir| dir.rank).sum::<Rank>();
|
||||
if total_age > max_age {
|
||||
let factor = 0.9 * max_age / total_age;
|
||||
for idx in (0..dirs.len()).rev() {
|
||||
let dir = &mut dirs[idx];
|
||||
dir.rank *= factor;
|
||||
if dir.rank < 1.0 {
|
||||
dirs.swap_remove(idx);
|
||||
}
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
});
|
||||
self.with_dirty_mut(|dirty_prev| *dirty_prev |= dirty);
|
||||
}
|
||||
|
||||
pub fn dedup(&mut self) {
|
||||
// Sort by path, so that equal paths are next to each other.
|
||||
self.sort_by_path();
|
||||
|
||||
let mut dirty = false;
|
||||
self.with_dirs_mut(|dirs| {
|
||||
for idx in (1..dirs.len()).rev() {
|
||||
// Check if curr_dir and next_dir have equal paths.
|
||||
let curr_dir = &dirs[idx];
|
||||
let next_dir = &dirs[idx - 1];
|
||||
if next_dir.path != curr_dir.path {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Merge curr_dir's rank and last_accessed into next_dir.
|
||||
let rank = curr_dir.rank;
|
||||
let last_accessed = curr_dir.last_accessed;
|
||||
let next_dir = &mut dirs[idx - 1];
|
||||
next_dir.last_accessed = next_dir.last_accessed.max(last_accessed);
|
||||
next_dir.rank += rank;
|
||||
|
||||
// Delete curr_dir.
|
||||
dirs.swap_remove(idx);
|
||||
dirty = true;
|
||||
}
|
||||
});
|
||||
self.with_dirty_mut(|dirty_prev| *dirty_prev |= dirty);
|
||||
}
|
||||
|
||||
pub fn sort_by_path(&mut self) {
|
||||
self.with_dirs_mut(|dirs| dirs.sort_unstable_by(|dir1, dir2| dir1.path.cmp(&dir2.path)));
|
||||
self.with_dirty_mut(|dirty| *dirty = true);
|
||||
}
|
||||
|
||||
pub fn sort_by_score(&mut self, now: Epoch) {
|
||||
self.with_dirs_mut(|dirs| {
|
||||
dirs.sort_unstable_by(|dir1: &Dir, dir2: &Dir| dir1.score(now).total_cmp(&dir2.score(now)))
|
||||
});
|
||||
self.with_dirty_mut(|dirty| *dirty = true);
|
||||
}
|
||||
|
||||
pub fn dirs(&self) -> &[Dir] {
|
||||
self.borrow_dirs()
|
||||
}
|
||||
|
||||
fn serialize(dirs: &[Dir<'_>]) -> Result<Vec<u8>> {
|
||||
(|| -> bincode::Result<_> {
|
||||
// Preallocate buffer with combined size of sections.
|
||||
let buffer_size = bincode::serialized_size(&Self::VERSION)? + bincode::serialized_size(&dirs)?;
|
||||
let mut buffer = Vec::with_capacity(buffer_size as usize);
|
||||
|
||||
// Serialize sections into buffer.
|
||||
bincode::serialize_into(&mut buffer, &Self::VERSION)?;
|
||||
bincode::serialize_into(&mut buffer, &dirs)?;
|
||||
|
||||
Ok(buffer)
|
||||
})()
|
||||
.context("could not serialize database")
|
||||
}
|
||||
|
||||
fn deserialize(bytes: &[u8]) -> Result<Vec<Dir>> {
|
||||
// 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
|
||||
let deserializer = &mut bincode::options().with_fixint_encoding().with_limit(MAX_SIZE);
|
||||
|
||||
// Split bytes into sections.
|
||||
let version_size = deserializer.serialized_size(&Self::VERSION).unwrap() as _;
|
||||
if bytes.len() < version_size {
|
||||
bail!("could not deserialize database: corrupted data");
|
||||
}
|
||||
let (bytes_version, bytes_dirs) = bytes.split_at(version_size);
|
||||
|
||||
// Deserialize sections.
|
||||
let version = deserializer.deserialize(bytes_version)?;
|
||||
let dirs = match version {
|
||||
Self::VERSION => deserializer.deserialize(bytes_dirs).context("could not deserialize database")?,
|
||||
version => {
|
||||
bail!("unsupported version (got {version}, supports {})", Self::VERSION)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(dirs)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Dir<'a> {
|
||||
#[serde(borrow)]
|
||||
pub path: Cow<'a, str>,
|
||||
pub rank: Rank,
|
||||
pub last_accessed: Epoch,
|
||||
}
|
||||
|
||||
impl Dir<'_> {
|
||||
pub fn score(&self, now: Epoch) -> Rank {
|
||||
const HOUR: Epoch = 60 * 60;
|
||||
const DAY: Epoch = 24 * HOUR;
|
||||
const WEEK: Epoch = 7 * DAY;
|
||||
|
||||
// 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 type Rank = f64;
|
||||
pub type Epoch = u64;
|
||||
|
|
@ -161,7 +161,9 @@ fn tmpfile<P: AsRef<Path>>(dir: P) -> Result<(File, PathBuf)> {
|
|||
match OpenOptions::new().write(true).create_new(true).open(&path) {
|
||||
Ok(file) => break Ok((file, path)),
|
||||
Err(e) if e.kind() == io::ErrorKind::AlreadyExists && attempts < MAX_ATTEMPTS => (),
|
||||
Err(e) => break Err(e).with_context(|| format!("could not create file: {}", path.display())),
|
||||
Err(e) => {
|
||||
break Err(e).with_context(|| format!("could not create file: {}", path.display()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue