mirror of
https://github.com/Llewellynvdm/zoxide.git
synced 2024-11-29 07:53:55 +00:00
Add interactive query/remove and import
This commit is contained in:
parent
bc17c25cf6
commit
5c3af59ba6
2
.github/workflows/cargo-clippy.yml
vendored
2
.github/workflows/cargo-clippy.yml
vendored
@ -17,5 +17,5 @@ jobs:
|
|||||||
components: clippy
|
components: clippy
|
||||||
- uses: actions-rs/clippy-check@v1
|
- uses: actions-rs/clippy-check@v1
|
||||||
with:
|
with:
|
||||||
args: --workspace --all-targets --all-features -- -D warnings -D clippy::all
|
args: --all-targets --all-features -- -D warnings -D clippy::all
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
2
.github/workflows/cargo-fmt.yml
vendored
2
.github/workflows/cargo-fmt.yml
vendored
@ -18,4 +18,4 @@ jobs:
|
|||||||
- uses: actions-rs/cargo@v1
|
- uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: fmt
|
command: fmt
|
||||||
args: --all -- --check
|
args: -- --check
|
||||||
|
2
.github/workflows/cargo-test.yml
vendored
2
.github/workflows/cargo-test.yml
vendored
@ -25,4 +25,4 @@ jobs:
|
|||||||
- uses: actions-rs/cargo@v1
|
- uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: test
|
command: test
|
||||||
args: --workspace --all-features --no-fail-fast
|
args: --all-features --no-fail-fast
|
||||||
|
2
.github/workflows/cargo-udeps.yml
vendored
2
.github/workflows/cargo-udeps.yml
vendored
@ -19,4 +19,4 @@ jobs:
|
|||||||
crate: cargo-udeps
|
crate: cargo-udeps
|
||||||
version: latest
|
version: latest
|
||||||
use-tool-cache: true
|
use-tool-cache: true
|
||||||
- run: cargo udeps --all-features --all-targets --workspace
|
- run: cargo udeps --all-features --all-targets
|
||||||
|
44
Cargo.lock
generated
44
Cargo.lock
generated
@ -217,9 +217,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.1.16"
|
version = "0.1.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c30f6d0bc6b00693347368a67d41b58f2fb851215ff1da49e90fe2c5c667151"
|
checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
@ -242,9 +242,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.78"
|
version = "0.2.80"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aa7087f49d294270db4e1928fc110c976cd4b9e5a16348e0a1df09afa99e6c98"
|
checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
@ -434,18 +434,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.116"
|
version = "1.0.117"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5"
|
checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.116"
|
version = "1.0.117"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8"
|
checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -460,9 +460,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.45"
|
version = "1.0.48"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea9c5432ff16d6152371f808fb5a871cd67368171b09bb21b43df8e4a47a3556"
|
checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -588,32 +588,16 @@ name = "zoxide"
|
|||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"askama",
|
||||||
|
"assert_cmd",
|
||||||
|
"bincode",
|
||||||
"clap",
|
"clap",
|
||||||
"dirs-next",
|
"dirs-next",
|
||||||
"dunce",
|
"dunce",
|
||||||
"glob",
|
"glob",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"zoxide-engine",
|
|
||||||
"zoxide-shell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zoxide-engine"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"bincode",
|
|
||||||
"ordered-float",
|
"ordered-float",
|
||||||
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zoxide-shell"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"askama",
|
|
||||||
"assert_cmd",
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
13
Cargo.toml
13
Cargo.toml
@ -11,15 +11,22 @@ license = "MIT"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.32"
|
anyhow = "1.0.32"
|
||||||
|
askama = { version = "0.10.3", default-features = false }
|
||||||
|
bincode = "1.3.1"
|
||||||
clap = "3.0.0-beta.2"
|
clap = "3.0.0-beta.2"
|
||||||
dirs-next = "1.0.2"
|
dirs-next = "1.0.2"
|
||||||
dunce = "1.0.1"
|
dunce = "1.0.1"
|
||||||
glob = "0.3.0"
|
glob = "0.3.0"
|
||||||
once_cell = "1.4.1"
|
once_cell = "1.4.1"
|
||||||
zoxide-engine = { path = "crates/zoxide-engine" }
|
ordered-float = "2.0.0"
|
||||||
zoxide-shell = { path = "crates/zoxide-shell" }
|
serde = { version = "1.0.116", features = ["derive"] }
|
||||||
|
tempfile = "3.1.0"
|
||||||
|
|
||||||
[workspace]
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
rand = "0.7.3"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
assert_cmd = "1.0.1"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
11
crates/zoxide-engine/.gitignore
vendored
11
crates/zoxide-engine/.gitignore
vendored
@ -1,11 +0,0 @@
|
|||||||
# Generated by Cargo
|
|
||||||
# will have compiled files and executables
|
|
||||||
debug/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
|
||||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
|
||||||
Cargo.lock
|
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
|
||||||
**/*.rs.bk
|
|
@ -1,14 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "zoxide-engine"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["Ajeet D'Souza <98ajeet@gmail.com>"]
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = "1.0.33"
|
|
||||||
bincode = "1.3.1"
|
|
||||||
ordered-float = "2.0.0"
|
|
||||||
serde = { version = "1.0.116", features = ["derive"] }
|
|
||||||
tempfile = "3.1.0"
|
|
@ -1,42 +0,0 @@
|
|||||||
use crate::query::Query;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Dir {
|
|
||||||
pub path: String,
|
|
||||||
pub rank: Rank,
|
|
||||||
pub last_accessed: Epoch,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Dir {
|
|
||||||
pub fn is_dir(&self) -> bool {
|
|
||||||
Path::new(&self.path).is_dir()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_match(&self, query: &Query) -> bool {
|
|
||||||
query.matches(&self.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_score(&self, now: Epoch) -> Rank {
|
|
||||||
const HOUR: Epoch = 60 * 60;
|
|
||||||
const DAY: Epoch = 24 * HOUR;
|
|
||||||
const WEEK: Epoch = 7 * DAY;
|
|
||||||
|
|
||||||
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;
|
|
@ -1,7 +0,0 @@
|
|||||||
pub mod dir;
|
|
||||||
mod query;
|
|
||||||
mod store;
|
|
||||||
|
|
||||||
pub use dir::{Dir, Epoch};
|
|
||||||
pub use query::Query;
|
|
||||||
pub use store::Store;
|
|
@ -1,43 +0,0 @@
|
|||||||
use zoxide_engine::Store;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_add() {
|
|
||||||
let path = "/foo/bar";
|
|
||||||
let now = 946684800;
|
|
||||||
|
|
||||||
let data_dir = tempfile::tempdir().unwrap();
|
|
||||||
{
|
|
||||||
let mut store = Store::open(data_dir.path()).unwrap();
|
|
||||||
store.add(path, now);
|
|
||||||
store.add(path, now);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let store = Store::open(data_dir.path()).unwrap();
|
|
||||||
assert_eq!(store.dirs.len(), 1);
|
|
||||||
|
|
||||||
let dir = &store.dirs[0];
|
|
||||||
assert_eq!(dir.path, path);
|
|
||||||
assert_eq!(dir.last_accessed, now);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_remove() {
|
|
||||||
let path = "/foo/bar";
|
|
||||||
let now = 946684800;
|
|
||||||
|
|
||||||
let data_dir = tempfile::tempdir().unwrap();
|
|
||||||
{
|
|
||||||
let mut store = Store::open(data_dir.path()).unwrap();
|
|
||||||
store.add(path, now);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let mut store = Store::open(data_dir.path()).unwrap();
|
|
||||||
assert!(store.remove(path));
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let mut store = Store::open(data_dir.path()).unwrap();
|
|
||||||
assert!(store.dirs.is_empty());
|
|
||||||
assert!(!store.remove(path));
|
|
||||||
}
|
|
||||||
}
|
|
11
crates/zoxide-shell/.gitignore
vendored
11
crates/zoxide-shell/.gitignore
vendored
@ -1,11 +0,0 @@
|
|||||||
# Generated by Cargo
|
|
||||||
# will have compiled files and executables
|
|
||||||
debug/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
|
||||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
|
||||||
Cargo.lock
|
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
|
||||||
**/*.rs.bk
|
|
@ -1,15 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "zoxide-shell"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["Ajeet D'Souza <98ajeet@gmail.com>"]
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = "1.0.33"
|
|
||||||
askama = { version = "0.10.3", default-features = false }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
assert_cmd = "1.0.1"
|
|
||||||
once_cell = "1.4.1"
|
|
@ -1,101 +0,0 @@
|
|||||||
use anyhow::{Context, Result};
|
|
||||||
use askama::Template;
|
|
||||||
|
|
||||||
use std::io::Write;
|
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Hook {
|
|
||||||
None,
|
|
||||||
Prompt,
|
|
||||||
Pwd,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Generator {
|
|
||||||
fn generate<W: Write>(&self, writer: &mut W) -> Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Template> Generator for T {
|
|
||||||
fn generate<W: Write>(&self, writer: &mut W) -> Result<()> {
|
|
||||||
let source = &self.render().context("could not render template")?;
|
|
||||||
writeln!(writer, "{}", source).context("could not write to output")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Opts<'a> {
|
|
||||||
pub cmd: Option<&'a str>,
|
|
||||||
pub hook: Hook,
|
|
||||||
pub echo: bool,
|
|
||||||
pub resolve_symlinks: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Opts<'_> {
|
|
||||||
pub const DEVNULL: &'static str = if cfg!(windows) { "NUL" } else { "/dev/null" };
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Template)]
|
|
||||||
#[template(path = "bash.txt")]
|
|
||||||
pub struct Bash<'a>(pub &'a Opts<'a>);
|
|
||||||
|
|
||||||
impl<'a> Deref for Bash<'a> {
|
|
||||||
type Target = Opts<'a>;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Template)]
|
|
||||||
#[template(path = "fish.txt")]
|
|
||||||
pub struct Fish<'a>(pub &'a Opts<'a>);
|
|
||||||
|
|
||||||
impl<'a> Deref for Fish<'a> {
|
|
||||||
type Target = Opts<'a>;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Template)]
|
|
||||||
#[template(path = "posix.txt")]
|
|
||||||
pub struct Posix<'a>(pub &'a Opts<'a>);
|
|
||||||
|
|
||||||
impl<'a> Deref for Posix<'a> {
|
|
||||||
type Target = Opts<'a>;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Template)]
|
|
||||||
#[template(path = "powershell.txt")]
|
|
||||||
pub struct PowerShell<'a>(pub &'a Opts<'a>);
|
|
||||||
|
|
||||||
impl<'a> Deref for PowerShell<'a> {
|
|
||||||
type Target = Opts<'a>;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Template)]
|
|
||||||
#[template(path = "xonsh.txt")]
|
|
||||||
pub struct Xonsh<'a>(pub &'a Opts<'a>);
|
|
||||||
|
|
||||||
impl<'a> Deref for Xonsh<'a> {
|
|
||||||
type Target = Opts<'a>;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Debug, Template)]
|
|
||||||
#[template(path = "zsh.txt")]
|
|
||||||
pub struct Zsh<'a>(pub &'a Opts<'a>);
|
|
||||||
|
|
||||||
impl<'a> Deref for Zsh<'a> {
|
|
||||||
type Target = Opts<'a>;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,181 +0,0 @@
|
|||||||
use askama::Template;
|
|
||||||
use assert_cmd::Command;
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use zoxide_shell::{Bash, Fish, Hook, Opts, Posix, PowerShell, Xonsh, Zsh};
|
|
||||||
|
|
||||||
fn opts() -> &'static [Opts<'static>] {
|
|
||||||
static OPTS: OnceCell<Vec<Opts>> = OnceCell::new();
|
|
||||||
OPTS.get_or_init(|| {
|
|
||||||
let mut opts = Vec::new();
|
|
||||||
for &echo in &[false, true] {
|
|
||||||
for &resolve_symlinks in &[false, true] {
|
|
||||||
for &hook in &[Hook::None, Hook::Prompt, Hook::Pwd] {
|
|
||||||
for &cmd in &[None, Some("z")] {
|
|
||||||
opts.push(Opts {
|
|
||||||
echo,
|
|
||||||
resolve_symlinks,
|
|
||||||
hook,
|
|
||||||
cmd,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opts
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_bash() {
|
|
||||||
for opts in opts() {
|
|
||||||
let source = crate::Bash(opts).render().unwrap();
|
|
||||||
Command::new("bash")
|
|
||||||
.args(&["-c", &source])
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stdout("")
|
|
||||||
.stderr("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_bash_posix() {
|
|
||||||
for opts in opts() {
|
|
||||||
let source = crate::Posix(opts).render().unwrap();
|
|
||||||
let assert = Command::new("bash")
|
|
||||||
.args(&["--posix", "-c", &source])
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stderr("");
|
|
||||||
|
|
||||||
if opts.hook != Hook::Pwd {
|
|
||||||
assert.stdout("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_dash() {
|
|
||||||
for opts in opts() {
|
|
||||||
let source = crate::Posix(opts).render().unwrap();
|
|
||||||
let assert = Command::new("bash")
|
|
||||||
.args(&["--posix", "-c", &source])
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stderr("");
|
|
||||||
|
|
||||||
if opts.hook != Hook::Pwd {
|
|
||||||
assert.stdout("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fish() {
|
|
||||||
for opts in opts() {
|
|
||||||
let source = crate::Fish(opts).render().unwrap();
|
|
||||||
Command::new("fish")
|
|
||||||
.args(&["-c", &source])
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stdout("")
|
|
||||||
.stderr("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pwsh() {
|
|
||||||
for opts in opts() {
|
|
||||||
let source = crate::PowerShell(opts).render().unwrap();
|
|
||||||
Command::new("pwsh")
|
|
||||||
.args(&["-c", &source])
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stdout("")
|
|
||||||
.stderr("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shellcheck_bash() {
|
|
||||||
for opts in opts() {
|
|
||||||
let source = crate::Bash(opts).render().unwrap();
|
|
||||||
Command::new("shellcheck")
|
|
||||||
.args(&["--shell", "bash", "-"])
|
|
||||||
.write_stdin(source.as_bytes())
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stdout("")
|
|
||||||
.stderr("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shellcheck_sh() {
|
|
||||||
for opts in opts() {
|
|
||||||
let source = crate::Posix(opts).render().unwrap();
|
|
||||||
Command::new("shellcheck")
|
|
||||||
.args(&["--shell", "sh", "-"])
|
|
||||||
.write_stdin(source.as_bytes())
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stdout("")
|
|
||||||
.stderr("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shfmt_bash() {
|
|
||||||
for opts in opts() {
|
|
||||||
let source = crate::Bash(opts).render().unwrap();
|
|
||||||
Command::new("shfmt")
|
|
||||||
.args(&["-d", "-s", "-ln", "bash", "-i", "4", "-ci", "-"])
|
|
||||||
.write_stdin(source.as_bytes())
|
|
||||||
.write_stdin(b"\n".as_ref())
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stdout("")
|
|
||||||
.stderr("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_shfmt_posix() {
|
|
||||||
for opts in opts() {
|
|
||||||
let source = crate::Posix(opts).render().unwrap();
|
|
||||||
Command::new("shfmt")
|
|
||||||
.args(&["-d", "-s", "-ln", "posix", "-i", "4", "-ci", "-"])
|
|
||||||
.write_stdin(source.as_bytes())
|
|
||||||
.write_stdin(b"\n".as_ref())
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stdout("")
|
|
||||||
.stderr("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_xonsh() {
|
|
||||||
for opts in opts() {
|
|
||||||
let source = crate::Xonsh(opts).render().unwrap();
|
|
||||||
Command::new("xonsh")
|
|
||||||
.args(&["-c", &source])
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stdout("")
|
|
||||||
.stderr("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_zsh() {
|
|
||||||
for opts in opts() {
|
|
||||||
let source = crate::Zsh(opts).render().unwrap();
|
|
||||||
Command::new("zsh")
|
|
||||||
.args(&["-c", &source])
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stdout("")
|
|
||||||
.stderr("");
|
|
||||||
}
|
|
||||||
}
|
|
49
src/cmd/add.rs
Normal file
49
src/cmd/add.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use crate::cmd::Cmd;
|
||||||
|
use crate::config;
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
|
use crate::store::Store;
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Clap;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Add a new directory or increment its rank
|
||||||
|
#[derive(Clap, Debug)]
|
||||||
|
pub struct Add {
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cmd for Add {
|
||||||
|
fn run(&self) -> Result<()> {
|
||||||
|
let path = match &self.path {
|
||||||
|
Some(path) => {
|
||||||
|
if config::zo_resolve_symlinks() {
|
||||||
|
util::canonicalize(&path)
|
||||||
|
} else {
|
||||||
|
util::resolve_path(&path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => util::current_dir(),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
if config::zo_exclude_dirs()?
|
||||||
|
.iter()
|
||||||
|
.any(|pattern| pattern.matches_path(&path))
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = util::path_to_str(&path)?;
|
||||||
|
let now = util::current_time()?;
|
||||||
|
|
||||||
|
let data_dir = config::zo_data_dir()?;
|
||||||
|
let max_age = config::zo_maxage()?;
|
||||||
|
|
||||||
|
let mut store = Store::open(&data_dir)?;
|
||||||
|
store.add(path, now);
|
||||||
|
store.age(max_age);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
51
src/cmd/import.rs
Normal file
51
src/cmd/import.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use crate::cmd::Cmd;
|
||||||
|
use crate::config;
|
||||||
|
use crate::import::{Autojump, Import as _, Z};
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
|
use crate::store::Store;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use clap::{ArgEnum, Clap};
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Import entries from another database
|
||||||
|
#[derive(Clap, Debug)]
|
||||||
|
pub struct Import {
|
||||||
|
path: PathBuf,
|
||||||
|
|
||||||
|
/// Application to import from
|
||||||
|
#[clap(arg_enum, long, default_value = "z")]
|
||||||
|
from: From,
|
||||||
|
|
||||||
|
/// Merge into existing database
|
||||||
|
#[clap(long)]
|
||||||
|
merge: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cmd for Import {
|
||||||
|
fn run(&self) -> Result<()> {
|
||||||
|
let data_dir = config::zo_data_dir()?;
|
||||||
|
|
||||||
|
let mut store = Store::open(&data_dir)?;
|
||||||
|
if !self.merge && !store.dirs.is_empty() {
|
||||||
|
bail!("zoxide database is not empty, specify --merge to continue anyway")
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolve_symlinks = config::zo_resolve_symlinks();
|
||||||
|
match self.from {
|
||||||
|
From::Autojump => Autojump {
|
||||||
|
resolve_symlinks,
|
||||||
|
now: util::current_time()?,
|
||||||
|
}
|
||||||
|
.import(&mut store, &self.path),
|
||||||
|
From::Z => Z { resolve_symlinks }.import(&mut store, &self.path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ArgEnum, Debug)]
|
||||||
|
enum From {
|
||||||
|
Autojump,
|
||||||
|
Z,
|
||||||
|
}
|
95
src/cmd/init.rs
Normal file
95
src/cmd/init.rs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
use crate::cmd::Cmd;
|
||||||
|
use crate::config;
|
||||||
|
use crate::shell::{self, Hook, Opts};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use askama::Template;
|
||||||
|
use clap::{ArgEnum, Clap};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
|
/// Generates shell configuration
|
||||||
|
#[derive(Clap, Debug)]
|
||||||
|
#[clap(after_help(env_help()))]
|
||||||
|
pub struct Init {
|
||||||
|
#[clap(arg_enum)]
|
||||||
|
shell: Shell,
|
||||||
|
|
||||||
|
/// Prevents zoxide from defining any commands
|
||||||
|
#[clap(long)]
|
||||||
|
no_aliases: bool,
|
||||||
|
|
||||||
|
/// Renames the 'z' command and corresponding aliases
|
||||||
|
#[clap(long, default_value = "z")]
|
||||||
|
cmd: String,
|
||||||
|
|
||||||
|
/// Chooses event upon which an entry is added to the database
|
||||||
|
#[clap(arg_enum, long, default_value = "pwd")]
|
||||||
|
hook: Hook,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cmd for Init {
|
||||||
|
fn run(&self) -> Result<()> {
|
||||||
|
let cmd = if self.no_aliases {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self.cmd.as_str())
|
||||||
|
};
|
||||||
|
|
||||||
|
let echo = config::zo_echo();
|
||||||
|
let resolve_symlinks = config::zo_resolve_symlinks();
|
||||||
|
|
||||||
|
let opts = &Opts {
|
||||||
|
cmd,
|
||||||
|
hook: self.hook,
|
||||||
|
echo,
|
||||||
|
resolve_symlinks,
|
||||||
|
};
|
||||||
|
|
||||||
|
let source = match self.shell {
|
||||||
|
Shell::Bash => shell::Bash(opts).render(),
|
||||||
|
Shell::Fish => shell::Fish(opts).render(),
|
||||||
|
Shell::Posix => shell::Posix(opts).render(),
|
||||||
|
Shell::Powershell => shell::PowerShell(opts).render(),
|
||||||
|
Shell::Xonsh => shell::Xonsh(opts).render(),
|
||||||
|
Shell::Zsh => shell::Zsh(opts).render(),
|
||||||
|
}
|
||||||
|
.context("could not render template")?;
|
||||||
|
println!("{}", source);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ArgEnum, Debug)]
|
||||||
|
enum Shell {
|
||||||
|
Bash,
|
||||||
|
Fish,
|
||||||
|
Posix,
|
||||||
|
Powershell,
|
||||||
|
Xonsh,
|
||||||
|
Zsh,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_help() -> &'static str {
|
||||||
|
static ENV_HELP: OnceCell<String> = OnceCell::new();
|
||||||
|
ENV_HELP.get_or_init(|| {
|
||||||
|
const PATH_SPLIT_SEPARATOR: u8 = if cfg!(any(target_os = "redox", target_os = "windows")) {
|
||||||
|
b';'
|
||||||
|
} else {
|
||||||
|
b':'
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"\
|
||||||
|
ENVIRONMENT VARIABLES:
|
||||||
|
_ZO_DATA_DIR Path for zoxide data files
|
||||||
|
[current: {data_dir}]
|
||||||
|
_ZO_ECHO Prints the matched directory before navigating to it when set to 1
|
||||||
|
_ZO_EXCLUDE_DIRS List of directory globs to be excluded, separated by '{path_split_separator}'
|
||||||
|
_ZO_FZF_OPTS Custom flags to pass to fzf
|
||||||
|
_ZO_MAXAGE Maximum total age after which entries start getting deleted
|
||||||
|
_ZO_RESOLVE_SYMLINKS Resolve symlinks when storing paths",
|
||||||
|
data_dir=config::zo_data_dir().unwrap_or_else(|_| "none".into()).display(),
|
||||||
|
path_split_separator=PATH_SPLIT_SEPARATOR as char)
|
||||||
|
})
|
||||||
|
}
|
17
src/cmd/mod.rs
Normal file
17
src/cmd/mod.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
mod add;
|
||||||
|
mod import;
|
||||||
|
mod init;
|
||||||
|
mod query;
|
||||||
|
mod remove;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
pub use add::Add;
|
||||||
|
pub use import::Import;
|
||||||
|
pub use init::Init;
|
||||||
|
pub use query::Query;
|
||||||
|
pub use remove::Remove;
|
||||||
|
|
||||||
|
pub trait Cmd {
|
||||||
|
fn run(&self) -> Result<()>;
|
||||||
|
}
|
77
src/cmd/query.rs
Normal file
77
src/cmd/query.rs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
use crate::cmd::Cmd;
|
||||||
|
use crate::config;
|
||||||
|
use crate::fzf::Fzf;
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
|
use crate::store::{self, Store};
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use clap::Clap;
|
||||||
|
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
/// Searches for a directory
|
||||||
|
#[derive(Clap, Debug)]
|
||||||
|
pub struct Query {
|
||||||
|
keywords: Vec<String>,
|
||||||
|
|
||||||
|
/// Lists all matching directories
|
||||||
|
#[clap(long, short, conflicts_with = "list")]
|
||||||
|
interactive: bool,
|
||||||
|
|
||||||
|
/// Lists all matching directories
|
||||||
|
#[clap(long, short, conflicts_with = "interactive")]
|
||||||
|
list: bool,
|
||||||
|
|
||||||
|
/// Prints score with results
|
||||||
|
#[clap(long, short)]
|
||||||
|
score: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cmd for Query {
|
||||||
|
fn run(&self) -> Result<()> {
|
||||||
|
let data_dir = config::zo_data_dir()?;
|
||||||
|
let mut store = Store::open(&data_dir)?;
|
||||||
|
|
||||||
|
let query = store::Query::new(&self.keywords);
|
||||||
|
let now = util::current_time()?;
|
||||||
|
|
||||||
|
let mut matches = store.iter_matches(&query, now);
|
||||||
|
|
||||||
|
if self.interactive {
|
||||||
|
let mut fzf = Fzf::new()?;
|
||||||
|
let handle = fzf.stdin();
|
||||||
|
for dir in matches {
|
||||||
|
writeln!(handle, "{}", dir.display_score(now)).context("could not write to fzf")?;
|
||||||
|
}
|
||||||
|
let selection = fzf.wait_select()?;
|
||||||
|
if self.score {
|
||||||
|
print!("{}", selection);
|
||||||
|
} else {
|
||||||
|
let path = selection
|
||||||
|
.get(5..)
|
||||||
|
.context("could not read selection from fzf")?;
|
||||||
|
print!("{}", path)
|
||||||
|
}
|
||||||
|
} else if self.list {
|
||||||
|
let stdout = io::stdout();
|
||||||
|
let handle = &mut stdout.lock();
|
||||||
|
for dir in matches {
|
||||||
|
if self.score {
|
||||||
|
writeln!(handle, "{}", dir.display_score(now))
|
||||||
|
} else {
|
||||||
|
writeln!(handle, "{}", dir.display())
|
||||||
|
}
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let dir = matches.next().context("no match found")?;
|
||||||
|
if self.score {
|
||||||
|
println!("{}", dir.display_score(now))
|
||||||
|
} else {
|
||||||
|
println!("{}", dir.display())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
57
src/cmd/remove.rs
Normal file
57
src/cmd/remove.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
use crate::cmd::Cmd;
|
||||||
|
use crate::config;
|
||||||
|
use crate::fzf::Fzf;
|
||||||
|
use crate::store::Query;
|
||||||
|
use crate::store::Store;
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use clap::Clap;
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
/// Removes a directory
|
||||||
|
#[derive(Clap, Debug)]
|
||||||
|
pub struct Remove {
|
||||||
|
#[clap(conflicts_with = "path", long, short, value_name = "keywords")]
|
||||||
|
interactive: Option<Vec<String>>,
|
||||||
|
#[clap(
|
||||||
|
conflicts_with = "interactive",
|
||||||
|
required_unless_present = "interactive"
|
||||||
|
)]
|
||||||
|
path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cmd for Remove {
|
||||||
|
fn run(&self) -> Result<()> {
|
||||||
|
let data_dir = config::zo_data_dir()?;
|
||||||
|
let mut store = Store::open(&data_dir)?;
|
||||||
|
|
||||||
|
let selection;
|
||||||
|
let path = match &self.interactive {
|
||||||
|
Some(keywords) => {
|
||||||
|
let query = Query::new(keywords);
|
||||||
|
let now = util::current_time()?;
|
||||||
|
|
||||||
|
let mut fzf = Fzf::new()?;
|
||||||
|
let handle = fzf.stdin();
|
||||||
|
for dir in store.iter_matches(&query, now) {
|
||||||
|
writeln!(handle, "{}", dir.display_score(now))
|
||||||
|
.context("could not write to fzf")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
selection = fzf.wait_select()?;
|
||||||
|
selection
|
||||||
|
.get(5..selection.len().saturating_sub(1))
|
||||||
|
.context("fzf returned invalid output")?
|
||||||
|
}
|
||||||
|
None => self.path.as_ref().unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !store.remove(path) {
|
||||||
|
bail!("path not found in store: {}", &path)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
|
use crate::store::Rank;
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use dirs_next as dirs;
|
use dirs_next as dirs;
|
||||||
use zoxide_engine::dir::Rank;
|
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
@ -21,6 +21,13 @@ pub fn zo_data_dir() -> Result<PathBuf> {
|
|||||||
Ok(data_dir)
|
Ok(data_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn zo_echo() -> bool {
|
||||||
|
match env::var_os("_ZO_ECHO") {
|
||||||
|
Some(var) => var == "1",
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn zo_exclude_dirs() -> Result<Vec<glob::Pattern>> {
|
pub fn zo_exclude_dirs() -> Result<Vec<glob::Pattern>> {
|
||||||
match env::var_os("_ZO_EXCLUDE_DIRS") {
|
match env::var_os("_ZO_EXCLUDE_DIRS") {
|
||||||
Some(dirs_osstr) => env::split_paths(&dirs_osstr)
|
Some(dirs_osstr) => env::split_paths(&dirs_osstr)
|
||||||
@ -55,13 +62,6 @@ pub fn zo_maxage() -> Result<Rank> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn zo_echo() -> bool {
|
|
||||||
match env::var_os("_ZO_ECHO") {
|
|
||||||
Some(var) => var == "1",
|
|
||||||
None => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn zo_resolve_symlinks() -> bool {
|
pub fn zo_resolve_symlinks() -> bool {
|
||||||
match env::var_os("_ZO_RESOLVE_SYMLINKS") {
|
match env::var_os("_ZO_RESOLVE_SYMLINKS") {
|
||||||
Some(var) => var == "1",
|
Some(var) => var == "1",
|
||||||
|
12
src/error.rs
Normal file
12
src/error.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SilentExit {
|
||||||
|
pub code: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SilentExit {
|
||||||
|
fn fmt(&self, _: &mut Formatter) -> fmt::Result {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
57
src/fzf.rs
Normal file
57
src/fzf.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
use crate::config;
|
||||||
|
use crate::error::SilentExit;
|
||||||
|
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
|
||||||
|
use std::process::{Child, ChildStdin, Command, Stdio};
|
||||||
|
|
||||||
|
pub struct Fzf {
|
||||||
|
child: Child,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fzf {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
let mut command = Command::new("fzf");
|
||||||
|
command
|
||||||
|
.arg("-n2..")
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped());
|
||||||
|
|
||||||
|
if let Some(fzf_opts) = config::zo_fzf_opts() {
|
||||||
|
command.env("FZF_DEFAULT_OPTS", fzf_opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Fzf {
|
||||||
|
child: command.spawn().context("could not launch fzf")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stdin(&mut self) -> &mut ChildStdin {
|
||||||
|
self.child.stdin.as_mut().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait_select(self) -> Result<String> {
|
||||||
|
let output = self
|
||||||
|
.child
|
||||||
|
.wait_with_output()
|
||||||
|
.context("wait failed on fzf")?;
|
||||||
|
|
||||||
|
match output.status.code() {
|
||||||
|
// normal exit
|
||||||
|
Some(0) => String::from_utf8(output.stdout).context("invalid unicode in fzf output"),
|
||||||
|
|
||||||
|
// no match
|
||||||
|
Some(1) => bail!("no match found"),
|
||||||
|
|
||||||
|
// error
|
||||||
|
Some(2) => bail!("fzf returned an error"),
|
||||||
|
|
||||||
|
// terminated by a signal
|
||||||
|
Some(code @ 130) => bail!(SilentExit { code }),
|
||||||
|
Some(128..=254) | None => bail!("fzf was terminated"),
|
||||||
|
|
||||||
|
// unknown
|
||||||
|
_ => bail!("fzf returned an unknown error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
60
src/import/autojump.rs
Normal file
60
src/import/autojump.rs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
use super::Import;
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
|
use crate::store::{Dir, Epoch, Store};
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufRead, BufReader};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub struct Autojump {
|
||||||
|
pub resolve_symlinks: bool,
|
||||||
|
pub now: Epoch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Import for Autojump {
|
||||||
|
fn import<P: AsRef<Path>>(&self, store: &mut Store, path: P) -> Result<()> {
|
||||||
|
let file = File::open(path).context("could not open autojump database")?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
|
||||||
|
for (idx, line) in reader.lines().enumerate() {
|
||||||
|
(|| -> Result<()> {
|
||||||
|
let line = line?;
|
||||||
|
if line.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let split_idx = line
|
||||||
|
.find('\t')
|
||||||
|
.with_context(|| format!("invalid entry: {}", line))?;
|
||||||
|
let (rank, path) = line.split_at(split_idx);
|
||||||
|
|
||||||
|
let rank = rank
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("invalid rank: {}", rank))?;
|
||||||
|
|
||||||
|
let path = if self.resolve_symlinks {
|
||||||
|
util::canonicalize
|
||||||
|
} else {
|
||||||
|
util::resolve_path
|
||||||
|
}(&path)?;
|
||||||
|
let path = util::path_to_str(&path)?;
|
||||||
|
|
||||||
|
if store.dirs.iter_mut().find(|dir| dir.path == path).is_none() {
|
||||||
|
store.dirs.push(Dir {
|
||||||
|
path: path.into(),
|
||||||
|
rank,
|
||||||
|
last_accessed: self.now,
|
||||||
|
});
|
||||||
|
store.modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})()
|
||||||
|
.with_context(|| format!("line {}: error reading from z database", idx + 1))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
14
src/import/mod.rs
Normal file
14
src/import/mod.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
mod autojump;
|
||||||
|
mod z;
|
||||||
|
|
||||||
|
use crate::store::Store;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub use autojump::Autojump;
|
||||||
|
pub use z::Z;
|
||||||
|
|
||||||
|
pub trait Import {
|
||||||
|
fn import<P: AsRef<Path>>(&self, store: &mut Store, path: P) -> Result<()>;
|
||||||
|
}
|
71
src/import/z.rs
Normal file
71
src/import/z.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use super::Import;
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
|
use crate::store::{Dir, Store};
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufRead, BufReader};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub struct Z {
|
||||||
|
pub resolve_symlinks: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Import for Z {
|
||||||
|
fn import<P: AsRef<Path>>(&self, store: &mut Store, path: P) -> Result<()> {
|
||||||
|
let file = File::open(path).context("could not open z database")?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
|
||||||
|
for (idx, line) in reader.lines().enumerate() {
|
||||||
|
(|| -> Result<()> {
|
||||||
|
let line = line?;
|
||||||
|
if line.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (path, rank, last_accessed) = (|| {
|
||||||
|
let mut split = line.rsplitn(3, '|');
|
||||||
|
let last_accessed = split.next()?;
|
||||||
|
let rank = split.next()?;
|
||||||
|
let path = split.next()?;
|
||||||
|
Some((path, rank, last_accessed))
|
||||||
|
})()
|
||||||
|
.with_context(|| format!("invalid entry: {}", line))?;
|
||||||
|
|
||||||
|
let path = if self.resolve_symlinks {
|
||||||
|
util::canonicalize
|
||||||
|
} else {
|
||||||
|
util::resolve_path
|
||||||
|
}(&path)?;
|
||||||
|
let path = util::path_to_str(&path)?;
|
||||||
|
|
||||||
|
let rank = rank
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("invalid rank: {}", rank))?;
|
||||||
|
|
||||||
|
let last_accessed = last_accessed
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("invalid epoch: {}", last_accessed))?;
|
||||||
|
|
||||||
|
match store.dirs.iter_mut().find(|dir| dir.path == path) {
|
||||||
|
Some(dir) => {
|
||||||
|
dir.rank += rank;
|
||||||
|
dir.last_accessed = dir.last_accessed.max(last_accessed);
|
||||||
|
}
|
||||||
|
None => store.dirs.push(Dir {
|
||||||
|
path: path.into(),
|
||||||
|
rank,
|
||||||
|
last_accessed,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
store.modified = true;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})()
|
||||||
|
.with_context(|| format!("line {}: error reading from z database", idx + 1))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +0,0 @@
|
|||||||
pub mod config;
|
|
||||||
pub mod util;
|
|
243
src/main.rs
243
src/main.rs
@ -1,39 +1,20 @@
|
|||||||
use anyhow::{Context, Result};
|
mod cmd;
|
||||||
use clap::{AppSettings, ArgEnum, Clap};
|
mod config;
|
||||||
use once_cell::sync::OnceCell;
|
mod error;
|
||||||
use zoxide::{config, util};
|
mod fzf;
|
||||||
use zoxide_engine::{Dir, Query, Store};
|
mod import;
|
||||||
use zoxide_shell::{self as zs, Generator};
|
mod shell;
|
||||||
|
mod store;
|
||||||
|
mod util;
|
||||||
|
|
||||||
use std::io::{self, Write};
|
use crate::cmd::{Add, Cmd, Import, Init, Query, Remove};
|
||||||
use std::path::{Path, PathBuf};
|
use crate::error::SilentExit;
|
||||||
|
|
||||||
fn env_help() -> &'static str {
|
use anyhow::Result;
|
||||||
static ENV_HELP: OnceCell<String> = OnceCell::new();
|
use clap::{AppSettings, Clap};
|
||||||
ENV_HELP.get_or_init(|| {
|
|
||||||
const PATH_SPLIT_SEPARATOR: u8 = if cfg!(any(target_os = "redox", target_os = "windows")) {
|
|
||||||
b';'
|
|
||||||
} else {
|
|
||||||
b':'
|
|
||||||
};
|
|
||||||
|
|
||||||
format!(
|
use std::process;
|
||||||
"\
|
|
||||||
ENVIRONMENT VARIABLES:
|
|
||||||
_ZO_DATA_DIR Path for zoxide data files
|
|
||||||
[current: {data_dir}]
|
|
||||||
_ZO_ECHO Prints the matched directory before navigating to it when set to 1
|
|
||||||
_ZO_EXCLUDE_DIRS List of directory globs to be excluded, separated by '{split_paths_separator}'
|
|
||||||
_ZO_FZF_OPTS Custom flags to pass to fzf
|
|
||||||
_ZO_MAXAGE Maximum total age after which entries start getting deleted
|
|
||||||
_ZO_RESOLVE_SYMLINKS Resolve symlinks when storing paths",
|
|
||||||
data_dir=config::zo_data_dir().unwrap_or_else(|_| "none".into()).display(),
|
|
||||||
split_paths_separator=PATH_SPLIT_SEPARATOR as char)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: import
|
|
||||||
// TODO: query interactive
|
|
||||||
#[derive(Debug, Clap)]
|
#[derive(Debug, Clap)]
|
||||||
#[clap(
|
#[clap(
|
||||||
about,
|
about,
|
||||||
@ -42,191 +23,27 @@ ENVIRONMENT VARIABLES:
|
|||||||
global_setting(AppSettings::VersionlessSubcommands),
|
global_setting(AppSettings::VersionlessSubcommands),
|
||||||
version = env!("ZOXIDE_VERSION"))]
|
version = env!("ZOXIDE_VERSION"))]
|
||||||
enum Opts {
|
enum Opts {
|
||||||
/// Adds a new directory or increments its rank
|
Add(Add),
|
||||||
Add { path: Option<PathBuf> },
|
Import(Import),
|
||||||
|
Init(Init),
|
||||||
/// Generates shell configuration
|
Query(Query),
|
||||||
#[clap(after_help(env_help()))]
|
Remove(Remove),
|
||||||
Init {
|
|
||||||
#[clap(arg_enum)]
|
|
||||||
shell: Shell,
|
|
||||||
|
|
||||||
/// Prevents zoxide from defining any commands
|
|
||||||
#[clap(long)]
|
|
||||||
no_aliases: bool,
|
|
||||||
|
|
||||||
/// Renames the 'z' command and corresponding aliases
|
|
||||||
#[clap(long, default_value = "z")]
|
|
||||||
cmd: String,
|
|
||||||
|
|
||||||
/// Chooses event upon which an entry is added to the database
|
|
||||||
#[clap(arg_enum, long, default_value = "pwd")]
|
|
||||||
hook: Hook,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Searches for a directory
|
|
||||||
Query {
|
|
||||||
keywords: Vec<String>,
|
|
||||||
|
|
||||||
/// Lists all matching directories
|
|
||||||
#[clap(long, short)]
|
|
||||||
list: bool,
|
|
||||||
|
|
||||||
/// Prints score with results
|
|
||||||
#[clap(long, short)]
|
|
||||||
score: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Removes a directory
|
|
||||||
Remove { path: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(ArgEnum, Debug)]
|
|
||||||
enum Shell {
|
|
||||||
Bash,
|
|
||||||
Fish,
|
|
||||||
Posix,
|
|
||||||
Powershell,
|
|
||||||
Xonsh,
|
|
||||||
Zsh,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(ArgEnum, Debug)]
|
|
||||||
enum Hook {
|
|
||||||
None,
|
|
||||||
Prompt,
|
|
||||||
Pwd,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() -> Result<()> {
|
pub fn main() -> Result<()> {
|
||||||
let opts = Opts::parse();
|
let opts = Opts::parse();
|
||||||
|
|
||||||
match opts {
|
let result: Result<()> = match opts {
|
||||||
Opts::Add { path } => {
|
Opts::Add(cmd) => cmd.run(),
|
||||||
let path = match path {
|
Opts::Import(cmd) => cmd.run(),
|
||||||
Some(path) => {
|
Opts::Init(cmd) => cmd.run(),
|
||||||
if config::zo_resolve_symlinks() {
|
Opts::Query(cmd) => cmd.run(),
|
||||||
util::canonicalize(&path)
|
Opts::Remove(cmd) => cmd.run(),
|
||||||
} else {
|
};
|
||||||
util::resolve_path(&path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => util::current_dir(),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
if config::zo_exclude_dirs()?
|
result.map_err(|e| match e.downcast::<SilentExit>() {
|
||||||
.iter()
|
Ok(SilentExit { code }) => process::exit(code),
|
||||||
.any(|pattern| pattern.matches_path(&path))
|
// TODO: change the error prefix to `zoxide:`
|
||||||
{
|
Err(e) => e,
|
||||||
return Ok(());
|
})
|
||||||
}
|
|
||||||
|
|
||||||
let path = util::path_to_str(&path)?;
|
|
||||||
let now = util::current_time()?;
|
|
||||||
|
|
||||||
let data_dir = config::zo_data_dir()?;
|
|
||||||
let max_age = config::zo_maxage()?;
|
|
||||||
|
|
||||||
let mut store = Store::open(&data_dir)?;
|
|
||||||
store.add(path, now);
|
|
||||||
store.age(max_age);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
Opts::Init {
|
|
||||||
shell,
|
|
||||||
no_aliases,
|
|
||||||
cmd,
|
|
||||||
hook,
|
|
||||||
} => {
|
|
||||||
let cmd = if no_aliases { None } else { Some(cmd.as_str()) };
|
|
||||||
|
|
||||||
let hook = match hook {
|
|
||||||
Hook::None => zs::Hook::None,
|
|
||||||
Hook::Prompt => zs::Hook::Prompt,
|
|
||||||
Hook::Pwd => zs::Hook::Pwd,
|
|
||||||
};
|
|
||||||
|
|
||||||
let echo = config::zo_echo();
|
|
||||||
let resolve_symlinks = config::zo_resolve_symlinks();
|
|
||||||
|
|
||||||
let opts = &zs::Opts {
|
|
||||||
cmd,
|
|
||||||
hook,
|
|
||||||
echo,
|
|
||||||
resolve_symlinks,
|
|
||||||
};
|
|
||||||
|
|
||||||
let stdout = io::stdout();
|
|
||||||
let handle = &mut stdout.lock();
|
|
||||||
|
|
||||||
match shell {
|
|
||||||
Shell::Bash => zs::Bash(opts).generate(handle),
|
|
||||||
Shell::Fish => zs::Fish(opts).generate(handle),
|
|
||||||
Shell::Posix => zs::Posix(opts).generate(handle),
|
|
||||||
Shell::Powershell => zs::PowerShell(opts).generate(handle),
|
|
||||||
Shell::Xonsh => zs::Xonsh(opts).generate(handle),
|
|
||||||
Shell::Zsh => zs::Zsh(opts).generate(handle),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
Opts::Query {
|
|
||||||
keywords,
|
|
||||||
list,
|
|
||||||
score,
|
|
||||||
} => {
|
|
||||||
let data_dir = config::zo_data_dir()?;
|
|
||||||
let mut store = Store::open(&data_dir)?;
|
|
||||||
|
|
||||||
let query = Query::new(&keywords);
|
|
||||||
let now = util::current_time()?;
|
|
||||||
|
|
||||||
let stdout = io::stdout();
|
|
||||||
let mut handle = stdout.lock();
|
|
||||||
|
|
||||||
let mut print_dir = |dir: &Dir| {
|
|
||||||
if score {
|
|
||||||
let dir_score = dir.get_score(now);
|
|
||||||
let dir_score_clamped = if dir_score > 9999.0 {
|
|
||||||
9999
|
|
||||||
} else if dir_score > 0.0 {
|
|
||||||
dir_score as _
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
writeln!(&mut handle, "{:>4} {}", dir_score_clamped, dir.path)
|
|
||||||
} else {
|
|
||||||
writeln!(&mut handle, "{}", dir.path)
|
|
||||||
}
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut matches = store
|
|
||||||
.iter_matches(&query, now)
|
|
||||||
.filter(|dir| Path::new(&dir.path).is_dir());
|
|
||||||
|
|
||||||
if list {
|
|
||||||
for dir in matches {
|
|
||||||
print_dir(dir);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let dir = matches.next().context("no match found")?;
|
|
||||||
print_dir(dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
Opts::Remove { path } => {
|
|
||||||
let data_dir = config::zo_data_dir()?;
|
|
||||||
|
|
||||||
let mut store = Store::open(&data_dir)?;
|
|
||||||
store.remove(path);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
234
src/shell.rs
Normal file
234
src/shell.rs
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
use clap::ArgEnum;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Opts<'a> {
|
||||||
|
pub cmd: Option<&'a str>,
|
||||||
|
pub hook: Hook,
|
||||||
|
pub echo: bool,
|
||||||
|
pub resolve_symlinks: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Opts<'_> {
|
||||||
|
pub const DEVNULL: &'static str = if cfg!(windows) { "NUL" } else { "/dev/null" };
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! make_template {
|
||||||
|
($name:ident, $path:expr) => {
|
||||||
|
#[derive(::std::fmt::Debug, ::askama::Template)]
|
||||||
|
#[template(path = $path)]
|
||||||
|
pub struct $name<'a>(pub &'a self::Opts<'a>);
|
||||||
|
|
||||||
|
impl<'a> ::std::ops::Deref for $name<'a> {
|
||||||
|
type Target = self::Opts<'a>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
make_template!(Bash, "bash.txt");
|
||||||
|
make_template!(Fish, "fish.txt");
|
||||||
|
make_template!(Posix, "posix.txt");
|
||||||
|
make_template!(PowerShell, "powershell.txt");
|
||||||
|
make_template!(Xonsh, "xonsh.txt");
|
||||||
|
make_template!(Zsh, "zsh.txt");
|
||||||
|
|
||||||
|
#[derive(ArgEnum, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Hook {
|
||||||
|
None,
|
||||||
|
Prompt,
|
||||||
|
Pwd,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use askama::Template;
|
||||||
|
use assert_cmd::Command;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
|
fn opts() -> &'static [Opts<'static>] {
|
||||||
|
static OPTS: OnceCell<Vec<Opts>> = OnceCell::new();
|
||||||
|
OPTS.get_or_init(|| {
|
||||||
|
const BOOLS: &[bool] = &[false, true];
|
||||||
|
const HOOKS: &[Hook] = &[Hook::None, Hook::Prompt, Hook::Pwd];
|
||||||
|
const CMDS: &[Option<&str>] = &[None, Some("z")];
|
||||||
|
|
||||||
|
let mut opts = Vec::new();
|
||||||
|
for &echo in BOOLS {
|
||||||
|
for &resolve_symlinks in BOOLS {
|
||||||
|
for &hook in HOOKS {
|
||||||
|
for &cmd in CMDS {
|
||||||
|
let opt = Opts {
|
||||||
|
echo,
|
||||||
|
resolve_symlinks,
|
||||||
|
hook,
|
||||||
|
cmd,
|
||||||
|
};
|
||||||
|
opts.push(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opts
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bash() {
|
||||||
|
for opts in opts() {
|
||||||
|
let source = Bash(opts).render().unwrap();
|
||||||
|
Command::new("bash")
|
||||||
|
.args(&["-c", &source])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("")
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bash_posix() {
|
||||||
|
for opts in opts() {
|
||||||
|
let source = Posix(opts).render().unwrap();
|
||||||
|
let assert = Command::new("bash")
|
||||||
|
.args(&["--posix", "-c", &source])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stderr("");
|
||||||
|
|
||||||
|
if opts.hook != Hook::Pwd {
|
||||||
|
assert.stdout("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dash() {
|
||||||
|
for opts in opts() {
|
||||||
|
let source = Posix(opts).render().unwrap();
|
||||||
|
let assert = Command::new("bash")
|
||||||
|
.args(&["--posix", "-c", &source])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stderr("");
|
||||||
|
|
||||||
|
if opts.hook != Hook::Pwd {
|
||||||
|
assert.stdout("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fish() {
|
||||||
|
for opts in opts() {
|
||||||
|
let source = Fish(opts).render().unwrap();
|
||||||
|
Command::new("fish")
|
||||||
|
.args(&["-c", &source])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("")
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pwsh() {
|
||||||
|
for opts in opts() {
|
||||||
|
let source = PowerShell(opts).render().unwrap();
|
||||||
|
Command::new("pwsh")
|
||||||
|
.args(&["-c", &source])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("")
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_shellcheck_bash() {
|
||||||
|
for opts in opts() {
|
||||||
|
let source = Bash(opts).render().unwrap();
|
||||||
|
Command::new("shellcheck")
|
||||||
|
.args(&["--shell", "bash", "-"])
|
||||||
|
.write_stdin(source.as_bytes())
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("")
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_shellcheck_sh() {
|
||||||
|
for opts in opts() {
|
||||||
|
let source = Posix(opts).render().unwrap();
|
||||||
|
Command::new("shellcheck")
|
||||||
|
.args(&["--shell", "sh", "-"])
|
||||||
|
.write_stdin(source.as_bytes())
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("")
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_shfmt_bash() {
|
||||||
|
for opts in opts() {
|
||||||
|
let source = Bash(opts).render().unwrap();
|
||||||
|
Command::new("shfmt")
|
||||||
|
.args(&["-d", "-s", "-ln", "bash", "-i", "4", "-ci", "-"])
|
||||||
|
.write_stdin(source.as_bytes())
|
||||||
|
.write_stdin(b"\n".as_ref())
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("")
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_shfmt_posix() {
|
||||||
|
for opts in opts() {
|
||||||
|
let source = Posix(opts).render().unwrap();
|
||||||
|
Command::new("shfmt")
|
||||||
|
.args(&["-d", "-s", "-ln", "posix", "-i", "4", "-ci", "-"])
|
||||||
|
.write_stdin(source.as_bytes())
|
||||||
|
.write_stdin(b"\n".as_ref())
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("")
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_xonsh() {
|
||||||
|
for opts in opts() {
|
||||||
|
let source = Xonsh(opts).render().unwrap();
|
||||||
|
Command::new("xonsh")
|
||||||
|
.args(&["-c", &source])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("")
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_zsh() {
|
||||||
|
for opts in opts() {
|
||||||
|
let source = Zsh(opts).render().unwrap();
|
||||||
|
Command::new("zsh")
|
||||||
|
.args(&["-c", &source])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("")
|
||||||
|
.stderr("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
73
src/store/dir.rs
Normal file
73
src/store/dir.rs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
use super::{Epoch, Query, Rank};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Dir {
|
||||||
|
pub path: String,
|
||||||
|
pub rank: Rank,
|
||||||
|
pub last_accessed: Epoch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dir {
|
||||||
|
pub fn is_match(&self, query: &Query) -> bool {
|
||||||
|
query.matches(&self.path) && Path::new(&self.path).is_dir()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_score(&self, now: Epoch) -> Rank {
|
||||||
|
const HOUR: Epoch = 60 * 60;
|
||||||
|
const DAY: Epoch = 24 * HOUR;
|
||||||
|
const WEEK: Epoch = 7 * DAY;
|
||||||
|
|
||||||
|
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 fn display(&self) -> DirDisplay {
|
||||||
|
DirDisplay { dir: self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display_score(&self, now: Epoch) -> DirDisplayScore {
|
||||||
|
DirDisplayScore { dir: self, now }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DirDisplay<'a> {
|
||||||
|
dir: &'a Dir,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for DirDisplay<'_> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.dir.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DirDisplayScore<'a> {
|
||||||
|
dir: &'a Dir,
|
||||||
|
now: Epoch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for DirDisplayScore<'_> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
let score = self.dir.get_score(self.now);
|
||||||
|
let score = if score > 9999.0 {
|
||||||
|
9999
|
||||||
|
} else if score > 0.0 {
|
||||||
|
score as _
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
write!(f, "{:>4} {}", score, self.dir.path)
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
use crate::dir::{Dir, Epoch, Rank};
|
mod dir;
|
||||||
use crate::query::Query;
|
mod query;
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use bincode::Options;
|
use bincode::Options;
|
||||||
@ -12,6 +12,12 @@ use std::fs;
|
|||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
pub use dir::Dir;
|
||||||
|
pub use query::Query;
|
||||||
|
|
||||||
|
pub type Rank = f64;
|
||||||
|
pub type Epoch = u64;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Store {
|
pub struct Store {
|
||||||
pub dirs: Vec<Dir>,
|
pub dirs: Vec<Dir>,
|
||||||
@ -199,28 +205,84 @@ impl Drop for Store {
|
|||||||
#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
|
#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub struct StoreVersion(pub u32);
|
pub struct StoreVersion(pub u32);
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
fn persist<P: AsRef<Path>>(mut file: NamedTempFile, path: P) -> Result<(), PersistError> {
|
fn persist<P: AsRef<Path>>(mut file: NamedTempFile, path: P) -> Result<(), PersistError> {
|
||||||
if cfg!(windows) {
|
use rand::distributions::{Distribution, Uniform};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
// File renames on Windows are not atomic and sometimes fail with `PermissionDenied`.
|
// File renames on Windows are not atomic and sometimes fail with `PermissionDenied`.
|
||||||
// This is extremely unlikely unless it's running in a loop on multiple threads.
|
// This is extremely unlikely unless it's running in a loop on multiple threads.
|
||||||
// Nevertheless, we guard against it by retrying the rename a fixed number of times.
|
// Nevertheless, we guard against it by retrying the rename a fixed number of times.
|
||||||
const MAX_TRIES: usize = 10;
|
const MAX_TRIES: usize = 10;
|
||||||
for _ in 0..MAX_TRIES {
|
let mut rng = None;
|
||||||
match file.persist(&path) {
|
|
||||||
Ok(_) => break,
|
for _ in 0..MAX_TRIES {
|
||||||
Err(e) if e.error.kind() == io::ErrorKind::PermissionDenied => {
|
match file.persist(&path) {
|
||||||
file = e.file;
|
Ok(_) => break,
|
||||||
thread::sleep(Duration::from_millis(50));
|
Err(e) if e.error.kind() == io::ErrorKind::PermissionDenied => {
|
||||||
}
|
let mut rng = rng.get_or_insert_with(rand::thread_rng);
|
||||||
Err(e) => return Err(e),
|
let between = Uniform::from(50..150);
|
||||||
|
let duration = Duration::from_millis(between.sample(&mut rng));
|
||||||
|
thread::sleep(duration);
|
||||||
|
file = e.file;
|
||||||
}
|
}
|
||||||
|
Err(e) => return Err(e),
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
file.persist(&path)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn persist<P: AsRef<Path>>(file: NamedTempFile, path: P) -> Result<(), PersistError> {
|
||||||
|
file.persist(&path)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add() {
|
||||||
|
let path = "/foo/bar";
|
||||||
|
let now = 946684800;
|
||||||
|
|
||||||
|
let data_dir = tempfile::tempdir().unwrap();
|
||||||
|
{
|
||||||
|
let mut store = Store::open(data_dir.path()).unwrap();
|
||||||
|
store.add(path, now);
|
||||||
|
store.add(path, now);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let store = Store::open(data_dir.path()).unwrap();
|
||||||
|
assert_eq!(store.dirs.len(), 1);
|
||||||
|
|
||||||
|
let dir = &store.dirs[0];
|
||||||
|
assert_eq!(dir.path, path);
|
||||||
|
assert_eq!(dir.last_accessed, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_remove() {
|
||||||
|
let path = "/foo/bar";
|
||||||
|
let now = 946684800;
|
||||||
|
|
||||||
|
let data_dir = tempfile::tempdir().unwrap();
|
||||||
|
{
|
||||||
|
let mut store = Store::open(data_dir.path()).unwrap();
|
||||||
|
store.add(path, now);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut store = Store::open(data_dir.path()).unwrap();
|
||||||
|
assert!(store.remove(path));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut store = Store::open(data_dir.path()).unwrap();
|
||||||
|
assert!(store.dirs.is_empty());
|
||||||
|
assert!(!store.remove(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
src/util.rs
27
src/util.rs
@ -1,4 +1,4 @@
|
|||||||
use zoxide_engine::Epoch;
|
use crate::store::Epoch;
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
|
|
||||||
@ -35,7 +35,6 @@ pub fn path_to_str<P: AsRef<Path>>(path: &P) -> Result<&str> {
|
|||||||
/// If path is already absolute, the path is still processed to be cleaned, as it can contained ".." or "." (or other)
|
/// If path is already absolute, the path is still processed to be cleaned, as it can contained ".." or "." (or other)
|
||||||
/// character.
|
/// character.
|
||||||
/// If path is relative, use the current directory to build the absolute path.
|
/// If path is relative, use the current directory to build the absolute path.
|
||||||
#[cfg(any(unix, windows))]
|
|
||||||
pub fn resolve_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
|
pub fn resolve_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let base_path;
|
let base_path;
|
||||||
@ -44,18 +43,7 @@ pub fn resolve_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
|
|||||||
let mut stack = Vec::new();
|
let mut stack = Vec::new();
|
||||||
|
|
||||||
// initialize root
|
// initialize root
|
||||||
if cfg!(unix) {
|
if cfg!(windows) {
|
||||||
match components.peek() {
|
|
||||||
Some(Component::RootDir) => {
|
|
||||||
let root = components.next().unwrap();
|
|
||||||
stack.push(root);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
base_path = current_dir()?;
|
|
||||||
stack.extend(base_path.components());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if cfg!(windows) {
|
|
||||||
use std::path::Prefix;
|
use std::path::Prefix;
|
||||||
|
|
||||||
fn get_drive_letter<P: AsRef<Path>>(path: P) -> Option<u8> {
|
fn get_drive_letter<P: AsRef<Path>>(path: P) -> Option<u8> {
|
||||||
@ -133,6 +121,17 @@ pub fn resolve_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
|
|||||||
stack.extend(base_path.components());
|
stack.extend(base_path.components());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
match components.peek() {
|
||||||
|
Some(Component::RootDir) => {
|
||||||
|
let root = components.next().unwrap();
|
||||||
|
stack.push(root);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
base_path = current_dir()?;
|
||||||
|
stack.extend(base_path.components());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for component in components {
|
for component in components {
|
||||||
|
@ -74,6 +74,8 @@ function __zoxide_z() {
|
|||||||
echo "zoxide: \\$OLDPWD is not set"
|
echo "zoxide: \\$OLDPWD is not set"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
elif [ "$#" -eq 1 ] && [ -d "$1" ]; then
|
||||||
|
__zoxide_cd "$1"
|
||||||
else
|
else
|
||||||
local __zoxide_result
|
local __zoxide_result
|
||||||
__zoxide_result="$(zoxide query -- "$@")" && __zoxide_cd "$__zoxide_result"
|
__zoxide_result="$(zoxide query -- "$@")" && __zoxide_cd "$__zoxide_result"
|
||||||
@ -108,8 +110,7 @@ function __zoxide_zr() {
|
|||||||
|
|
||||||
# Remove an entry from the database using interactive selection.
|
# Remove an entry from the database using interactive selection.
|
||||||
function __zoxide_zri() {
|
function __zoxide_zri() {
|
||||||
local __zoxide_result
|
zoxide remove -i "$@"
|
||||||
__zoxide_result="$(zoxide query -i -- "$@")" && zoxide remove "$__zoxide_result"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{{ SECTION }}
|
{{ SECTION }}
|
@ -56,6 +56,8 @@ function __zoxide_z
|
|||||||
__zoxide_cd $HOME
|
__zoxide_cd $HOME
|
||||||
else if begin; test $argc -eq 1; and test $argv[1] = '-'; end
|
else if begin; test $argc -eq 1; and test $argv[1] = '-'; end
|
||||||
__zoxide_cd -
|
__zoxide_cd -
|
||||||
|
else if begin; test $argc -eq 1; and test -d $argv[1]; end
|
||||||
|
__zoxide_cd $argv[1]
|
||||||
else
|
else
|
||||||
set -l __zoxide_result (zoxide query -- $argv)
|
set -l __zoxide_result (zoxide query -- $argv)
|
||||||
and __zoxide_cd $__zoxide_result
|
and __zoxide_cd $__zoxide_result
|
||||||
@ -90,8 +92,7 @@ end
|
|||||||
|
|
||||||
# Remove an entry from the database using interactive selection.
|
# Remove an entry from the database using interactive selection.
|
||||||
function __zoxide_zri
|
function __zoxide_zri
|
||||||
set -l __zoxide_result (zoxide query -i -- $argv)
|
zoxide remove -i $argv
|
||||||
and zoxide remove $__zoxide_result
|
|
||||||
end
|
end
|
||||||
|
|
||||||
{{ SECTION }}
|
{{ SECTION }}
|
@ -77,6 +77,8 @@ __zoxide_z() {
|
|||||||
echo "zoxide: \\$OLDPWD is not set"
|
echo "zoxide: \\$OLDPWD is not set"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
elif [ "$#" -eq 1 ] && [ -d "$1" ]; then
|
||||||
|
__zoxide_cd "$1"
|
||||||
else
|
else
|
||||||
__zoxide_result="$(zoxide query -- "$@")" && __zoxide_cd "$__zoxide_result"
|
__zoxide_result="$(zoxide query -- "$@")" && __zoxide_cd "$__zoxide_result"
|
||||||
fi
|
fi
|
||||||
@ -109,7 +111,7 @@ __zoxide_zr() {
|
|||||||
|
|
||||||
# Remove an entry from the database using interactive selection.
|
# Remove an entry from the database using interactive selection.
|
||||||
__zoxide_zri() {
|
__zoxide_zri() {
|
||||||
__zoxide_result="$(zoxide query -i -- "$@")" && zoxide remove "$__zoxide_result"
|
zoxide remove -i "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
{{ SECTION }}
|
{{ SECTION }}
|
@ -65,6 +65,9 @@ function __zoxide_z {
|
|||||||
elseif ($args.Length -eq 1 -and $args[0] -eq '-') {
|
elseif ($args.Length -eq 1 -and $args[0] -eq '-') {
|
||||||
__zoxide_cd -
|
__zoxide_cd -
|
||||||
}
|
}
|
||||||
|
elseif ($args.Length -eq 1 -and ( Test-Path $args[0] -PathType Container) ) {
|
||||||
|
__zoxide_cd $args[0]
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
$__zoxide_result = zoxide query -- @args
|
$__zoxide_result = zoxide query -- @args
|
||||||
if ($LASTEXITCODE -eq 0) {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
@ -74,7 +77,7 @@ function __zoxide_z {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Jump to a directory using interactive search.
|
# Jump to a directory using interactive search.
|
||||||
function zi {
|
function __zoxide_zi {
|
||||||
$__zoxide_result = zoxide query -i -- @args
|
$__zoxide_result = zoxide query -i -- @args
|
||||||
if ($LASTEXITCODE -eq 0) {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
__zoxide_cd $__zoxide_result
|
__zoxide_cd $__zoxide_result
|
||||||
@ -103,10 +106,7 @@ function __zoxide_zr {
|
|||||||
|
|
||||||
# Remove an entry from the database using interactive selection.
|
# Remove an entry from the database using interactive selection.
|
||||||
function __zoxide_zri {
|
function __zoxide_zri {
|
||||||
$_zoxide_result = zoxide query -i -- @args
|
zoxide remove -i @args
|
||||||
if ($LASTEXITCODE -eq 0) {
|
|
||||||
zoxide remove $_zoxide_result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{{ SECTION }}
|
{{ SECTION }}
|
||||||
@ -136,11 +136,4 @@ Set-Alias {{cmd}}ri __zoxide_zri
|
|||||||
# To initialize zoxide with PowerShell, add the following line to your
|
# To initialize zoxide with PowerShell, add the following line to your
|
||||||
# PowerShell configuration file (the location is stored in $profile):
|
# PowerShell configuration file (the location is stored in $profile):
|
||||||
#
|
#
|
||||||
# Invoke-Expression (& {
|
# Invoke-Expression (& { $hook = if ($PSVersionTable.PSVersion.Major -ge 6) { 'pwd' } else { 'prompt' } (zoxide init powershell --hook $hook) -join "`n" })
|
||||||
# $hook = if ($PSVersionTable.PSVersion.Major -ge 6) {
|
|
||||||
# 'pwd'
|
|
||||||
# } else {
|
|
||||||
# 'prompt'
|
|
||||||
# }
|
|
||||||
# (zoxide init powershell --hook $hook) -join "`n"
|
|
||||||
# })
|
|
@ -105,19 +105,8 @@ def __zoxide_zr(args: [str]):
|
|||||||
zoxide remove @(args)
|
zoxide remove @(args)
|
||||||
|
|
||||||
# Remove an entry from the database using interactive selection.
|
# Remove an entry from the database using interactive selection.
|
||||||
def __zoxide_zri(keywords: [str]):
|
def __zoxide_zri(args: [str]):
|
||||||
try:
|
zoxide remove -i @(args)
|
||||||
__zoxide_cmd = subprocess.run(["zoxide", "query", "--"] + keywords, check=True, stdout=subprocess.PIPE)
|
|
||||||
except CalledProcessError as e:
|
|
||||||
return e.returncode
|
|
||||||
|
|
||||||
try:
|
|
||||||
__zoxide_result = __zoxide_cmd.stdout[:-1].decode("utf-8")
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
print(f"zoxide: invalid unicode in result: {__zoxide_result}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
zoxide remove @(__zoxide_result)
|
|
||||||
|
|
||||||
{{ SECTION }}
|
{{ SECTION }}
|
||||||
# Convenient aliases for zoxide. Disable these using --no-aliases.
|
# Convenient aliases for zoxide. Disable these using --no-aliases.
|
@ -95,8 +95,7 @@ function __zoxide_zr() {
|
|||||||
|
|
||||||
# Remove an entry from the database using interactive selection.
|
# Remove an entry from the database using interactive selection.
|
||||||
function __zoxide_zri() {
|
function __zoxide_zri() {
|
||||||
local __zoxide_result
|
zoxide remove -i "$@"
|
||||||
__zoxide_result="$(zoxide query -i -- "$@")" && zoxide remove "$__zoxide_result"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{{ SECTION }}
|
{{ SECTION }}
|
Loading…
Reference in New Issue
Block a user