diff --git a/.gitignore b/.gitignore index 1c137f9..bca6f9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,123 +1,7 @@ -# Created by https://www.gitignore.io/api/rust -# Edit at https://www.gitignore.io/?templates=rust - -### Rust ### # Generated by Cargo # will have compiled files and executables -/target/ +debug/ +target/ # These are backup files generated by rustfmt **/*.rs.bk - -# End of https://www.gitignore.io/api/rust - - -# Created by https://www.gitignore.io/api/python -# Edit at https://www.gitignore.io/?templates=python - -### Python ### -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# End of https://www.gitignore.io/api/python diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b51e5e..35918de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `zoxide init --no-aliases` no longer generates `z` or `zi`. +### Removed + +- Deprecated PWD hooks for POSIX shells. + ## [0.4.3] - 2020-07-04 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index ead7163..122b0bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,474 +1,619 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] -name = "ansi_term" -version = "0.11.0" +name = "anyhow" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1fd36ffbb1fb7c834eac128ea8d0e310c5aeb635548f9d58861e1308d46e71c" + +[[package]] +name = "askama" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e7ebd44d0047fd48206c83c5cd3214acc7b9d87f001da170145c47ef7d12" dependencies = [ - "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "askama_derive", + "askama_escape", + "askama_shared", ] [[package]] -name = "anyhow" -version = "1.0.32" +name = "askama_derive" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d7169690c4f56343dcd821ab834972a22570a2662a19a84fd7775d5e1c3881" +dependencies = [ + "askama_shared", + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "arrayref" -version = "0.3.6" +name = "askama_escape" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb" [[package]] -name = "arrayvec" -version = "0.5.1" +name = "askama_shared" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62fc272363345c8cdc030e4c259d9d028237f8b057dc9bb327772a257bde6bb5" +dependencies = [ + "askama_escape", + "nom", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "assert_cmd" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c88b9ca26f9c16ec830350d309397e74ee9abdfd8eb1f71cb6ecc71a3fc818da" +dependencies = [ + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.78 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "hermit-abi", + "libc", + "winapi", ] [[package]] name = "autocfg" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "bincode" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d" dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.116 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder", + "serde", ] [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "blake2b_simd" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "arrayref 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "constant_time_eq 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "byteorder" version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" [[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "2.33.3" +version = "3.0.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" dependencies = [ - "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", + "unicode-width", + "vec_map", ] [[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "crossbeam-utils" -version = "0.7.2" +name = "clap_derive" +version = "3.0.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" dependencies = [ - "autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "dirs" -version = "3.0.1" +name = "difference" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + +[[package]] +name = "dirs-next" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf36e65a80337bea855cd4ef9b8401ffce06a7baedf2e85ec467b1ac3f6e82b6" dependencies = [ - "dirs-sys 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 1.0.0", + "dirs-sys-next", ] [[package]] -name = "dirs-sys" -version = "0.3.5" +name = "dirs-sys-next" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99de365f605554ae33f115102a02057d4fc18b01f3284d6870be0938743cfe7d" dependencies = [ - "libc 0.2.78 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_users 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "redox_users", + "winapi", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "dunce" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "float-ord" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2641c4a7c0c4101df53ea572bffdc561c146f6c2eb09e4df02bc4811e3feeb4" [[package]] name = "getrandom" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.78 (registry+https://github.com/rust-lang/crates.io-index)", - "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10", + "libc", + "wasi", ] [[package]] name = "glob" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" [[package]] name = "heck" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" dependencies = [ - "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-segmentation", ] [[package]] name = "hermit-abi" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c30f6d0bc6b00693347368a67d41b58f2fb851215ff1da49e90fe2c5c667151" dependencies = [ - "libc 0.2.78 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", +] + +[[package]] +name = "indexmap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +dependencies = [ + "autocfg", + "hashbrown", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa7087f49d294270db4e1928fc110c976cd4b9e5a16348e0a1df09afa99e6c98" + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "num-traits" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad" + +[[package]] +name = "ordered-float" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fe9037165d7023b1228bc4ae9a2fa1a2b0095eca6c2998c624723dfd01314a5" +dependencies = [ + "num-traits", +] + +[[package]] +name = "os_str_bytes" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac6fe3538f701e339953a3ebbe4f39941aababa8a3f6964635b24ab526daeac" [[package]] name = "ppv-lite86" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" + +[[package]] +name = "predicates" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bfead12e90dccead362d62bb2c90a5f6fc4584963645bc7f71a735e0b0735a" +dependencies = [ + "difference", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06075c3a3e92559ff8929e7a280684489ea27fe44805174c3ebd9328dcb37178" + +[[package]] +name = "predicates-tree" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e63c4859013b38a76eca2414c64911fba30def9e3202ac461a2d22831220124" +dependencies = [ + "predicates-core", + "treeline", +] [[package]] name = "proc-macro-error" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ - "proc-macro-error-attr 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", - "version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", ] [[package]] name = "proc-macro-error-attr" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "version_check", ] [[package]] name = "proc-macro2" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" dependencies = [ - "unicode-xid 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid", ] [[package]] name = "quote" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" dependencies = [ - "proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", ] [[package]] name = "rand" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "getrandom 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.78 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", ] [[package]] name = "rand_chacha" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ - "ppv-lite86 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ppv-lite86", + "rand_core", ] [[package]] name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "getrandom 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom", ] [[package]] name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core", ] [[package]] name = "redox_syscall" version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_users" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" dependencies = [ - "getrandom 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.57 (registry+https://github.com/rust-lang/crates.io-index)", - "rust-argon2 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom", + "redox_syscall", ] [[package]] -name = "rust-argon2" -version = "0.8.2" +name = "remove_dir_all" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", - "blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)", - "constant_time_eq 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi", ] [[package]] name = "serde" version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5" dependencies = [ - "serde_derive 1.0.116 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8" dependencies = [ - "proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "strsim" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "structopt" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "structopt-derive 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "structopt-derive" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro-error 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9c5432ff16d6152371f808fb5a871cd67368171b09bb21b43df8e4a47a3556" dependencies = [ - "proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", ] [[package]] name = "textwrap" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" dependencies = [ - "unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width", ] +[[package]] +name = "treeline" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" + [[package]] name = "unicode-segmentation" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" [[package]] name = "unicode-width" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" [[package]] name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "uuid" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] name = "vec_map" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "zoxide" version = "0.4.3" dependencies = [ - "anyhow 1.0.32 (registry+https://github.com/rust-lang/crates.io-index)", - "bincode 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)", - "dirs 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "dunce 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "float-ord 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.116 (registry+https://github.com/rust-lang/crates.io-index)", - "structopt 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)", - "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow", + "clap", + "dirs-next", + "dunce", + "glob", + "once_cell", + "zoxide-engine", + "zoxide-shell", ] -[metadata] -"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" -"checksum anyhow 1.0.32 (registry+https://github.com/rust-lang/crates.io-index)" = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" -"checksum arrayref 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" -"checksum arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" -"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -"checksum autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -"checksum base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" -"checksum bincode 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d" -"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -"checksum blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" -"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" -"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" -"checksum clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)" = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" -"checksum constant_time_eq 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" -"checksum crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" -"checksum dirs 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" -"checksum dirs-sys 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" -"checksum dunce 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b2641c4a7c0c4101df53ea572bffdc561c146f6c2eb09e4df02bc4811e3feeb4" -"checksum float-ord 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e" -"checksum getrandom 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" -"checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" -"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" -"checksum hermit-abi 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c30f6d0bc6b00693347368a67d41b58f2fb851215ff1da49e90fe2c5c667151" -"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -"checksum libc 0.2.78 (registry+https://github.com/rust-lang/crates.io-index)" = "aa7087f49d294270db4e1928fc110c976cd4b9e5a16348e0a1df09afa99e6c98" -"checksum ppv-lite86 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" -"checksum proc-macro-error 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -"checksum proc-macro-error-attr 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -"checksum proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" -"checksum quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" -"checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -"checksum rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -"checksum redox_syscall 0.1.57 (registry+https://github.com/rust-lang/crates.io-index)" = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" -"checksum redox_users 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" -"checksum rust-argon2 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" -"checksum serde 1.0.116 (registry+https://github.com/rust-lang/crates.io-index)" = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5" -"checksum serde_derive 1.0.116 (registry+https://github.com/rust-lang/crates.io-index)" = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8" -"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" -"checksum structopt 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)" = "a33f6461027d7f08a13715659b2948e1602c31a3756aeae9378bfe7518c72e82" -"checksum structopt-derive 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c92e775028122a4b3dd55d58f14fc5120289c69bee99df1d117ae30f84b225c9" -"checksum syn 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)" = "9c51d92969d209b54a98397e1b91c8ae82d8c87a7bb87df0b29aa2ad81454228" -"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" -"checksum unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" -"checksum unicode-xid 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" -"checksum uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" -"checksum vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" -"checksum version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" -"checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -"checksum winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "zoxide-engine" +version = "0.1.0" +dependencies = [ + "anyhow", + "bincode", + "ordered-float", + "serde", + "tempfile", +] + +[[package]] +name = "zoxide-shell" +version = "0.1.0" +dependencies = [ + "anyhow", + "askama", + "assert_cmd", + "once_cell", +] diff --git a/Cargo.toml b/Cargo.toml index 1d5d68a..dff2544 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,24 +5,21 @@ authors = ["Ajeet D'Souza <98ajeet@gmail.com>"] description = "A faster way to navigate your filesystem" repository = "https://github.com/ajeetdsouza/zoxide/" edition = "2018" - keywords = ["cli"] categories = ["command-line-utilities", "filesystem"] license = "MIT" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -anyhow = "1.0.28" -bincode = "1.2.1" -clap = "2.33.0" -dirs = "3.0.0" -dunce = "1.0.0" -float-ord = "0.2.0" +anyhow = "1.0.32" +clap = "3.0.0-beta.2" +dirs-next = "1.0.2" +dunce = "1.0.1" glob = "0.3.0" -serde = { version = "1.0.106", features = ["derive"] } -structopt = "0.3.12" -uuid = { version = "0.8.1", features = ["v4"] } +once_cell = "1.4.1" +zoxide-engine = { path = "crates/zoxide-engine" } +zoxide-shell = { path = "crates/zoxide-shell" } + +[workspace] [profile.release] codegen-units = 1 diff --git a/build.rs b/build.rs index b9ae403..119fbfa 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,7 @@ +use std::process::Command; + fn main() { - let git_describe = std::process::Command::new("git") + let git_describe = Command::new("git") .args(&["describe", "--tags", "--broken"]) .output() .ok() diff --git a/crates/zoxide-engine/.gitignore b/crates/zoxide-engine/.gitignore new file mode 100644 index 0000000..ff47c2d --- /dev/null +++ b/crates/zoxide-engine/.gitignore @@ -0,0 +1,11 @@ +# 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 diff --git a/crates/zoxide-engine/Cargo.toml b/crates/zoxide-engine/Cargo.toml new file mode 100644 index 0000000..4d5e077 --- /dev/null +++ b/crates/zoxide-engine/Cargo.toml @@ -0,0 +1,14 @@ +[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" diff --git a/crates/zoxide-engine/src/dir.rs b/crates/zoxide-engine/src/dir.rs new file mode 100644 index 0000000..c613c30 --- /dev/null +++ b/crates/zoxide-engine/src/dir.rs @@ -0,0 +1,42 @@ +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 - 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 = i64; // use a signed integer so subtraction can be performed on it diff --git a/crates/zoxide-engine/src/lib.rs b/crates/zoxide-engine/src/lib.rs new file mode 100644 index 0000000..4e2622c --- /dev/null +++ b/crates/zoxide-engine/src/lib.rs @@ -0,0 +1,7 @@ +pub mod dir; +mod query; +mod store; + +pub use dir::{Dir, Epoch}; +pub use query::Query; +pub use store::Store; diff --git a/crates/zoxide-engine/src/query.rs b/crates/zoxide-engine/src/query.rs new file mode 100644 index 0000000..abca62c --- /dev/null +++ b/crates/zoxide-engine/src/query.rs @@ -0,0 +1,100 @@ +use std::path::Path; + +pub struct Query(Vec); + +impl Query { + pub fn new(keywords: I) -> Query + where + I: IntoIterator, + S: AsRef, + { + Query( + keywords + .into_iter() + .map(|s: S| s.as_ref().to_lowercase()) + .collect(), + ) + } + + pub fn keywords(&self) -> &[String] { + &self.0 + } + + pub fn matches>(&self, path: S) -> bool { + let path = path.as_ref().to_lowercase(); + let keywords = self.keywords(); + + let get_filenames = || { + let query_name = Path::new(keywords.last()?).file_name()?.to_str().unwrap(); + let dir_name = Path::new(&path).file_name()?.to_str().unwrap(); + Some((query_name, dir_name)) + }; + + if let Some((query_name, dir_name)) = get_filenames() { + if !dir_name.contains(query_name) { + return false; + } + } + + let mut subpath = path.as_str(); + + for keyword in keywords.iter() { + match subpath.find(keyword) { + Some(idx) => subpath = &subpath[idx + keyword.len()..], + None => return false, + } + } + + true + } +} + +#[cfg(test)] +mod tests { + use super::Query; + + #[test] + fn test_query_normalization() { + assert!(Query::new(&["fOo", "bAr"]).matches("/foo/bar")); + } + + #[test] + fn test_query_filename() { + assert!(Query::new(&["ba"]).matches("/foo/bar")); + } + + #[test] + fn test_query_not_filename() { + assert!(!Query::new(&["fo"]).matches("/foo/bar")); + } + + #[test] + fn test_query_not_filename_slash() { + assert!(!Query::new(&["foo/"]).matches("/foo/bar")); + } + + #[test] + fn test_query_path_separator() { + assert!(Query::new(&["/", "fo", "/", "ar"]).matches("/foo/bar")); + } + + #[test] + fn test_query_path_separator_between() { + assert!(Query::new(&["oo/ba"]).matches("/foo/bar")); + } + + #[test] + fn test_query_overlap_text() { + assert!(!Query::new(&["foo", "o", "bar"]).matches("/foo/bar")); + } + + #[test] + fn test_query_overlap_slash() { + assert!(!Query::new(&["/foo/", "/bar"]).matches("/foo/bar")); + } + + #[test] + fn test_query_consecutive_slash() { + assert!(Query::new(&["/foo/", "/baz"]).matches("/foo/bar/baz")); + } +} diff --git a/crates/zoxide-engine/src/store.rs b/crates/zoxide-engine/src/store.rs new file mode 100644 index 0000000..ab2183c --- /dev/null +++ b/crates/zoxide-engine/src/store.rs @@ -0,0 +1,186 @@ +use crate::dir::{Dir, Epoch, Rank}; +use crate::query::Query; + +use anyhow::{bail, Context, Result}; +use bincode::Options; +use ordered_float::OrderedFloat; +use serde::{Deserialize, Serialize}; +use tempfile::NamedTempFile; + +use std::cmp::Reverse; +use std::fs; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; + +#[derive(Debug)] +pub struct Store { + pub dirs: Vec, + pub modified: bool, + data_dir: PathBuf, +} + +impl Store { + pub const CURRENT_VERSION: StoreVersion = StoreVersion(3); + const MAX_SIZE: u64 = 8 * 1024 * 1024; // 8 MiB + + pub fn open>(data_dir: P) -> Result { + let data_dir = data_dir.into(); + let path = Self::get_path(&data_dir); + + let buffer = match fs::read(&path) { + Ok(buffer) => buffer, + Err(e) if e.kind() == io::ErrorKind::NotFound => { + fs::create_dir_all(&data_dir).with_context(|| { + format!("unable to create data directory: {}", path.display()) + })?; + return Ok(Store { + dirs: Vec::new(), + modified: false, + data_dir, + }); + } + Err(e) => { + Err(e).with_context(|| format!("could not read from store: {}", path.display()))? + } + }; + + let deserializer = &mut bincode::options() + .with_fixint_encoding() + .with_limit(Self::MAX_SIZE); + + let version_size = deserializer + .serialized_size(&Self::CURRENT_VERSION) + .unwrap() as _; + + if buffer.len() < version_size { + bail!("data store may be corrupted: {}", path.display()); + } + + let (buffer_version, buffer_dirs) = buffer.split_at(version_size); + + let version = deserializer + .deserialize(buffer_version) + .with_context(|| format!("could not deserialize store version: {}", path.display()))?; + + let dirs = match version { + Self::CURRENT_VERSION => deserializer + .deserialize(buffer_dirs) + .with_context(|| format!("could not deserialize store: {}", path.display()))?, + version => bail!( + "unsupported store version, got={}, supported={}: {}", + version.0, + Self::CURRENT_VERSION.0, + path.display() + ), + }; + + Ok(Store { + dirs, + modified: false, + data_dir, + }) + } + + pub fn save(&mut self) -> Result<()> { + if !self.modified { + return Ok(()); + } + + let (buffer, buffer_size) = (|| -> bincode::Result<_> { + let version_size = bincode::serialized_size(&Self::CURRENT_VERSION)?; + let dirs_size = bincode::serialized_size(&self.dirs)?; + + let buffer_size = version_size + dirs_size; + let mut buffer = Vec::with_capacity(buffer_size as _); + + bincode::serialize_into(&mut buffer, &Self::CURRENT_VERSION)?; + bincode::serialize_into(&mut buffer, &self.dirs)?; + + Ok((buffer, buffer_size)) + })() + .context("could not serialize store")?; + + let mut file = NamedTempFile::new_in(&self.data_dir).unwrap(); + let _ = file.as_file().set_len(buffer_size); + file.write_all(&buffer).unwrap(); + file.persist(Self::get_path(&self.data_dir)).unwrap(); + + self.modified = false; + Ok(()) + } + + pub fn add>(&mut self, path: S, now: Epoch) { + let path = path.as_ref(); + debug_assert!(Path::new(path).is_absolute()); + + match self.dirs.iter_mut().find(|dir| dir.path == path) { + None => self.dirs.push(Dir { + path: path.into(), + last_accessed: now, + rank: 1.0, + }), + Some(dir) => { + dir.last_accessed = now; + dir.rank += 1.0; + } + }; + + self.modified = true; + } + + pub fn iter_matches<'a>( + &'a mut self, + query: &'a Query, + now: Epoch, + ) -> impl DoubleEndedIterator { + self.dirs + .sort_unstable_by_key(|dir| Reverse(OrderedFloat(dir.get_score(now)))); + self.dirs.iter().filter(move |dir| dir.is_match(&query)) + } + + pub fn remove>(&mut self, path: S) -> bool { + let path = path.as_ref(); + + if let Some(idx) = self.dirs.iter().position(|dir| dir.path == path) { + self.dirs.swap_remove(idx); + self.modified = true; + return true; + } + + false + } + + pub fn age(&mut self, max_age: Rank) { + let sum_age = self.dirs.iter().map(|dir| dir.rank).sum::(); + + if sum_age > max_age { + let factor = 0.9 * max_age / sum_age; + + for idx in (0..self.dirs.len()).rev() { + let dir = &mut self.dirs[idx]; + + dir.rank *= factor; + if dir.rank < 1.0 { + self.dirs.swap_remove(idx); + } + } + + self.modified = true; + } + } + + fn get_path>(data_dir: P) -> PathBuf { + data_dir.as_ref().join("db.zo") + } +} + +impl Drop for Store { + fn drop(&mut self) { + if let Err(e) = self.save() { + println!("Error: {}", e) + } + } +} + +#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct StoreVersion(pub u32); diff --git a/crates/zoxide-engine/tests/store.rs b/crates/zoxide-engine/tests/store.rs new file mode 100644 index 0000000..a271b61 --- /dev/null +++ b/crates/zoxide-engine/tests/store.rs @@ -0,0 +1,43 @@ +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)); + } +} diff --git a/crates/zoxide-shell/.gitignore b/crates/zoxide-shell/.gitignore new file mode 100644 index 0000000..ff47c2d --- /dev/null +++ b/crates/zoxide-shell/.gitignore @@ -0,0 +1,11 @@ +# 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 diff --git a/crates/zoxide-shell/Cargo.toml b/crates/zoxide-shell/Cargo.toml new file mode 100644 index 0000000..2713ad5 --- /dev/null +++ b/crates/zoxide-shell/Cargo.toml @@ -0,0 +1,15 @@ +[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" diff --git a/crates/zoxide-shell/src/lib.rs b/crates/zoxide-shell/src/lib.rs new file mode 100644 index 0000000..69bf3a4 --- /dev/null +++ b/crates/zoxide-shell/src/lib.rs @@ -0,0 +1,97 @@ +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(&self, writer: &mut W) -> Result<()>; +} + +impl Generator for T { + fn generate(&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, +} + +#[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 + } +} diff --git a/crates/zoxide-shell/templates/bash.txt b/crates/zoxide-shell/templates/bash.txt new file mode 100644 index 0000000..1b16d36 --- /dev/null +++ b/crates/zoxide-shell/templates/bash.txt @@ -0,0 +1,142 @@ +{%- let SECTION = "# =============================================================================\n#" -%} +{%- let NOT_CONFIGURED = "# -- not configured --" -%} + +{{ SECTION }} +# Utility functions for zoxide. +# + +# pwd based on the value of _ZO_RESOLVE_SYMLINKS. +function __zoxide_pwd() { +{%- if resolve_symlinks %} + pwd -P +{%- else %} + pwd -L +{%- endif %} +} + +# cd + custom logic based on the value of _ZO_ECHO. +function __zoxide_cd() { + # shellcheck disable=SC2164 + cd "$@" {%- if echo %} && __zoxide_pwd {%- endif %} +} + +{{ SECTION }} +# Hook configuration for zoxide. +# + +# Hook to add new entries to the database. +{%- match hook %} +{%- when Hook::None %} +{{ NOT_CONFIGURED }} + +{%- when Hook::Prompt %} +function __zoxide_hook() { + zoxide add "$(__zoxide_pwd)" +} + +{%- when Hook::Pwd %} +function __zoxide_hook() { + local -r __zoxide_pwd_tmp="$(__zoxide_pwd)" + if [ -z "$__zoxide_pwd_old" ]; then + __zoxide_pwd_old="$__zoxide_pwd_tmp" + elif [ "$__zoxide_pwd_old" != "$__zoxide_pwd_tmp" ]; then + __zoxide_pwd_old="$__zoxide_pwd_tmp" + zoxide add "$__zoxide_pwd_old" + fi +} +{%- endmatch %} + +# Initialize hook. +{%- if hook == Hook::None %} +{{ NOT_CONFIGURED }} + +{%- else %} +case "$PROMPT_COMMAND" in + *__zoxide_hook*) ;; + *) PROMPT_COMMAND="${PROMPT_COMMAND:+${PROMPT_COMMAND};}__zoxide_hook" ;; +esac + +{%- endif %} + +{{ SECTION }} +# When using zoxide with --no-aliases, alias these internal functions as +# desired. +# + +# Jump to a directory using only keywords. +function __zoxide_z() { + if [ "$#" -eq 0 ]; then + __zoxide_cd ~ + elif [ "$#" -eq 1 ] && [ "$1" = '-' ]; then + if [ -n "$OLDPWD" ]; then + __zoxide_cd "$OLDPWD" + else + echo "zoxide: \\$OLDPWD is not set" + return 1 + fi + else + local __zoxide_result + __zoxide_result="$(zoxide query -- "$@")" && __zoxide_cd "$__zoxide_result" + fi +} + +# Jump to a directory using interactive search. +function __zoxide_zi() { + local __zoxide_result + __zoxide_result="$(zoxide query -i -- "$@")" && __zoxide_cd "$__zoxide_result" +} + +# Add a new entry to the database. +function __zoxide_za() { + zoxide add "$@" +} + +# Query an entry from the database using only keywords. +function __zoxide_zq() { + zoxide query "$@" +} + +# Query an entry from the database using interactive selection. +function __zoxide_zqi() { + zoxide query -i "$@" +} + +# Remove an entry from the database using the exact path. +function __zoxide_zr() { + zoxide remove "$@" +} + +# Remove an entry from the database using interactive selection. +function __zoxide_zri() { + local __zoxide_result + __zoxide_result="$(zoxide query -i -- "$@")" && zoxide remove "$__zoxide_result" +} + +{{ SECTION }} +# Convenient aliases for zoxide. Disable these using --no-aliases. +# + +{%- match cmd %} +{%- when Some with (cmd) %} + +alias {{cmd}}='__zoxide_z' +alias {{cmd}}i='__zoxide_zi' + +alias {{cmd}}a='__zoxide_za' + +alias {{cmd}}q='__zoxide_zq' +alias {{cmd}}qi='__zoxide_zqi' + +alias {{cmd}}r='__zoxide_zr' +alias {{cmd}}ri='__zoxide_zri' + +{%- when None %} +{{ NOT_CONFIGURED }} + +{%- endmatch %} + +{{ SECTION }} +# To initialize zoxide with bash, add the following line to your bash +# configuration file (usually ~/.bashrc): +# +# eval "$(zoxide init bash)" diff --git a/crates/zoxide-shell/templates/fish.txt b/crates/zoxide-shell/templates/fish.txt new file mode 100644 index 0000000..4340f88 --- /dev/null +++ b/crates/zoxide-shell/templates/fish.txt @@ -0,0 +1,139 @@ +{%- let SECTION = "# =============================================================================\n#" -%} +{%- let NOT_CONFIGURED = "# -- not configured --" -%} + +{{ SECTION }} +# Utility functions for zoxide. +# + +# pwd based on the value of _ZO_RESOLVE_SYMLINKS. +function __zoxide_pwd +{%- if resolve_symlinks %} + pwd -P +{%- else %} + pwd -L +{%- endif %} +end + +# cd + custom logic based on the value of _ZO_ECHO. +function __zoxide_cd + cd $argv + {%- if echo %} + and __zoxide_pwd + {%- endif %} + and commandline -f repaint +end + +{{ SECTION }} +# Hook configuration for zoxide. +# + +# Initialize hook to add new entries to the database. +{%- match hook %} +{%- when Hook::None %} +function __zoxide_hook + +{%- when Hook::Prompt %} +function __zoxide_hook --on-event fish_prompt + +{%- when Hook::Pwd %} +function __zoxide_hook --on-variable PWD + +{%- endmatch %} + zoxide add (__zoxide_pwd) +end + +{{ SECTION }} +# When using zoxide with --no-aliases, alias these internal functions as +# desired. +# + +# Jump to a directory using only keywords. +function __zoxide_z + set argc (count $argv) + if test $argc -eq 0 + __zoxide_cd $HOME + else if begin; test $argc -eq 1; and test $argv[1] = '-'; end + __zoxide_cd - + else + set -l __zoxide_result (zoxide query -- $argv) + and __zoxide_cd $__zoxide_result + end +end + +# Jump to a directory using interactive search. +function __zoxide_zi + set -l __zoxide_result (zoxide query -i -- $argv) + and __zoxide_cd $__zoxide_result +end + +# Add a new entry to the database. +function __zoxide_za + zoxide add $argv +end + +# Query an entry from the database using only keywords. +function __zoxide_zq + zoxide query $argv +end + +# Query an entry from the database using interactive selection. +function __zoxide_zqi + zoxide query -i $argv +end + +# Remove an entry from the database using the exact path. +function __zoxide_zr + zoxide remove $argv +end + +# Remove an entry from the database using interactive selection. +function __zoxide_zri + set -l __zoxide_result (zoxide query -i -- $argv) + and zoxide remove $__zoxide_result +end + +{{ SECTION }} +# Convenient aliases for zoxide. Disable these using --no-aliases. +# + +{%- match cmd %} +{%- when Some with (cmd) %} + +function {{cmd}} + __zoxide_z $argv +end + +function {{cmd}}i + __zoxide_zi $argv +end + +function {{cmd}}a + __zoxide_za $argv +end + +function {{cmd}}q + __zoxide_zq $argv +end + +function {{cmd}}qi + __zoxide_zqi $argv +end + +function {{cmd}}r + __zoxide_zr $argv +end + +function {{cmd}}ri + __zoxide_zri $argv +end + +{%- when None %} +{{ NOT_CONFIGURED }} + +{%- endmatch %} + +{{ SECTION }} +# To initialize zoxide with fish, add the following line to your fish +# configuration file (usually ~/.config/fish/config.fish): +# +# zoxide init fish | source diff --git a/crates/zoxide-shell/templates/posix.txt b/crates/zoxide-shell/templates/posix.txt new file mode 100644 index 0000000..0210f26 --- /dev/null +++ b/crates/zoxide-shell/templates/posix.txt @@ -0,0 +1,142 @@ +{%- let SECTION = "# =============================================================================\n#" -%} +{%- let NOT_CONFIGURED = "# -- not configured --" -%} + +{% if hook == Hook::Pwd -%} +echo "\ +zoxide: PWD hooks are not supported on POSIX shells. + Use '--hook prompt' when initializing zoxide." +{%- endif %} + +{{ SECTION }} +# Utility functions for zoxide. +# + +# pwd based on the value of _ZO_RESOLVE_SYMLINKS. +__zoxide_pwd() { +{%- if resolve_symlinks %} + pwd -P +{%- else %} + pwd -L +{%- endif %} +} + +# cd + custom logic based on the value of _ZO_ECHO. +__zoxide_cd() { + # shellcheck disable=SC2164 + cd "$@" {%- if echo %} && __zoxide_pwd {%- endif %} +} + +{{ SECTION }} +# Hook configuration for zoxide. +# + +# Hook to add new entries to the database. +{%- match hook %} +{%- when Hook::None %} +{{ NOT_CONFIGURED }} + +{%- when Hook::Prompt %} +__zoxide_hook() { + zoxide add "$(__zoxide_pwd)" +} + +{%- when Hook::Pwd %} +{{ NOT_CONFIGURED }} + +{%- endmatch %} + +# Initialize hook. +{%- match hook %} +{%- when Hook::None %} +{{ NOT_CONFIGURED }} + +{%- when Hook::Prompt %} +case "$PS1" in + *\$\(__zoxide_hook\)*) ;; + *) PS1="${PS1}\$(__zoxide_hook)" ;; +esac + +{%- when Hook::Pwd %} +{{ NOT_CONFIGURED }} + +{%- endmatch %} + +{{ SECTION }} +# When using zoxide with --no-aliases, alias these internal functions as +# desired. +# + +# Jump to a directory using only keywords. +__zoxide_z() { + if [ "$#" -eq 0 ]; then + __zoxide_cd ~ + elif [ "$#" -eq 1 ] && [ "$1" = '-' ]; then + if [ -n "$OLDPWD" ]; then + __zoxide_cd "$OLDPWD" + else + echo "zoxide: \\$OLDPWD is not set" + return 1 + fi + else + __zoxide_result="$(zoxide query -- "$@")" && __zoxide_cd "$__zoxide_result" + fi +} + +# Jump to a directory using interactive search. +__zoxide_zi() { + __zoxide_result="$(zoxide query -i -- "$@")" && __zoxide_cd "$__zoxide_result" +} + +# Add a new entry to the database. +__zoxide_za() { + zoxide add "$@" +} + +# Query an entry from the database using only keywords. +__zoxide_zq() { + zoxide query "$@" +} + +# Query an entry from the database using interactive selection. +__zoxide_zqi() { + zoxide query -i "$@" +} + +# Remove an entry from the database using the exact path. +__zoxide_zr() { + zoxide remove "$@" +} + +# Remove an entry from the database using interactive selection. +__zoxide_zri() { + __zoxide_result="$(zoxide query -i -- "$@")" && zoxide remove "$__zoxide_result" +} + +{{ SECTION }} +# Convenient aliases for zoxide. Disable these using --no-aliases. +# + +{%- match cmd %} +{%- when Some with (cmd) %} + +alias {{cmd}}='__zoxide_z' +alias {{cmd}}i='__zoxide_zi' + +alias {{cmd}}a='__zoxide_za' + +alias {{cmd}}q='__zoxide_zq' +alias {{cmd}}qi='__zoxide_zqi' + +alias {{cmd}}r='__zoxide_zr' +alias {{cmd}}ri='__zoxide_zri' + +{%- when None %} +{{ NOT_CONFIGURED }} + +{%- endmatch %} + +{{ SECTION }} +# To initialize zoxide with your POSIX shell, add the following line to your +# shell configuration file: +# +# eval "$(zoxide init posix --hook prompt)" diff --git a/crates/zoxide-shell/templates/powershell.txt b/crates/zoxide-shell/templates/powershell.txt new file mode 100644 index 0000000..6dfb2de --- /dev/null +++ b/crates/zoxide-shell/templates/powershell.txt @@ -0,0 +1,146 @@ +{%- let SECTION = "# =============================================================================\n#" -%} +{%- let NOT_CONFIGURED = "# -- not configured --" -%} + +{{ SECTION }} +# Utility functions for zoxide. +# + +# pwd based on the value of _ZO_RESOLVE_SYMLINKS. +function __zoxide_pwd { + $(Get-Location).Path +} + +# cd + custom logic based on the value of _ZO_ECHO. +function __zoxide_cd($dir) { + Set-Location $dir -ea Stop + {%- if echo %} + __zoxide_pwd + {%- endif %} +} + +{{ SECTION }} +# Hook configuration for zoxide. +# + +# Hook to add new entries to the database. +function __zoxide_hook { + zoxide add $(__zoxide_pwd) +} + +# Initialize hook. +{%- match hook %} +{%- when Hook::None %} +{{ NOT_CONFIGURED }} + +{%- when Hook::Prompt %} +$PreZoxidePrompt = $function:prompt +function prompt { + $null = __zoxide_hook + & $PreZoxidePrompt +} + +{%- when Hook::Pwd %} +if ($PSVersionTable.PSVersion.Major -ge 6) { + $ExecutionContext.InvokeCommand.LocationChangedAction = { + $null = __zoxide_hook + } +} else { + Write-Error "` +zoxide: PWD hooks are not supported below PowerShell 6. + Use '--hook prompt' when initializing zoxide." +} + +{%- endmatch %} + +{{ SECTION }} +# When using zoxide with --no-aliases, alias these internal functions as +# desired. +# + +# Jump to a directory using only keywords. +function __zoxide_z { + if ($args.Length -eq 0) { + __zoxide_cd ~ + } + elseif ($args.Length -eq 1 -and $args[0] -eq '-') { + __zoxide_cd - + } + else { + $__zoxide_result = zoxide query -- @args + if ($LASTEXITCODE -eq 0) { + __zoxide_cd $__zoxide_result + } + } +} + +# Jump to a directory using interactive search. +function zi { + $__zoxide_result = zoxide query -i -- @args + if ($LASTEXITCODE -eq 0) { + __zoxide_cd $__zoxide_result + } +} + +# Add a new entry to the database. +function __zoxide_za { + zoxide add @args +} + +# Query an entry from the database using only keywords. +function __zoxide_zq { + zoxide query @args +} + +# Query an entry from the database using interactive selection. +function __zoxide_zqi { + zoxide query -i @args +} + +# Remove an entry from the database using the exact path. +function __zoxide_zr { + zoxide remove @args +} + +# Remove an entry from the database using interactive selection. +function __zoxide_zri { + $_zoxide_result = zoxide query -i -- @args + if ($LASTEXITCODE -eq 0) { + zoxide remove $_zoxide_result + } +} + +{{ SECTION }} +# Convenient aliases for zoxide. Disable these using --no-aliases. +# + +{%- match cmd %} +{%- when Some with (cmd) %} + +Set-Alias {{cmd}} __zoxide_z +Set-Alias {{cmd}}i __zoxide_zi + +Set-Alias {{cmd}}a __zoxide_za + +Set-Alias {{cmd}}q __zoxide_zq +Set-Alias {{cmd}}qi __zoxide_zqi + +Set-Alias {{cmd}}r __zoxide_zr +Set-Alias {{cmd}}ri __zoxide_zri + +{%- when None %} +{{ NOT_CONFIGURED }} + +{%- endmatch %} + +{{ SECTION }} +# To initialize zoxide with PowerShell, add the following line to your +# PowerShell configuration file (the location is stored in $profile): +# +# Invoke-Expression (& { +# $hook = if ($PSVersionTable.PSVersion.Major -ge 6) { +# 'pwd' +# } else { +# 'prompt' +# } +# (zoxide init powershell --hook $hook) -join "`n" +# }) diff --git a/crates/zoxide-shell/templates/xonsh.txt b/crates/zoxide-shell/templates/xonsh.txt new file mode 100644 index 0000000..1bb5d95 --- /dev/null +++ b/crates/zoxide-shell/templates/xonsh.txt @@ -0,0 +1,114 @@ +{%- let SECTION = "# =============================================================================\n#" -%} +{%- let NOT_CONFIGURED = "# -- not configured --" -%} + +{%- if resolve_symlinks -%} +import os +{%- endif %} +import os.path + +{{ SECTION }} +# Utility functions for zoxide. +# + +# pwd based on the value of _ZO_RESOLVE_SYMLINKS. +def __zoxide_pwd() -> str: +{%- if resolve_symlinks %} + return os.getcwd() +{%- else %} + return $PWD +{%- endif %} + +# cd + custom logic based on the value of _ZO_ECHO. +def __zoxide_cd(path: str): + cd @(path) {%- if echo %} and print(__zoxide_pwd()) {%- endif %} + +{{ SECTION }} +# Hook configuration for zoxide. +# + +# Initialize hook to add new entries to the database. +{%- match hook %} +{%- when Hook::None %} +{{ NOT_CONFIGURED }} + +{%- when Hook::Prompt %} +@events.on_post_prompt + +{%- when Hook::Pwd %} +@events.on_chdir + +{%- endmatch %} +def __zoxide_hook(**kwargs): + zoxide add @(__zoxide_pwd()) + +{{ SECTION }} +# When using zoxide with --no-aliases, alias these internal functions as +# desired. +# + +# Jump to a directory using only keywords. +def __zoxide_z(keywords: [str]): + if keywords == []: + __zoxide_cd($HOME) + elif keywords == ['-']: + __zoxide_cd('-') + elif len(keywords) == 1 and os.path.isdir(keywords[0]): + __zoxide_cd(keywords[0]) + else: + __zoxide_result = $(zoxide query -- @(keywords))[:-1] + if __zoxide_result: + __zoxide_cd(__zoxide_result) + +# Jump to a directory using interactive search. +def __zoxide_zi(keywords: [str]): + __zoxide_result = $(zoxide query -- @(keywords))[:-1] + if __zoxide_result: + __zoxide_cd(__zoxide_result) + +# Add a new entry to the database. +def __zoxide_za(args: [str]): + zoxide add @(args) + +# Query an entry from the database using only keywords. +def __zoxide_zq(args: [str]): + zoxide query @(args) + +# Query an entry from the database using interactive selection. +def __zoxide_zqi(args: [str]): + zoxide query -i @(args) + +# Remove an entry from the database using the exact path. +def __zoxide_zr(args: [str]): + zoxide remove @(args) + +# Remove an entry from the database using interactive selection. +def __zoxide_zri(keywords: [str]): + __zoxide_result = $(zoxide query -- @(keywords))[:-1] + if __zoxide_result: + zoxide remove @(__zoxide_result) + +{{ SECTION }} +# Convenient aliases for zoxide. Disable these using --no-aliases. +# + +{%- match cmd %} +{%- when Some with (cmd) %} + +aliases['{{cmd}}'] = __zoxide_z +aliases['{{cmd}}i'] = __zoxide_zi +aliases['{{cmd}}a'] = __zoxide_za +aliases['{{cmd}}q'] = __zoxide_zq +aliases['{{cmd}}qi'] = __zoxide_zqi +aliases['{{cmd}}r'] = __zoxide_zr +aliases['{{cmd}}ri'] = __zoxide_zri + +{%- when None %} +{{ NOT_CONFIGURED }} + +{%- endmatch %} + +{{ SECTION }} +# To initialize zoxide with xonsh, add the following line to your xonsh +# configuration file (usually ~/.xonshrc): +# +# execx($(zoxide init xonsh), 'exec', __xonsh__.ctx, filename='zoxide') diff --git a/crates/zoxide-shell/templates/zsh.txt b/crates/zoxide-shell/templates/zsh.txt new file mode 100644 index 0000000..bac4a47 --- /dev/null +++ b/crates/zoxide-shell/templates/zsh.txt @@ -0,0 +1,129 @@ +{%- let SECTION = "# =============================================================================\n#" -%} +{%- let NOT_CONFIGURED = "# -- not configured --" -%} + +{{ SECTION }} +# Utility functions for zoxide. +# + +# pwd based on the value of _ZO_RESOLVE_SYMLINKS. +function __zoxide_pwd() { +{%- if resolve_symlinks %} + pwd -P +{%- else %} + pwd -L +{%- endif %} +} + +# cd + custom logic based on the value of _ZO_ECHO. +function __zoxide_cd() { + cd "$@" {%- if echo %} && __zoxide_pwd {%- endif %} +} + +{{ SECTION }} +# Hook configuration for zoxide. +# + +# Hook to add new entries to the database. +function __zoxide_hook() { + zoxide add "$(__zoxide_pwd)" +} + +# Initialize hook. +{%- match hook %} +{%- when Hook::None %} +{{ NOT_CONFIGURED }} + +{%- when Hook::Prompt %} +[[ -n "${precmd_functions[(r)__zoxide_hook]}" ]] || { + precmd_functions+=(__zoxide_hook) +} + +{%- when Hook::Pwd %} +chpwd_functions=(${chpwd_functions[@]} "__zoxide_hook") + +{%- endmatch %} + +{{ SECTION }} +# When using zoxide with --no-aliases, alias these internal functions as +# desired. +# + +# Jump to a directory using only keywords. +function __zoxide_z() { + if [ "$#" -eq 0 ]; then + __zoxide_cd ~ + elif [ "$#" -eq 1 ] && [ "$1" = '-' ]; then + if [ -n "$OLDPWD" ]; then + __zoxide_cd "$OLDPWD" + else + echo "zoxide: \\$OLDPWD is not set" + return 1 + fi + elif [ "$#" -eq 1 ] && [ -d "$1" ]; then + __zoxide_cd "$1" + else + local __zoxide_result + __zoxide_result="$(zoxide query -- "$@")" && __zoxide_cd "$__zoxide_result" + fi +} + +# Jump to a directory using interactive search. +function __zoxide_zi() { + local __zoxide_result + __zoxide_result="$(zoxide query -i -- "$@")" && __zoxide_cd "$__zoxide_result" +} + +# Add a new entry to the database. +function __zoxide_za() { + zoxide add "$@" +} + +# Query an entry from the database using only keywords. +function __zoxide_zq() { + zoxide query "$@" +} + +# Query an entry from the database using interactive selection. +function __zoxide_zqi() { + zoxide query -i "$@" +} + +# Remove an entry from the database using the exact path. +function __zoxide_zr() { + zoxide remove "$@" +} + +# Remove an entry from the database using interactive selection. +function __zoxide_zri() { + local __zoxide_result + __zoxide_result="$(zoxide query -i -- "$@")" && zoxide remove "$__zoxide_result" +} + +{{ SECTION }} +# Convenient aliases for zoxide. Disable these using --no-aliases. +# + +{%- match cmd %} +{%- when Some with (cmd) %} + +alias {{cmd}}='__zoxide_z' +alias {{cmd}}i='__zoxide_zi' + +alias {{cmd}}a='__zoxide_za' + +alias {{cmd}}q='__zoxide_zq' +alias {{cmd}}qi='__zoxide_zqi' + +alias {{cmd}}r='__zoxide_zr' +alias {{cmd}}ri='__zoxide_zri' + +{%- when None %} +{{ NOT_CONFIGURED }} + +{%- endmatch %} + +{{ SECTION }} +# To initialize zoxide with zsh, add the following line to your zsh +# configuration file (usually ~/.zshrc): +# +# eval "$(zoxide init zsh)" diff --git a/crates/zoxide-shell/tests/syntax.rs b/crates/zoxide-shell/tests/syntax.rs new file mode 100644 index 0000000..84ec495 --- /dev/null +++ b/crates/zoxide-shell/tests/syntax.rs @@ -0,0 +1,151 @@ +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> = 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_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(""); + } +} diff --git a/src/config.rs b/src/config.rs index 8c14e19..1cd9ca7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,10 +1,9 @@ -use crate::db::Rank; - use anyhow::{bail, Context, Result}; +use dirs_next as dirs; +use zoxide_engine::dir::Rank; use std::env; use std::ffi::OsString; -use std::fs; use std::path::PathBuf; pub fn zo_data_dir() -> Result { @@ -19,11 +18,6 @@ pub fn zo_data_dir() -> Result { }, }; - // This will fail when `data_dir` points to a file or a broken symlink, but - // will no-op on a valid symlink (to a directory), or an actual directory. - fs::create_dir_all(&data_dir) - .with_context(|| format!("could not create data directory: {}", data_dir.display()))?; - Ok(data_dir) } diff --git a/src/db.rs b/src/db.rs deleted file mode 100644 index ea7cebf..0000000 --- a/src/db.rs +++ /dev/null @@ -1,294 +0,0 @@ -use anyhow::{bail, Context, Result}; -use bincode::Options; -use float_ord::FloatOrd; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use std::cmp::Reverse; -use std::fmt::{self, Display, Formatter}; -use std::fs::{self, OpenOptions}; -use std::io::{self, Write}; -use std::path::{Path, PathBuf}; - -#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] -struct DbVersion(u32); - -pub struct Db { - pub dirs: Vec, - pub modified: bool, - data_dir: PathBuf, -} - -impl Db { - const CURRENT_VERSION: DbVersion = DbVersion(3); - const MAX_SIZE: u64 = 8 * 1024 * 1024; // 8 MiB - - pub fn open(data_dir: PathBuf) -> Result { - fs::create_dir_all(&data_dir) - .with_context(|| format!("unable to create data directory: {}", data_dir.display()))?; - - let path = Self::get_path(&data_dir); - - let buffer = match fs::read(&path) { - Ok(buffer) => buffer, - Err(e) if e.kind() == io::ErrorKind::NotFound => { - return Ok(Db { - dirs: Vec::new(), - modified: false, - data_dir, - }) - } - Err(e) => { - return Err(e) - .with_context(|| format!("could not read from database: {}", path.display())) - } - }; - - if buffer.is_empty() { - return Ok(Db { - dirs: Vec::new(), - modified: false, - data_dir, - }); - } - - let deserializer = &mut bincode::options() - .with_fixint_encoding() - .with_limit(Self::MAX_SIZE); - - let version_size = deserializer - .serialized_size(&Self::CURRENT_VERSION) - .context("could not determine size of database version field")? - as _; - - if buffer.len() < version_size { - bail!("database is corrupted: {}", path.display()); - } - - let (buffer_version, buffer_dirs) = buffer.split_at(version_size); - - let version = deserializer.deserialize(buffer_version).with_context(|| { - format!("could not deserialize database version: {}", path.display()) - })?; - - let dirs = match version { - Self::CURRENT_VERSION => deserializer - .deserialize(buffer_dirs) - .with_context(|| format!("could not deserialize database: {}", path.display()))?, - DbVersion(version_num) => bail!( - "zoxide {} does not support schema v{}: {}", - env!("ZOXIDE_VERSION"), - version_num, - path.display(), - ), - }; - - Ok(Db { - dirs, - modified: false, - data_dir, - }) - } - - pub fn save(&mut self) -> Result<()> { - if !self.modified { - return Ok(()); - } - - let (buffer, buffer_size) = (|| -> bincode::Result<_> { - let version_size = bincode::serialized_size(&Self::CURRENT_VERSION)?; - let dirs_size = bincode::serialized_size(&self.dirs)?; - - let buffer_size = version_size + dirs_size; - let mut buffer = Vec::with_capacity(buffer_size as _); - - bincode::serialize_into(&mut buffer, &Self::CURRENT_VERSION)?; - bincode::serialize_into(&mut buffer, &self.dirs)?; - - Ok((buffer, buffer_size)) - })() - .context("could not serialize database")?; - - let db_path_tmp = Self::get_path_tmp(&self.data_dir); - - let mut db_file_tmp = OpenOptions::new() - .create_new(true) - .write(true) - .open(&db_path_tmp) - .with_context(|| { - format!( - "could not create temporary database: {}", - db_path_tmp.display() - ) - })?; - - // File::set_len() can fail on some filesystems, so we ignore errors - let _ = db_file_tmp.set_len(buffer_size); - - (|| -> anyhow::Result<()> { - db_file_tmp.write_all(&buffer).with_context(|| { - format!( - "could not write to temporary database: {}", - db_path_tmp.display() - ) - })?; - - let db_path = Self::get_path(&self.data_dir); - - fs::rename(&db_path_tmp, &db_path) - .with_context(|| format!("could not create database: {}", db_path.display())) - })() - .map_err(|e| { - fs::remove_file(&db_path_tmp) - .with_context(|| { - format!( - "could not remove temporary database: {}", - db_path_tmp.display() - ) - }) - .err() - .unwrap_or(e) - })?; - - self.modified = true; - - Ok(()) - } - - pub fn matches<'a>( - &'a mut self, - now: Epoch, - keywords: &[String], - ) -> impl Iterator { - self.dirs - .sort_unstable_by_key(|dir| Reverse(FloatOrd(dir.get_score(now)))); - - let keywords: Vec = keywords - .iter() - .map(|keyword| keyword.to_lowercase()) - .collect(); - - self.dirs - .iter() - .filter(move |dir| dir.is_match(&keywords) && dir.is_valid()) - } - - fn get_path>(data_dir: P) -> PathBuf { - data_dir.as_ref().join("db.zo") - } - - fn get_path_tmp>(data_dir: P) -> PathBuf { - let file_name = format!("db-{}.zo.tmp", Uuid::new_v4()); - data_dir.as_ref().join(file_name) - } -} - -impl Drop for Db { - fn drop(&mut self) { - if let Err(e) = self.save() { - eprintln!("{:#}", e); - } - } -} - -pub type Rank = f64; -pub type Epoch = i64; // use a signed integer so subtraction can be performed on it - -#[derive(Debug, Deserialize, Serialize)] -pub struct Dir { - pub path: String, - pub rank: Rank, - pub last_accessed: Epoch, -} - -impl Dir { - pub fn is_valid(&self) -> bool { - self.rank.is_finite() && self.rank >= 1.0 && Path::new(&self.path).is_dir() - } - - pub fn is_match(&self, query: &[String]) -> bool { - let path_lower = self.path.to_lowercase(); - - let get_filenames = || { - let query_name = Path::new(query.last()?).file_name()?.to_str()?; - let dir_name = Path::new(&path_lower).file_name()?.to_str()?; - Some((query_name, dir_name)) - }; - - if let Some((query_name, dir_name)) = get_filenames() { - if !dir_name.contains(query_name) { - return false; - } - } - - let mut subpath = path_lower.as_str(); - - for subquery in query.iter() { - match subpath.find(subquery) { - Some(idx) => subpath = &subpath[idx + subquery.len()..], - None => return false, - } - } - - true - } - - 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 - 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) -> DirScoreDisplay { - DirScoreDisplay { 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 DirScoreDisplay<'a> { - dir: &'a Dir, - now: Epoch, -} - -impl Display for DirScoreDisplay<'_> { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - const SCORE_MIN: Rank = 0.0; - const SCORE_MAX: Rank = 9999.0; - - let score = self.dir.get_score(self.now); - - let score_clamped = if score > SCORE_MAX { - SCORE_MAX - } else if score > SCORE_MIN { - score - } else { - SCORE_MIN - }; - - write!(f, "{:>4.0} {}", score_clamped, self.dir.path) - } -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 14a1c05..0000000 --- a/src/error.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::fmt::{self, Display}; - -#[derive(Debug)] -pub struct SilentExit { - pub code: i32, -} - -impl Display for SilentExit { - fn fmt(&self, _: &mut fmt::Formatter) -> fmt::Result { - Ok(()) - } -} diff --git a/src/fzf.rs b/src/fzf.rs deleted file mode 100644 index abc3e8e..0000000 --- a/src/fzf.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::config::zo_fzf_opts; -use crate::error::SilentExit; - -use anyhow::{bail, Context, Result}; - -use std::io::Write; -use std::process::{Child, Command, Stdio}; - -pub struct Fzf { - child: Child, -} - -impl Fzf { - pub fn new() -> Result { - let mut command = Command::new("fzf"); - command - .arg("-n2..") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()); - - if let Some(fzf_opts) = zo_fzf_opts() { - command.env("FZF_DEFAULT_OPTS", fzf_opts); - } - - let child = command.spawn().context("could not launch fzf")?; - - Ok(Fzf { child }) - } - - pub fn write(&mut self, line: String) -> Result<()> { - // unwrap() is safe here since we have captured `stdin` - let stdin = self.child.stdin.as_mut().unwrap(); - writeln!(stdin, "{}", line).context("could not write into fzf stdin") - } - - pub fn wait_select(self) -> Result> { - 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 utf-8 sequence in fzf output") - .map(Some), - - // no match - Some(1) => Ok(None), - - // 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"), - } - } -} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..165cd84 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod config; +pub mod util; diff --git a/src/main.rs b/src/main.rs index 3650524..d0adf7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,42 +1,232 @@ -#![forbid(unsafe_code)] +use anyhow::{Context, Result}; +use clap::{AppSettings, ArgEnum, Clap}; +use once_cell::sync::OnceCell; +use zoxide::{config, util}; +use zoxide_engine::{Dir, Query, Store}; +use zoxide_shell::{self as zs, Generator}; -mod config; -mod db; -mod error; -mod fzf; -mod subcommand; -mod util; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; -use crate::error::SilentExit; +fn env_help() -> &'static str { + static ENV_HELP: OnceCell = OnceCell::new(); + ENV_HELP.get_or_init(|| { + const PATH_SPLIT_SEPARATOR: u8 = if cfg!(any(target_os = "redox", target_os = "windows")) { + b';' + } else { + b':' + }; -use anyhow::Result; -use structopt::StructOpt; + 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 directories 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) + }) +} -use std::process; +// TODO: import +// TODO: query interactive +#[derive(Debug, Clap)] +#[clap( + about, + author, + global_setting(AppSettings::ColoredHelp), + global_setting(AppSettings::GlobalVersion), + global_setting(AppSettings::VersionlessSubcommands), + version = env!("ZOXIDE_VERSION"))] +enum Opts { + /// Adds a new directory or increments its rank + Add { path: Option }, -#[derive(Debug, StructOpt)] -#[structopt(about, version = env!("ZOXIDE_VERSION"))] -enum Zoxide { - Add(subcommand::Add), - Import(subcommand::Import), - Init(subcommand::Init), - Query(subcommand::Query), - Remove(subcommand::Remove), + /// Generates shell configuration + #[clap(after_help(env_help()))] + 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, + + /// 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<()> { - let opt = Zoxide::from_args(); + let opts = Opts::parse(); - let res = match opt { - Zoxide::Add(add) => add.run(), - Zoxide::Import(import) => import.run(), - Zoxide::Init(init) => init.run(), - Zoxide::Query(query) => query.run(), - Zoxide::Remove(remove) => remove.run(), - }; + match opts { + Opts::Add { path } => { + let path = match path { + Some(path) => { + if config::zo_resolve_symlinks() { + util::canonicalize(&path) + } else { + util::resolve_path(&path) + } + } + None => util::current_dir(), + }?; - res.map_err(|e| match e.downcast::() { - Ok(SilentExit { code }) => process::exit(code), - Err(e) => e, - }) + 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(()) + } + + 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::Bash(opts).generate(handle), + Shell::Posix => zs::Bash(opts).generate(handle), + Shell::Powershell => zs::Bash(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(()) + } + } } diff --git a/src/subcommand/add.rs b/src/subcommand/add.rs deleted file mode 100644 index 5ed3864..0000000 --- a/src/subcommand/add.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::config; -use crate::db::{Db, Dir, Rank}; -use crate::util; - -use anyhow::Result; -use structopt::StructOpt; - -use std::path::{Path, PathBuf}; - -/// Add a new directory or increment its rank -#[derive(Debug, StructOpt)] -#[structopt()] -pub struct Add { - path: Option, -} - -impl Add { - pub fn run(&self) -> Result<()> { - let current_dir; - let path = match &self.path { - Some(path) => path, - None => { - current_dir = util::get_current_dir()?; - ¤t_dir - } - }; - - add(&path) - } -} - -fn add>(path: P) -> Result<()> { - let path = path.as_ref(); - let path = if config::zo_resolve_symlinks() { - util::canonicalize(&path)? - } else { - util::resolve_path(&path)? - }; - - if config::zo_exclude_dirs()? - .iter() - .any(|pattern| pattern.matches_path(&path)) - { - return Ok(()); - } - - let mut db = util::get_db()?; - let now = util::get_current_time()?; - let path = util::path_to_str(&path)?; - let maxage = config::zo_maxage()?; - - match db.dirs.iter_mut().find(|dir| dir.path == path) { - None => db.dirs.push(Dir { - path: path.to_string(), - last_accessed: now, - rank: 1.0, - }), - Some(dir) => { - dir.last_accessed = now; - dir.rank += 1.0; - } - }; - - age(&mut db, maxage); - db.modified = true; - - Ok(()) -} - -fn age(db: &mut Db, maxage: Rank) { - let sum_age = db.dirs.iter().map(|dir| dir.rank).sum::(); - - if sum_age > maxage { - let factor = 0.9 * maxage / sum_age; - for dir in &mut db.dirs { - dir.rank *= factor; - } - - for idx in (0..db.dirs.len()).rev() { - let dir = &db.dirs[idx]; - if dir.rank < 1.0 { - db.dirs.swap_remove(idx); - } - } - } -} diff --git a/src/subcommand/import.rs b/src/subcommand/import.rs deleted file mode 100644 index 03d3433..0000000 --- a/src/subcommand/import.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::config; -use crate::db::{Db, Dir}; -use crate::util; - -use anyhow::{bail, Context, Result}; -use structopt::StructOpt; - -use std::fs; -use std::path::{Path, PathBuf}; - -/// Import from z database -#[derive(Debug, StructOpt)] -#[structopt()] -pub struct Import { - path: PathBuf, - - /// Merge entries into existing database - #[structopt(long)] - merge: bool, -} - -impl Import { - pub fn run(&self) -> Result<()> { - import(&self.path, self.merge) - } -} - -fn import>(path: P, merge: bool) -> Result<()> { - let path = path.as_ref(); - let mut db = util::get_db()?; - - if !db.dirs.is_empty() && !merge { - bail!( - "To prevent conflicts, you can only import from z with an empty zoxide database!\n\ - If you wish to merge the two, specify the `--merge` flag." - ); - } - - let buffer = fs::read_to_string(&path) - .with_context(|| format!("could not read z database: {}", path.display()))?; - - for (idx, line) in buffer.lines().enumerate() { - if let Err(e) = import_line(&mut db, line, config::zo_resolve_symlinks()) { - let line_num = idx + 1; - eprintln!("Error on line {}: {}", line_num, e); - } - } - - db.modified = true; - println!("Completed import."); - - Ok(()) -} - -fn import_line(db: &mut Db, line: &str, resolve_symlinks: bool) -> Result<()> { - let mut split_line = line.rsplitn(3, '|'); - - let (path, epoch_str, rank_str) = (|| { - let epoch_str = split_line.next()?; - let rank_str = split_line.next()?; - let path = split_line.next()?; - Some((path, epoch_str, rank_str)) - })() - .with_context(|| format!("invalid entry: {}", line))?; - - let epoch = epoch_str - .parse::() - .with_context(|| format!("invalid epoch: {}", epoch_str))?; - - let rank = rank_str - .parse::() - .with_context(|| format!("invalid rank: {}", rank_str))?; - - let path = if resolve_symlinks { - util::canonicalize(&path)? - } else { - util::resolve_path(&path)? - }; - let path = util::path_to_str(&path)?; - - // If the path exists in the database, add the ranks and set the epoch to - // the more recent of the parsed epoch and the already present epoch. - if let Some(dir) = db.dirs.iter_mut().find(|dir| dir.path == path) { - dir.rank += rank; - dir.last_accessed = epoch.max(dir.last_accessed); - } else { - db.dirs.push(Dir { - path: path.to_string(), - rank, - last_accessed: epoch, - }); - } - - Ok(()) -} diff --git a/src/subcommand/init/bash.rs b/src/subcommand/init/bash.rs deleted file mode 100644 index cd6044a..0000000 --- a/src/subcommand/init/bash.rs +++ /dev/null @@ -1,182 +0,0 @@ -use anyhow::Result; - -use std::io::Write; - -use super::{Hook, Init}; -use crate::config; - -pub fn run(writer: &mut W, options: &Init) -> Result<()> { - const NOT_CONFIGURED: &str = "\ -# -- not configured --"; - - let __zoxide_pwd = if config::zo_resolve_symlinks() { - "\ -__zoxide_pwd() { - pwd -P -}" - } else { - "\ -__zoxide_pwd() { - pwd -L -}" - }; - - let __zoxide_cd = if config::zo_echo() { - "\ -__zoxide_cd() { - cd \"$@\" || return \"$?\" - __zoxide_pwd -}" - } else { - "\ -__zoxide_cd() { - cd \"$@\" || return \"$?\" -}" - }; - - let __zoxide_hook = match options.hook { - Hook::none => NOT_CONFIGURED, - Hook::prompt => { - "\ -__zoxide_hook() { - zoxide add \"$(__zoxide_pwd)\" -}" - } - Hook::pwd => { - "\ -__zoxide_hook() { - local -r __zoxide_pwd_tmp=\"$(__zoxide_pwd)\" - if [ -z \"$__zoxide_pwd_old\" ]; then - __zoxide_pwd_old=\"$__zoxide_pwd_tmp\" - elif [ \"$__zoxide_pwd_old\" != \"$__zoxide_pwd_tmp\" ]; then - __zoxide_pwd_old=\"$__zoxide_pwd_tmp\" - zoxide add \"$__zoxide_pwd_old\" - fi -}" - } - }; - - let hook_init = match options.hook { - Hook::none => NOT_CONFIGURED, - _ => { - "\ -case \"$PROMPT_COMMAND\" in - *__zoxide_hook*) ;; - *) PROMPT_COMMAND=\"${PROMPT_COMMAND:+${PROMPT_COMMAND};}__zoxide_hook\" ;; -esac" - } - }; - - let aliases = if options.no_aliases { - NOT_CONFIGURED.into() - } else { - format!( - "\ -alias {}='__zoxide_z' -alias {cmd}i='__zoxide_zi' -alias {cmd}a='__zoxide_za' - -alias {cmd}q='__zoxide_zq' -alias {cmd}qi='__zoxide_zqi' - -alias {cmd}r='__zoxide_zr' -alias {cmd}ri='__zoxide_zri'", - cmd = options.cmd - ) - }; - - write!( - writer, - "\ -# ============================================================================= -# -# Utility functions for zoxide. -# - -# pwd based on the value of _ZO_RESOLVE_SYMLINKS. -{__zoxide_pwd} - -# cd + custom logic based on the value of _ZO_ECHO. -{__zoxide_cd} - -# ============================================================================= -# -# Hook configuration for zoxide. -# - -# Hook to add new entries to the database. -{__zoxide_hook} - -# Initialize hook. -{hook_init} - -# ============================================================================= -# -# When using zoxide with --no-aliases, alias these internal functions as -# desired. -# - -# Jump to a directory using only keywords. -__zoxide_z() {{ - if [ \"$#\" -eq 0 ]; then - __zoxide_cd ~ - elif [ \"$#\" -eq 1 ] && [ \"$1\" = '-' ]; then - if [ -n \"$OLDPWD\" ]; then - __zoxide_cd \"$OLDPWD\" - else - echo \"zoxide: \\$OLDPWD is not set\" - return 1 - fi - else - local __zoxide_result - __zoxide_result=\"$(zoxide query -- \"$@\")\" && __zoxide_cd \"$__zoxide_result\" - fi -}} - -# Jump to a directory using interactive search. -__zoxide_zi() {{ - local __zoxide_result - __zoxide_result=\"$(zoxide query -i -- \"$@\")\" && __zoxide_cd \"$__zoxide_result\" -}} - -# Add a new entry to the database. -alias __zoxide_za='zoxide add' - -# Query an entry from the database using only keywords. -alias __zoxide_zq='zoxide query' - -# Query an entry from the database using interactive selection. -alias __zoxide_zqi='zoxide query -i' - -# Remove an entry from the database using the exact path. -alias __zoxide_zr='zoxide remove' - -# Remove an entry from the database using interactive selection. -__zoxide_zri() {{ - local __zoxide_result - __zoxide_result=\"$(zoxide query -i -- \"$@\")\" && zoxide remove \"$__zoxide_result\" -}} - -# ============================================================================= -# -# Convenient aliases for zoxide. Disable these using --no-aliases. -# - -{aliases} - -# ============================================================================= -# -# To initialize zoxide with bash, add the following line to your bash -# configuration file (usually ~/.bashrc): -# -# eval \"$(zoxide init bash)\" -", - __zoxide_pwd = __zoxide_pwd, - __zoxide_cd = __zoxide_cd, - __zoxide_hook = __zoxide_hook, - hook_init = hook_init, - aliases = aliases, - )?; - - Ok(()) -} diff --git a/src/subcommand/init/fish.rs b/src/subcommand/init/fish.rs deleted file mode 100644 index c98fe8d..0000000 --- a/src/subcommand/init/fish.rs +++ /dev/null @@ -1,191 +0,0 @@ -use anyhow::Result; - -use std::io::Write; - -use super::{Hook, Init}; -use crate::config; - -pub fn run(writer: &mut W, options: &Init) -> Result<()> { - const NOT_CONFIGURED: &str = "\ -# -- not configured --"; - - let __zoxide_pwd = if config::zo_resolve_symlinks() { - "\ -function __zoxide_pwd - pwd -P -end" - } else { - "\ -function __zoxide_pwd - pwd -L -end" - }; - - let __zoxide_cd = if config::zo_echo() { - "\ -function __zoxide_cd - cd $argv - or return $status - - commandline -f repaint - __zoxide_pwd -end" - } else { - "\ -function __zoxide_cd - cd $argv - or return $status - - commandline -f repaint -end" - }; - - let __zoxide_hook = "\ -function __zoxide_hook - zoxide add (__zoxide_pwd) -end"; - - let hook_init = match options.hook { - Hook::none => NOT_CONFIGURED, - Hook::prompt => { - "\ -function __zoxide_hook_prompt --on-event fish_prompt - __zoxide_hook -end" - } - Hook::pwd => { - "\ -function __zoxide_hook_pwd --on-variable PWD - __zoxide_hook -end" - } - }; - - let aliases = if options.no_aliases { - NOT_CONFIGURED.into() - } else { - format!( - "\ -function {cmd} - __zoxide_z $argv -end - -function {cmd}i - __zoxide_zi $argv -end - -function {cmd}a - __zoxide_za $argv -end - -function {cmd}q - __zoxide_zq $argv -end - -function {cmd}qi - __zoxide_zqi $argv -end - -function {cmd}r - __zoxide_zr $argv -end - -function {cmd}ri - __zoxide_zri $argv -end", - cmd = options.cmd - ) - }; - - writeln!( - writer, - "\ -# ============================================================================= -# -# Utility functions for zoxide. -# - -# pwd based on the value of _ZO_RESOLVE_SYMLINKS. -{__zoxide_pwd} - -# cd + custom logic based on the value of _ZO_ECHO. -{__zoxide_cd} - -# ============================================================================= -# -# Hook configuration for zoxide. -# - -# Hook to add new entries to the database. -{__zoxide_hook} - -# Initialize hook. -{hook_init} - -# ============================================================================= -# -# When using zoxide with --no-aliases, alias these internal functions as -# desired. -# - -# Jump to a directory using only keywords. -function __zoxide_z - set argc (count $argv) - - if test $argc -eq 0 - __zoxide_cd $HOME - else if begin; test $argc -eq 1; and test $argv[1] = '-'; end - __zoxide_cd - - else - set -l __zoxide_result (zoxide query -- $argv) - and __zoxide_cd $__zoxide_result - end -end - -# Jump to a directory using interactive search. -function __zoxide_zi - set -l __zoxide_result (zoxide query -i -- $argv) - and __zoxide_cd $__zoxide_result -end - -# Add a new entry to the database. -abbr -a __zoxide_za 'zoxide add' - -# Query an entry from the database using only keywords. -abbr -a __zoxide_zq 'zoxide query' - -# Query an entry from the database using interactive selection. -abbr -a __zoxide_zqi 'zoxide query -i' - -# Remove an entry from the database using the exact path. -abbr -a __zoxide_zr 'zoxide remove' - -# Remove an entry from the database using interactive selection. -function __zoxide_zri - set -l __zoxide_result (zoxide query -i -- $argv) - and zoxide remove $__zoxide_result -end - -# ============================================================================= -# -# Convenient aliases for zoxide. Disable these using --no-aliases. -# - -{aliases} - -# ============================================================================= -# -# To initialize zoxide with fish, add the following line to your fish -# configuration file (usually ~/.config/fish/config.fish): -# -# zoxide init fish | source -", - __zoxide_pwd = __zoxide_pwd, - __zoxide_cd = __zoxide_cd, - __zoxide_hook = __zoxide_hook, - hook_init = hook_init, - aliases = aliases, - )?; - - Ok(()) -} diff --git a/src/subcommand/init/mod.rs b/src/subcommand/init/mod.rs deleted file mode 100644 index 47414ed..0000000 --- a/src/subcommand/init/mod.rs +++ /dev/null @@ -1,74 +0,0 @@ -mod bash; -mod fish; -mod posix; -mod powershell; -mod zsh; - -use anyhow::{Context, Result}; -use clap::arg_enum; -use structopt::StructOpt; - -use std::io; - -/// Generates shell configuration -#[derive(Debug, StructOpt)] -#[structopt()] -pub struct Init { - #[structopt(possible_values = &Shell::variants(), case_insensitive = true)] - shell: Shell, - - /// Renames the 'z' command and corresponding aliases - #[structopt(long, alias = "z-cmd", default_value = "z")] - cmd: String, - - /// Prevents zoxide from defining any commands - #[structopt(long, alias = "no-define-aliases")] - no_aliases: bool, - - /// Chooses event on which an entry is added to the database - #[structopt( - long, - possible_values = &Hook::variants(), - default_value = "pwd", - case_insensitive = true - )] - hook: Hook, -} - -impl Init { - pub fn run(&self) -> Result<()> { - let stdout = io::stdout(); - let mut handle = stdout.lock(); - - match self.shell { - Shell::bash => bash::run(&mut handle, self), - Shell::fish => fish::run(&mut handle, self), - Shell::posix => posix::run(&mut handle, self), - Shell::powershell => powershell::run(&mut handle, self), - Shell::zsh => zsh::run(&mut handle, self), - } - .context("could not initialize zoxide") - } -} - -arg_enum! { - #[allow(non_camel_case_types)] - #[derive(Debug)] - enum Shell { - bash, - fish, - posix, - powershell, - zsh, - } -} - -arg_enum! { - #[allow(non_camel_case_types)] - #[derive(Debug)] - enum Hook { - none, - prompt, - pwd, - } -} diff --git a/src/subcommand/init/posix.rs b/src/subcommand/init/posix.rs deleted file mode 100644 index bd4216d..0000000 --- a/src/subcommand/init/posix.rs +++ /dev/null @@ -1,232 +0,0 @@ -use super::{Hook, Init}; -use crate::config; -use crate::util; - -use std::io::Write; - -use anyhow::Result; -use uuid::Uuid; - -pub fn run(writer: &mut W, options: &Init) -> Result<()> { - const NOT_CONFIGURED: &str = "\ -# -- not configured --"; - - let __zoxide_pwd = if config::zo_resolve_symlinks() { - "\ -__zoxide_pwd() { - pwd -P -}" - } else { - "\ -__zoxide_pwd() { - pwd -L -}" - }; - - let __zoxide_cd = if config::zo_echo() { - "\ -__zoxide_cd() { - cd \"$@\" || return \"$?\" - __zoxide_pwd -}" - } else { - "\ -__zoxide_cd() { - cd \"$@\" || return \"$?\" -}" - }; - - let __zoxide_hook = match options.hook { - Hook::none => NOT_CONFIGURED.into(), - Hook::prompt => "\ -__zoxide_hook() { - zoxide add \"$(__zoxide_pwd)\" -}" - .into(), - Hook::pwd => { - let mut tmp_path = std::env::temp_dir(); - tmp_path.push("zoxide"); - let tmp_path_str = util::path_to_str(&tmp_path)?; - - let pwd_path = tmp_path.join(format!("pwd-{}", Uuid::new_v4())); - let pwd_path_str = util::path_to_str(&pwd_path)?; - - format!( - "\ -# PWD hooks in POSIX use a temporary file, located at `$__zoxide_pwd_path`, to track -# changes in the current directory. These files are removed upon restart, -# but they should ideally also be cleaned up once the shell exits using traps. -# -# This can be done as follows: -# -# trap '__zoxide_cleanup' EXIT HUP KILL TERM -# trap '__zoxide_cleanup; trap - INT; kill -s INT \"$$\"' INT -# trap '__zoxide_cleanup; trap - QUIT; kill -s QUIT \"$$\"' QUIT -# -# By default, traps are not set up because they override all previous traps. -# It is therefore up to the user to add traps to their shell configuration. - -__zoxide_tmp_path={tmp_path} -__zoxide_pwd_path={pwd_path} - -__zoxide_cleanup() {{ - rm -f \"$__zoxide_pwd_path\" -}} - -__zoxide_setpwd() {{ - mkdir -p \"$__zoxide_tmp_path\" - echo \"$PWD\" > \"$__zoxide_pwd_path\" -}} - -__zoxide_setpwd - -__zoxide_hook() {{ - _ZO_OLDPWD=\"$(cat \"$__zoxide_pwd_path\")\" - if [ -z \"$_ZO_OLDPWD\" ] || [ \"$_ZO_OLDPWD\" != \"$PWD\" ]; then - __zoxide_setpwd && zoxide add \"$(pwd -L)\" > /dev/null - fi -}}", - tmp_path = posix_quote(tmp_path_str), - pwd_path = posix_quote(pwd_path_str), - ) - } - }; - - let hook_init = match options.hook { - Hook::none => NOT_CONFIGURED, - _ => { - "\ -case \"$PS1\" in - *\\$\\(__zoxide_hook\\)*) ;; - *) PS1=\"${PS1}\\$(__zoxide_hook)\" ;; -esac" - } - }; - - let aliases = if options.no_aliases { - NOT_CONFIGURED.into() - } else { - format!( - "\ -alias {cmd}='__zoxide_z' -alias {cmd}i='__zoxide_zi' - -alias {cmd}a='__zoxide_za' - -alias {cmd}q='__zoxide_zq' -alias {cmd}qi='__zoxide_zqi' - -alias {cmd}r='__zoxide_zr' -alias {cmd}ri='__zoxide_zri'", - cmd = options.cmd - ) - }; - - writeln!( - writer, - "\ -# ============================================================================= -# -# Utility functions for zoxide. -# - -# pwd based on the value of _ZO_RESOLVE_SYMLINKS. -{__zoxide_pwd} - -# cd + custom logic based on the value of _ZO_ECHO. -{__zoxide_cd} - -# ============================================================================= -# -# Hook configuration for zoxide. -# - -# Hook to add new entries to the database. -{__zoxide_hook} - -# Initialize hook. -{hook_init} - -# ============================================================================= -# -# When using zoxide with --no-aliases, alias these internal functions as -# desired. -# - -# Jump to a directory using only keywords. -__zoxide_z() {{ - if [ \"$#\" -eq 0 ]; then - __zoxide_cd ~ - elif [ \"$#\" -eq 1 ] && [ \"$1\" = '-' ]; then - if [ -n \"$OLDPWD\" ]; then - __zoxide_cd \"$OLDPWD\" - else - echo \"zoxide: \\$OLDPWD is not set\" - return 1 - fi - else - __zoxide_result=\"$(zoxide query -- \"$@\")\" && __zoxide_cd \"$__zoxide_result\" - fi -}} - -# Jump to a directory using interactive search. -__zoxide_zi() {{ - __zoxide_result=\"$(zoxide query -i -- \"$@\")\" && __zoxide_cd \"$__zoxide_result\" -}} - -# Add a new entry to the database. -alias __zoxide_za='zoxide add' - -# Query an entry from the database using only keywords. -alias __zoxide_zq='zoxide query' - -# Query an entry from the database using interactive selection. -alias __zoxide_zqi='zoxide query -i' - -# Remove an entry from the database using the exact path. -alias __zoxide_zr='zoxide remove' - -# Remove an entry from the database using interactive selection. -__zoxide_zri() {{ - __zoxide_result=\"$(zoxide query -i -- \"$@\")\" && zoxide remove \"$__zoxide_result\" -}} - -# ============================================================================= -# -# Convenient aliases for zoxide. Disable these using --no-aliases. -# - -{aliases} - -# ============================================================================= -# -# To initialize zoxide with your POSIX shell, add the following line to your -# shell configuration file: -# -# eval \"$(zoxide init posix --prompt hook)\" -", - __zoxide_pwd = __zoxide_pwd, - __zoxide_cd = __zoxide_cd, - __zoxide_hook = __zoxide_hook, - hook_init = hook_init, - aliases = aliases, - )?; - - Ok(()) -} - -fn posix_quote(string: &str) -> String { - let mut quoted = String::with_capacity(string.len() + 2); - - quoted.push('\''); - for ch in string.chars() { - match ch { - '\\' => quoted.push_str(r"\\"), - '\'' => quoted.push_str(r"'\''"), - _ => quoted.push(ch), - } - } - quoted.push('\''); - - quoted -} diff --git a/src/subcommand/init/powershell.rs b/src/subcommand/init/powershell.rs deleted file mode 100644 index 48d1f50..0000000 --- a/src/subcommand/init/powershell.rs +++ /dev/null @@ -1,180 +0,0 @@ -use anyhow::Result; - -use std::io::Write; - -use super::{Hook, Init}; -use crate::config; - -pub fn run(writer: &mut W, options: &Init) -> Result<()> { - const NOT_CONFIGURED: &str = "\ -# -- not configured --"; - - let __zoxide_pwd = "\ -function __zoxide_pwd { - $(Get-Location).Path -}"; - - let __zoxide_cd = if config::zo_echo() { - "\ -function __zoxide_cd($dir) { - Set-Location $dir -ea Stop - __zoxide_pwd -}" - } else { - "\ -function __zoxide_cd($dir) { - Set-Location $dir -ea Stop -}" - }; - - let __zoxide_hook = "\ -function __zoxide_hook { - zoxide add $(__zoxide_pwd) -}"; - - let hook_init = match options.hook { - Hook::none => NOT_CONFIGURED, - Hook::prompt => { - "\ -$PreZoxidePrompt = $function:prompt -function prompt { - $null = __zoxide_hook - & $PreZoxidePrompt -}" - } - Hook::pwd => { - "\ -if ($PSVersionTable.PSVersion.Major -ge 6) { - $ExecutionContext.InvokeCommand.LocationChangedAction = { - $null = __zoxide_hook - } -} else { - Write-Error \"zoxide: PWD hooks are not supported below PowerShell 6, use 'zoxide init powershell --hook prompt' instead.\" -}" - } - }; - - let aliases = if options.no_aliases { - NOT_CONFIGURED.into() - } else { - format!( - "\ -Set-Alias {cmd} __zoxide_z -Set-Alias {cmd}i __zoxide_zi - -Set-Alias {cmd}a __zoxide_za - -Set-Alias {cmd}q __zoxide_zq -Set-Alias {cmd}qi __zoxide_zqi - -Set-Alias {cmd}r __zoxide_zr -Set-Alias {cmd}ri __zoxide_zri", - cmd = options.cmd - ) - }; - - writeln!( - writer, - "\ -# ============================================================================= -# -# Utility functions for zoxide. -# - -# pwd based on the value of _ZO_RESOLVE_SYMLINKS. -{__zoxide_pwd} - -# cd + custom logic based on the value of _ZO_ECHO. -{__zoxide_cd} - -# ============================================================================= -# -# Hook configuration for zoxide. -# - -# Hook to add new entries to the database. -{__zoxide_hook} - -# Initialize hook. -{hook_init} - -# ============================================================================= -# -# When using zoxide with --no-aliases, alias these internal functions as -# desired. -# - -# Jump to a directory using only keywords. -function __zoxide_z {{ - if ($args.Length -eq 0) {{ - __zoxide_cd ~ - }} - elseif ($args.Length -eq 1 -and $args[0] -eq '-') {{ - __zoxide_cd - - }} - else {{ - $__zoxide_result = zoxide query -- @args - if ($LASTEXITCODE -eq 0) {{ - __zoxide_cd $__zoxide_result - }} - }} -}} - -# Jump to a directory using interactive search. -function zi {{ - $__zoxide_result = zoxide query -i -- @args - if ($LASTEXITCODE -eq 0) {{ - __zoxide_cd $__zoxide_result - }} -}} - -# Add a new entry to the database. -function __zoxide_za {{ zoxide add @args }} - -# Query an entry from the database using only keywords. -function __zoxide_zq {{ zoxide query @args }} - -# Query an entry from the database using interactive selection. -function __zoxide_zqi {{ zoxide query -i @args }} - -# Remove an entry from the database using the exact path. -function __zoxide_zr {{ zoxide remove @args }} - -# Remove an entry from the database using interactive selection. -function __zoxide_zri {{ - $_zoxide_result = zoxide query -i -- @args - if ($LASTEXITCODE -eq 0) {{ - zoxide remove $_zoxide_result - }} -}} - -# ============================================================================= -# -# Convenient aliases for zoxide. Disable these using --no-aliases. -# - -{aliases} - -# ============================================================================= -# -# To initialize zoxide with PowerShell, add the following line to your -# PowerShell configuration file (the location is stored in $profile): -# -# Invoke-Expression (& {{ -# $hook = if ($PSVersionTable.PSVersion.Major -ge 6) {{ -# 'pwd' -# }} else {{ -# 'prompt' -# }} -# (zoxide init powershell --hook $hook) -join \"`n\" -# }}) -", - __zoxide_pwd = __zoxide_pwd, - __zoxide_cd = __zoxide_cd, - __zoxide_hook = __zoxide_hook, - hook_init = hook_init, - aliases = aliases, - )?; - - Ok(()) -} diff --git a/src/subcommand/init/zsh.rs b/src/subcommand/init/zsh.rs deleted file mode 100644 index 152f0f2..0000000 --- a/src/subcommand/init/zsh.rs +++ /dev/null @@ -1,174 +0,0 @@ -use anyhow::Result; - -use std::io::Write; - -use super::{Hook, Init}; -use crate::config; - -pub fn run(writer: &mut W, options: &Init) -> Result<()> { - const NOT_CONFIGURED: &str = "\ -# -- not configured --"; - - let __zoxide_pwd = if config::zo_resolve_symlinks() { - "\ -__zoxide_pwd() { - pwd -P -}" - } else { - "\ -__zoxide_pwd() { - pwd -L -}" - }; - - let __zoxide_cd = if config::zo_echo() { - "\ -__zoxide_cd() { - cd \"$@\" || return \"$?\" - __zoxide_pwd -}" - } else { - "\ -__zoxide_cd() { - cd \"$@\" || return \"$?\" -}" - }; - - let __zoxide_hook = match options.hook { - Hook::none => NOT_CONFIGURED, - _ => { - "\ -__zoxide_hook() { - zoxide add \"$(__zoxide_pwd)\" -}" - } - }; - - let hook_init = match options.hook { - Hook::none => NOT_CONFIGURED, - Hook::prompt => { - "\ -[[ -n \"${precmd_functions[(r)__zoxide_hook]}\" ]] || { - precmd_functions+=(__zoxide_hook) -}" - } - Hook::pwd => { - "\ -chpwd_functions=(${chpwd_functions[@]} \"__zoxide_hook\")" - } - }; - - let aliases = if options.no_aliases { - NOT_CONFIGURED.into() - } else { - format!( - "\ -alias {cmd}='__zoxide_z' -alias {cmd}i='__zoxide_zi' - -alias {cmd}a='__zoxide_za' - -alias {cmd}q='__zoxide_zq' -alias {cmd}qi='__zoxide_zqi' - -alias {cmd}r='__zoxide_zr' -alias {cmd}ri='__zoxide_zri'", - cmd = options.cmd - ) - }; - - write!( - writer, - "\ -# ============================================================================= -# -# Utility functions for zoxide. -# - -# pwd based on the value of _ZO_RESOLVE_SYMLINKS. -{__zoxide_pwd} - -# cd + custom logic based on the value of _ZO_ECHO. -{__zoxide_cd} - -# ============================================================================= -# -# Hook configuration for zoxide. -# - -# Hook to add new entries to the database. -{__zoxide_hook} - -# Initialize hook. -{hook_init} - -# ============================================================================= -# -# When using zoxide with --no-aliases, alias these internal functions as -# desired. -# - -# Jump to a directory using only keywords. -__zoxide_z() {{ - if [ \"$#\" -eq 0 ]; then - __zoxide_cd ~ - elif [ \"$#\" -eq 1 ] && [ \"$1\" = '-' ]; then - if [ -n \"$OLDPWD\" ]; then - __zoxide_cd \"$OLDPWD\" - else - echo \"zoxide: \\$OLDPWD is not set\" - return 1 - fi - else - local __zoxide_result - __zoxide_result=\"$(zoxide query -- \"$@\")\" && __zoxide_cd \"$__zoxide_result\" - fi -}} - -# Jump to a directory using interactive search. -__zoxide_zi() {{ - local __zoxide_result - __zoxide_result=\"$(zoxide query -i -- \"$@\")\" && __zoxide_cd \"$__zoxide_result\" -}} - -# Add a new entry to the database. -alias __zoxide_za='zoxide add' - -# Query an entry from the database using only keywords. -alias __zoxide_zq='zoxide query' - -# Query an entry from the database using interactive selection. -alias __zoxide_zqi='zoxide query -i' - -# Remove an entry from the database using the exact path. -alias __zoxide_zr='zoxide remove' - -# Remove an entry from the database using interactive selection. -__zoxide_zri() {{ - local __zoxide_result - __zoxide_result=\"$(zoxide query -i -- \"$@\")\" && zoxide remove \"$__zoxide_result\" -}} - -# ============================================================================= -# -# Convenient aliases for zoxide. Disable these using --no-aliases. -# - -{aliases} - -# ============================================================================= -# -# To initialize zoxide with zsh, add the following line to your zsh -# configuration file (usually ~/.zshrc): -# -# eval \"$(zoxide init zsh)\" -", - __zoxide_pwd = __zoxide_pwd, - __zoxide_cd = __zoxide_cd, - __zoxide_hook = __zoxide_hook, - hook_init = hook_init, - aliases = aliases, - )?; - - Ok(()) -} diff --git a/src/subcommand/mod.rs b/src/subcommand/mod.rs deleted file mode 100644 index 6ca5818..0000000 --- a/src/subcommand/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod add; -mod import; -mod init; -mod query; -mod remove; - -pub use add::Add; -pub use import::Import; -pub use init::Init; -pub use query::Query; -pub use remove::Remove; diff --git a/src/subcommand/query.rs b/src/subcommand/query.rs deleted file mode 100644 index e4d10bd..0000000 --- a/src/subcommand/query.rs +++ /dev/null @@ -1,127 +0,0 @@ -use crate::db::Dir; -use crate::fzf::Fzf; -use crate::util; - -use anyhow::{bail, Context, Result}; -use structopt::StructOpt; - -use std::io::{self, Write}; -use std::path::Path; - -/// Search for a directory -#[derive(Debug, StructOpt)] -#[structopt()] -pub struct Query { - keywords: Vec, - - /// Opens an interactive selection menu using fzf - #[structopt(short, long, conflicts_with = "list")] - interactive: bool, - - /// List all matching directories - #[structopt(short, long, conflicts_with = "interactive")] - list: bool, - - /// Display score along with result - #[structopt(short, long)] - score: bool, -} - -impl Query { - pub fn run(&self) -> Result<()> { - if self.list { - return self.query_list(); - } - - if self.interactive { - return self.query_interactive(); - } - - // if the input is already a valid path, simply print it as-is - if let [path] = self.keywords.as_slice() { - if Path::new(path).is_dir() { - let dir = Dir { - path: path.to_string(), - rank: 0.0, - last_accessed: 0, - }; - - if self.score { - println!("{}", dir.display_score(0)) - } else { - println!("{}", dir.display()); - } - - return Ok(()); - } - } - - self.query() - } - - fn query(&self) -> Result<()> { - let mut db = util::get_db()?; - let now = util::get_current_time()?; - - let mut matches = db.matches(now, &self.keywords); - - match matches.next() { - Some(dir) => { - if self.score { - println!("{}", dir.display_score(now)) - } else { - println!("{}", dir.display()); - } - } - None => bail!("no match found"), - } - - Ok(()) - } - - fn query_interactive(&self) -> Result<()> { - let mut db = util::get_db()?; - let now = util::get_current_time()?; - - let mut fzf = Fzf::new()?; - - for dir in db.matches(now, &self.keywords) { - fzf.write(format!("{}", dir.display_score(now)))?; - } - - match fzf.wait_select()? { - Some(selection) => { - if self.score { - print!("{}", selection) - } else { - let selection = selection - .get(5..) - .with_context(|| format!("fzf returned invalid output: {}", selection))?; - print!("{}", selection) - } - } - None => bail!("no match found"), - }; - - Ok(()) - } - - fn query_list(&self) -> Result<()> { - let mut db = util::get_db()?; - let now = util::get_current_time()?; - - let stdout = io::stdout(); - let mut handle = stdout.lock(); - - for dir in db.matches(now, &self.keywords) { - if self.score { - writeln!(handle, "{}", dir.display_score(now)) - } else { - writeln!(handle, "{}", dir.display()) - } - .unwrap(); - } - - Ok(()) - } -} diff --git a/src/subcommand/remove.rs b/src/subcommand/remove.rs deleted file mode 100644 index 4fd26bc..0000000 --- a/src/subcommand/remove.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::util; - -use anyhow::{bail, Result}; -use structopt::StructOpt; - -/// Remove a directory -#[derive(Debug, StructOpt)] -#[structopt()] -pub struct Remove { - path: String, -} - -impl Remove { - pub fn run(&self) -> Result<()> { - remove(&self.path) - } -} - -fn remove(path: &str) -> Result<()> { - let mut db = util::get_db()?; - - if let Some(idx) = db.dirs.iter().position(|dir| dir.path == path) { - db.dirs.swap_remove(idx); - db.modified = true; - return Ok(()); - } - - let path = util::resolve_path(&path)?; - let path = util::path_to_str(&path)?; - - if let Some(idx) = db.dirs.iter().position(|dir| dir.path == path) { - db.dirs.swap_remove(idx); - db.modified = true; - return Ok(()); - } - - bail!("could not find path in database: {}", path) -} diff --git a/src/util.rs b/src/util.rs index ec8e4dc..73a0024 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,17 +1,21 @@ -use crate::config; -use crate::db::{Db, Epoch}; +use zoxide_engine::Epoch; use anyhow::{bail, Context, Result}; + use std::env; use std::path::{Component, Path, PathBuf}; use std::time::SystemTime; -pub fn get_db() -> Result { - let data_dir = config::zo_data_dir()?; - Db::open(data_dir) +pub fn canonicalize>(path: &P) -> Result { + dunce::canonicalize(path) + .with_context(|| format!("could not resolve path: {}", path.as_ref().display())) } -pub fn get_current_time() -> Result { +pub fn current_dir() -> Result { + env::current_dir().context("could not get current directory") +} + +pub fn current_time() -> Result { let current_time = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .context("system clock set to invalid time")? @@ -20,9 +24,10 @@ pub fn get_current_time() -> Result { Ok(current_time as _) } -pub fn canonicalize>(path: &P) -> Result { +pub fn path_to_str>(path: &P) -> Result<&str> { let path = path.as_ref(); - dunce::canonicalize(path).with_context(|| format!("could not resolve path: {}", path.display())) + path.to_str() + .with_context(|| format!("invalid UTF-8 in path: {}", path.display())) } /// Resolves the absolute version of a path. @@ -31,7 +36,7 @@ pub fn canonicalize>(path: &P) -> Result { /// character. /// If path is relative, use the current directory to build the absolute path. #[cfg(any(unix, windows))] -pub fn resolve_path>(path: P) -> Result { +pub fn resolve_path>(path: &P) -> Result { let path = path.as_ref(); let base_path; @@ -46,7 +51,7 @@ pub fn resolve_path>(path: P) -> Result { stack.push(root); } _ => { - base_path = get_current_dir()?; + base_path = current_dir()?; stack.extend(base_path.components()); } } @@ -73,7 +78,7 @@ pub fn resolve_path>(path: P) -> Result { } fn get_drive_relative(drive_letter: u8) -> Result { - let path = get_current_dir()?; + let path = current_dir()?; if Some(drive_letter) == get_drive_letter(&path) { return Ok(path); } @@ -124,7 +129,7 @@ pub fn resolve_path>(path: P) -> Result { stack.extend(base_path.components()); } _ => { - base_path = get_current_dir()?; + base_path = current_dir()?; stack.extend(base_path.components()); } } @@ -149,13 +154,3 @@ pub fn resolve_path>(path: P) -> Result { } Ok(result) } - -pub fn get_current_dir() -> Result { - env::current_dir().context("could not get current path") -} - -pub fn path_to_str>(path: &P) -> Result<&str> { - let path = path.as_ref(); - path.to_str() - .with_context(|| format!("invalid utf-8 sequence in path: {}", path.display())) -}