Add interactive query/remove and import

This commit is contained in:
Ajeet D'Souza 2020-10-26 23:25:04 +05:30
parent bc17c25cf6
commit 5c3af59ba6
41 changed files with 1045 additions and 751 deletions

View File

@ -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 }}

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -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",
]

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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;

View File

@ -1,7 +0,0 @@
pub mod dir;
mod query;
mod store;
pub use dir::{Dir, Epoch};
pub use query::Query;
pub use store::Store;

View File

@ -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));
}
}

View File

@ -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

View File

@ -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"

View File

@ -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
}
}

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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(())
}
}

View File

@ -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
View 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
View 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
View 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
View 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
View 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(())
}
}

View File

@ -1,2 +0,0 @@
pub mod config;
pub mod util;

View File

@ -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
View 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
View 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)
}
}

View File

@ -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));
}
}
}

View File

@ -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 {

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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"
# })

View File

@ -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.

View File

@ -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 }}