Edit subcommand (#498)

This commit is contained in:
Ajeet D'Souza 2023-01-07 22:58:10 +05:30 committed by GitHub
parent cf0c9c002e
commit 3ab0a7b8fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1248 additions and 644 deletions

View File

@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- `edit` subcommand to adjust the scores of entries.
### Fixed ### Fixed
- Zsh: completions clashing with `zsh-autocomplete`. - Zsh: completions clashing with `zsh-autocomplete`.
@ -17,8 +21,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Zsh: don't hide output from `chpwd` hooks. - Zsh: don't hide output from `chpwd` hooks.
- Nushell: upgrade minimum supported version to v0.73.0. - Nushell: upgrade minimum supported version to v0.73.0.
- Zsh: fix extra space in interactive completions when no match is found. - Zsh: fix extra space in interactive completions when no match is found.
- Fzf: `<TAB>` now cycles through completions. - Fzf: various improvements.
- Fzf: enable colors in preview when possible on macOS / BSD. - Nushell: Accidental redefinition of hooks when initialized twice.
### Removed
- `remove -i` subcommand: use `edit` instead.
## [0.8.3] - 2022-09-02 ## [0.8.3] - 2022-09-02

343
Cargo.lock generated
View File

@ -3,19 +3,31 @@
version = 3 version = 3
[[package]] [[package]]
name = "aho-corasick" name = "Inflector"
version = "0.7.19" version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]] [[package]]
name = "anyhow" name = "aliasable"
version = "1.0.66" version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
[[package]]
name = "anyhow"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
[[package]] [[package]]
name = "askama" name = "askama"
@ -62,9 +74,9 @@ dependencies = [
[[package]] [[package]]
name = "assert_cmd" name = "assert_cmd"
version = "2.0.5" version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5c2ca00549910ec251e3bd15f87aeeb206c9456b9a77b43ff6c97c54042a472" checksum = "fa3d466004a8b4cb1bc34044240a2fd29d17607e2e3bd613eb44fd48e8100da3"
dependencies = [ dependencies = [
"bstr", "bstr",
"doc-comment", "doc-comment",
@ -74,23 +86,6 @@ dependencies = [
"wait-timeout", "wait-timeout",
] ]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]] [[package]]
name = "bincode" name = "bincode"
version = "1.3.3" version = "1.3.3"
@ -108,15 +103,22 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bstr" name = "bstr"
version = "0.2.17" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" checksum = "b45ea9b00a7b3f2988e9a65ad3917e62123c38dba709b666506207be96d1790b"
dependencies = [ dependencies = [
"lazy_static",
"memchr", "memchr",
"once_cell",
"regex-automata", "regex-automata",
"serde",
] ]
[[package]]
name = "cc"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -125,14 +127,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.0.18" version = "4.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335867764ed2de42325fafe6d18b8af74ba97ee0c590fa016f157535b42ab04b" checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39"
dependencies = [ dependencies = [
"atty",
"bitflags", "bitflags",
"clap_derive", "clap_derive",
"clap_lex", "clap_lex",
"is-terminal",
"once_cell", "once_cell",
"strsim", "strsim",
"termcolor", "termcolor",
@ -140,18 +142,18 @@ dependencies = [
[[package]] [[package]]
name = "clap_complete" name = "clap_complete"
version = "4.0.3" version = "4.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfe581a2035db4174cdbdc91265e1aba50f381577f0510d0ad36c7bc59cc84a3" checksum = "10861370d2ba66b0f5989f83ebf35db6421713fd92351790e7fdd6c36774c56b"
dependencies = [ dependencies = [
"clap", "clap",
] ]
[[package]] [[package]]
name = "clap_complete_fig" name = "clap_complete_fig"
version = "4.0.1" version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b36d1abc7184a737efc9f589e6e783e8b56c72e71fca748cf9947ed0a6f46d44" checksum = "46b30e010e669cd021e5004f3be26cff6b7c08d2a8a0d65b48d43a8cc0efd6c3"
dependencies = [ dependencies = [
"clap", "clap",
"clap_complete", "clap_complete",
@ -159,9 +161,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.0.18" version = "4.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16a1b0f6422af32d5da0c58e2703320f379216ee70198241c84173a8c5ac28f3" checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro-error", "proc-macro-error",
@ -179,15 +181,6 @@ dependencies = [
"os_str_bytes", "os_str_bytes",
] ]
[[package]]
name = "crossbeam-utils"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "difflib" name = "difflib"
version = "0.4.0" version = "0.4.0"
@ -232,6 +225,27 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]]
name = "errno"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
dependencies = [
"errno-dragonfly",
"libc",
"winapi",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "1.8.0" version = "1.8.0"
@ -260,15 +274,15 @@ dependencies = [
[[package]] [[package]]
name = "glob" name = "glob"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "globset" name = "globset"
version = "0.4.9" version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"bstr", "bstr",
@ -285,20 +299,19 @@ checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.19" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
dependencies = [ dependencies = [
"libc", "libc",
] ]
[[package]] [[package]]
name = "ignore" name = "ignore"
version = "0.4.18" version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" checksum = "a05705bc64e0b66a806c3740bd6578ea66051b157ec42dc219c785cbf185aef3"
dependencies = [ dependencies = [
"crossbeam-utils",
"globset", "globset",
"lazy_static", "lazy_static",
"log", "log",
@ -319,6 +332,28 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "io-lifetimes"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "is-terminal"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189"
dependencies = [
"hermit-abi",
"io-lifetimes",
"rustix",
"windows-sys",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.5" version = "0.10.5"
@ -336,9 +371,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.137" version = "0.2.139"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
[[package]]
name = "linux-raw-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
[[package]] [[package]]
name = "log" name = "log"
@ -379,21 +420,21 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.25.0" version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb" checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694"
dependencies = [ dependencies = [
"autocfg",
"bitflags", "bitflags",
"cfg-if", "cfg-if",
"libc", "libc",
"static_assertions",
] ]
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.1" version = "7.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" checksum = "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c"
dependencies = [ dependencies = [
"memchr", "memchr",
"minimal-lexical", "minimal-lexical",
@ -401,21 +442,44 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.16.0" version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]] [[package]]
name = "os_str_bytes" name = "os_str_bytes"
version = "6.3.1" version = "6.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
[[package]]
name = "ouroboros"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca"
dependencies = [
"aliasable",
"ouroboros_macro",
]
[[package]]
name = "ouroboros_macro"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d"
dependencies = [
"Inflector",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "predicates" name = "predicates"
version = "2.1.1" version = "2.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd"
dependencies = [ dependencies = [
"difflib", "difflib",
"itertools", "itertools",
@ -424,15 +488,15 @@ dependencies = [
[[package]] [[package]]
name = "predicates-core" name = "predicates-core"
version = "1.0.3" version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" checksum = "72f883590242d3c6fc5bf50299011695fa6590c2c70eac95ee1bdb9a733ad1a2"
[[package]] [[package]]
name = "predicates-tree" name = "predicates-tree"
version = "1.0.5" version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" checksum = "54ff541861505aabf6ea722d2131ee980b8276e10a1297b94e896dd8b621850d"
dependencies = [ dependencies = [
"predicates-core", "predicates-core",
"termtree", "termtree",
@ -464,18 +528,18 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.47" version = "1.0.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.21" version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -502,9 +566,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.6.0" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -519,9 +583,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.27" version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]] [[package]]
name = "remove_dir_all" name = "remove_dir_all"
@ -534,9 +598,9 @@ dependencies = [
[[package]] [[package]]
name = "rstest" name = "rstest"
version = "0.15.0" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9c9dc66cc29792b663ffb5269be669f1613664e69ad56441fdb895c2347b930" checksum = "b07f2d176c472198ec1e6551dc7da28f1c089652f66a7b722676c2238ebc0edf"
dependencies = [ dependencies = [
"rstest_macros", "rstest_macros",
"rustc_version", "rustc_version",
@ -544,15 +608,16 @@ dependencies = [
[[package]] [[package]]
name = "rstest_macros" name = "rstest_macros"
version = "0.14.0" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5015e68a0685a95ade3eee617ff7101ab6a3fc689203101ca16ebc16f2b89c66" checksum = "7229b505ae0706e64f37ffc54a9c163e11022a6636d58fe1f3f52018257ff9f7"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustc_version", "rustc_version",
"syn", "syn",
"unicode-ident",
] ]
[[package]] [[package]]
@ -575,6 +640,20 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rustix"
version = "0.36.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549"
dependencies = [
"bitflags",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@ -586,24 +665,24 @@ dependencies = [
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.14" version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.147" version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.147" version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -616,6 +695,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
@ -624,9 +709,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.103" version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -658,24 +743,24 @@ dependencies = [
[[package]] [[package]]
name = "termtree" name = "termtree"
version = "0.2.4" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" checksum = "95059e91184749cb66be6dc994f67f182b6d897cb3df74a5bf66b5e709295fd8"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.37" version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.37" version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -702,9 +787,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.5" version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
[[package]] [[package]]
name = "version_check" name = "version_check"
@ -780,6 +865,63 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
[[package]] [[package]]
name = "xtask" name = "xtask"
version = "0.1.0" version = "0.1.0"
@ -806,6 +948,7 @@ dependencies = [
"fastrand", "fastrand",
"glob", "glob",
"nix", "nix",
"ouroboros",
"rstest", "rstest",
"rstest_reuse", "rstest_reuse",
"serde", "serde",

View File

@ -31,11 +31,12 @@ dunce = "1.0.1"
fastrand = "1.7.0" fastrand = "1.7.0"
glob = "0.3.0" glob = "0.3.0"
ignore = "0.4.18" ignore = "0.4.18"
nix = { version = "0.25.0", default-features = false, features = [ nix = { version = "0.26.1", default-features = false, features = [
"fs", "fs",
"user", "user",
] } ] }
rstest = { version = "0.15.0", default-features = false } ouroboros = "0.15.5"
rstest = { version = "0.16.0", default-features = false }
rstest_reuse = "0.4.0" rstest_reuse = "0.4.0"
serde = { version = "1.0.116", features = ["derive"] } serde = { version = "1.0.116", features = ["derive"] }
shell-words = "1.0.0" shell-words = "1.0.0"
@ -51,6 +52,7 @@ dirs.workspace = true
dunce.workspace = true dunce.workspace = true
fastrand.workspace = true fastrand.workspace = true
glob.workspace = true glob.workspace = true
ouroboros.workspace = true
serde.workspace = true serde.workspace = true
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]

View File

@ -37,6 +37,61 @@ _arguments "${_arguments_options[@]}" \
'*::paths:_files -/' \ '*::paths:_files -/' \
&& ret=0 && ret=0
;; ;;
(edit)
_arguments "${_arguments_options[@]}" \
'-h[Print help information]' \
'--help[Print help information]' \
'-V[Print version information]' \
'--version[Print version information]' \
":: :_zoxide__edit_commands" \
"*::: :->edit" \
&& ret=0
case $state in
(edit)
words=($line[1] "${words[@]}")
(( CURRENT += 1 ))
curcontext="${curcontext%:*:*}:zoxide-edit-command-$line[1]:"
case $line[1] in
(decrement)
_arguments "${_arguments_options[@]}" \
'-h[Print help information]' \
'--help[Print help information]' \
'-V[Print version information]' \
'--version[Print version information]' \
':path:' \
&& ret=0
;;
(delete)
_arguments "${_arguments_options[@]}" \
'-h[Print help information]' \
'--help[Print help information]' \
'-V[Print version information]' \
'--version[Print version information]' \
':path:' \
&& ret=0
;;
(increment)
_arguments "${_arguments_options[@]}" \
'-h[Print help information]' \
'--help[Print help information]' \
'-V[Print version information]' \
'--version[Print version information]' \
':path:' \
&& ret=0
;;
(reload)
_arguments "${_arguments_options[@]}" \
'-h[Print help information]' \
'--help[Print help information]' \
'-V[Print version information]' \
'--version[Print version information]' \
&& ret=0
;;
esac
;;
esac
;;
(import) (import)
_arguments "${_arguments_options[@]}" \ _arguments "${_arguments_options[@]}" \
'--from=[Application to import from]:FROM:(autojump z)' \ '--from=[Application to import from]:FROM:(autojump z)' \
@ -79,8 +134,6 @@ _arguments "${_arguments_options[@]}" \
;; ;;
(remove) (remove)
_arguments "${_arguments_options[@]}" \ _arguments "${_arguments_options[@]}" \
'-i[Use interactive selection]' \
'--interactive[Use interactive selection]' \
'-h[Print help information]' \ '-h[Print help information]' \
'--help[Print help information]' \ '--help[Print help information]' \
'-V[Print version information]' \ '-V[Print version information]' \
@ -97,6 +150,7 @@ esac
_zoxide_commands() { _zoxide_commands() {
local commands; commands=( local commands; commands=(
'add:Add a new directory or increment its rank' \ 'add:Add a new directory or increment its rank' \
'edit:Edit the database' \
'import:Import entries from another application' \ 'import:Import entries from another application' \
'init:Generate shell configuration' \ 'init:Generate shell configuration' \
'query:Search for a directory in the database' \ 'query:Search for a directory in the database' \
@ -109,11 +163,36 @@ _zoxide__add_commands() {
local commands; commands=() local commands; commands=()
_describe -t commands 'zoxide add commands' commands "$@" _describe -t commands 'zoxide add commands' commands "$@"
} }
(( $+functions[_zoxide__edit__decrement_commands] )) ||
_zoxide__edit__decrement_commands() {
local commands; commands=()
_describe -t commands 'zoxide edit decrement commands' commands "$@"
}
(( $+functions[_zoxide__edit__delete_commands] )) ||
_zoxide__edit__delete_commands() {
local commands; commands=()
_describe -t commands 'zoxide edit delete commands' commands "$@"
}
(( $+functions[_zoxide__edit_commands] )) ||
_zoxide__edit_commands() {
local commands; commands=(
'decrement:' \
'delete:' \
'increment:' \
'reload:' \
)
_describe -t commands 'zoxide edit commands' commands "$@"
}
(( $+functions[_zoxide__import_commands] )) || (( $+functions[_zoxide__import_commands] )) ||
_zoxide__import_commands() { _zoxide__import_commands() {
local commands; commands=() local commands; commands=()
_describe -t commands 'zoxide import commands' commands "$@" _describe -t commands 'zoxide import commands' commands "$@"
} }
(( $+functions[_zoxide__edit__increment_commands] )) ||
_zoxide__edit__increment_commands() {
local commands; commands=()
_describe -t commands 'zoxide edit increment commands' commands "$@"
}
(( $+functions[_zoxide__init_commands] )) || (( $+functions[_zoxide__init_commands] )) ||
_zoxide__init_commands() { _zoxide__init_commands() {
local commands; commands=() local commands; commands=()
@ -124,6 +203,11 @@ _zoxide__query_commands() {
local commands; commands=() local commands; commands=()
_describe -t commands 'zoxide query commands' commands "$@" _describe -t commands 'zoxide query commands' commands "$@"
} }
(( $+functions[_zoxide__edit__reload_commands] )) ||
_zoxide__edit__reload_commands() {
local commands; commands=()
_describe -t commands 'zoxide edit reload commands' commands "$@"
}
(( $+functions[_zoxide__remove_commands] )) || (( $+functions[_zoxide__remove_commands] )) ||
_zoxide__remove_commands() { _zoxide__remove_commands() {
local commands; commands=() local commands; commands=()

View File

@ -26,6 +26,7 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock {
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Add a new directory or increment its rank') [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Add a new directory or increment its rank')
[CompletionResult]::new('edit', 'edit', [CompletionResultType]::ParameterValue, 'Edit the database')
[CompletionResult]::new('import', 'import', [CompletionResultType]::ParameterValue, 'Import entries from another application') [CompletionResult]::new('import', 'import', [CompletionResultType]::ParameterValue, 'Import entries from another application')
[CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'Generate shell configuration') [CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'Generate shell configuration')
[CompletionResult]::new('query', 'query', [CompletionResultType]::ParameterValue, 'Search for a directory in the database') [CompletionResult]::new('query', 'query', [CompletionResultType]::ParameterValue, 'Search for a directory in the database')
@ -39,6 +40,45 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock {
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
break break
} }
'zoxide;edit' {
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('decrement', 'decrement', [CompletionResultType]::ParameterValue, 'decrement')
[CompletionResult]::new('delete', 'delete', [CompletionResultType]::ParameterValue, 'delete')
[CompletionResult]::new('increment', 'increment', [CompletionResultType]::ParameterValue, 'increment')
[CompletionResult]::new('reload', 'reload', [CompletionResultType]::ParameterValue, 'reload')
break
}
'zoxide;edit;decrement' {
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
break
}
'zoxide;edit;delete' {
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
break
}
'zoxide;edit;increment' {
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
break
}
'zoxide;edit;reload' {
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
break
}
'zoxide;import' { 'zoxide;import' {
[CompletionResult]::new('--from', 'from', [CompletionResultType]::ParameterName, 'Application to import from') [CompletionResult]::new('--from', 'from', [CompletionResultType]::ParameterName, 'Application to import from')
[CompletionResult]::new('--merge', 'merge', [CompletionResultType]::ParameterName, 'Merge into existing database') [CompletionResult]::new('--merge', 'merge', [CompletionResultType]::ParameterName, 'Merge into existing database')
@ -74,8 +114,6 @@ Register-ArgumentCompleter -Native -CommandName 'zoxide' -ScriptBlock {
break break
} }
'zoxide;remove' { 'zoxide;remove' {
[CompletionResult]::new('-i', 'i', [CompletionResultType]::ParameterName, 'Use interactive selection')
[CompletionResult]::new('--interactive', 'interactive', [CompletionResultType]::ParameterName, 'Use interactive selection')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')

View File

@ -15,6 +15,9 @@ _zoxide() {
zoxide,add) zoxide,add)
cmd="zoxide__add" cmd="zoxide__add"
;; ;;
zoxide,edit)
cmd="zoxide__edit"
;;
zoxide,import) zoxide,import)
cmd="zoxide__import" cmd="zoxide__import"
;; ;;
@ -27,6 +30,18 @@ _zoxide() {
zoxide,remove) zoxide,remove)
cmd="zoxide__remove" cmd="zoxide__remove"
;; ;;
zoxide__edit,decrement)
cmd="zoxide__edit__decrement"
;;
zoxide__edit,delete)
cmd="zoxide__edit__delete"
;;
zoxide__edit,increment)
cmd="zoxide__edit__increment"
;;
zoxide__edit,reload)
cmd="zoxide__edit__reload"
;;
*) *)
;; ;;
esac esac
@ -34,7 +49,7 @@ _zoxide() {
case "${cmd}" in case "${cmd}" in
zoxide) zoxide)
opts="-h -V --help --version add import init query remove" opts="-h -V --help --version add edit import init query remove"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -61,6 +76,76 @@ _zoxide() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
zoxide__edit)
opts="-h -V --help --version decrement delete increment reload"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
zoxide__edit__decrement)
opts="-h -V --help --version <PATH>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
zoxide__edit__delete)
opts="-h -V --help --version <PATH>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
zoxide__edit__increment)
opts="-h -V --help --version <PATH>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
zoxide__edit__reload)
opts="-h -V --help --version"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
zoxide__import) zoxide__import)
opts="-h -V --from --merge --help --version <PATH>" opts="-h -V --from --merge --help --version <PATH>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
@ -120,7 +205,7 @@ _zoxide() {
return 0 return 0
;; ;;
zoxide__remove) zoxide__remove)
opts="-i -h -V --interactive --help --version [PATHS]..." opts="-h -V --help --version [PATHS]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0

View File

@ -23,6 +23,7 @@ set edit:completion:arg-completer[zoxide] = {|@words|
cand -V 'Print version information' cand -V 'Print version information'
cand --version 'Print version information' cand --version 'Print version information'
cand add 'Add a new directory or increment its rank' cand add 'Add a new directory or increment its rank'
cand edit 'Edit the database'
cand import 'Import entries from another application' cand import 'Import entries from another application'
cand init 'Generate shell configuration' cand init 'Generate shell configuration'
cand query 'Search for a directory in the database' cand query 'Search for a directory in the database'
@ -34,6 +35,40 @@ set edit:completion:arg-completer[zoxide] = {|@words|
cand -V 'Print version information' cand -V 'Print version information'
cand --version 'Print version information' cand --version 'Print version information'
} }
&'zoxide;edit'= {
cand -h 'Print help information'
cand --help 'Print help information'
cand -V 'Print version information'
cand --version 'Print version information'
cand decrement 'decrement'
cand delete 'delete'
cand increment 'increment'
cand reload 'reload'
}
&'zoxide;edit;decrement'= {
cand -h 'Print help information'
cand --help 'Print help information'
cand -V 'Print version information'
cand --version 'Print version information'
}
&'zoxide;edit;delete'= {
cand -h 'Print help information'
cand --help 'Print help information'
cand -V 'Print version information'
cand --version 'Print version information'
}
&'zoxide;edit;increment'= {
cand -h 'Print help information'
cand --help 'Print help information'
cand -V 'Print version information'
cand --version 'Print version information'
}
&'zoxide;edit;reload'= {
cand -h 'Print help information'
cand --help 'Print help information'
cand -V 'Print version information'
cand --version 'Print version information'
}
&'zoxide;import'= { &'zoxide;import'= {
cand --from 'Application to import from' cand --from 'Application to import from'
cand --merge 'Merge into existing database' cand --merge 'Merge into existing database'
@ -66,8 +101,6 @@ set edit:completion:arg-completer[zoxide] = {|@words|
cand --version 'Print version information' cand --version 'Print version information'
} }
&'zoxide;remove'= { &'zoxide;remove'= {
cand -i 'Use interactive selection'
cand --interactive 'Use interactive selection'
cand -h 'Print help information' cand -h 'Print help information'
cand --help 'Print help information' cand --help 'Print help information'
cand -V 'Print version information' cand -V 'Print version information'

View File

@ -1,12 +1,27 @@
complete -c zoxide -n "__fish_use_subcommand" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_use_subcommand" -s h -l help -d 'Print help information'
complete -c zoxide -n "__fish_use_subcommand" -s V -l version -d 'Print version information' complete -c zoxide -n "__fish_use_subcommand" -s V -l version -d 'Print version information'
complete -c zoxide -n "__fish_use_subcommand" -f -a "add" -d 'Add a new directory or increment its rank' complete -c zoxide -n "__fish_use_subcommand" -f -a "add" -d 'Add a new directory or increment its rank'
complete -c zoxide -n "__fish_use_subcommand" -f -a "edit" -d 'Edit the database'
complete -c zoxide -n "__fish_use_subcommand" -f -a "import" -d 'Import entries from another application' complete -c zoxide -n "__fish_use_subcommand" -f -a "import" -d 'Import entries from another application'
complete -c zoxide -n "__fish_use_subcommand" -f -a "init" -d 'Generate shell configuration' complete -c zoxide -n "__fish_use_subcommand" -f -a "init" -d 'Generate shell configuration'
complete -c zoxide -n "__fish_use_subcommand" -f -a "query" -d 'Search for a directory in the database' complete -c zoxide -n "__fish_use_subcommand" -f -a "query" -d 'Search for a directory in the database'
complete -c zoxide -n "__fish_use_subcommand" -f -a "remove" -d 'Remove a directory from the database' complete -c zoxide -n "__fish_use_subcommand" -f -a "remove" -d 'Remove a directory from the database'
complete -c zoxide -n "__fish_seen_subcommand_from add" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_seen_subcommand_from add" -s h -l help -d 'Print help information'
complete -c zoxide -n "__fish_seen_subcommand_from add" -s V -l version -d 'Print version information' complete -c zoxide -n "__fish_seen_subcommand_from add" -s V -l version -d 'Print version information'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -s h -l help -d 'Print help information'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -s V -l version -d 'Print version information'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -f -a "decrement"
complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -f -a "delete"
complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -f -a "increment"
complete -c zoxide -n "__fish_seen_subcommand_from edit; and not __fish_seen_subcommand_from decrement; and not __fish_seen_subcommand_from delete; and not __fish_seen_subcommand_from increment; and not __fish_seen_subcommand_from reload" -f -a "reload"
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from decrement" -s h -l help -d 'Print help information'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from decrement" -s V -l version -d 'Print version information'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from delete" -s h -l help -d 'Print help information'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from delete" -s V -l version -d 'Print version information'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from increment" -s h -l help -d 'Print help information'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from increment" -s V -l version -d 'Print version information'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from reload" -s h -l help -d 'Print help information'
complete -c zoxide -n "__fish_seen_subcommand_from edit; and __fish_seen_subcommand_from reload" -s V -l version -d 'Print version information'
complete -c zoxide -n "__fish_seen_subcommand_from import" -l from -d 'Application to import from' -r -f -a "{autojump ,z }" complete -c zoxide -n "__fish_seen_subcommand_from import" -l from -d 'Application to import from' -r -f -a "{autojump ,z }"
complete -c zoxide -n "__fish_seen_subcommand_from import" -l merge -d 'Merge into existing database' complete -c zoxide -n "__fish_seen_subcommand_from import" -l merge -d 'Merge into existing database'
complete -c zoxide -n "__fish_seen_subcommand_from import" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_seen_subcommand_from import" -s h -l help -d 'Print help information'
@ -23,6 +38,5 @@ complete -c zoxide -n "__fish_seen_subcommand_from query" -s l -l list -d 'List
complete -c zoxide -n "__fish_seen_subcommand_from query" -s s -l score -d 'Print score with results' complete -c zoxide -n "__fish_seen_subcommand_from query" -s s -l score -d 'Print score with results'
complete -c zoxide -n "__fish_seen_subcommand_from query" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_seen_subcommand_from query" -s h -l help -d 'Print help information'
complete -c zoxide -n "__fish_seen_subcommand_from query" -s V -l version -d 'Print version information' complete -c zoxide -n "__fish_seen_subcommand_from query" -s V -l version -d 'Print version information'
complete -c zoxide -n "__fish_seen_subcommand_from remove" -s i -l interactive -d 'Use interactive selection'
complete -c zoxide -n "__fish_seen_subcommand_from remove" -s h -l help -d 'Print help information' complete -c zoxide -n "__fish_seen_subcommand_from remove" -s h -l help -d 'Print help information'
complete -c zoxide -n "__fish_seen_subcommand_from remove" -s V -l version -d 'Print version information' complete -c zoxide -n "__fish_seen_subcommand_from remove" -s V -l version -d 'Print version information'

View File

@ -21,6 +21,87 @@ const completion: Fig.Spec = {
template: "folders", template: "folders",
}, },
}, },
{
name: "edit",
description: "Edit the database",
subcommands: [
{
name: "decrement",
hidden: true,
options: [
{
name: ["-h", "--help"],
description: "Print help information",
},
{
name: ["-V", "--version"],
description: "Print version information",
},
],
args: {
name: "path",
},
},
{
name: "delete",
hidden: true,
options: [
{
name: ["-h", "--help"],
description: "Print help information",
},
{
name: ["-V", "--version"],
description: "Print version information",
},
],
args: {
name: "path",
},
},
{
name: "increment",
hidden: true,
options: [
{
name: ["-h", "--help"],
description: "Print help information",
},
{
name: ["-V", "--version"],
description: "Print version information",
},
],
args: {
name: "path",
},
},
{
name: "reload",
hidden: true,
options: [
{
name: ["-h", "--help"],
description: "Print help information",
},
{
name: ["-V", "--version"],
description: "Print version information",
},
],
},
],
options: [
{
name: ["-h", "--help"],
description: "Print help information",
},
{
name: ["-V", "--version"],
description: "Print version information",
},
],
},
{ {
name: "import", name: "import",
description: "Import entries from another application", description: "Import entries from another application",
@ -166,10 +247,6 @@ const completion: Fig.Spec = {
name: "remove", name: "remove",
description: "Remove a directory from the database", description: "Remove a directory from the database",
options: [ options: [
{
name: ["-i", "--interactive"],
description: "Use interactive selection",
},
{ {
name: ["-h", "--help"], name: ["-h", "--help"],
description: "Print help information", description: "Print help information",

View File

@ -35,7 +35,7 @@ Note: zoxide only supports fish v3.4.0 and above.
Add this to your env file (find it by running \fB$nu.env-path\fR in Nushell): Add this to your env file (find it by running \fB$nu.env-path\fR in Nushell):
.sp .sp
.nf .nf
\fBzoxide init nushell --hook prompt | save -f ~/.zoxide.nu\fR \fBzoxide init nushell | save -f ~/.zoxide.nu\fR
.fi .fi
.sp .sp
Now, add this to the end of your config file (find it by running Now, add this to the end of your config file (find it by running

View File

@ -1,4 +1,3 @@
comment_width = 100
group_imports = "StdExternalCrate" group_imports = "StdExternalCrate"
imports_granularity = "Module" imports_granularity = "Module"
max_width = 120 max_width = 120

View File

@ -3,22 +3,20 @@ use std::path::Path;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use crate::cmd::{Add, Run}; use crate::cmd::{Add, Run};
use crate::db::DatabaseFile; use crate::db::Database;
use crate::{config, util}; use crate::{config, util};
impl Run for Add { impl Run for Add {
fn run(&self) -> Result<()> { fn run(&self) -> Result<()> {
// These characters can't be printed cleanly to a single line, so they can cause confusion // These characters can't be printed cleanly to a single line, so they can cause confusion
// when writing to fzf / stdout. // when writing to stdout.
const EXCLUDE_CHARS: &[char] = &['\n', '\r']; const EXCLUDE_CHARS: &[char] = &['\n', '\r'];
let data_dir = config::data_dir()?;
let exclude_dirs = config::exclude_dirs()?; let exclude_dirs = config::exclude_dirs()?;
let max_age = config::maxage()?; let max_age = config::maxage()?;
let now = util::current_time()?; let now = util::current_time()?;
let mut db = DatabaseFile::new(data_dir); let mut db = Database::open()?;
let mut db = db.open()?;
for path in &self.paths { for path in &self.paths {
let path = if config::resolve_symlinks() { util::canonicalize } else { util::resolve_path }(path)?; let path = if config::resolve_symlinks() { util::canonicalize } else { util::resolve_path }(path)?;
@ -31,14 +29,12 @@ impl Run for Add {
if !Path::new(path).is_dir() { if !Path::new(path).is_dir() {
bail!("not a directory: {path}"); bail!("not a directory: {path}");
} }
db.add(path, now); db.add_update(path, 1.0, now);
} }
if db.modified { if db.dirty() {
db.age(max_age); db.age(max_age);
db.save()?;
} }
db.save()
Ok(())
} }
} }

View File

@ -2,7 +2,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use clap::{Parser, ValueEnum, ValueHint}; use clap::{Parser, Subcommand, ValueEnum, ValueHint};
const ENV_HELP: &str = "Environment variables: const ENV_HELP: &str = "Environment variables:
_ZO_DATA_DIR Path for zoxide data files _ZO_DATA_DIR Path for zoxide data files
@ -24,6 +24,7 @@ const ENV_HELP: &str = "Environment variables:
)] )]
pub enum Cmd { pub enum Cmd {
Add(Add), Add(Add),
Edit(Edit),
Import(Import), Import(Import),
Init(Init), Init(Init),
Query(Query), Query(Query),
@ -37,6 +38,25 @@ pub struct Add {
pub paths: Vec<PathBuf>, pub paths: Vec<PathBuf>,
} }
/// Edit the database
#[derive(Debug, Parser)]
pub struct Edit {
#[clap(subcommand)]
pub cmd: Option<EditCommand>,
}
#[derive(Clone, Debug, Subcommand)]
pub enum EditCommand {
#[clap(hide = true)]
Decrement { path: String },
#[clap(hide = true)]
Delete { path: String },
#[clap(hide = true)]
Increment { path: String },
#[clap(hide = true)]
Reload,
}
/// Import entries from another application /// Import entries from another application
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct Import { pub struct Import {
@ -125,9 +145,6 @@ pub struct Query {
/// Remove a directory from the database /// Remove a directory from the database
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct Remove { pub struct Remove {
/// Use interactive selection
#[clap(long, short)]
pub interactive: bool,
#[clap(value_hint = ValueHint::DirPath)] #[clap(value_hint = ValueHint::DirPath)]
pub paths: Vec<String>, pub paths: Vec<String>,
} }

83
src/cmd/edit.rs Normal file
View File

@ -0,0 +1,83 @@
use std::io::{self, Write};
use anyhow::Result;
use crate::cmd::{Edit, EditCommand, Run};
use crate::db::Database;
use crate::error::BrokenPipeHandler;
use crate::util::{self, Fzf, FzfChild};
impl Run for Edit {
fn run(&self) -> Result<()> {
let now = util::current_time()?;
let db = &mut Database::open()?;
match &self.cmd {
Some(cmd) => {
match cmd {
EditCommand::Decrement { path } => db.add(path, -1.0, now),
EditCommand::Delete { path } => {
db.remove(path);
}
EditCommand::Increment { path } => db.add(path, 1.0, now),
EditCommand::Reload => {}
}
db.save()?;
let stdout = &mut io::stdout().lock();
for dir in db.dirs().iter().rev() {
write!(stdout, "{}\0", dir.display().with_score(now).with_separator('\t')).pipe_exit("fzf")?;
}
Ok(())
}
None => {
db.sort_by_score(now);
db.save()?;
Self::get_fzf()?.wait()?;
Ok(())
}
}
}
}
impl Edit {
fn get_fzf() -> Result<FzfChild> {
Fzf::new()?
.args([
// Search mode
"--scheme=path",
// Search result
"--tiebreak=end,chunk,index",
// Interface
"--bind=\
btab:up,\
ctrl-r:reload(zoxide edit reload),\
ctrl-d:reload(zoxide edit delete {2..}),\
ctrl-w:reload(zoxide edit increment {2..}),\
ctrl-s:reload(zoxide edit decrement {2..}),\
ctrl-z:ignore,\
double-click:ignore,\
enter:abort,\
start:reload(zoxide edit reload),\
tab:down",
"--cycle",
"--keep-right",
// Layout
"--border=sharp",
"--border-label= zoxide-edit ",
"--header=\
ctrl-r:reload \tctrl-d:delete
ctrl-w:increment\tctrl-s:decrement
SCORE\tPATH",
"--info=inline",
"--layout=reverse",
"--padding=1,0,0,0",
// Display
"--color=label:bold",
"--tabstop=1",
])
.enable_preview()
.spawn()
}
}

View File

@ -3,24 +3,21 @@ use std::fs;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use crate::cmd::{Import, ImportFrom, Run}; use crate::cmd::{Import, ImportFrom, Run};
use crate::config; use crate::db::Database;
use crate::db::{Database, DatabaseFile, Dir};
impl Run for Import { impl Run for Import {
fn run(&self) -> Result<()> { fn run(&self) -> Result<()> {
let buffer = fs::read_to_string(&self.path) let buffer = fs::read_to_string(&self.path)
.with_context(|| format!("could not open database for importing: {}", &self.path.display()))?; .with_context(|| format!("could not open database for importing: {}", &self.path.display()))?;
let data_dir = config::data_dir()?; let mut db = Database::open()?;
let mut db = DatabaseFile::new(data_dir); if !self.merge && !db.dirs().is_empty() {
let db = &mut db.open()?;
if !self.merge && !db.dirs.is_empty() {
bail!("current database is not empty, specify --merge to continue anyway"); bail!("current database is not empty, specify --merge to continue anyway");
} }
match self.from { match self.from {
ImportFrom::Autojump => from_autojump(db, &buffer), ImportFrom::Autojump => import_autojump(&mut db, &buffer),
ImportFrom::Z => from_z(db, &buffer), ImportFrom::Z => import_z(&mut db, &buffer),
} }
.context("import error")?; .context("import error")?;
@ -28,7 +25,7 @@ impl Run for Import {
} }
} }
fn from_autojump<'a>(db: &mut Database<'a>, buffer: &'a str) -> Result<()> { fn import_autojump(db: &mut Database, buffer: &str) -> Result<()> {
for line in buffer.lines() { for line in buffer.lines() {
if line.is_empty() { if line.is_empty() {
continue; continue;
@ -43,18 +40,16 @@ fn from_autojump<'a>(db: &mut Database<'a>, buffer: &'a str) -> Result<()> {
let path = split.next().with_context(|| format!("invalid entry: {line}"))?; let path = split.next().with_context(|| format!("invalid entry: {line}"))?;
db.dirs.push(Dir { path: path.into(), rank, last_accessed: 0 }); db.add_unchecked(path, rank, 0);
db.modified = true;
} }
if db.modified { if db.dirty() {
db.dedup(); db.dedup();
} }
Ok(()) Ok(())
} }
fn from_z<'a>(db: &mut Database<'a>, buffer: &'a str) -> Result<()> { fn import_z(db: &mut Database, buffer: &str) -> Result<()> {
for line in buffer.lines() { for line in buffer.lines() {
if line.is_empty() { if line.is_empty() {
continue; continue;
@ -69,14 +64,12 @@ fn from_z<'a>(db: &mut Database<'a>, buffer: &'a str) -> Result<()> {
let path = split.next().with_context(|| format!("invalid entry: {line}"))?; let path = split.next().with_context(|| format!("invalid entry: {line}"))?;
db.dirs.push(Dir { path: path.into(), rank, last_accessed }); db.add_unchecked(path, rank, last_accessed);
db.modified = true;
} }
if db.modified { if db.dirty() {
db.dedup(); db.dedup();
} }
Ok(()) Ok(())
} }
@ -86,33 +79,33 @@ fn sigmoid(x: f64) -> f64 {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::sigmoid; use super::*;
use crate::db::{Database, Dir}; use crate::db::Dir;
#[test] #[test]
fn from_autojump() { fn from_autojump() {
let buffer = r#" let data_dir = tempfile::tempdir().unwrap();
let mut db = Database::open_dir(data_dir.path()).unwrap();
for (path, rank, last_accessed) in [
("/quux/quuz", 1.0, 100),
("/corge/grault/garply", 6.0, 600),
("/waldo/fred/plugh", 3.0, 300),
("/xyzzy/thud", 8.0, 800),
("/foo/bar", 9.0, 900),
] {
db.add_unchecked(path, rank, last_accessed);
}
let buffer = "\
7.0 /baz 7.0 /baz
2.0 /foo/bar 2.0 /foo/bar
5.0 /quux/quuz 5.0 /quux/quuz";
"#; import_autojump(&mut db, buffer).unwrap();
let dirs = vec![ db.sort_by_path();
Dir { path: "/quux/quuz".into(), rank: 1.0, last_accessed: 100 }, println!("got: {:?}", &db.dirs());
Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 },
Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 },
Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 },
Dir { path: "/foo/bar".into(), rank: 9.0, last_accessed: 900 },
];
let data_dir = tempfile::tempdir().unwrap();
let data_dir = &data_dir.path().to_path_buf();
let mut db = Database { dirs: dirs.into(), modified: false, data_dir };
super::from_autojump(&mut db, buffer).unwrap(); let exp = [
db.dirs.sort_by(|dir1, dir2| dir1.path.cmp(&dir2.path));
println!("got: {:?}", &db.dirs.as_slice());
let exp = &[
Dir { path: "/baz".into(), rank: sigmoid(7.0), last_accessed: 0 }, Dir { path: "/baz".into(), rank: sigmoid(7.0), last_accessed: 0 },
Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 }, Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 },
Dir { path: "/foo/bar".into(), rank: 9.0 + sigmoid(2.0), last_accessed: 900 }, Dir { path: "/foo/bar".into(), rank: 9.0 + sigmoid(2.0), last_accessed: 900 },
@ -122,7 +115,7 @@ mod tests {
]; ];
println!("exp: {exp:?}"); println!("exp: {exp:?}");
for (dir1, dir2) in db.dirs.iter().zip(exp) { for (dir1, dir2) in db.dirs().iter().zip(exp) {
assert_eq!(dir1.path, dir2.path); assert_eq!(dir1.path, dir2.path);
assert!((dir1.rank - dir2.rank).abs() < 0.01); assert!((dir1.rank - dir2.rank).abs() < 0.01);
assert_eq!(dir1.last_accessed, dir2.last_accessed); assert_eq!(dir1.last_accessed, dir2.last_accessed);
@ -131,29 +124,29 @@ mod tests {
#[test] #[test]
fn from_z() { fn from_z() {
let buffer = r#" let data_dir = tempfile::tempdir().unwrap();
let mut db = Database::open_dir(data_dir.path()).unwrap();
for (path, rank, last_accessed) in [
("/quux/quuz", 1.0, 100),
("/corge/grault/garply", 6.0, 600),
("/waldo/fred/plugh", 3.0, 300),
("/xyzzy/thud", 8.0, 800),
("/foo/bar", 9.0, 900),
] {
db.add_unchecked(path, rank, last_accessed);
}
let buffer = "\
/baz|7|700 /baz|7|700
/quux/quuz|4|400 /quux/quuz|4|400
/foo/bar|2|200 /foo/bar|2|200
/quux/quuz|5|500 /quux/quuz|5|500";
"#; import_z(&mut db, buffer).unwrap();
let dirs = vec![ db.sort_by_path();
Dir { path: "/quux/quuz".into(), rank: 1.0, last_accessed: 100 }, println!("got: {:?}", &db.dirs());
Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 },
Dir { path: "/waldo/fred/plugh".into(), rank: 3.0, last_accessed: 300 },
Dir { path: "/xyzzy/thud".into(), rank: 8.0, last_accessed: 800 },
Dir { path: "/foo/bar".into(), rank: 9.0, last_accessed: 900 },
];
let data_dir = tempfile::tempdir().unwrap();
let data_dir = &data_dir.path().to_path_buf();
let mut db = Database { dirs: dirs.into(), modified: false, data_dir };
super::from_z(&mut db, buffer).unwrap(); let exp = [
db.dirs.sort_by(|dir1, dir2| dir1.path.cmp(&dir2.path));
println!("got: {:?}", &db.dirs.as_slice());
let exp = &[
Dir { path: "/baz".into(), rank: 7.0, last_accessed: 700 }, Dir { path: "/baz".into(), rank: 7.0, last_accessed: 700 },
Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 }, Dir { path: "/corge/grault/garply".into(), rank: 6.0, last_accessed: 600 },
Dir { path: "/foo/bar".into(), rank: 11.0, last_accessed: 900 }, Dir { path: "/foo/bar".into(), rank: 11.0, last_accessed: 900 },
@ -163,7 +156,7 @@ mod tests {
]; ];
println!("exp: {exp:?}"); println!("exp: {exp:?}");
for (dir1, dir2) in db.dirs.iter().zip(exp) { for (dir1, dir2) in db.dirs().iter().zip(exp) {
assert_eq!(dir1.path, dir2.path); assert_eq!(dir1.path, dir2.path);
assert!((dir1.rank - dir2.rank).abs() < 0.01); assert!((dir1.rank - dir2.rank).abs() < 0.01);
assert_eq!(dir1.last_accessed, dir2.last_accessed); assert_eq!(dir1.last_accessed, dir2.last_accessed);

View File

@ -11,10 +11,8 @@ use crate::shell::{self, Opts};
impl Run for Init { impl Run for Init {
fn run(&self) -> Result<()> { fn run(&self) -> Result<()> {
let cmd = if self.no_cmd { None } else { Some(self.cmd.as_str()) }; let cmd = if self.no_cmd { None } else { Some(self.cmd.as_str()) };
let echo = config::echo(); let echo = config::echo();
let resolve_symlinks = config::resolve_symlinks(); let resolve_symlinks = config::resolve_symlinks();
let opts = &Opts { cmd, hook: self.hook, echo, resolve_symlinks }; let opts = &Opts { cmd, hook: self.hook, echo, resolve_symlinks };
let source = match self.shell { let source = match self.shell {

View File

@ -1,5 +1,6 @@
mod add; mod add;
mod cmd; mod cmd;
mod edit;
mod import; mod import;
mod init; mod init;
mod query; mod query;
@ -17,6 +18,7 @@ impl Run for Cmd {
fn run(&self) -> Result<()> { fn run(&self) -> Result<()> {
match self { match self {
Cmd::Add(cmd) => cmd.run(), Cmd::Add(cmd) => cmd.run(),
Cmd::Edit(cmd) => cmd.run(),
Cmd::Import(cmd) => cmd.run(), Cmd::Import(cmd) => cmd.run(),
Cmd::Init(cmd) => cmd.run(), Cmd::Init(cmd) => cmd.run(),
Cmd::Query(cmd) => cmd.run(), Cmd::Query(cmd) => cmd.run(),

View File

@ -4,15 +4,13 @@ use anyhow::{Context, Result};
use crate::cmd::{Query, Run}; use crate::cmd::{Query, Run};
use crate::config; use crate::config;
use crate::db::{Database, DatabaseFile}; use crate::db::{Database, Epoch, Stream};
use crate::error::BrokenPipeHandler; use crate::error::BrokenPipeHandler;
use crate::util::{self, Fzf}; use crate::util::{self, Fzf, FzfChild};
impl Run for Query { impl Run for Query {
fn run(&self) -> Result<()> { fn run(&self) -> Result<()> {
let data_dir = config::data_dir()?; let mut db = crate::db::Database::open()?;
let mut db = DatabaseFile::new(data_dir);
let mut db = db.open()?;
self.query(&mut db).and(db.save()) self.query(&mut db).and(db.save())
} }
} }
@ -20,7 +18,44 @@ impl Run for Query {
impl Query { impl Query {
fn query(&self, db: &mut Database) -> Result<()> { fn query(&self, db: &mut Database) -> Result<()> {
let now = util::current_time()?; let now = util::current_time()?;
let mut stream = self.get_stream(db, now);
if self.interactive {
let mut fzf = Self::get_fzf()?;
let selection = loop {
match stream.next() {
Some(dir) => {
if let Some(selection) = fzf.write(dir, now)? {
break selection;
}
}
None => break fzf.wait()?,
}
};
if self.score {
print!("{selection}");
} else {
let path = selection.get(7..).context("could not read selection from fzf")?;
print!("{path}");
}
} else if self.list {
let handle = &mut io::stdout().lock();
while let Some(dir) = stream.next() {
let dir = if self.score { dir.display().with_score(now) } else { dir.display() };
writeln!(handle, "{dir}").pipe_exit("stdout")?;
}
} else {
let handle = &mut io::stdout();
let dir = stream.next().context("no match found")?;
let dir = if self.score { dir.display().with_score(now) } else { dir.display() };
writeln!(handle, "{dir}").pipe_exit("stdout")?;
}
Ok(())
}
fn get_stream<'a>(&self, db: &'a mut Database, now: Epoch) -> Stream<'a> {
let mut stream = db.stream(now).with_keywords(&self.keywords); let mut stream = db.stream(now).with_keywords(&self.keywords);
if !self.all { if !self.all {
let resolve_symlinks = config::resolve_symlinks(); let resolve_symlinks = config::resolve_symlinks();
@ -29,46 +64,36 @@ impl Query {
if let Some(path) = &self.exclude { if let Some(path) = &self.exclude {
stream = stream.with_exclude(path); stream = stream.with_exclude(path);
} }
stream
}
if self.interactive { fn get_fzf() -> Result<FzfChild> {
let mut fzf = Fzf::new(false)?; let mut fzf = Fzf::new()?;
let stdin = fzf.stdin(); if let Some(fzf_opts) = config::fzf_opts() {
fzf.env("FZF_DEFAULT_OPTS", fzf_opts)
let selection = loop {
let Some(dir) = stream.next() else { break fzf.select()? };
match writeln!(stdin, "{}", dir.display_score(now)) {
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => break fzf.select()?,
result => result.context("could not write to fzf")?,
}
};
if self.score {
print!("{selection}");
} else {
let path = selection.get(5..).context("could not read selection from fzf")?;
print!("{path}");
}
} else if self.list {
let handle = &mut io::stdout().lock();
while let Some(dir) = stream.next() {
if self.score {
writeln!(handle, "{}", dir.display_score(now))
} else {
writeln!(handle, "{}", dir.display())
}
.pipe_exit("stdout")?;
}
handle.flush().pipe_exit("stdout")?;
} else { } else {
let dir = stream.next().context("no match found")?; fzf.args([
if self.score { // Search mode
writeln!(io::stdout(), "{}", dir.display_score(now)) "--scheme=path",
} else { // Search result
writeln!(io::stdout(), "{}", dir.display()) "--tiebreak=end,chunk,index",
} // Interface
.pipe_exit("stdout")?; "--bind=ctrl-z:ignore,btab:up,tab:down",
"--cycle",
"--keep-right",
// Layout
"--border=sharp", // rounded edges don't display correctly on some terminals
"--height=45%",
"--info=inline",
"--layout=reverse",
// Display
"--tabstop=1",
// Scripting
"--exit-0",
"--select-1",
])
.enable_preview()
} }
.spawn()
Ok(())
} }
} }

View File

@ -1,50 +1,19 @@
use std::io::{self, Write}; use anyhow::{bail, Result};
use anyhow::{bail, Context, Result};
use crate::cmd::{Remove, Run}; use crate::cmd::{Remove, Run};
use crate::config; use crate::db::Database;
use crate::db::DatabaseFile; use crate::util;
use crate::util::{self, Fzf};
impl Run for Remove { impl Run for Remove {
fn run(&self) -> Result<()> { fn run(&self) -> Result<()> {
let data_dir = config::data_dir()?; let mut db = Database::open()?;
let mut db = DatabaseFile::new(data_dir);
let mut db = db.open()?;
if self.interactive { for path in &self.paths {
let keywords = &self.paths; if !db.remove(path) {
let now = util::current_time()?; let path_abs = util::resolve_path(path)?;
let mut stream = db.stream(now).with_keywords(keywords); let path_abs = util::path_to_str(&path_abs)?;
if path_abs == path || !db.remove(path_abs) {
let mut fzf = Fzf::new(true)?; bail!("path not found in database: {path}")
let stdin = fzf.stdin();
let selection = loop {
let Some(dir) = stream.next() else { break fzf.select()? };
match writeln!(stdin, "{}", dir.display_score(now)) {
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => break fzf.select()?,
result => result.context("could not write to fzf")?,
}
};
let paths = selection.lines().filter_map(|line| line.get(5..));
for path in paths {
if !db.remove(path) {
db.modified = false;
bail!("path not found in database: {path}");
}
}
} else {
for path in &self.paths {
if !db.remove(path) {
let path_abs = util::resolve_path(path)?;
let path_abs = util::path_to_str(&path_abs)?;
if path_abs == path || !db.remove(path_abs) {
db.modified = false;
bail!("path not found in database: {path}")
}
} }
} }
} }

View File

@ -1,83 +1,11 @@
use std::borrow::Cow; use std::{
use std::fmt::{self, Display, Formatter}; borrow::Cow,
use std::ops::{Deref, DerefMut}; fmt::{self, Display, Formatter},
};
use anyhow::{bail, Context, Result};
use bincode::Options as _;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)] use crate::util::{DAY, HOUR, WEEK};
pub struct DirList<'a>(#[serde(borrow)] pub Vec<Dir<'a>>);
impl DirList<'_> {
const VERSION: u32 = 3;
pub fn new() -> DirList<'static> {
DirList(Vec::new())
}
pub fn from_bytes(bytes: &[u8]) -> Result<DirList> {
// Assume a maximum size for the database. This prevents bincode from throwing strange
// errors when it encounters invalid data.
const MAX_SIZE: u64 = 32 << 20; // 32 MiB
let deserializer = &mut bincode::options().with_fixint_encoding().with_limit(MAX_SIZE);
// Split bytes into sections.
let version_size = deserializer.serialized_size(&Self::VERSION).unwrap() as _;
if bytes.len() < version_size {
bail!("could not deserialize database: corrupted data");
}
let (bytes_version, bytes_dirs) = bytes.split_at(version_size);
// Deserialize sections.
(|| {
let version = deserializer.deserialize(bytes_version)?;
match version {
Self::VERSION => Ok(deserializer.deserialize(bytes_dirs)?),
version => {
bail!("unsupported version (got {version}, supports {})", Self::VERSION)
}
}
})()
.context("could not deserialize database")
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
(|| -> bincode::Result<_> {
// Preallocate buffer with combined size of sections.
let version_size = bincode::serialized_size(&Self::VERSION)?;
let dirs_size = bincode::serialized_size(&self)?;
let buffer_size = version_size + dirs_size;
let mut buffer = Vec::with_capacity(buffer_size as _);
// Serialize sections into buffer.
bincode::serialize_into(&mut buffer, &Self::VERSION)?;
bincode::serialize_into(&mut buffer, &self)?;
Ok(buffer)
})()
.context("could not serialize database")
}
}
impl<'a> Deref for DirList<'a> {
type Target = Vec<Dir<'a>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> DerefMut for DirList<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> From<Vec<Dir<'a>>> for DirList<'a> {
fn from(dirs: Vec<Dir<'a>>) -> Self {
DirList(dirs)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Dir<'a> { pub struct Dir<'a> {
@ -88,11 +16,11 @@ pub struct Dir<'a> {
} }
impl Dir<'_> { impl Dir<'_> {
pub fn score(&self, now: Epoch) -> Rank { pub fn display(&self) -> DirDisplay<'_> {
const HOUR: Epoch = 60 * 60; DirDisplay::new(self)
const DAY: Epoch = 24 * HOUR; }
const WEEK: Epoch = 7 * DAY;
pub fn score(&self, now: Epoch) -> Rank {
// The older the entry, the lesser its importance. // The older the entry, the lesser its importance.
let duration = now.saturating_sub(self.last_accessed); let duration = now.saturating_sub(self.last_accessed);
if duration < HOUR { if duration < HOUR {
@ -105,56 +33,39 @@ impl Dir<'_> {
self.rank * 0.25 self.rank * 0.25
} }
} }
pub fn display(&self) -> DirDisplay {
DirDisplay { dir: self }
}
pub fn display_score(&self, now: Epoch) -> DirDisplayScore {
DirDisplayScore { dir: self, now }
}
} }
pub struct DirDisplay<'a> { pub struct DirDisplay<'a> {
dir: &'a Dir<'a>, dir: &'a Dir<'a>,
now: Option<Epoch>,
separator: char,
}
impl<'a> DirDisplay<'a> {
fn new(dir: &'a Dir) -> Self {
Self { dir, separator: ' ', now: None }
}
pub fn with_score(mut self, now: Epoch) -> Self {
self.now = Some(now);
self
}
pub fn with_separator(mut self, separator: char) -> Self {
self.separator = separator;
self
}
} }
impl Display for DirDisplay<'_> { impl Display for DirDisplay<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Some(now) = self.now {
let score = self.dir.score(now).clamp(0.0, 9999.0);
write!(f, "{score:>6.1}{}", self.separator)?;
}
write!(f, "{}", self.dir.path) write!(f, "{}", self.dir.path)
} }
} }
pub struct DirDisplayScore<'a> {
dir: &'a Dir<'a>,
now: Epoch,
}
impl Display for DirDisplayScore<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let score = self.dir.score(self.now).clamp(0.0, 9999.0) as u32;
write!(f, "{:>4} {}", score, self.dir.path)
}
}
pub type Rank = f64; pub type Rank = f64;
pub type Epoch = u64; pub type Epoch = u64;
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::*;
#[test]
fn zero_copy() {
let dirs = DirList(vec![Dir { path: "/".into(), rank: 0.0, last_accessed: 0 }]);
let bytes = dirs.to_bytes().unwrap();
let dirs = DirList::from_bytes(&bytes).unwrap();
for dir in dirs.iter() {
assert!(matches!(dir.path, Cow::Borrowed(_)))
}
}
}

View File

@ -4,143 +4,220 @@ mod stream;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::{fs, io}; use std::{fs, io};
use anyhow::{Context, Result}; use anyhow::{bail, Context, Result};
pub use dir::{Dir, DirList, Epoch, Rank}; use bincode::Options;
pub use stream::Stream; use ouroboros::self_referencing;
use crate::util; pub use crate::db::dir::{Dir, Epoch, Rank};
pub use crate::db::stream::Stream;
use crate::{config, util};
#[derive(Debug)] #[self_referencing]
pub struct Database<'file> { pub struct Database {
pub dirs: DirList<'file>, path: PathBuf,
pub modified: bool, bytes: Vec<u8>,
pub data_dir: &'file Path, #[borrows(bytes)]
#[covariant]
pub dirs: Vec<Dir<'this>>,
dirty: bool,
} }
impl<'file> Database<'file> { impl Database {
pub fn save(&mut self) -> Result<()> { const VERSION: u32 = 3;
if !self.modified {
return Ok(());
}
let buffer = self.dirs.to_bytes()?; pub fn open() -> Result<Self> {
let path = db_path(self.data_dir); let data_dir = config::data_dir()?;
util::write(path, buffer).context("could not write to database")?; Self::open_dir(data_dir)
self.modified = false;
Ok(())
} }
/// Adds a new directory or increments its rank. Also updates its last accessed time. pub fn open_dir(data_dir: impl AsRef<Path>) -> Result<Self> {
pub fn add<S: AsRef<str>>(&mut self, path: S, now: Epoch) { let data_dir = data_dir.as_ref();
let path = path.as_ref(); let path = data_dir.join("db.zo");
match self.dirs.iter_mut().find(|dir| dir.path == path) {
Some(dir) => {
dir.last_accessed = now;
dir.rank += 1.0;
}
None => self.dirs.push(Dir { path: path.to_string().into(), last_accessed: now, rank: 1.0 }),
};
self.modified = true;
}
pub fn dedup(&mut self) {
// Sort by path, so that equal paths are next to each other.
self.dirs.sort_by(|dir1, dir2| dir1.path.cmp(&dir2.path));
for idx in (1..self.dirs.len()).rev() {
// Check if curr_dir and next_dir have equal paths.
let curr_dir = &self.dirs[idx];
let next_dir = &self.dirs[idx - 1];
if next_dir.path != curr_dir.path {
continue;
}
// Merge curr_dir's rank and last_accessed into next_dir.
let rank = curr_dir.rank;
let last_accessed = curr_dir.last_accessed;
let next_dir = &mut self.dirs[idx - 1];
next_dir.last_accessed = next_dir.last_accessed.max(last_accessed);
next_dir.rank += rank;
// Delete curr_dir.
self.dirs.swap_remove(idx);
self.modified = true;
}
}
// Streaming iterator for directories.
pub fn stream(&mut self, now: Epoch) -> Stream<'_, 'file> {
Stream::new(self, now)
}
/// Removes the directory with `path` from the store. This does not preserve ordering, but is
/// O(1).
pub fn remove<S: AsRef<str>>(&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::<Rank>();
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;
}
}
}
pub struct DatabaseFile {
buffer: Vec<u8>,
data_dir: PathBuf,
}
impl DatabaseFile {
pub fn new<P: Into<PathBuf>>(data_dir: P) -> Self {
DatabaseFile { buffer: Vec::new(), data_dir: data_dir.into() }
}
pub fn open(&mut self) -> Result<Database> {
// Read the entire database to memory. For smaller files, this is faster than
// mmap / streaming, and allows for zero-copy deserialization.
let path = db_path(&self.data_dir);
match fs::read(&path) { match fs::read(&path) {
Ok(buffer) => { Ok(bytes) => Self::try_new(path, bytes, |bytes| Self::deserialize(bytes), false),
self.buffer = buffer;
let dirs = DirList::from_bytes(&self.buffer)
.with_context(|| format!("could not deserialize database: {}", path.display()))?;
Ok(Database { dirs, modified: false, data_dir: &self.data_dir })
}
Err(e) if e.kind() == io::ErrorKind::NotFound => { Err(e) if e.kind() == io::ErrorKind::NotFound => {
// Create data directory, but don't create any file yet. The file will be created // Create data directory, but don't create any file yet. The file will be created
// later by [`Database::save`] if any data is modified. // later by [`Database::save`] if any data is modified.
fs::create_dir_all(&self.data_dir) fs::create_dir_all(data_dir)
.with_context(|| format!("unable to create data directory: {}", self.data_dir.display()))?; .with_context(|| format!("unable to create data directory: {}", data_dir.display()))?;
Ok(Database { dirs: DirList::new(), modified: false, data_dir: &self.data_dir }) Ok(Self::new(path, Vec::new(), |_| Vec::new(), false))
} }
Err(e) => Err(e).with_context(|| format!("could not read from database: {}", path.display())), Err(e) => Err(e).with_context(|| format!("could not read from database: {}", path.display())),
} }
} }
}
fn db_path<P: AsRef<Path>>(data_dir: P) -> PathBuf { pub fn save(&mut self) -> Result<()> {
const DB_FILENAME: &str = "db.zo"; // Only write to disk if the database is modified.
data_dir.as_ref().join(DB_FILENAME) if !self.dirty() {
return Ok(());
}
let bytes = Self::serialize(self.dirs())?;
util::write(self.borrow_path(), bytes).context("could not write to database")?;
self.with_dirty_mut(|dirty| *dirty = false);
Ok(())
}
/// Increments the rank of a directory, or creates it if it does not exist.
pub fn add(&mut self, path: impl AsRef<str> + Into<String>, by: Rank, now: Epoch) {
self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) {
Some(dir) => dir.rank = (dir.rank + by).max(0.0),
None => dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now }),
});
self.with_dirty_mut(|dirty| *dirty = true);
}
/// Creates a new directory. This will create a duplicate entry if this
/// directory is always in the database, it is expected that the user either
/// does a check before calling this, or calls `dedup()` afterward.
pub fn add_unchecked(&mut self, path: impl AsRef<str> + Into<String>, rank: Rank, now: Epoch) {
self.with_dirs_mut(|dirs| dirs.push(Dir { path: path.into().into(), rank, last_accessed: now }));
self.with_dirty_mut(|dirty| *dirty = true);
}
/// Increments the rank and updates the last_accessed of a directory, or
/// creates it if it does not exist.
pub fn add_update(&mut self, path: impl AsRef<str> + Into<String>, by: Rank, now: Epoch) {
self.with_dirs_mut(|dirs| match dirs.iter_mut().find(|dir| dir.path == path.as_ref()) {
Some(dir) => {
dir.rank = (dir.rank + by).max(0.0);
dir.last_accessed = now;
}
None => dirs.push(Dir { path: path.into().into(), rank: by.max(0.0), last_accessed: now }),
});
self.with_dirty_mut(|dirty| *dirty = true);
}
/// Removes the directory with `path` from the store. This does not preserve
/// ordering, but is O(1).
pub fn remove(&mut self, path: impl AsRef<str>) -> bool {
match self.dirs().iter().position(|dir| dir.path == path.as_ref()) {
Some(idx) => {
self.swap_remove(idx);
true
}
None => false,
}
}
pub fn swap_remove(&mut self, idx: usize) {
self.with_dirs_mut(|dirs| dirs.swap_remove(idx));
self.with_dirty_mut(|dirty| *dirty = true);
}
pub fn age(&mut self, max_age: Rank) {
let mut dirty = false;
self.with_dirs_mut(|dirs| {
let total_age = dirs.iter().map(|dir| dir.rank).sum::<Rank>();
if total_age > max_age {
let factor = 0.9 * max_age / total_age;
for idx in (0..dirs.len()).rev() {
let dir = &mut dirs[idx];
dir.rank *= factor;
if dir.rank < 1.0 {
dirs.swap_remove(idx);
}
}
dirty = true;
}
});
self.with_dirty_mut(|dirty_prev| *dirty_prev |= dirty);
}
pub fn stream(&mut self, now: Epoch) -> Stream {
Stream::new(self, now)
}
pub fn dedup(&mut self) {
// Sort by path, so that equal paths are next to each other.
self.sort_by_path();
let mut dirty = false;
self.with_dirs_mut(|dirs| {
for idx in (1..dirs.len()).rev() {
// Check if curr_dir and next_dir have equal paths.
let curr_dir = &dirs[idx];
let next_dir = &dirs[idx - 1];
if next_dir.path != curr_dir.path {
continue;
}
// Merge curr_dir's rank and last_accessed into next_dir.
let rank = curr_dir.rank;
let last_accessed = curr_dir.last_accessed;
let next_dir = &mut dirs[idx - 1];
next_dir.last_accessed = next_dir.last_accessed.max(last_accessed);
next_dir.rank += rank;
// Delete curr_dir.
dirs.swap_remove(idx);
dirty = true;
}
});
self.with_dirty_mut(|dirty_prev| *dirty_prev |= dirty);
}
pub fn sort_by_path(&mut self) {
self.with_dirs_mut(|dirs| dirs.sort_unstable_by(|dir1, dir2| dir1.path.cmp(&dir2.path)));
self.with_dirty_mut(|dirty| *dirty = true);
}
pub fn sort_by_score(&mut self, now: Epoch) {
self.with_dirs_mut(|dirs| {
dirs.sort_unstable_by(|dir1: &Dir, dir2: &Dir| dir1.score(now).total_cmp(&dir2.score(now)))
});
self.with_dirty_mut(|dirty| *dirty = true);
}
pub fn dirty(&self) -> bool {
*self.borrow_dirty()
}
pub fn dirs(&self) -> &[Dir] {
self.borrow_dirs()
}
fn serialize(dirs: &[Dir<'_>]) -> Result<Vec<u8>> {
(|| -> bincode::Result<_> {
// Preallocate buffer with combined size of sections.
let buffer_size = bincode::serialized_size(&Self::VERSION)? + bincode::serialized_size(&dirs)?;
let mut buffer = Vec::with_capacity(buffer_size as usize);
// Serialize sections into buffer.
bincode::serialize_into(&mut buffer, &Self::VERSION)?;
bincode::serialize_into(&mut buffer, &dirs)?;
Ok(buffer)
})()
.context("could not serialize database")
}
fn deserialize(bytes: &[u8]) -> Result<Vec<Dir>> {
// Assume a maximum size for the database. This prevents bincode from throwing strange
// errors when it encounters invalid data.
const MAX_SIZE: u64 = 32 << 20; // 32 MiB
let deserializer = &mut bincode::options().with_fixint_encoding().with_limit(MAX_SIZE);
// Split bytes into sections.
let version_size = deserializer.serialized_size(&Self::VERSION).unwrap() as _;
if bytes.len() < version_size {
bail!("could not deserialize database: corrupted data");
}
let (bytes_version, bytes_dirs) = bytes.split_at(version_size);
// Deserialize sections.
let version = deserializer.deserialize(bytes_version)?;
let dirs = match version {
Self::VERSION => deserializer.deserialize(bytes_dirs).context("could not deserialize database")?,
version => {
bail!("unsupported version (got {version}, supports {})", Self::VERSION)
}
};
Ok(dirs)
}
} }
#[cfg(test)] #[cfg(test)]
@ -149,50 +226,49 @@ mod tests {
#[test] #[test]
fn add() { fn add() {
let data_dir = tempfile::tempdir().unwrap();
let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" }; let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" };
let now = 946684800; let now = 946684800;
let data_dir = tempfile::tempdir().unwrap();
{ {
let mut db = DatabaseFile::new(data_dir.path()); let mut db = Database::open_dir(data_dir.path()).unwrap();
let mut db = db.open().unwrap(); db.add(path, 1.0, now);
db.add(path, now); db.add(path, 1.0, now);
db.add(path, now);
db.save().unwrap(); db.save().unwrap();
} }
{
let mut db = DatabaseFile::new(data_dir.path());
let db = db.open().unwrap();
assert_eq!(db.dirs.len(), 1);
let dir = &db.dirs[0]; {
let db = Database::open_dir(data_dir.path()).unwrap();
assert_eq!(db.dirs().len(), 1);
let dir = &db.dirs()[0];
assert_eq!(dir.path, path); assert_eq!(dir.path, path);
assert!((dir.rank - 2.0).abs() < 0.01);
assert_eq!(dir.last_accessed, now); assert_eq!(dir.last_accessed, now);
} }
} }
#[test] #[test]
fn remove() { fn remove() {
let data_dir = tempfile::tempdir().unwrap();
let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" }; let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" };
let now = 946684800; let now = 946684800;
let data_dir = tempfile::tempdir().unwrap();
{ {
let mut db = DatabaseFile::new(data_dir.path()); let mut db = Database::open_dir(data_dir.path()).unwrap();
let mut db = db.open().unwrap(); db.add(path, 1.0, now);
db.add(path, now);
db.save().unwrap(); db.save().unwrap();
} }
{ {
let mut db = DatabaseFile::new(data_dir.path()); let mut db = Database::open_dir(data_dir.path()).unwrap();
let mut db = db.open().unwrap();
assert!(db.remove(path)); assert!(db.remove(path));
db.save().unwrap(); db.save().unwrap();
} }
{ {
let mut db = DatabaseFile::new(data_dir.path()); let mut db = Database::open_dir(data_dir.path()).unwrap();
let mut db = db.open().unwrap(); assert!(db.dirs().is_empty());
assert!(db.dirs.is_empty());
assert!(!db.remove(path)); assert!(!db.remove(path));
db.save().unwrap(); db.save().unwrap();
} }

View File

@ -3,10 +3,10 @@ use std::ops::Range;
use std::{fs, path}; use std::{fs, path};
use crate::db::{Database, Dir, Epoch}; use crate::db::{Database, Dir, Epoch};
use crate::util; use crate::util::{self, MONTH};
pub struct Stream<'db, 'file> { pub struct Stream<'a> {
db: &'db mut Database<'file>, db: &'a mut Database,
idxs: Rev<Range<usize>>, idxs: Rev<Range<usize>>,
keywords: Vec<String>, keywords: Vec<String>,
@ -18,14 +18,14 @@ pub struct Stream<'db, 'file> {
exclude_path: Option<String>, exclude_path: Option<String>,
} }
impl<'db, 'file> Stream<'db, 'file> { impl<'a> Stream<'a> {
pub fn new(db: &'db mut Database<'file>, now: Epoch) -> Self { pub fn new(db: &'a mut Database, now: Epoch) -> Self {
// Iterate in descending order of score. db.sort_by_score(now);
db.dirs.sort_unstable_by(|dir1, dir2| dir1.score(now).total_cmp(&dir2.score(now))); let idxs = (0..db.dirs().len()).rev();
let idxs = (0..db.dirs.len()).rev();
// If a directory is deleted and hasn't been used for 90 days, delete it from the database. // If a directory is deleted and hasn't been used for 3 months, delete
let expire_below = now.saturating_sub(90 * 24 * 60 * 60); // it from the database.
let expire_below = now.saturating_sub(3 * MONTH);
Stream { Stream {
db, db,
@ -54,9 +54,9 @@ impl<'db, 'file> Stream<'db, 'file> {
self self
} }
pub fn next(&mut self) -> Option<&Dir<'file>> { pub fn next(&mut self) -> Option<&Dir> {
while let Some(idx) = self.idxs.next() { while let Some(idx) = self.idxs.next() {
let dir = &self.db.dirs[idx]; let dir = &self.db.dirs()[idx];
if !self.matches_keywords(&dir.path) { if !self.matches_keywords(&dir.path) {
continue; continue;
@ -64,8 +64,7 @@ impl<'db, 'file> Stream<'db, 'file> {
if !self.matches_exists(&dir.path) { if !self.matches_exists(&dir.path) {
if dir.last_accessed < self.expire_below { if dir.last_accessed < self.expire_below {
self.db.dirs.swap_remove(idx); self.db.swap_remove(idx);
self.db.modified = true;
} }
continue; continue;
} }
@ -74,7 +73,7 @@ impl<'db, 'file> Stream<'db, 'file> {
continue; continue;
} }
let dir = &self.db.dirs[idx]; let dir = &self.db.dirs()[idx];
return Some(dir); return Some(dir);
} }
@ -147,8 +146,8 @@ mod tests {
#[case(&["/foo/", "/bar"], "/foo/bar", false)] #[case(&["/foo/", "/bar"], "/foo/bar", false)]
#[case(&["/foo/", "/bar"], "/foo/baz/bar", true)] #[case(&["/foo/", "/bar"], "/foo/baz/bar", true)]
fn query(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) { fn query(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) {
let mut db = Database { dirs: Vec::new().into(), modified: false, data_dir: &PathBuf::new() }; let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false);
let stream = db.stream(0).with_keywords(keywords); let stream = Stream::new(db, 0).with_keywords(keywords);
assert_eq!(is_match, stream.matches_keywords(path)); assert_eq!(is_match, stream.matches_keywords(path));
} }
} }

View File

@ -1,7 +1,8 @@
use std::ffi::OsStr;
use std::fs::{self, File, OpenOptions}; use std::fs::{self, File, OpenOptions};
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use std::process::{Child, ChildStdin, Command, Stdio}; use std::process::{Child, Command, Stdio};
use std::time::SystemTime; use std::time::SystemTime;
use std::{env, mem}; use std::{env, mem};
@ -9,88 +10,133 @@ use std::{env, mem};
use anyhow::anyhow; use anyhow::anyhow;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use crate::config; use crate::db::{Dir, Epoch};
use crate::db::Epoch;
use crate::error::SilentExit; use crate::error::SilentExit;
pub struct Fzf { pub const SECOND: Epoch = 1;
child: Child, pub const MINUTE: Epoch = 60 * SECOND;
} pub const HOUR: Epoch = 60 * MINUTE;
pub const DAY: Epoch = 24 * HOUR;
pub const WEEK: Epoch = 7 * DAY;
pub const MONTH: Epoch = 30 * DAY;
pub struct Fzf(Command);
impl Fzf { impl Fzf {
pub fn new(multiple: bool) -> Result<Self> { const ERR_FZF_NOT_FOUND: &str = "could not find fzf, is it installed?";
const ERR_FZF_NOT_FOUND: &str = "could not find fzf, is it installed?";
pub fn new() -> Result<Self> {
// On Windows, CreateProcess implicitly searches the current working // On Windows, CreateProcess implicitly searches the current working
// directory for the executable, which is a potential security issue. // directory for the executable, which is a potential security issue.
// Instead, we resolve the path to the executable and then pass it to // Instead, we resolve the path to the executable and then pass it to
// CreateProcess. // CreateProcess.
#[cfg(windows)] #[cfg(windows)]
let mut command = Command::new(which::which("fzf.exe").map_err(|_| anyhow!(ERR_FZF_NOT_FOUND))?); let program = which::which("fzf.exe").map_err(|_| anyhow!(Self::ERR_FZF_NOT_FOUND))?;
#[cfg(not(windows))] #[cfg(not(windows))]
let mut command = Command::new("fzf"); let program = "fzf";
if multiple {
command.arg("--multi");
} else {
command.arg("--bind=tab:down,btab:up");
}
command.arg("--nth=2..").stdin(Stdio::piped()).stdout(Stdio::piped());
if let Some(fzf_opts) = config::fzf_opts() {
command.env("FZF_DEFAULT_OPTS", fzf_opts);
} else {
command.args([
// Search result
"--no-sort",
// Interface
"--cycle",
"--keep-right",
// Layout
"--height=50%",
"--info=inline",
"--layout=reverse",
// Scripting
"--exit-0",
"--select-1",
// Key/Event bindings
"--bind=ctrl-z:ignore",
]);
if cfg!(unix) {
// Non-POSIX args are only available on certain operating systems.
const PREVIEW_CMD: &str = if cfg!(target_os = "linux") {
r"\command -p ls -Cp --color=always --group-directories-first {2..}"
} else {
r"\command -p ls -Cp {2..}"
};
command.args(["--preview", PREVIEW_CMD, "--preview-window=down,30%"]).envs([
("CLICOLOR", "1"),
("CLICOLOR_FORCE", "1"),
("SHELL", "sh"),
]);
}
}
let child = match command.spawn() { let mut cmd = Command::new(program);
Ok(child) => child, cmd.args([
Err(e) if e.kind() == io::ErrorKind::NotFound => bail!(ERR_FZF_NOT_FOUND), // Search mode
Err(e) => Err(e).context("could not launch fzf")?, "--delimiter=\t",
}; "--nth=2",
// Scripting
"--read0",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped());
Ok(Fzf { child }) Ok(Fzf(cmd))
} }
pub fn stdin(&mut self) -> &mut ChildStdin { pub fn enable_preview(&mut self) -> &mut Self {
self.child.stdin.as_mut().unwrap() // Previews are only supported on UNIX.
if !cfg!(unix) {
return self;
}
self.args([
// Non-POSIX args are only available on certain operating systems.
if cfg!(target_os = "linux") {
r"--preview=\command -p ls -Cp --color=always --group-directories-first {2..}"
} else {
r"--preview=\command -p ls -Cp {2..}"
},
// Rounded edges don't display correctly on some terminals.
"--preview-window=down,30%,sharp",
])
.envs([
// Enables colorized `ls` output on macOS / FreeBSD.
("CLICOLOR", "1"),
// Forces colorized `ls` output when the output is not a
// TTY (like in fzf's preview window) on macOS /
// FreeBSD.
("CLICOLOR_FORCE", "1"),
// Ensures that the preview command is run in a
// POSIX-compliant shell, regardless of what shell the
// user has selected.
("SHELL", "sh"),
])
} }
pub fn select(mut self) -> Result<String> { pub fn args<I, S>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.0.args(args);
self
}
pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.0.env(key, val);
self
}
pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.0.envs(vars);
self
}
pub fn spawn(&mut self) -> Result<FzfChild> {
match self.0.spawn() {
Ok(child) => Ok(FzfChild(child)),
Err(e) if e.kind() == io::ErrorKind::NotFound => bail!(Self::ERR_FZF_NOT_FOUND),
Err(e) => Err(e).context("could not launch fzf"),
}
}
}
pub struct FzfChild(Child);
impl FzfChild {
pub fn write(&mut self, dir: &Dir, now: Epoch) -> Result<Option<String>> {
let handle = self.0.stdin.as_mut().unwrap();
match write!(handle, "{}\0", dir.display().with_score(now).with_separator('\t')) {
Ok(()) => Ok(None),
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => self.wait().map(Some),
Err(e) => Err(e).context("could not write to fzf"),
}
}
pub fn wait(&mut self) -> Result<String> {
// Drop stdin to prevent deadlock. // Drop stdin to prevent deadlock.
mem::drop(self.child.stdin.take()); mem::drop(self.0.stdin.take());
let mut stdout = self.child.stdout.take().unwrap(); let mut stdout = self.0.stdout.take().unwrap();
let mut output = String::new(); let mut output = String::new();
stdout.read_to_string(&mut output).context("failed to read from fzf")?; stdout.read_to_string(&mut output).context("failed to read from fzf")?;
let status = self.child.wait().context("wait failed on fzf")?; let status = self.0.wait().context("wait failed on fzf")?;
match status.code() { match status.code() {
Some(0) => Ok(output), Some(0) => Ok(output),
Some(1) => bail!("no match found"), Some(1) => bail!("no match found"),
@ -160,31 +206,30 @@ fn tmpfile<P: AsRef<Path>>(dir: P) -> Result<(File, PathBuf)> {
// Atomically create the tmpfile. // Atomically create the tmpfile.
match OpenOptions::new().write(true).create_new(true).open(&path) { match OpenOptions::new().write(true).create_new(true).open(&path) {
Ok(file) => break Ok((file, path)), Ok(file) => break Ok((file, path)),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists && attempts < MAX_ATTEMPTS => (), Err(e) if e.kind() == io::ErrorKind::AlreadyExists && attempts < MAX_ATTEMPTS => {}
Err(e) => break Err(e).with_context(|| format!("could not create file: {}", path.display())), Err(e) => {
break Err(e).with_context(|| format!("could not create file: {}", path.display()));
}
} }
} }
} }
/// Similar to [`fs::rename`], but retries on Windows. /// Similar to [`fs::rename`], but with retries on Windows.
fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> { fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
const MAX_ATTEMPTS: usize = 5;
let from = from.as_ref(); let from = from.as_ref();
let to = to.as_ref(); let to = to.as_ref();
if cfg!(windows) { const MAX_ATTEMPTS: usize = if cfg!(windows) { 5 } else { 1 };
let mut attempts = 0; let mut attempts = 0;
loop {
attempts += 1; loop {
match fs::rename(from, to) { match fs::rename(from, to) {
Err(e) if e.kind() == io::ErrorKind::PermissionDenied && attempts < MAX_ATTEMPTS => (), Err(e) if e.kind() == io::ErrorKind::PermissionDenied && attempts < MAX_ATTEMPTS => attempts += 1,
result => break result, result => {
break result.with_context(|| format!("could not rename file: {} -> {}", from.display(), to.display()))
} }
} }
} else {
fs::rename(from, to)
} }
.with_context(|| format!("could not rename file: {} -> {}", from.display(), to.display()))
} }
pub fn canonicalize<P: AsRef<Path>>(path: &P) -> Result<PathBuf> { pub fn canonicalize<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
@ -300,7 +345,7 @@ pub fn resolve_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
for component in components { for component in components {
match component { match component {
Component::Normal(_) => stack.push(component), Component::Normal(_) => stack.push(component),
Component::CurDir => (), Component::CurDir => {}
Component::ParentDir => { Component::ParentDir => {
if stack.last() != Some(&Component::RootDir) { if stack.last() != Some(&Component::RootDir) {
stack.pop(); stack.pop();
@ -316,5 +361,9 @@ pub fn resolve_path<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
/// Convert a string to lowercase, with a fast path for ASCII strings. /// Convert a string to lowercase, with a fast path for ASCII strings.
pub fn to_lowercase<S: AsRef<str>>(s: S) -> String { pub fn to_lowercase<S: AsRef<str>>(s: S) -> String {
let s = s.as_ref(); let s = s.as_ref();
if s.is_ascii() { s.to_ascii_lowercase() } else { s.to_lowercase() } if s.is_ascii() {
s.to_ascii_lowercase()
} else {
s.to_lowercase()
}
} }

View File

@ -12,22 +12,25 @@
{%- else -%} {%- else -%}
# Initialize hook to add new entries to the database. # Initialize hook to add new entries to the database.
if (not ($env | default false __zoxide_hooked | get __zoxide_hooked)) {
let-env __zoxide_hooked = true
{%- if hook == InitHook::Prompt %} {%- if hook == InitHook::Prompt %}
let-env config = ($env | default {} config).config let-env config = ($env | default {} config).config
let-env config = ($env.config | default {} hooks) let-env config = ($env.config | default {} hooks)
let-env config = ($env.config | update hooks ($env.config.hooks | default [] pre_prompt)) let-env config = ($env.config | update hooks ($env.config.hooks | default [] pre_prompt))
let-env config = ($env.config | update hooks.pre_prompt ($env.config.hooks.pre_prompt | append { let-env config = ($env.config | update hooks.pre_prompt ($env.config.hooks.pre_prompt | append {
zoxide add -- $env.PWD zoxide add -- $env.PWD
})) }))
{%- else if hook == InitHook::Pwd %} {%- else if hook == InitHook::Pwd %}
let-env config = ($env | default {} config).config let-env config = ($env | default {} config).config
let-env config = ($env.config | default {} hooks) let-env config = ($env.config | default {} hooks)
let-env config = ($env.config | update hooks ($env.config.hooks | default {} env_change)) let-env config = ($env.config | update hooks ($env.config.hooks | default {} env_change))
let-env config = ($env.config | update hooks.env_change ($env.config.hooks.env_change | default [] PWD)) let-env config = ($env.config | update hooks.env_change ($env.config.hooks.env_change | default [] PWD))
let-env config = ($env.config | update hooks.env_change.PWD ($env.config.hooks.env_change.PWD | append {|_, dir| let-env config = ($env.config | update hooks.env_change.PWD ($env.config.hooks.env_change.PWD | append {|_, dir|
zoxide add -- $dir zoxide add -- $dir
})) }))
{%- endif %} {%- endif %}
}
{%- endif %} {%- endif %}
@ -77,7 +80,7 @@ alias {{cmd}}i = __zoxide_zi
{{ section }} {{ section }}
# Add this to your env file (find it by running `$nu.env-path` in Nushell): # Add this to your env file (find it by running `$nu.env-path` in Nushell):
# #
# zoxide init nushell --hook prompt | save -f ~/.zoxide.nu # zoxide init nushell | save -f ~/.zoxide.nu
# #
# Now, add this to the end of your config file (find it by running # Now, add this to the end of your config file (find it by running
# `$nu.config-path` in Nushell): # `$nu.config-path` in Nushell):