* Add init subcommand for bash, fish, and zsh

* Migrate errors from failure to anyhow
* Migrate argument parsing from clap to structopt
* Empty z command now goes to $HOME
* DB updates are now atomic and isolated
This commit is contained in:
Ajeet D'Souza 2020-03-11 00:02:40 +05:30
parent 6d91b4e3c4
commit f0c5e28fd7
12 changed files with 434 additions and 388 deletions

145
Cargo.lock generated
View File

@ -8,6 +8,11 @@ dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "anyhow"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "arrayref"
version = "0.3.6"
@ -33,26 +38,6 @@ name = "autocfg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "backtrace"
version = "0.3.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace-sys 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)",
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "backtrace-sys"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "base64"
version = "0.11.0"
@ -87,11 +72,6 @@ name = "byteorder"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cc"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cfg-if"
version = "0.1.10"
@ -146,26 +126,6 @@ dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "failure"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)",
"failure_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "failure_derive"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
"synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "fs2"
version = "0.4.3"
@ -185,6 +145,14 @@ dependencies = [
"wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "heck"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "hermit-abi"
version = "0.1.8"
@ -203,6 +171,30 @@ name = "libc"
version = "0.2.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "proc-macro-error"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro-error-attr 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
"version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "proc-macro-error-attr"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
"syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "proc-macro2"
version = "1.0.9"
@ -245,11 +237,6 @@ dependencies = [
"crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rustc-demangle"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde"
version = "1.0.104"
@ -273,6 +260,28 @@ name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "structopt"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"structopt-derive 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "structopt-derive"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro-error 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "syn"
version = "1.0.16"
@ -284,14 +293,13 @@ dependencies = [
]
[[package]]
name = "synstructure"
version = "0.12.3"
name = "syn-mid"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -302,6 +310,11 @@ dependencies = [
"unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "unicode-segmentation"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unicode-width"
version = "0.1.7"
@ -317,6 +330,11 @@ name = "vec_map"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "version_check"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
@ -343,58 +361,61 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "zoxide"
version = "0.1.1"
version = "0.2.0"
dependencies = [
"anyhow 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)",
"bincode 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
"dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"fs2 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
"structopt 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[metadata]
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
"checksum anyhow 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)" = "7825f6833612eb2414095684fcf6c635becf3ce97fe48cf6421321e93bfbd53c"
"checksum arrayref 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
"checksum arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"
"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
"checksum backtrace 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)" = "ad235dabf00f36301792cfe82499880ba54c6486be094d1047b02bacb67c14e8"
"checksum backtrace-sys 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)" = "e17b52e737c40a7d75abca20b29a19a0eb7ba9fc72c5a72dd282a0a3c2c0dc35"
"checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
"checksum bincode 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5753e2a71534719bf3f4e57006c3a4f0d2c672a4b676eec84161f763eca87dbf"
"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
"checksum blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a"
"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
"checksum cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)" = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd"
"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
"checksum constant_time_eq 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
"checksum crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
"checksum dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3"
"checksum dirs-sys 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b"
"checksum failure 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "b8529c2421efa3066a5cbd8063d2244603824daccb6936b079010bb2aa89464b"
"checksum failure_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "030a733c8287d6213886dd487564ff5c8f6aae10278b3588ed177f9d18f8d231"
"checksum fs2 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
"checksum hermit-abi 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8"
"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
"checksum libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018"
"checksum proc-macro-error 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "e7959c6467d962050d639361f7703b2051c43036d03493c36f01d440fdd3138a"
"checksum proc-macro-error-attr 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "e4002d9f55991d5e019fb940a90e1a95eb80c24e77cb2462dd4dc869604d543a"
"checksum proc-macro2 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435"
"checksum quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f"
"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
"checksum redox_users 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431"
"checksum rust-argon2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017"
"checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
"checksum serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449"
"checksum serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64"
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
"checksum structopt 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "3fe43617218c0805c6eb37160119dc3c548110a67786da7218d1c6555212f073"
"checksum structopt-derive 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c6e79c80e0f4efd86ca960218d4e056249be189ff1c42824dcd9a7f51a56f0bd"
"checksum syn 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)" = "123bd9499cfb380418d509322d7a6d52e5315f064fe4b3ad18a53d6b92c07859"
"checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
"checksum syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a"
"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
"checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce"
"checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"

View File

@ -1,6 +1,6 @@
[package]
name = "zoxide"
version = "0.1.1"
version = "0.2.0"
authors = ["Ajeet D'Souza <98ajeet@gmail.com>"]
description = "A cd command that learns your habits"
repository = "https://github.com/ajeetdsouza/zoxide/"
@ -13,12 +13,13 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.26"
bincode = "1.2.1"
clap = "2.33.0"
dirs = "2.0.2"
failure = "0.1.7"
fs2 = "0.4.3"
serde = { version = "1.0.104", features = ["derive"] }
structopt = "0.3.11"
[profile.release]
codegen-units = 1

View File

@ -2,6 +2,8 @@
[![crates.io](https://img.shields.io/crates/v/zoxide)](https://crates.io/crates/zoxide)
![.github/workflows/release.yml](https://github.com/ajeetdsouza/zoxide/workflows/.github/workflows/release.yml/badge.svg)
A cd command that learns your habits
## Table of contents
@ -12,6 +14,7 @@ A cd command that learns your habits
- [Installing `zoxide`](#installing-zoxide)
- [Adding `zoxide` to your shell](#adding-zoxide-to-your-shell)
- [zsh](#zsh)
- [bash](#bash)
- [fish](#fish)
- [Configuration](#configuration)
- [Environment variables](#environment-variables)
@ -60,50 +63,26 @@ If you want the interactive fuzzy selection feature, you will also need to insta
#### zsh
Using [antibody](https://github.com/getantibody/antibody):
Add the following line to your `~/.zshrc`:
```sh
antibody bundle ajeetdsouza/zoxide
eval "$(zoxide init zsh)"
```
Using [zinit](https://github.com/zdharma/zinit):
#### bash
Add the following line to your `~/.bashrc`:
```sh
zinit light ajeetdsouza/zoxide
eval "$(zoxide init bash)"
```
Using [antigen](https://github.com/zsh-users/antigen):
```sh
antigen bundle ajeetdsouza/zoxide
```
Using [zgen](https://github.com/tarjoilija/zgen):
```sh
zgen load ajeetdsouza/zoxide
```
Using [zplug](https://github.com/zplug/zplug):
```sh
zplug "ajeetdsouza/zoxide"
```
If you'd rather not use a package manager, add the contents of [zoxide.plugin.zsh](zoxide.plugin.zsh) to your `.zshrc`.
#### fish
Using [fisher](https://github.com/jorgebucaran/fisher):
Add the following line to your `~/.config/fish/config.fish`:
```sh
fisher add ajeetdsouza/zoxide
```
Using [oh-my-fish](https://github.com/oh-my-fish/oh-my-fish):
```sh
omf install https://github.com/ajeetdsouza/zoxide
zoxide init fish | source
```
## Configuration

View File

@ -1,13 +0,0 @@
function z
if test (count $argv) -gt 0
set _Z_RESULT (zoxide query $argv)
switch "$_Z_RESULT"
case 'query: *'
cd (string sub -s 8 -- "$_Z_RESULT")
commandline -f repaint
case '*'
echo -n "$_Z_RESULT"
end
end
end

View File

@ -1,8 +1 @@
function zoxide-add --on-event fish_prompt
zoxide add
end
abbr -a zi "z -i"
abbr -a za "zoxide add"
abbr -a zq "zoxide query"
abbr -a zr "zoxide remove"
zoxide init fish | source

View File

@ -1,36 +0,0 @@
use crate::error::AppError;
use crate::types::Rank;
use failure::{bail, ResultExt};
use std::env;
use std::ffi::OsString;
pub const ZO_DATA: &str = "_ZO_DATA";
pub const ZO_MAXAGE: &str = "_ZO_MAXAGE";
pub fn get_zo_data() -> Result<OsString, failure::Error> {
let path = match env::var_os(ZO_DATA) {
Some(path) => path,
None => {
let mut path = dirs::home_dir().ok_or_else(|| AppError::GetHomeDirError)?;
path.push(".zo");
path.into_os_string()
}
};
Ok(path)
}
pub fn get_zo_maxage() -> Result<Rank, failure::Error> {
if let Some(maxage_osstr) = env::var_os(ZO_MAXAGE) {
match maxage_osstr.to_str() {
Some(maxage_str) => {
let maxage = maxage_str
.parse::<Rank>()
.with_context(|_| AppError::EnvError(ZO_MAXAGE.to_owned()))?;
Ok(maxage)
}
None => bail!(AppError::EnvError(ZO_MAXAGE.to_owned())),
}
} else {
Ok(5000.0)
}
}

136
src/db.rs
View File

@ -1,67 +1,85 @@
use crate::config::get_zo_maxage;
use crate::dir::Dir;
use crate::error::AppError;
use crate::types::{Rank, Timestamp};
use failure::ResultExt;
use crate::util::get_zo_maxage;
use anyhow::{anyhow, Context, Result};
use fs2::FileExt;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::fs::{self, File, OpenOptions};
use std::io::{self, BufReader, BufWriter};
use std::io::{Read, Write};
use std::path::Path;
use std::path::{Path, PathBuf};
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct DB {
pub dirs: Vec<Dir>,
path: PathBuf,
path_tmp: PathBuf,
#[serde(skip)]
pub modified: bool,
file_tmp: File,
dirs: Vec<Dir>,
modified: bool,
}
impl DB {
pub fn open<P: AsRef<Path>>(path: P) -> Result<DB, failure::Error> {
match File::open(path) {
pub fn open<P: AsRef<Path>>(path: P) -> Result<DB> {
let path = path.as_ref().to_path_buf();
let mut path_tmp = path.clone();
path_tmp.set_file_name(".zo.tmp");
let file_tmp = OpenOptions::new()
.write(true)
.create(true)
.open(&path_tmp)
.with_context(|| anyhow!("could not open temporary database"))?;
file_tmp
.lock_exclusive()
.with_context(|| anyhow!("could not lock temporary database"))?;
let dirs = match File::open(&path) {
Ok(file) => {
file.lock_shared()
.with_context(|_| AppError::FileLockError)?;
let rd = BufReader::new(file);
let db = DB::read_from(rd).with_context(|_| AppError::DBReadError)?;
Ok(db)
let rd = BufReader::new(&file);
bincode::deserialize_from(rd)
.with_context(|| anyhow!("could not deserialize database"))?
}
Err(err) => match err.kind() {
io::ErrorKind::NotFound => Ok(DB::default()),
_ => Err(err).with_context(|_| AppError::FileOpenError)?,
io::ErrorKind::NotFound => Vec::<Dir>::new(),
_ => return Err(err).with_context(|| anyhow!("could not open database")),
},
}
};
Ok(DB {
path,
path_tmp,
file_tmp,
dirs,
modified: false,
})
}
pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<(), failure::Error> {
pub fn save(&mut self) -> Result<()> {
if self.modified {
let file = File::create(path).with_context(|_| AppError::FileOpenError)?;
file.lock_exclusive()
.with_context(|_| AppError::FileLockError)?;
let wr = BufWriter::new(file);
self.write_into(wr)
.with_context(|_| AppError::DBWriteError)?;
self.file_tmp
.set_len(0)
.with_context(|| "could not truncate temporary database")?;
let wr = BufWriter::new(&self.file_tmp);
bincode::serialize_into(wr, &self.dirs)
.with_context(|| anyhow!("could not serialize database"))?;
fs::rename(&self.path_tmp, &self.path)
.with_context(|| anyhow!("could not move temporary database"))?;
}
Ok(())
}
pub fn read_from<R: Read>(rd: R) -> Result<DB, bincode::Error> {
bincode::deserialize_from(rd)
}
pub fn write_into<W: Write>(&self, wr: W) -> Result<(), bincode::Error> {
bincode::serialize_into(wr, &self)
}
pub fn add<P: AsRef<Path>>(&mut self, path: P, now: Timestamp) -> Result<(), failure::Error> {
pub fn add<P: AsRef<Path>>(&mut self, path: P, now: Timestamp) -> Result<()> {
let path_abs = path
.as_ref()
.canonicalize()
.with_context(|_| AppError::PathAccessError)?;
let path_str = path_abs.to_str().ok_or_else(|| AppError::UnicodeError)?;
.with_context(|| anyhow!("could not access directory: {}", path.as_ref().display()))?;
let path_str = path_abs
.to_str()
.ok_or_else(|| anyhow!("invalid unicode in path: {}", path_abs.display()))?;
match self.dirs.iter_mut().find(|dir| dir.path == path_str) {
None => self.dirs.push(Dir {
@ -92,8 +110,6 @@ impl DB {
}
pub fn query(&mut self, keywords: &[String], now: Timestamp) -> Option<Dir> {
// TODO: expand "~" in queries
loop {
let (idx, dir) = self
.dirs
@ -111,12 +127,29 @@ impl DB {
}
}
pub fn remove<P: AsRef<Path>>(&mut self, path: P) -> Result<(), failure::Error> {
let path_abs = path
.as_ref()
.canonicalize()
.with_context(|_| AppError::PathAccessError)?;
let path_str = path_abs.to_str().ok_or_else(|| AppError::UnicodeError)?;
pub fn query_all(&mut self, mut keywords: Vec<String>) -> Vec<Dir> {
self.remove_invalid();
for keyword in &mut keywords {
keyword.make_ascii_lowercase();
}
self.dirs
.iter()
.filter(|dir| dir.is_match(&keywords))
.cloned()
.collect()
}
pub fn remove<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let path_abs = match path.as_ref().canonicalize() {
Ok(path_abs) => path_abs,
Err(_) => path.as_ref().to_path_buf(),
};
let path_str = path_abs
.to_str()
.ok_or_else(|| anyhow!("invalid unicode in path"))?;
if let Some(idx) = self.dirs.iter().position(|dir| dir.path == path_str) {
self.dirs.remove(idx);
@ -126,11 +159,10 @@ impl DB {
Ok(())
}
pub fn remove_invalid(&mut self) {
let dirs_len = self.dirs.len();
self.dirs.retain(|dir| dir.is_dir());
if self.dirs.len() != dirs_len {
fn remove_invalid(&mut self) {
let orig_len = self.dirs.len();
self.dirs.retain(Dir::is_dir);
if orig_len != self.dirs.len() {
self.modified = true;
}
}

View File

@ -2,7 +2,7 @@ use crate::types::{Rank, Timestamp};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Dir {
pub path: String,
pub rank: Rank,
@ -17,16 +17,14 @@ impl Dir {
pub fn is_match(&self, query: &[String]) -> bool {
let path = self.path.to_ascii_lowercase();
if let Some(dir_name) = Path::new(&path).file_name() {
if let Some(query_last) = query.last() {
if let Some(query_dir_name) = Path::new(query_last).file_name() {
// `unwrap()` here should be safe because the values are already encoded as UTF-8
let dir_name_str = dir_name.to_str().unwrap().to_ascii_lowercase();
let query_dir_name_str = query_dir_name.to_str().unwrap().to_ascii_lowercase();
if let Some(query_name) = query.last().and_then(|word| Path::new(word).file_name()) {
if let Some(path_name) = Path::new(&path).file_name() {
// `unwrap()` here should be safe because the values are already encoded as UTF-8
let query_name = query_name.to_str().unwrap();
let path_name = path_name.to_str().unwrap();
if !dir_name_str.contains(&query_dir_name_str) {
return false;
}
if !path_name.contains(&query_name) {
return false;
}
}
}

View File

@ -1,35 +0,0 @@
use failure::Fail;
#[derive(Debug, Fail)]
pub enum AppError {
#[fail(display = "found invalid UTF-8 code sequence")]
UnicodeError,
#[fail(display = "system clock is set to invalid time")]
SystemTimeError,
#[fail(display = "unable to open database file")]
FileOpenError,
#[fail(display = "unable to lock database file")]
FileLockError,
#[fail(display = "could not read from database")]
DBReadError,
#[fail(display = "could not write to database")]
DBWriteError,
#[fail(display = "could not launch fzf")]
FzfLaunchError,
#[fail(display = "could not communicate with fzf")]
FzfIoError,
#[fail(display = "could not retrieve home directory")]
GetHomeDirError,
#[fail(display = "could not retrieve current directory")]
GetCurrentDirError,
#[fail(display = "could not access path")]
PathAccessError,
#[fail(display = "could not decode ${} in env", 0)]
EnvError(String),
}

View File

@ -1,20 +1,51 @@
mod config;
mod db;
mod dir;
mod error;
mod types;
mod util;
use crate::config::get_zo_data;
use crate::db::DB;
use crate::error::AppError;
use crate::types::Timestamp;
use crate::util::{fzf_helper, get_current_time};
use clap::{app_from_crate, crate_authors, crate_description, crate_name, crate_version};
use clap::{App, Arg, SubCommand};
use failure::ResultExt;
use crate::util::{fzf_helper, get_current_time, get_db};
use anyhow::{anyhow, Context, Result};
use clap::arg_enum;
use std::env;
use std::path::Path;
use structopt::StructOpt;
// TODO: use structopt to parse env variables: <https://github.com/TeXitoi/structopt/blob/master/examples/env.rs>
arg_enum! {
#[allow(non_camel_case_types)]
#[derive(Debug)]
enum Shell {
bash,
fish,
zsh,
}
}
#[derive(Debug, StructOpt)]
#[structopt(about = "A cd command that learns your habits")]
enum Zoxide {
#[structopt(about = "Add a new directory or increment its rank")]
Add { path: Option<String> },
#[structopt(about = "Prints shell configuration")]
Init {
#[structopt(possible_values = &Shell::variants(), case_insensitive = true)]
shell: Shell,
},
#[structopt(about = "Search for a directory")]
Query {
keywords: Vec<String>,
#[structopt(short, long, help = "Opens an interactive selection menu using fzf")]
interactive: bool,
},
#[structopt(about = "Remove a directory")]
Remove { path: String },
}
fn zoxide_query(db: &mut DB, mut keywords: Vec<String>, now: Timestamp) -> Option<String> {
if let [path] = keywords.as_slice() {
@ -36,105 +67,168 @@ fn zoxide_query(db: &mut DB, mut keywords: Vec<String>, now: Timestamp) -> Optio
fn zoxide_query_interactive(
db: &mut DB,
mut keywords: Vec<String>,
keywords: Vec<String>,
now: Timestamp,
) -> Result<Option<String>, failure::Error> {
db.remove_invalid();
for keyword in &mut keywords {
keyword.make_ascii_lowercase();
}
let dirs = db
.dirs
.iter()
.filter(|dir| dir.is_match(&keywords))
.cloned()
.collect();
) -> Result<Option<String>> {
let dirs = db.query_all(keywords);
fzf_helper(now, dirs)
}
fn zoxide_app() -> App<'static, 'static> {
app_from_crate!()
.subcommand(
SubCommand::with_name("add")
.about("Add a new directory or increment its rank")
.author(crate_authors!())
.version(crate_version!())
.arg(Arg::with_name("PATH")),
)
.subcommand(
SubCommand::with_name("query")
.about("Search for a directory")
.author(crate_authors!())
.version(crate_version!())
.arg(
Arg::with_name("interactive")
.short("i")
.long("interactive")
.takes_value(false)
.help("Opens an interactive selection menu using fzf"),
)
.arg(Arg::with_name("KEYWORD").min_values(0)),
)
.subcommand(
SubCommand::with_name("remove")
.about("Remove a directory")
.author(crate_authors!())
.version(crate_version!())
.arg(Arg::with_name("PATH").required(true)),
)
}
pub fn main() -> Result<()> {
let opt = Zoxide::from_args();
match opt {
Zoxide::Add { path: path_opt } => {
let mut db = get_db()?;
let now = get_current_time()?;
fn zoxide() -> Result<(), failure::Error> {
let matches = zoxide_app().get_matches();
match path_opt {
Some(path) => db.add(path, now),
None => {
let current_dir = env::current_dir()
.with_context(|| anyhow!("unable to fetch current directory"))?;
db.add(current_dir, now)
}
}?;
let db_path = get_zo_data()?;
let mut db = DB::open(&db_path)?;
if let Some(matches) = matches.subcommand_matches("query") {
let now = get_current_time()?;
let keywords = matches
.values_of_os("KEYWORD")
.unwrap_or_default()
.map(|keyword| match keyword.to_str() {
Some(keyword) => Ok(keyword.to_owned()),
None => Err(AppError::UnicodeError),
})
.collect::<Result<Vec<String>, _>>()?;
let path_opt = if matches.is_present("interactive") {
zoxide_query_interactive(&mut db, keywords, now)
} else {
Ok(zoxide_query(&mut db, keywords, now))
}?;
if let Some(path) = path_opt {
println!("query: {}", path.trim());
db.save()?;
}
} else if let Some(matches) = matches.subcommand_matches("add") {
let now = get_current_time()?;
match matches.value_of_os("PATH") {
Some(path) => db.add(path, now)?,
None => {
let path = env::current_dir().with_context(|_| AppError::GetCurrentDirError)?;
db.add(path, now)?;
Zoxide::Init { shell } => {
match shell {
Shell::bash => {
println!("{}", INIT_BASH);
println!("{}", INIT_BASH_ALIAS);
}
Shell::fish => {
println!("{}", INIT_FISH);
println!("{}", INIT_FISH_ALIAS);
}
Shell::zsh => {
println!("{}", INIT_ZSH);
println!("{}", INIT_ZSH_ALIAS);
}
};
}
Zoxide::Query {
keywords,
interactive,
} => {
let mut db = get_db()?;
let now = get_current_time()?;
let path_opt = if interactive {
zoxide_query_interactive(&mut db, keywords, now)?
} else {
zoxide_query(&mut db, keywords, now)
};
if let Some(path) = path_opt {
println!("query: {}", path.trim());
}
};
} else if let Some(matches) = matches.subcommand_matches("remove") {
// unwrap is safe here because PATH has been set as a required field
let path = matches.value_of_os("PATH").unwrap();
db.remove(path)?;
}
}
Zoxide::Remove { path } => {
let mut db = get_db()?;
db.remove(path)?;
db.save()?;
}
};
db.save(db_path)
Ok(())
}
fn main() {
if let Err(err) = zoxide() {
eprintln!("zoxide: {}", err);
std::process::exit(1);
}
const INIT_BASH: &str = r#"
_zoxide_precmd() {
zoxide add
}
case "$PROMPT_COMMAND" in
*_zoxide_precmd*) ;;
*) PROMPT_COMMAND="_zoxide_precmd${PROMPT_COMMAND:+;$PROMPT_COMMAND}" ;;
esac
z() {
if [ $# -ne 0 ]; then
_Z_RESULT=$(zoxide query "$@")
case $_Z_RESULT in
"query: "*)
cd "${_Z_RESULT:7}"
;;
*)
echo -n "${_Z_RESULT}"
;;
esac
else
cd "${HOME}"
fi
}
"#;
const INIT_BASH_ALIAS: &str = r#"
alias zi="z -i"
alias za="zoxide add"
alias zq="zoxide query"
alias zr="zoxide remove"
"#;
const INIT_FISH: &str = r#"
function _zoxide_precmd --on-event fish_prompt
zoxide add
end
function z
if test (count $argv) -gt 0
set _Z_RESULT (zoxide query $argv)
switch "$_Z_RESULT"
case 'query: *'
cd (string sub -s 8 -- "$_Z_RESULT")
commandline -f repaint
case '*'
echo -n "$_Z_RESULT"
end
else
cd "$HOME"
commandline -f repaint
end
end
"#;
const INIT_FISH_ALIAS: &str = r#"
abbr -a zi "z -i"
abbr -a za "zoxide add"
abbr -a zq "zoxide query"
abbr -a zr "zoxide remove"
"#;
const INIT_ZSH: &str = r#"
_zoxide_precmd() {
zoxide add
}
[[ -n "${precmd_functions[(r)_zoxide_precmd]}" ]] || {
precmd_functions+=(_zoxide_precmd)
}
z() {
if [ $# -ne 0 ]; then
_Z_RESULT=$(zoxide query "$@")
case $_Z_RESULT in
"query: "*)
cd "${_Z_RESULT:7}"
;;
*)
echo -n "${_Z_RESULT}"
;;
esac
else
cd "${HOME}"
fi
}
"#;
const INIT_ZSH_ALIAS: &str = r#"
alias zi="z -i"
alias za="zoxide add"
alias zq="zoxide query"
alias zr="zoxide remove"
"#;

View File

@ -1,29 +1,67 @@
use crate::db::DB;
use crate::dir::Dir;
use crate::error::AppError;
use crate::types::Timestamp;
use failure::ResultExt;
use crate::types::{Rank, Timestamp};
use anyhow::{anyhow, Context, Result};
use std::env;
use std::io::{Read, Write};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::time::SystemTime;
pub fn get_current_time() -> Result<Timestamp, failure::Error> {
pub fn get_zo_data() -> Result<PathBuf> {
const ZO_DATA: &str = "_ZO_DATA";
Ok(match env::var_os(ZO_DATA) {
Some(path) => PathBuf::from(path),
None => {
let mut path =
dirs::home_dir().ok_or_else(|| anyhow!("could not locate home directory"))?;
path.push(".zo");
path
}
})
}
pub fn get_zo_maxage() -> Result<Rank> {
const ZO_MAXAGE: &str = "_ZO_MAXAGE";
let maxage = match env::var_os(ZO_MAXAGE) {
Some(maxage_var) => maxage_var
.to_str()
.ok_or_else(|| anyhow!("invalid Unicode in ${}", ZO_MAXAGE))?
.parse::<i64>()
.with_context(|| anyhow!("could not parse ${} as integer", ZO_MAXAGE))?
as Rank,
None => 1000.0,
};
Ok(maxage)
}
pub fn get_db() -> Result<DB> {
let path = get_zo_data()?;
DB::open(path)
}
pub fn get_current_time() -> Result<Timestamp> {
let current_time = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.with_context(|_| AppError::SystemTimeError)?
.with_context(|| "system clock set to invalid time")?
.as_secs();
Ok(current_time as Timestamp)
}
pub fn fzf_helper(now: Timestamp, mut dirs: Vec<Dir>) -> Result<Option<String>, failure::Error> {
pub fn fzf_helper(now: Timestamp, mut dirs: Vec<Dir>) -> Result<Option<String>> {
let fzf = Command::new("fzf")
.arg("-n2..")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.with_context(|_| AppError::FzfLaunchError)?;
.with_context(|| anyhow!("could not launch fzf"))?;
let mut fzf_stdin = fzf.stdin.ok_or_else(|| AppError::FzfIoError)?;
let mut fzf_stdin = fzf
.stdin
.ok_or_else(|| anyhow!("could not connect to fzf stdin"))?;
for dir in dirs.iter_mut() {
dir.rank = dir.get_frecency(now);
@ -42,15 +80,17 @@ pub fn fzf_helper(now: Timestamp, mut dirs: Vec<Dir>) -> Result<Option<String>,
};
writeln!(fzf_stdin, "{:>4} {}", frecency, dir.path)
.with_context(|_| AppError::FzfIoError)?;
.with_context(|| anyhow!("could not write into fzf stdin"))?;
}
let mut fzf_stdout = fzf.stdout.ok_or_else(|| AppError::FzfIoError)?;
let mut fzf_stdout = fzf
.stdout
.ok_or_else(|| anyhow!("could not connect to fzf stdout"))?;
let mut output = String::new();
fzf_stdout
.read_to_string(&mut output)
.with_context(|_| AppError::FzfIoError)?;
.with_context(|| anyhow!("could not read from fzf stdout"))?;
Ok(output.get(12..).map(str::to_owned))
}

View File

@ -1,29 +1 @@
#!/usr/bin/env sh
_zoxide_precmd() {
zoxide add
}
[[ -n "${precmd_functions[(r)_zoxide_precmd]}" ]] || {
precmd_functions+=(_zoxide_precmd)
}
z() {
if [ $# -ne 0 ]; then
_Z_RESULT=$(zoxide query "$@")
case $_Z_RESULT in
"query: "*)
cd "${_Z_RESULT:7}"
;;
*)
echo "${_Z_RESULT}"
;;
esac
fi
}
alias zi="z -i"
alias za="zoxide add"
alias zq="zoxide query"
alias zr="zoxide remove"
eval "$(zoxide init zsh)"