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
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `edit` subcommand to adjust the scores of entries.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Zsh: completions clashing with `zsh-autocomplete`.
|
- 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: `<TAB>` now cycles through completions.
|
||||||
- Fzf: enable colors in preview when possible on macOS / BSD.
|
- Fzf: enable colors in preview when possible on macOS / BSD.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- `remove` subcommand: use `edit` instead.
|
||||||
|
|
||||||
## [0.8.3] - 2022-09-02
|
## [0.8.3] - 2022-09-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "Inflector"
|
||||||
|
version = "0.11.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "0.7.19"
|
version = "0.7.19"
|
||||||
|
|
@ -11,6 +17,12 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aliasable"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.66"
|
version = "1.0.66"
|
||||||
|
|
@ -411,6 +423,29 @@ version = "6.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9"
|
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]]
|
[[package]]
|
||||||
name = "predicates"
|
name = "predicates"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
|
|
@ -806,6 +841,7 @@ dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"glob",
|
"glob",
|
||||||
"nix",
|
"nix",
|
||||||
|
"ouroboros",
|
||||||
"rstest",
|
"rstest",
|
||||||
"rstest_reuse",
|
"rstest_reuse",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ nix = { version = "0.25.0", default-features = false, features = [
|
||||||
"fs",
|
"fs",
|
||||||
"user",
|
"user",
|
||||||
] }
|
] }
|
||||||
|
ouroboros = "0.15.5"
|
||||||
rstest = { version = "0.15.0", default-features = false }
|
rstest = { version = "0.15.0", default-features = false }
|
||||||
rstest_reuse = "0.4.0"
|
rstest_reuse = "0.4.0"
|
||||||
serde = { version = "1.0.116", features = ["derive"] }
|
serde = { version = "1.0.116", features = ["derive"] }
|
||||||
|
|
@ -51,6 +52,7 @@ dirs.workspace = true
|
||||||
dunce.workspace = true
|
dunce.workspace = true
|
||||||
fastrand.workspace = true
|
fastrand.workspace = true
|
||||||
glob.workspace = true
|
glob.workspace = true
|
||||||
|
ouroboros.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
|
|
||||||
|
|
@ -134,8 +134,6 @@ _arguments "${_arguments_options[@]}" \
|
||||||
;;
|
;;
|
||||||
(remove)
|
(remove)
|
||||||
_arguments "${_arguments_options[@]}" \
|
_arguments "${_arguments_options[@]}" \
|
||||||
'-i[Use interactive selection]' \
|
|
||||||
'--interactive[Use interactive selection]' \
|
|
||||||
'-h[Print help information]' \
|
'-h[Print help information]' \
|
||||||
'--help[Print help information]' \
|
'--help[Print help information]' \
|
||||||
'-V[Print version information]' \
|
'-V[Print version information]' \
|
||||||
|
|
|
||||||
|
|
@ -114,8 +114,6 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
'zoxide;remove' {
|
'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('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information')
|
||||||
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information')
|
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information')
|
||||||
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
|
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ _zoxide() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
zoxide__remove)
|
zoxide__remove)
|
||||||
opts="-i -h -V --interactive --help --version [PATHS]..."
|
opts="-h -V --help --version [PATHS]..."
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
|
||||||
|
|
@ -101,8 +101,6 @@ set edit:completion:arg-completer[zoxide] = {|@words|
|
||||||
cand --version 'Print version information'
|
cand --version 'Print version information'
|
||||||
}
|
}
|
||||||
&'zoxide;remove'= {
|
&'zoxide;remove'= {
|
||||||
cand -i 'Use interactive selection'
|
|
||||||
cand --interactive 'Use interactive selection'
|
|
||||||
cand -h 'Print help information'
|
cand -h 'Print help information'
|
||||||
cand --help 'Print help information'
|
cand --help 'Print help information'
|
||||||
cand -V 'Print version 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 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 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 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 h -l help -d 'Print help information'
|
||||||
complete -c zoxide -n "__fish_seen_subcommand_from remove" -s V -l version -d 'Print version 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",
|
name: "remove",
|
||||||
description: "Remove a directory from the database",
|
description: "Remove a directory from the database",
|
||||||
options: [
|
options: [
|
||||||
{
|
|
||||||
name: ["-i", "--interactive"],
|
|
||||||
description: "Use interactive selection",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: ["-h", "--help"],
|
name: ["-h", "--help"],
|
||||||
description: "Print help information",
|
description: "Print help information",
|
||||||
|
|
|
||||||
|
|
@ -141,9 +141,6 @@ pub struct Query {
|
||||||
/// Remove a directory from the database
|
/// Remove a directory from the database
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
pub struct Remove {
|
pub struct Remove {
|
||||||
/// Use interactive selection
|
|
||||||
#[clap(long, short)]
|
|
||||||
pub interactive: bool,
|
|
||||||
#[clap(value_hint = ValueHint::DirPath)]
|
#[clap(value_hint = ValueHint::DirPath)]
|
||||||
pub paths: Vec<String>,
|
pub paths: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,76 +4,61 @@ use std::process::Command;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::cmd::{Edit, EditCommand, Run};
|
use crate::cmd::{Edit, EditCommand, Run};
|
||||||
use crate::db::{Database, DatabaseFile, Epoch};
|
use crate::store::{Epoch, Store};
|
||||||
use crate::{config, util};
|
use crate::util;
|
||||||
|
|
||||||
impl Run for Edit {
|
impl Run for Edit {
|
||||||
fn run(&self) -> Result<()> {
|
fn run(&self) -> Result<()> {
|
||||||
let now = util::current_time()?;
|
let now = util::current_time()?;
|
||||||
|
let db = &mut Store::open()?;
|
||||||
let data_dir = config::data_dir()?;
|
|
||||||
let mut db = DatabaseFile::new(data_dir);
|
|
||||||
let db = &mut db.open()?;
|
|
||||||
|
|
||||||
match &self.cmd {
|
match &self.cmd {
|
||||||
Some(EditCommand::Decrement { path }) => {
|
Some(EditCommand::Decrement { path }) => {
|
||||||
if let Some(dir) = db.dirs.iter_mut().find(|dir| &dir.path == path) {
|
db.increment(path, -1.0, now);
|
||||||
dir.rank = (dir.rank - 1.0).max(0.0);
|
|
||||||
}
|
|
||||||
db.modified = true;
|
|
||||||
db.save()?;
|
db.save()?;
|
||||||
print_dirs(db, now);
|
print_dirs(db, now);
|
||||||
}
|
}
|
||||||
Some(EditCommand::Delete { path }) => {
|
Some(EditCommand::Delete { path }) => {
|
||||||
if let Some(idx) = db.dirs.iter().position(|dir| &dir.path == path) {
|
db.remove(path);
|
||||||
db.dirs.remove(idx);
|
|
||||||
}
|
|
||||||
db.modified = true;
|
|
||||||
db.save()?;
|
db.save()?;
|
||||||
print_dirs(db, now);
|
print_dirs(db, now);
|
||||||
}
|
}
|
||||||
Some(EditCommand::Increment { path }) => {
|
Some(EditCommand::Increment { path }) => {
|
||||||
if let Some(dir) = db.dirs.iter_mut().find(|dir| &dir.path == path) {
|
db.increment(path, 1.0, now);
|
||||||
dir.rank += 1.0;
|
|
||||||
}
|
|
||||||
db.modified = true;
|
|
||||||
db.save()?;
|
db.save()?;
|
||||||
print_dirs(db, now);
|
print_dirs(db, now);
|
||||||
}
|
}
|
||||||
Some(EditCommand::Reload) => print_dirs(db, now),
|
Some(EditCommand::Reload) => print_dirs(db, now),
|
||||||
None => {
|
None => {
|
||||||
db.dirs.sort_unstable_by(|dir1, dir2| dir2.score(now).total_cmp(&dir1.score(now)));
|
db.sort_by_score(now);
|
||||||
db.modified = true;
|
|
||||||
db.save()?;
|
db.save()?;
|
||||||
|
|
||||||
let mut fzf = Command::new("fzf");
|
let mut fzf = Command::new("fzf");
|
||||||
fzf.args([
|
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
|
// Search mode
|
||||||
"--delimiter=\\x00 ",
|
"--delimiter=\\x00 ",
|
||||||
"--nth=2",
|
"--nth=2",
|
||||||
// Search result
|
// Search result
|
||||||
"--no-sort",
|
"--no-sort",
|
||||||
// Interface
|
// 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",
|
"--cycle",
|
||||||
"--keep-right",
|
"--keep-right",
|
||||||
// Layout
|
// Layout
|
||||||
|
"--header=\
|
||||||
|
ctrl-r:reload ctrl-w:delete
|
||||||
|
ctrl-a:increment ctrl-d:decrement
|
||||||
|
|
||||||
|
SCORE PATH",
|
||||||
"--info=inline",
|
"--info=inline",
|
||||||
"--layout=reverse",
|
"--layout=reverse",
|
||||||
// Key/Event bindings
|
|
||||||
"--bind=ctrl-z:ignore",
|
|
||||||
])
|
])
|
||||||
.envs([("FZF_DEFAULT_COMMAND", "zoxide edit reload")]);
|
.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();
|
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();
|
writeln!(stdout, "{:>5}\x00 {}", dir.score(now), &dir.path).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
use std::io::{self, Write};
|
use anyhow::{bail, Result};
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
|
||||||
|
|
||||||
use crate::cmd::{Remove, Run};
|
use crate::cmd::{Remove, Run};
|
||||||
use crate::config;
|
|
||||||
use crate::db::DatabaseFile;
|
use crate::db::DatabaseFile;
|
||||||
use crate::util::{self, Fzf};
|
use crate::{config, util};
|
||||||
|
|
||||||
impl Run for Remove {
|
impl Run for Remove {
|
||||||
fn run(&self) -> Result<()> {
|
fn run(&self) -> Result<()> {
|
||||||
|
|
@ -13,30 +10,6 @@ impl Run for Remove {
|
||||||
let mut db = DatabaseFile::new(data_dir);
|
let mut db = DatabaseFile::new(data_dir);
|
||||||
let mut db = db.open()?;
|
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) {
|
|
||||||
db.modified = false;
|
|
||||||
bail!("path not found in database: {path}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for path in &self.paths {
|
for path in &self.paths {
|
||||||
if !db.remove(path) {
|
if !db.remove(path) {
|
||||||
let path_abs = util::resolve_path(path)?;
|
let path_abs = util::resolve_path(path)?;
|
||||||
|
|
@ -47,7 +20,6 @@ impl Run for Remove {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
db.save()
|
db.save()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ mod config;
|
||||||
mod db;
|
mod db;
|
||||||
mod error;
|
mod error;
|
||||||
mod shell;
|
mod shell;
|
||||||
|
mod store;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use std::env;
|
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) {
|
match OpenOptions::new().write(true).create_new(true).open(&path) {
|
||||||
Ok(file) => break Ok((file, path)),
|
Ok(file) => break Ok((file, path)),
|
||||||
Err(e) if e.kind() == io::ErrorKind::AlreadyExists && attempts < MAX_ATTEMPTS => (),
|
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