Merge branch 'bookmark-cmd' of github.com:azaleacolburn/zoxide into bookmark-cmd
This commit is contained in:
commit
51066ae7d0
|
|
@ -40,7 +40,7 @@ jobs:
|
||||||
if: ${{ matrix.os != 'windows-latest' }}
|
if: ${{ matrix.os != 'windows-latest' }}
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@v15
|
- uses: cachix/cachix-action@v16
|
||||||
if: ${{ matrix.os != 'windows-latest' && env.CACHIX_AUTH_TOKEN != '' }}
|
if: ${{ matrix.os != 'windows-latest' && env.CACHIX_AUTH_TOKEN != '' }}
|
||||||
with:
|
with:
|
||||||
authToken: ${{ env.CACHIX_AUTH_TOKEN }}
|
authToken: ${{ env.CACHIX_AUTH_TOKEN }}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,12 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Bash: zoxide will now rewrite the prompt when using Space-Tab completions.
|
||||||
|
|
||||||
## [0.9.7] - 2025-02-10
|
## [0.9.7] - 2025-02-10
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -461,14 +461,17 @@ Environment variables[^2] can be used for configuration. They must be set before
|
||||||
| [lf] | File manager | See the [wiki][lf-wiki] |
|
| [lf] | File manager | See the [wiki][lf-wiki] |
|
||||||
| [nnn] | File manager | [nnn-autojump] |
|
| [nnn] | File manager | [nnn-autojump] |
|
||||||
| [ranger] | File manager | [ranger-zoxide] |
|
| [ranger] | File manager | [ranger-zoxide] |
|
||||||
|
| [rfm] | File manager | Natively supported |
|
||||||
|
| [sesh] | `tmux` session manager | Natively supported |
|
||||||
| [telescope.nvim] | Fuzzy finder for Neovim | [telescope-zoxide] |
|
| [telescope.nvim] | Fuzzy finder for Neovim | [telescope-zoxide] |
|
||||||
| [t] | `tmux` session manager | Natively supported |
|
|
||||||
| [tmux-session-wizard] | `tmux` session manager | Natively supported |
|
| [tmux-session-wizard] | `tmux` session manager | Natively supported |
|
||||||
|
| [tmux-sessionx] | `tmux` session manager | Natively supported |
|
||||||
| [vim] / [neovim] | Text editor | [zoxide.vim] |
|
| [vim] / [neovim] | Text editor | [zoxide.vim] |
|
||||||
| [xplr] | File manager | [zoxide.xplr] |
|
| [xplr] | File manager | [zoxide.xplr] |
|
||||||
| [xxh] | Transports shell configuration over SSH | [xxh-plugin-prerun-zoxide] |
|
| [xxh] | Transports shell configuration over SSH | [xxh-plugin-prerun-zoxide] |
|
||||||
| [yazi] | File manager | Natively supported |
|
| [yazi] | File manager | Natively supported |
|
||||||
| [zabb] | Finds the shortest possible query for a path | Natively supported |
|
| [zabb] | Finds the shortest possible query for a path | Natively supported |
|
||||||
|
| [zesh] | `zellij` session manager | Natively supported |
|
||||||
| [zsh-autocomplete] | Realtime completions for zsh | Natively supported |
|
| [zsh-autocomplete] | Realtime completions for zsh | Natively supported |
|
||||||
|
|
||||||
[^1]:
|
[^1]:
|
||||||
|
|
@ -528,15 +531,17 @@ Environment variables[^2] can be used for configuration. They must be set before
|
||||||
[ranger]: https://github.com/ranger/ranger
|
[ranger]: https://github.com/ranger/ranger
|
||||||
[raspbian packages]: https://archive.raspbian.org/raspbian/pool/main/r/rust-zoxide/
|
[raspbian packages]: https://archive.raspbian.org/raspbian/pool/main/r/rust-zoxide/
|
||||||
[releases]: https://github.com/ajeetdsouza/zoxide/releases
|
[releases]: https://github.com/ajeetdsouza/zoxide/releases
|
||||||
|
[rfm]: https://github.com/dsxmachina/rfm
|
||||||
[scoop]: https://github.com/ScoopInstaller/Main/tree/master/bucket/zoxide.json
|
[scoop]: https://github.com/ScoopInstaller/Main/tree/master/bucket/zoxide.json
|
||||||
|
[sesh]: https://github.com/joshmedeski/sesh
|
||||||
[slackbuilds]: https://slackbuilds.org/repository/15.0/system/zoxide/
|
[slackbuilds]: https://slackbuilds.org/repository/15.0/system/zoxide/
|
||||||
[slackbuilds-howto]: https://slackbuilds.org/howto/
|
[slackbuilds-howto]: https://slackbuilds.org/howto/
|
||||||
[solus packages]: https://github.com/getsolus/packages/tree/main/packages/z/zoxide/
|
[solus packages]: https://github.com/getsolus/packages/tree/main/packages/z/zoxide/
|
||||||
[t]: https://github.com/joshmedeski/t-smart-tmux-session-manager
|
|
||||||
[telescope-zoxide]: https://github.com/jvgrootveld/telescope-zoxide
|
[telescope-zoxide]: https://github.com/jvgrootveld/telescope-zoxide
|
||||||
[telescope.nvim]: https://github.com/nvim-telescope/telescope.nvim
|
[telescope.nvim]: https://github.com/nvim-telescope/telescope.nvim
|
||||||
[termux]: https://github.com/termux/termux-packages/tree/master/packages/zoxide
|
[termux]: https://github.com/termux/termux-packages/tree/master/packages/zoxide
|
||||||
[tmux-session-wizard]: https://github.com/27medkamal/tmux-session-wizard
|
[tmux-session-wizard]: https://github.com/27medkamal/tmux-session-wizard
|
||||||
|
[tmux-sessionx]: https://github.com/omerxx/tmux-sessionx
|
||||||
[tutorial]: contrib/tutorial.webp
|
[tutorial]: contrib/tutorial.webp
|
||||||
[ubuntu packages]: https://packages.ubuntu.com/jammy/zoxide
|
[ubuntu packages]: https://packages.ubuntu.com/jammy/zoxide
|
||||||
[vim]: https://github.com/vim/vim
|
[vim]: https://github.com/vim/vim
|
||||||
|
|
@ -547,6 +552,7 @@ Environment variables[^2] can be used for configuration. They must be set before
|
||||||
[xxh]: https://github.com/xxh/xxh
|
[xxh]: https://github.com/xxh/xxh
|
||||||
[yazi]: https://github.com/sxyazi/yazi
|
[yazi]: https://github.com/sxyazi/yazi
|
||||||
[zabb]: https://github.com/Mellbourn/zabb
|
[zabb]: https://github.com/Mellbourn/zabb
|
||||||
|
[zesh]: https://github.com/roberte777/zesh
|
||||||
[zoxide.el]: https://gitlab.com/Vonfry/zoxide.el
|
[zoxide.el]: https://gitlab.com/Vonfry/zoxide.el
|
||||||
[zoxide.vim]: https://github.com/nanotee/zoxide.vim
|
[zoxide.vim]: https://github.com/nanotee/zoxide.vim
|
||||||
[zoxide.xplr]: https://github.com/sayanarijit/zoxide.xplr
|
[zoxide.xplr]: https://github.com/sayanarijit/zoxide.xplr
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,18 @@
|
||||||
use super::{Bookmark, Run};
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use super::{Bookmark, Run};
|
||||||
|
use crate::db::Database;
|
||||||
|
|
||||||
impl Run for Bookmark {
|
impl Run for Bookmark {
|
||||||
fn run(&self) -> Result<()> {}
|
fn run(&self) -> Result<()> {
|
||||||
|
let mut db = crate::db::Database::open()?;
|
||||||
|
self.add_bookmark(&mut db).and(db.save())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Bookmark {}
|
impl Bookmark {
|
||||||
|
fn add_bookmark(&self, db: &mut Database) -> Result<()> {
|
||||||
|
db.add_bookmark(self.bookmark_id.clone(), self.path.clone());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,13 @@ impl Run for Query {
|
||||||
impl Query {
|
impl Query {
|
||||||
fn query(&self, db: &mut Database) -> Result<()> {
|
fn query(&self, db: &mut Database) -> Result<()> {
|
||||||
let now = util::current_time()?;
|
let now = util::current_time()?;
|
||||||
|
|
||||||
|
if let Ok(is_mark) = self.try_bookmark(db) {
|
||||||
|
if is_mark {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut stream = self.get_stream(db, now)?;
|
let mut stream = self.get_stream(db, now)?;
|
||||||
|
|
||||||
if self.interactive {
|
if self.interactive {
|
||||||
|
|
@ -28,7 +35,22 @@ impl Query {
|
||||||
self.query_first(&mut stream, now)
|
self.query_first(&mut stream, now)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn try_bookmark(&self, db: &Database) -> Result<bool, ()> {
|
||||||
|
// NOTE We only assume bookmarking if they supply one keyword
|
||||||
|
// Could be trivially changed to iterate over keywords
|
||||||
|
if self.keywords.len() == 1 {
|
||||||
|
let keyword = &self.keywords[0];
|
||||||
|
if let Some(path) = db.get_bookmark(keyword) {
|
||||||
|
let handle = &mut io::stdout();
|
||||||
|
return match writeln!(handle, "{}", path.to_str().unwrap()).pipe_exit("stdout") {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(_) => Err(()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
fn query_interactive(&self, stream: &mut Stream, now: Epoch) -> Result<()> {
|
fn query_interactive(&self, stream: &mut Stream, now: Epoch) -> Result<()> {
|
||||||
let mut fzf = Self::get_fzf()?;
|
let mut fzf = Self::get_fzf()?;
|
||||||
let selection = loop {
|
let selection = loop {
|
||||||
|
|
|
||||||
195
src/db/mod.rs
195
src/db/mod.rs
|
|
@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
|
||||||
use std::{fs, io};
|
use std::{fs, io};
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use bincode::Options;
|
use bincode::{Options, serialize};
|
||||||
use ouroboros::self_referencing;
|
use ouroboros::self_referencing;
|
||||||
|
|
||||||
pub use crate::db::dir::{Dir, Epoch, Rank};
|
pub use crate::db::dir::{Dir, Epoch, Rank};
|
||||||
|
|
@ -19,11 +19,11 @@ pub struct Database {
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
#[borrows(bytes)]
|
#[borrows(bytes)]
|
||||||
#[covariant]
|
#[covariant]
|
||||||
pub dirs: Vec<Dir<'this>>,
|
// NOTE Directories and Bookmarks
|
||||||
map_bytes: Vec<u8>,
|
// They must be the same field otherwise two closures which take bytes and yield each
|
||||||
#[borrows(map_bytes)]
|
// respectively would have to be constructed, causing bytes requiring bytes to have static
|
||||||
#[covariant]
|
// lifetime
|
||||||
pub bookmarks: HashMap<String, Dir<'this>>,
|
pub dirs: (Vec<Dir<'this>>, HashMap<String, PathBuf>),
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,65 +32,29 @@ impl Database {
|
||||||
|
|
||||||
pub fn open() -> Result<Self> {
|
pub fn open() -> Result<Self> {
|
||||||
let data_dir = config::data_dir()?;
|
let data_dir = config::data_dir()?;
|
||||||
let bookmarks_dir: PathBuf = config::bookmarks_dir()?;
|
Self::open_dir(data_dir)
|
||||||
Self::open_dir(data_dir, bookmarks_dir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_dir(data_dir: impl AsRef<Path>, bookmarks_dir: impl AsRef<Path>) -> Result<Self> {
|
pub fn open_dir(data_dir: impl AsRef<Path>) -> Result<Self> {
|
||||||
let data_dir = data_dir.as_ref();
|
let data_dir = data_dir.as_ref();
|
||||||
let path = data_dir.join("db.zo");
|
let path = data_dir.join("db.zo");
|
||||||
let path = fs::canonicalize(&path).unwrap_or(path);
|
let path = fs::canonicalize(&path).unwrap_or(path);
|
||||||
|
|
||||||
let bookmarks_dir = bookmarks_dir.as_ref();
|
match fs::read(&path) {
|
||||||
let bookmarks_path = bookmarks_dir.join("db_bm.zo");
|
Ok(bytes) => {
|
||||||
let bookmarks_path = fs::canonicalize(&bookmarks_path).unwrap_or(bookmarks_path);
|
Self::try_new(path.clone(), bytes, |bytes| Self::deserialize(bytes, path), false)
|
||||||
|
}
|
||||||
match (fs::read(&path), fs::read(&bookmarks_path)) {
|
Err(e) if e.kind() == io::ErrorKind::NotFound => {
|
||||||
(Ok(bytes), Ok(bookmarks_bytes)) => Self::try_new(
|
|
||||||
path,
|
|
||||||
bytes,
|
|
||||||
|bytes| Self::deserialize(bytes),
|
|
||||||
bookmarks_bytes,
|
|
||||||
|bookmarks_bytes| Self::deserialize_bookmarks(bookmarks_bytes),
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
(Err(e), _) if e.kind() == io::ErrorKind::NotFound => {
|
|
||||||
// Create data directory, but don't create any file yet. The file will be
|
// Create data directory, but don't create any file yet. The file will be
|
||||||
// created later by [`Database::save`] if any data is modified.
|
// created later by [`Database::save`] if any data is modified.
|
||||||
fs::create_dir_all(data_dir).with_context(|| {
|
fs::create_dir_all(data_dir).with_context(|| {
|
||||||
format!("unable to create data directory: {}", data_dir.display())
|
format!("unable to create data directory: {}", data_dir.display())
|
||||||
})?;
|
})?;
|
||||||
Ok(Self::new(
|
Ok(Self::new(path, Vec::new(), |_| (Vec::new(), HashMap::new()), false))
|
||||||
path,
|
|
||||||
Vec::new(),
|
|
||||||
|_| Vec::new(),
|
|
||||||
Vec::new(),
|
|
||||||
|_| HashMap::new(),
|
|
||||||
false,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
(_, Err(e)) if e.kind() == io::ErrorKind::NotFound => {
|
Err(e) => {
|
||||||
// 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 bookmarks directory: {}", data_dir.display())
|
|
||||||
})?;
|
|
||||||
Ok(Self::new(
|
|
||||||
path,
|
|
||||||
Vec::new(),
|
|
||||||
|_| Vec::new(),
|
|
||||||
Vec::new(),
|
|
||||||
|_| HashMap::new(),
|
|
||||||
false,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
(Err(e), _) => {
|
|
||||||
Err(e).with_context(|| format!("could not read from database: {}", path.display()))
|
Err(e).with_context(|| format!("could not read from database: {}", path.display()))
|
||||||
}
|
}
|
||||||
(_, Err(e)) => Err(e).with_context(|| {
|
|
||||||
format!("could not read from bookmarks database: {}", bookmarks_path.display())
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,7 +64,7 @@ impl Database {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let bytes = Self::serialize(self.dirs())?;
|
let bytes = Self::serialize((self.dirs(), self.bookmarks()))?;
|
||||||
util::write(self.borrow_path(), bytes).context("could not write to database")?;
|
util::write(self.borrow_path(), bytes).context("could not write to database")?;
|
||||||
self.with_dirty_mut(|dirty| *dirty = false);
|
self.with_dirty_mut(|dirty| *dirty = false);
|
||||||
|
|
||||||
|
|
@ -109,10 +73,10 @@ impl Database {
|
||||||
|
|
||||||
/// Increments the rank of a directory, or creates it if it does not exist.
|
/// Increments the rank of a directory, or creates it if it does not exist.
|
||||||
pub fn add(&mut self, path: impl AsRef<str> + Into<String>, by: Rank, now: Epoch) {
|
pub fn add(&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()) {
|
self.with_dirs_mut(|dirs| match dirs.0.iter_mut().find(|dir| dir.path == path.as_ref()) {
|
||||||
Some(dir) => dir.rank = (dir.rank + by).max(0.0),
|
Some(dir) => dir.rank = (dir.rank + by).max(0.0),
|
||||||
None => {
|
None => {
|
||||||
dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now })
|
dirs.0.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now })
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
self.with_dirty_mut(|dirty| *dirty = true);
|
self.with_dirty_mut(|dirty| *dirty = true);
|
||||||
|
|
@ -123,7 +87,7 @@ impl Database {
|
||||||
/// does a check before calling this, or calls `dedup()` afterward.
|
/// does a check before calling this, or calls `dedup()` afterward.
|
||||||
pub fn add_unchecked(&mut self, path: impl AsRef<str> + Into<String>, rank: Rank, now: Epoch) {
|
pub fn add_unchecked(&mut self, path: impl AsRef<str> + Into<String>, rank: Rank, now: Epoch) {
|
||||||
self.with_dirs_mut(|dirs| {
|
self.with_dirs_mut(|dirs| {
|
||||||
dirs.push(Dir { path: path.into().into(), rank, last_accessed: now })
|
dirs.0.push(Dir { path: path.into().into(), rank, last_accessed: now })
|
||||||
});
|
});
|
||||||
self.with_dirty_mut(|dirty| *dirty = true);
|
self.with_dirty_mut(|dirty| *dirty = true);
|
||||||
}
|
}
|
||||||
|
|
@ -131,13 +95,13 @@ impl Database {
|
||||||
/// Increments the rank and updates the last_accessed of a directory, or
|
/// Increments the rank and updates the last_accessed of a directory, or
|
||||||
/// creates it if it does not exist.
|
/// creates it if it does not exist.
|
||||||
pub fn add_update(&mut self, path: impl AsRef<str> + Into<String>, by: Rank, now: Epoch) {
|
pub fn add_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()) {
|
self.with_dirs_mut(|dirs| match dirs.0.iter_mut().find(|dir| dir.path == path.as_ref()) {
|
||||||
Some(dir) => {
|
Some(dir) => {
|
||||||
dir.rank = (dir.rank + by).max(0.0);
|
dir.rank = (dir.rank + by).max(0.0);
|
||||||
dir.last_accessed = now;
|
dir.last_accessed = now;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now })
|
dirs.0.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now })
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
self.with_dirty_mut(|dirty| *dirty = true);
|
self.with_dirty_mut(|dirty| *dirty = true);
|
||||||
|
|
@ -156,21 +120,21 @@ impl Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn swap_remove(&mut self, idx: usize) {
|
pub fn swap_remove(&mut self, idx: usize) {
|
||||||
self.with_dirs_mut(|dirs| dirs.swap_remove(idx));
|
self.with_dirs_mut(|dirs| dirs.0.swap_remove(idx));
|
||||||
self.with_dirty_mut(|dirty| *dirty = true);
|
self.with_dirty_mut(|dirty| *dirty = true);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn age(&mut self, max_age: Rank) {
|
pub fn age(&mut self, max_age: Rank) {
|
||||||
let mut dirty = false;
|
let mut dirty = false;
|
||||||
self.with_dirs_mut(|dirs| {
|
self.with_dirs_mut(|dirs| {
|
||||||
let total_age = dirs.iter().map(|dir| dir.rank).sum::<Rank>();
|
let total_age = dirs.0.iter().map(|dir| dir.rank).sum::<Rank>();
|
||||||
if total_age > max_age {
|
if total_age > max_age {
|
||||||
let factor = 0.9 * max_age / total_age;
|
let factor = 0.9 * max_age / total_age;
|
||||||
for idx in (0..dirs.len()).rev() {
|
for idx in (0..dirs.0.len()).rev() {
|
||||||
let dir = &mut dirs[idx];
|
let dir = &mut dirs.0[idx];
|
||||||
dir.rank *= factor;
|
dir.rank *= factor;
|
||||||
if dir.rank < 1.0 {
|
if dir.rank < 1.0 {
|
||||||
dirs.swap_remove(idx);
|
dirs.0.swap_remove(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dirty = true;
|
dirty = true;
|
||||||
|
|
@ -185,10 +149,10 @@ impl Database {
|
||||||
|
|
||||||
let mut dirty = false;
|
let mut dirty = false;
|
||||||
self.with_dirs_mut(|dirs| {
|
self.with_dirs_mut(|dirs| {
|
||||||
for idx in (1..dirs.len()).rev() {
|
for idx in (1..dirs.0.len()).rev() {
|
||||||
// Check if curr_dir and next_dir have equal paths.
|
// Check if curr_dir and next_dir have equal paths.
|
||||||
let curr_dir = &dirs[idx];
|
let curr_dir = &dirs.0[idx];
|
||||||
let next_dir = &dirs[idx - 1];
|
let next_dir = &dirs.0[idx - 1];
|
||||||
if next_dir.path != curr_dir.path {
|
if next_dir.path != curr_dir.path {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -196,12 +160,12 @@ impl Database {
|
||||||
// Merge curr_dir's rank and last_accessed into next_dir.
|
// Merge curr_dir's rank and last_accessed into next_dir.
|
||||||
let rank = curr_dir.rank;
|
let rank = curr_dir.rank;
|
||||||
let last_accessed = curr_dir.last_accessed;
|
let last_accessed = curr_dir.last_accessed;
|
||||||
let next_dir = &mut dirs[idx - 1];
|
let next_dir = &mut dirs.0[idx - 1];
|
||||||
next_dir.last_accessed = next_dir.last_accessed.max(last_accessed);
|
next_dir.last_accessed = next_dir.last_accessed.max(last_accessed);
|
||||||
next_dir.rank += rank;
|
next_dir.rank += rank;
|
||||||
|
|
||||||
// Delete curr_dir.
|
// Delete curr_dir.
|
||||||
dirs.swap_remove(idx);
|
dirs.0.swap_remove(idx);
|
||||||
dirty = true;
|
dirty = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -209,13 +173,13 @@ impl Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sort_by_path(&mut self) {
|
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_dirs_mut(|dirs| dirs.0.sort_unstable_by(|dir1, dir2| dir1.path.cmp(&dir2.path)));
|
||||||
self.with_dirty_mut(|dirty| *dirty = true);
|
self.with_dirty_mut(|dirty| *dirty = true);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sort_by_score(&mut self, now: Epoch) {
|
pub fn sort_by_score(&mut self, now: Epoch) {
|
||||||
self.with_dirs_mut(|dirs| {
|
self.with_dirs_mut(|dirs| {
|
||||||
dirs.sort_unstable_by(|dir1: &Dir, dir2: &Dir| {
|
dirs.0.sort_unstable_by(|dir1: &Dir, dir2: &Dir| {
|
||||||
dir1.score(now).total_cmp(&dir2.score(now))
|
dir1.score(now).total_cmp(&dir2.score(now))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
@ -227,10 +191,23 @@ impl Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dirs(&self) -> &[Dir] {
|
pub fn dirs(&self) -> &[Dir] {
|
||||||
self.borrow_dirs()
|
&self.borrow_dirs().0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialize(dirs: &[Dir<'_>]) -> Result<Vec<u8>> {
|
pub fn bookmarks(&self) -> &HashMap<String, PathBuf> {
|
||||||
|
&self.borrow_dirs().1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_bookmark(&self, id: &str) -> Option<&PathBuf> {
|
||||||
|
self.borrow_dirs().1.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_bookmark(&mut self, id: String, path: PathBuf) {
|
||||||
|
self.with_dirs_mut(|dirs| dirs.1.insert(id, path));
|
||||||
|
self.with_dirty_mut(|is_dirty| *is_dirty = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize(dirs: (&[Dir<'_>], &HashMap<String, PathBuf>)) -> Result<Vec<u8>> {
|
||||||
(|| -> bincode::Result<_> {
|
(|| -> bincode::Result<_> {
|
||||||
// Preallocate buffer with combined size of sections.
|
// Preallocate buffer with combined size of sections.
|
||||||
let buffer_size =
|
let buffer_size =
|
||||||
|
|
@ -246,7 +223,7 @@ impl Database {
|
||||||
.context("could not serialize database")
|
.context("could not serialize database")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize(bytes: &[u8]) -> Result<Vec<Dir>> {
|
fn deserialize(bytes: &[u8], path: PathBuf) -> Result<(Vec<Dir>, HashMap<String, PathBuf>)> {
|
||||||
// Assume a maximum size for the database. This prevents bincode from throwing
|
// Assume a maximum size for the database. This prevents bincode from throwing
|
||||||
// strange errors when it encounters invalid data.
|
// strange errors when it encounters invalid data.
|
||||||
const MAX_SIZE: u64 = 32 << 20; // 32 MiB
|
const MAX_SIZE: u64 = 32 << 20; // 32 MiB
|
||||||
|
|
@ -263,34 +240,24 @@ impl Database {
|
||||||
let version = deserializer.deserialize(bytes_version)?;
|
let version = deserializer.deserialize(bytes_version)?;
|
||||||
let dirs = match version {
|
let dirs = match version {
|
||||||
Self::VERSION => {
|
Self::VERSION => {
|
||||||
deserializer.deserialize(bytes_dirs).context("could not deserialize database")?
|
match deserializer.deserialize::<(Vec<Dir>, HashMap<String, PathBuf>)>(bytes_dirs) {
|
||||||
}
|
Err(err) => {
|
||||||
version => {
|
let dirs: Vec<Dir> = match deserializer.deserialize(bytes_dirs) {
|
||||||
bail!("unsupported version (got {version}, supports {})", Self::VERSION)
|
Ok(dirs) => dirs,
|
||||||
}
|
Err(_) => return Err(err).context("could not deserialize database"),
|
||||||
};
|
};
|
||||||
|
let bookmarks: HashMap<String, PathBuf> = HashMap::new();
|
||||||
Ok(dirs)
|
match serialize(&(&dirs, &bookmarks)) {
|
||||||
}
|
Ok(_) => {
|
||||||
|
util::write(path, bytes).context("could not write to database")?;
|
||||||
fn deserialize_bookmarks(bytes: &[u8]) -> Result<HashMap<String, Dir>> {
|
println!("yellow");
|
||||||
// Assume a maximum size for the database. This prevents bincode from throwing
|
return Ok((dirs, bookmarks));
|
||||||
// strange errors when it encounters invalid data.
|
}
|
||||||
const MAX_SIZE: u64 = 32 << 20; // 32 MiB
|
Err(_) => return Err(err).context("could not deserialize database"),
|
||||||
let deserializer = &mut bincode::options().with_fixint_encoding().with_limit(MAX_SIZE);
|
};
|
||||||
|
}
|
||||||
// Split bytes into sections.
|
Ok(dirs) => dirs,
|
||||||
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 => {
|
version => {
|
||||||
bail!("unsupported version (got {version}, supports {})", Self::VERSION)
|
bail!("unsupported version (got {version}, supports {})", Self::VERSION)
|
||||||
|
|
@ -311,18 +278,15 @@ mod tests {
|
||||||
let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" };
|
let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" };
|
||||||
let now = 946684800;
|
let now = 946684800;
|
||||||
|
|
||||||
let bookmarks_dir = tempfile::tempdir().unwrap();
|
|
||||||
let bookmarks_path = if cfg!(windows) { r"C:\foo\bar2" } else { "/foo/bar2" };
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut db = Database::open_dir(data_dir.path(), bookmarks_dir.path()).unwrap();
|
let mut db = Database::open_dir(data_dir.path()).unwrap();
|
||||||
db.add(bookmarks_path, 1.0, now);
|
db.add(path, 1.0, now);
|
||||||
db.add(bookmarks_path, 1.0, now);
|
db.add(path, 1.0, now);
|
||||||
db.save().unwrap();
|
db.save().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let db = Database::open_dir(data_dir.path(), bookmarks_dir.path()).unwrap();
|
let db = Database::open_dir(data_dir.path()).unwrap();
|
||||||
assert_eq!(db.dirs().len(), 1);
|
assert_eq!(db.dirs().len(), 1);
|
||||||
|
|
||||||
let dir = &db.dirs()[0];
|
let dir = &db.dirs()[0];
|
||||||
|
|
@ -338,26 +302,23 @@ mod tests {
|
||||||
let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" };
|
let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" };
|
||||||
let now = 946684800;
|
let now = 946684800;
|
||||||
|
|
||||||
let bookmarks_dir = tempfile::tempdir().unwrap();
|
|
||||||
let bookmarks_path = if cfg!(windows) { r"C:\foo\bar2" } else { "/foo/bar2" };
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut db = Database::open_dir(data_dir.path(), bookmarks_dir.path()).unwrap();
|
let mut db = Database::open_dir(data_dir.path()).unwrap();
|
||||||
db.add(bookmarks_path, 1.0, now);
|
db.add(path, 1.0, now);
|
||||||
db.add(bookmarks_path, 1.0, now);
|
db.add(path, 1.0, now);
|
||||||
db.save().unwrap();
|
db.save().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut db = Database::open_dir(data_dir.path(), bookmarks_dir.path()).unwrap();
|
let mut db = Database::open_dir(data_dir.path()).unwrap();
|
||||||
assert!(db.remove(bookmarks_path));
|
assert!(db.remove(path));
|
||||||
db.save().unwrap();
|
db.save().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut db = Database::open_dir(data_dir.path(), bookmarks_dir.path()).unwrap();
|
let mut db = Database::open_dir(data_dir.path()).unwrap();
|
||||||
assert!(db.dirs().is_empty());
|
assert!(db.dirs().is_empty());
|
||||||
assert!(!db.remove(bookmarks_path));
|
assert!(!db.remove(path));
|
||||||
db.save().unwrap();
|
db.save().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,7 @@ impl StreamOptions {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
@ -180,7 +181,8 @@ mod tests {
|
||||||
#[case(&["/foo/", "/bar"], "/foo/bar", false)]
|
#[case(&["/foo/", "/bar"], "/foo/bar", false)]
|
||||||
#[case(&["/foo/", "/bar"], "/foo/baz/bar", true)]
|
#[case(&["/foo/", "/bar"], "/foo/baz/bar", true)]
|
||||||
fn query(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) {
|
fn query(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) {
|
||||||
let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false);
|
let db =
|
||||||
|
&mut Database::new(PathBuf::new(), Vec::new(), |_| (Vec::new(), HashMap::new()), false);
|
||||||
let options = StreamOptions::new(0).with_keywords(keywords.iter());
|
let options = StreamOptions::new(0).with_keywords(keywords.iter());
|
||||||
let stream = Stream::new(db, options);
|
let stream = Stream::new(db, options);
|
||||||
assert_eq!(is_match, stream.filter_by_keywords(path));
|
assert_eq!(is_match, stream.filter_by_keywords(path));
|
||||||
|
|
|
||||||
|
|
@ -86,8 +86,6 @@ function __zoxide_doctor() {
|
||||||
# When using zoxide with --no-cmd, alias these internal functions as desired.
|
# When using zoxide with --no-cmd, alias these internal functions as desired.
|
||||||
#
|
#
|
||||||
|
|
||||||
__zoxide_z_prefix='z#'
|
|
||||||
|
|
||||||
# Jump to a directory using only keywords.
|
# Jump to a directory using only keywords.
|
||||||
function __zoxide_z() {
|
function __zoxide_z() {
|
||||||
__zoxide_doctor
|
__zoxide_doctor
|
||||||
|
|
@ -101,10 +99,6 @@ function __zoxide_z() {
|
||||||
__zoxide_cd "$1"
|
__zoxide_cd "$1"
|
||||||
elif [[ $# -eq 2 && $1 == '--' ]]; then
|
elif [[ $# -eq 2 && $1 == '--' ]]; then
|
||||||
__zoxide_cd "$2"
|
__zoxide_cd "$2"
|
||||||
elif [[ ${@: -1} == "${__zoxide_z_prefix}"?* ]]; then
|
|
||||||
# shellcheck disable=SC2124
|
|
||||||
\builtin local result="${@: -1}"
|
|
||||||
__zoxide_cd "{{ "${result:${#__zoxide_z_prefix}}" }}"
|
|
||||||
else
|
else
|
||||||
\builtin local result
|
\builtin local result
|
||||||
# shellcheck disable=SC2312
|
# shellcheck disable=SC2312
|
||||||
|
|
@ -144,8 +138,11 @@ function {{cmd}}i() {
|
||||||
# - Completions don't work on `dumb` terminals.
|
# - Completions don't work on `dumb` terminals.
|
||||||
if [[ ${BASH_VERSINFO[0]:-0} -eq 4 && ${BASH_VERSINFO[1]:-0} -ge 4 || ${BASH_VERSINFO[0]:-0} -ge 5 ]] &&
|
if [[ ${BASH_VERSINFO[0]:-0} -eq 4 && ${BASH_VERSINFO[1]:-0} -ge 4 || ${BASH_VERSINFO[0]:-0} -ge 5 ]] &&
|
||||||
[[ :"${SHELLOPTS}": =~ :(vi|emacs): && ${TERM} != 'dumb' ]]; then
|
[[ :"${SHELLOPTS}": =~ :(vi|emacs): && ${TERM} != 'dumb' ]]; then
|
||||||
# Use `printf '\e[5n'` to redraw line after fzf closes.
|
|
||||||
\builtin bind '"\e[0n": redraw-current-line' &>/dev/null
|
function __zoxide_z_complete_helper() {
|
||||||
|
READLINE_LINE="{{ cmd }} ${__zoxide_result@Q}"
|
||||||
|
READLINE_POINT={{ "${#READLINE_LINE}" }}
|
||||||
|
}
|
||||||
|
|
||||||
function __zoxide_z_complete() {
|
function __zoxide_z_complete() {
|
||||||
# Only show completions when the cursor is at the end of the line.
|
# Only show completions when the cursor is at the end of the line.
|
||||||
|
|
@ -157,12 +154,15 @@ if [[ ${BASH_VERSINFO[0]:-0} -eq 4 && ${BASH_VERSINFO[1]:-0} -ge 4 || ${BASH_VER
|
||||||
\builtin compgen -A directory -- "${COMP_WORDS[-1]}" || \builtin true
|
\builtin compgen -A directory -- "${COMP_WORDS[-1]}" || \builtin true
|
||||||
)
|
)
|
||||||
# If there is a space after the last word, use interactive selection.
|
# If there is a space after the last word, use interactive selection.
|
||||||
elif [[ -z ${COMP_WORDS[-1]} ]] && [[ ${COMP_WORDS[-2]} != "${__zoxide_z_prefix}"?* ]]; then
|
elif [[ -z ${COMP_WORDS[-1]} ]]; then
|
||||||
\builtin local result
|
|
||||||
# shellcheck disable=SC2312
|
# shellcheck disable=SC2312
|
||||||
result="$(\command zoxide query --exclude "$(__zoxide_pwd)" --interactive -- "{{ "${COMP_WORDS[@]:1:${#COMP_WORDS[@]}-2}" }}")" &&
|
__zoxide_result="$(\command zoxide query --exclude "$(__zoxide_pwd)" --interactive -- "{{ "${COMP_WORDS[@]:1:${#COMP_WORDS[@]}-2}" }}")" && {
|
||||||
COMPREPLY=("${__zoxide_z_prefix}${result}/")
|
\builtin bind '"\e[0n": redraw-current-line'
|
||||||
\builtin printf '\e[5n'
|
\builtin printf '\e[5n'
|
||||||
|
|
||||||
|
\builtin bind -x '"\e[0n": __zoxide_z_complete_helper "${result}"'
|
||||||
|
\builtin printf '\e[5n'
|
||||||
|
}
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue