From c2ee3fe3e99e555d108fe84ac10d25efbadca897 Mon Sep 17 00:00:00 2001 From: Ajeet D'Souza <98ajeet@gmail.com> Date: Sat, 7 Jan 2023 22:24:05 +0530 Subject: [PATCH] Refactor --- Cargo.lock | 303 ++++++++++++++++++++++++++------------ Cargo.toml | 4 +- rustfmt.toml | 1 - src/cmd/add.rs | 2 +- src/cmd/edit.rs | 113 ++++++-------- src/cmd/init.rs | 2 - src/cmd/query.rs | 111 ++++++++------ src/db/dir.rs | 41 +++++- src/db/mod.rs | 26 +++- src/{db2 => db}/stream.rs | 28 ++-- src/db2/dir.rs | 160 -------------------- src/db2/mod.rs | 75 ---------- src/main.rs | 1 - src/util.rs | 253 +++++++++++++------------------ 14 files changed, 501 insertions(+), 619 deletions(-) rename src/{db2 => db}/stream.rs (82%) delete mode 100644 src/db2/dir.rs delete mode 100644 src/db2/mod.rs diff --git a/Cargo.lock b/Cargo.lock index c366bd8..8df0463 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" [[package]] name = "aho-corasick" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] @@ -25,9 +25,9 @@ checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" [[package]] name = "anyhow" -version = "1.0.66" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] name = "askama" @@ -74,9 +74,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.0.5" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5c2ca00549910ec251e3bd15f87aeeb206c9456b9a77b43ff6c97c54042a472" +checksum = "fa3d466004a8b4cb1bc34044240a2fd29d17607e2e3bd613eb44fd48e8100da3" dependencies = [ "bstr", "doc-comment", @@ -86,23 +86,6 @@ dependencies = [ "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]] name = "bincode" version = "1.3.3" @@ -120,15 +103,22 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr" -version = "0.2.17" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +checksum = "b45ea9b00a7b3f2988e9a65ad3917e62123c38dba709b666506207be96d1790b" dependencies = [ - "lazy_static", "memchr", + "once_cell", "regex-automata", + "serde", ] +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" + [[package]] name = "cfg-if" version = "1.0.0" @@ -137,14 +127,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.0.18" +version = "4.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335867764ed2de42325fafe6d18b8af74ba97ee0c590fa016f157535b42ab04b" +checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39" dependencies = [ - "atty", "bitflags", "clap_derive", "clap_lex", + "is-terminal", "once_cell", "strsim", "termcolor", @@ -152,18 +142,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.0.3" +version = "4.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfe581a2035db4174cdbdc91265e1aba50f381577f0510d0ad36c7bc59cc84a3" +checksum = "10861370d2ba66b0f5989f83ebf35db6421713fd92351790e7fdd6c36774c56b" dependencies = [ "clap", ] [[package]] name = "clap_complete_fig" -version = "4.0.1" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b36d1abc7184a737efc9f589e6e783e8b56c72e71fca748cf9947ed0a6f46d44" +checksum = "46b30e010e669cd021e5004f3be26cff6b7c08d2a8a0d65b48d43a8cc0efd6c3" dependencies = [ "clap", "clap_complete", @@ -171,9 +161,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.0.18" +version = "4.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a1b0f6422af32d5da0c58e2703320f379216ee70198241c84173a8c5ac28f3" +checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" dependencies = [ "heck", "proc-macro-error", @@ -191,15 +181,6 @@ dependencies = [ "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]] name = "difflib" version = "0.4.0" @@ -244,6 +225,27 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "fastrand" version = "1.8.0" @@ -272,15 +274,15 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" dependencies = [ "aho-corasick", "bstr", @@ -297,20 +299,19 @@ checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" dependencies = [ "libc", ] [[package]] name = "ignore" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +checksum = "a05705bc64e0b66a806c3740bd6578ea66051b157ec42dc219c785cbf185aef3" dependencies = [ - "crossbeam-utils", "globset", "lazy_static", "log", @@ -331,6 +332,28 @@ dependencies = [ "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]] name = "itertools" version = "0.10.5" @@ -348,9 +371,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.139" 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]] name = "log" @@ -391,21 +420,21 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "nix" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb" +checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694" dependencies = [ - "autocfg", "bitflags", "cfg-if", "libc", + "static_assertions", ] [[package]] name = "nom" -version = "7.1.1" +version = "7.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c" dependencies = [ "memchr", "minimal-lexical", @@ -413,15 +442,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "os_str_bytes" -version = "6.3.1" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "ouroboros" @@ -448,9 +477,9 @@ dependencies = [ [[package]] name = "predicates" -version = "2.1.1" +version = "2.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" dependencies = [ "difflib", "itertools", @@ -459,15 +488,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" +checksum = "72f883590242d3c6fc5bf50299011695fa6590c2c70eac95ee1bdb9a733ad1a2" [[package]] name = "predicates-tree" -version = "1.0.5" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" +checksum = "54ff541861505aabf6ea722d2131ee980b8276e10a1297b94e896dd8b621850d" dependencies = [ "predicates-core", "termtree", @@ -499,18 +528,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.47" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] @@ -537,9 +566,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ "aho-corasick", "memchr", @@ -554,9 +583,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "remove_dir_all" @@ -569,9 +598,9 @@ dependencies = [ [[package]] name = "rstest" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c9dc66cc29792b663ffb5269be669f1613664e69ad56441fdb895c2347b930" +checksum = "b07f2d176c472198ec1e6551dc7da28f1c089652f66a7b722676c2238ebc0edf" dependencies = [ "rstest_macros", "rustc_version", @@ -579,15 +608,16 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.14.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5015e68a0685a95ade3eee617ff7101ab6a3fc689203101ca16ebc16f2b89c66" +checksum = "7229b505ae0706e64f37ffc54a9c163e11022a6636d58fe1f3f52018257ff9f7" dependencies = [ "cfg-if", "proc-macro2", "quote", "rustc_version", "syn", + "unicode-ident", ] [[package]] @@ -610,6 +640,20 @@ dependencies = [ "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]] name = "same-file" version = "1.0.6" @@ -621,24 +665,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" [[package]] name = "serde" -version = "1.0.147" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", @@ -651,6 +695,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.10.0" @@ -659,9 +709,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.103" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", @@ -693,24 +743,24 @@ dependencies = [ [[package]] name = "termtree" -version = "0.2.4" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" +checksum = "95059e91184749cb66be6dc994f67f182b6d897cb3df74a5bf66b5e709295fd8" [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", @@ -737,9 +787,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "version_check" @@ -815,6 +865,63 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "xtask" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index fb1e930..c6834f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,12 +31,12 @@ dunce = "1.0.1" fastrand = "1.7.0" glob = "0.3.0" ignore = "0.4.18" -nix = { version = "0.25.0", default-features = false, features = [ +nix = { version = "0.26.1", default-features = false, features = [ "fs", "user", ] } ouroboros = "0.15.5" -rstest = { version = "0.15.0", default-features = false } +rstest = { version = "0.16.0", default-features = false } rstest_reuse = "0.4.0" serde = { version = "1.0.116", features = ["derive"] } shell-words = "1.0.0" diff --git a/rustfmt.toml b/rustfmt.toml index 5106da6..ebaf638 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,3 @@ -comment_width = 100 group_imports = "StdExternalCrate" imports_granularity = "Module" max_width = 120 diff --git a/src/cmd/add.rs b/src/cmd/add.rs index b7ba404..21514b8 100644 --- a/src/cmd/add.rs +++ b/src/cmd/add.rs @@ -9,7 +9,7 @@ use crate::{config, util}; impl Run for Add { fn run(&self) -> Result<()> { // 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']; let exclude_dirs = config::exclude_dirs()?; diff --git a/src/cmd/edit.rs b/src/cmd/edit.rs index 2b93a5c..2bbaedc 100644 --- a/src/cmd/edit.rs +++ b/src/cmd/edit.rs @@ -3,9 +3,9 @@ use std::io::{self, Write}; use anyhow::Result; use crate::cmd::{Edit, EditCommand, Run}; -use crate::db::{Database, Epoch}; +use crate::db::Database; use crate::error::BrokenPipeHandler; -use crate::util::{self, Fz}; +use crate::util::{self, Fzf, FzfChild}; impl Run for Edit { fn run(&self) -> Result<()> { @@ -23,76 +23,61 @@ impl Run for Edit { EditCommand::Reload => {} } db.save()?; - print_dirs(db, now) + + 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()?; - - let mut fzf = Fz::new()?; - fzf.args([ - // Search mode - "--delimiter=\t", - "--nth=2", - "--scheme=path", - // Search result - "--tiebreak=end,chunk,index", - // Interface - "--bind=\ -ctrl-r:reload(zoxide edit reload),\ -ctrl-w:reload(zoxide edit delete {2..}),\ -ctrl-a:reload(zoxide edit increment {2..}),\ -ctrl-d:reload(zoxide edit decrement {2..}),\ -ctrl-z:ignore,\ -double-click:ignore,\ -enter:abort", - "--cycle", - "--keep-right", - // Layout - "--border=rounded", - "--border-label= zoxide-edit ", - "--header=\ -ctrl-r:reload \tctrl-w:delete -ctrl-a:increment\tctrl-d:decrement - - SCORE\tPATH", - "--info=inline", - "--layout=reverse", - "--padding=1,0,0,0", - // Display - "--color=label:bold", - "--tabstop=1", - // Scripting - "--read0", - ]) - .envs([ - ("CLICOLOR", "1"), - ("CLICOLOR_FORCE", "1"), - ("FZF_DEFAULT_COMMAND", "zoxide edit reload"), - ]); - - if cfg!(unix) { - // Non-POSIX args are only available on certain operating systems. - const PREVIEW_ARG: &str = 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..}" - }; - fzf.args([PREVIEW_ARG, "--preview-window=down,30%"]).env("SHELL", "sh"); - } - - let mut fzf = fzf.spawn()?; - fzf.wait() + Self::get_fzf()?.wait()?; + Ok(()) } } } } -fn print_dirs(db: &Database, now: Epoch) -> Result<()> { - let stdout = &mut io::stdout().lock(); - for dir in db.dirs().iter().rev() { - let score = dir.score(now).clamp(0.0, 9999.0); - write!(stdout, "{:>6.1}\t{}\x00", score, &dir.path).pipe_exit("fzf")?; +impl Edit { + fn get_fzf() -> Result { + 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() } - Ok(()) } diff --git a/src/cmd/init.rs b/src/cmd/init.rs index 32f6896..2c7609d 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -11,10 +11,8 @@ use crate::shell::{self, Opts}; impl Run for Init { fn run(&self) -> Result<()> { let cmd = if self.no_cmd { None } else { Some(self.cmd.as_str()) }; - let echo = config::echo(); let resolve_symlinks = config::resolve_symlinks(); - let opts = &Opts { cmd, hook: self.hook, echo, resolve_symlinks }; let source = match self.shell { diff --git a/src/cmd/query.rs b/src/cmd/query.rs index 210231c..14a25ef 100644 --- a/src/cmd/query.rs +++ b/src/cmd/query.rs @@ -4,15 +4,13 @@ use anyhow::{Context, Result}; use crate::cmd::{Query, Run}; use crate::config; -use crate::db2::{Database, DatabaseFile}; +use crate::db::{Database, Epoch, Stream}; use crate::error::BrokenPipeHandler; -use crate::util::{self, Fzf}; +use crate::util::{self, Fzf, FzfChild}; impl Run for Query { fn run(&self) -> Result<()> { - let data_dir = config::data_dir()?; - let mut db = DatabaseFile::new(data_dir); - let mut db = db.open()?; + let mut db = crate::db::Database::open()?; self.query(&mut db).and(db.save()) } } @@ -20,7 +18,44 @@ impl Run for Query { impl Query { fn query(&self, db: &mut Database) -> Result<()> { 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); if !self.all { let resolve_symlinks = config::resolve_symlinks(); @@ -29,46 +64,36 @@ impl Query { if let Some(path) = &self.exclude { stream = stream.with_exclude(path); } + stream + } - if self.interactive { - let mut fzf = Fzf::new(false)?; - 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")?, - } - }; - - 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")?; + fn get_fzf() -> Result { + let mut fzf = Fzf::new()?; + if let Some(fzf_opts) = config::fzf_opts() { + fzf.env("FZF_DEFAULT_OPTS", fzf_opts) } else { - let dir = stream.next().context("no match found")?; - if self.score { - writeln!(io::stdout(), "{}", dir.display_score(now)) - } else { - writeln!(io::stdout(), "{}", dir.display()) - } - .pipe_exit("stdout")?; + fzf.args([ + // Search mode + "--scheme=path", + // Search result + "--tiebreak=end,chunk,index", + // Interface + "--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() } - - Ok(()) + .spawn() } } diff --git a/src/db/dir.rs b/src/db/dir.rs index 2b87054..d545b40 100644 --- a/src/db/dir.rs +++ b/src/db/dir.rs @@ -1,4 +1,7 @@ -use std::borrow::Cow; +use std::{ + borrow::Cow, + fmt::{self, Display, Formatter}, +}; use serde::{Deserialize, Serialize}; @@ -13,6 +16,10 @@ pub struct Dir<'a> { } impl Dir<'_> { + pub fn display(&self) -> DirDisplay<'_> { + DirDisplay::new(self) + } + pub fn score(&self, now: Epoch) -> Rank { // The older the entry, the lesser its importance. let duration = now.saturating_sub(self.last_accessed); @@ -28,5 +35,37 @@ impl Dir<'_> { } } +pub struct DirDisplay<'a> { + dir: &'a Dir<'a>, + now: Option, + 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<'_> { + 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) + } +} + pub type Rank = f64; pub type Epoch = u64; diff --git a/src/db/mod.rs b/src/db/mod.rs index 1d3c951..0e85e28 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,4 +1,5 @@ mod dir; +mod stream; use std::path::{Path, PathBuf}; use std::{fs, io}; @@ -8,6 +9,7 @@ use bincode::Options; use ouroboros::self_referencing; pub use crate::db::dir::{Dir, Epoch, Rank}; +pub use crate::db::stream::Stream; use crate::{config, util}; #[self_referencing] @@ -16,7 +18,7 @@ pub struct Database { bytes: Vec, #[borrows(bytes)] #[covariant] - dirs: Vec>, + pub dirs: Vec>, dirty: bool, } @@ -25,10 +27,11 @@ impl Database { pub fn open() -> Result { let data_dir = config::data_dir()?; - Self::open_dir(&data_dir) + Self::open_dir(data_dir) } - pub fn open_dir(data_dir: &Path) -> Result { + pub fn open_dir(data_dir: impl AsRef) -> Result { + let data_dir = data_dir.as_ref(); let path = data_dir.join("db.zo"); match fs::read(&path) { @@ -90,15 +93,18 @@ impl Database { /// 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) -> bool { - let deleted = self.with_dirs_mut(|dirs| match dirs.iter().position(|dir| dir.path == path.as_ref()) { + match self.dirs().iter().position(|dir| dir.path == path.as_ref()) { Some(idx) => { - dirs.swap_remove(idx); + self.swap_remove(idx); true } None => false, - }); - self.with_dirty_mut(|dirty| *dirty |= deleted); - deleted + } + } + + 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) { @@ -120,6 +126,10 @@ impl Database { 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(); diff --git a/src/db2/stream.rs b/src/db/stream.rs similarity index 82% rename from src/db2/stream.rs rename to src/db/stream.rs index be0f836..cdd280d 100644 --- a/src/db2/stream.rs +++ b/src/db/stream.rs @@ -2,11 +2,11 @@ use std::iter::Rev; use std::ops::Range; use std::{fs, path}; -use crate::db2::{Database, Dir, Epoch}; +use crate::db::{Database, Dir, Epoch}; use crate::util::{self, MONTH}; -pub struct Stream<'db, 'file> { - db: &'db mut Database<'file>, +pub struct Stream<'a> { + db: &'a mut Database, idxs: Rev>, keywords: Vec, @@ -18,11 +18,10 @@ pub struct Stream<'db, 'file> { exclude_path: Option, } -impl<'db, 'file> Stream<'db, 'file> { - pub fn new(db: &'db mut Database<'file>, now: Epoch) -> Self { - // Iterate in descending order of score. - db.dirs.sort_unstable_by(|dir1, dir2| dir1.score(now).total_cmp(&dir2.score(now))); - let idxs = (0..db.dirs.len()).rev(); +impl<'a> Stream<'a> { + pub fn new(db: &'a mut Database, now: Epoch) -> Self { + db.sort_by_score(now); + let idxs = (0..db.dirs().len()).rev(); // If a directory is deleted and hasn't been used for 3 months, delete // it from the database. @@ -55,9 +54,9 @@ impl<'db, 'file> Stream<'db, 'file> { self } - pub fn next(&mut self) -> Option<&Dir<'file>> { + pub fn next(&mut self) -> Option<&Dir> { 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) { continue; @@ -65,8 +64,7 @@ impl<'db, 'file> Stream<'db, 'file> { if !self.matches_exists(&dir.path) { if dir.last_accessed < self.expire_below { - self.db.dirs.swap_remove(idx); - self.db.modified = true; + self.db.swap_remove(idx); } continue; } @@ -75,7 +73,7 @@ impl<'db, 'file> Stream<'db, 'file> { continue; } - let dir = &self.db.dirs[idx]; + let dir = &self.db.dirs()[idx]; return Some(dir); } @@ -148,8 +146,8 @@ mod tests { #[case(&["/foo/", "/bar"], "/foo/bar", false)] #[case(&["/foo/", "/bar"], "/foo/baz/bar", true)] 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 stream = db.stream(0).with_keywords(keywords); + let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false); + let stream = Stream::new(db, 0).with_keywords(keywords); assert_eq!(is_match, stream.matches_keywords(path)); } } diff --git a/src/db2/dir.rs b/src/db2/dir.rs deleted file mode 100644 index d0bda75..0000000 --- a/src/db2/dir.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::borrow::Cow; -use std::fmt::{self, Display, Formatter}; -use std::ops::{Deref, DerefMut}; - -use anyhow::{bail, Context, Result}; -use bincode::Options as _; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize)] -pub struct DirList<'a>(#[serde(borrow)] pub Vec>); - -impl DirList<'_> { - const VERSION: u32 = 3; - - pub fn new() -> DirList<'static> { - DirList(Vec::new()) - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - // 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> { - (|| -> 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>; - - 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>> for DirList<'a> { - fn from(dirs: Vec>) -> Self { - DirList(dirs) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Dir<'a> { - #[serde(borrow)] - pub path: Cow<'a, str>, - pub rank: Rank, - pub last_accessed: Epoch, -} - -impl Dir<'_> { - pub fn score(&self, now: Epoch) -> Rank { - const HOUR: Epoch = 60 * 60; - const DAY: Epoch = 24 * HOUR; - const WEEK: Epoch = 7 * DAY; - - // The older the entry, the lesser its importance. - let duration = now.saturating_sub(self.last_accessed); - if duration < HOUR { - self.rank * 4.0 - } else if duration < DAY { - self.rank * 2.0 - } else if duration < WEEK { - self.rank * 0.5 - } else { - self.rank * 0.25 - } - } - - pub fn display(&self) -> DirDisplay { - DirDisplay { dir: self } - } - - pub fn display_score(&self, now: Epoch) -> DirDisplayScore { - DirDisplayScore { dir: self, now } - } -} - -pub struct DirDisplay<'a> { - dir: &'a Dir<'a>, -} - -impl Display for DirDisplay<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - 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 u64; - write!(f, "{:>4} {}", score, self.dir.path) - } -} - -pub type Rank = f64; -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(_))) - } - } -} diff --git a/src/db2/mod.rs b/src/db2/mod.rs deleted file mode 100644 index afb05ae..0000000 --- a/src/db2/mod.rs +++ /dev/null @@ -1,75 +0,0 @@ -mod dir; -mod stream; - -use std::path::{Path, PathBuf}; -use std::{fs, io}; - -use anyhow::{Context, Result}; -pub use dir::{Dir, DirList, Epoch, Rank}; -pub use stream::Stream; - -use crate::util; - -#[derive(Debug)] -pub struct Database<'file> { - pub dirs: DirList<'file>, - pub modified: bool, - pub data_dir: &'file Path, -} - -impl<'file> Database<'file> { - pub fn save(&mut self) -> Result<()> { - if !self.modified { - return Ok(()); - } - - let buffer = self.dirs.to_bytes()?; - let path = db_path(self.data_dir); - util::write(path, buffer).context("could not write to database")?; - self.modified = false; - Ok(()) - } - - // Streaming iterator for directories. - pub fn stream(&mut self, now: Epoch) -> Stream<'_, 'file> { - Stream::new(self, now) - } -} - -pub struct DatabaseFile { - buffer: Vec, - data_dir: PathBuf, -} - -impl DatabaseFile { - pub fn new>(data_dir: P) -> Self { - DatabaseFile { buffer: Vec::new(), data_dir: data_dir.into() } - } - - pub fn open(&mut self) -> Result { - // 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) { - Ok(buffer) => { - 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 => { - // Create data directory, but don't create any file yet. The file will be created - // later by [`Database::save`] if any data is modified. - fs::create_dir_all(&self.data_dir) - .with_context(|| format!("unable to create data directory: {}", self.data_dir.display()))?; - Ok(Database { dirs: DirList::new(), modified: false, data_dir: &self.data_dir }) - } - Err(e) => Err(e).with_context(|| format!("could not read from database: {}", path.display())), - } - } -} - -fn db_path>(data_dir: P) -> PathBuf { - const DB_FILENAME: &str = "db.zo"; - data_dir.as_ref().join(DB_FILENAME) -} diff --git a/src/main.rs b/src/main.rs index 12448f4..6c7545f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,6 @@ use rstest_reuse; mod cmd; mod config; mod db; -mod db2; mod error; mod shell; mod util; diff --git a/src/util.rs b/src/util.rs index 8e03abb..7bf1bff 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,8 +1,8 @@ +use std::ffi::OsStr; use std::fs::{self, File, OpenOptions}; use std::io::{self, Read, Write}; -use std::ops::{Deref, DerefMut}; 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::{env, mem}; @@ -10,8 +10,7 @@ use std::{env, mem}; use anyhow::anyhow; use anyhow::{bail, Context, Result}; -use crate::config; -use crate::db2::Epoch; +use crate::db::{Dir, Epoch}; use crate::error::SilentExit; pub const SECOND: Epoch = 1; @@ -21,9 +20,9 @@ pub const DAY: Epoch = 24 * HOUR; pub const WEEK: Epoch = 7 * DAY; pub const MONTH: Epoch = 30 * DAY; -pub struct Fz(Command); +pub struct Fzf(Command); -impl Fz { +impl Fzf { const ERR_FZF_NOT_FOUND: &str = "could not find fzf, is it installed?"; pub fn new() -> Result { @@ -35,7 +34,77 @@ impl Fz { let program = which::which("fzf.exe").map_err(|_| anyhow!(Self::ERR_FZF_NOT_FOUND))?; #[cfg(not(windows))] let program = "fzf"; - Ok(Fz(Command::new(program))) + + let mut cmd = Command::new(program); + cmd.args([ + // Search mode + "--delimiter=\t", + "--nth=2", + // Scripting + "--read0", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()); + + Ok(Fzf(cmd)) + } + + pub fn enable_preview(&mut self) -> &mut Self { + // 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 args(&mut self, args: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + self.0.args(args); + self + } + + pub fn env(&mut self, key: K, val: V) -> &mut Self + where + K: AsRef, + V: AsRef, + { + self.0.env(key, val); + self + } + + pub fn envs(&mut self, vars: I) -> &mut Self + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + self.0.envs(vars); + self } pub fn spawn(&mut self) -> Result { @@ -47,140 +116,27 @@ impl Fz { } } -impl Deref for Fz { - type Target = Command; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Fz { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - pub struct FzfChild(Child); impl FzfChild { - pub fn select(&mut self) -> Result { - // Drop stdin to prevent deadlock. - mem::drop(self.stdin.take()); + pub fn write(&mut self, dir: &Dir, now: Epoch) -> Result> { + 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"), + } + } - let mut stdout = self.stdout.take().unwrap(); + pub fn wait(&mut self) -> Result { + // Drop stdin to prevent deadlock. + mem::drop(self.0.stdin.take()); + + let mut stdout = self.0.stdout.take().unwrap(); let mut output = String::new(); stdout.read_to_string(&mut output).context("failed to read from fzf")?; - self.wait()?; - Ok(output) - } - - pub fn wait(&mut self) -> Result<()> { let status = self.0.wait().context("wait failed on fzf")?; - match status.code() { - Some(0) => Ok(()), - Some(1) => bail!("no match found"), - Some(2) => bail!("fzf returned an error"), - Some(130) => bail!(SilentExit { code: 130 }), - Some(128..=254) | None => bail!("fzf was terminated"), - _ => bail!("fzf returned an unknown error"), - } - } -} - -impl Deref for FzfChild { - type Target = Child; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for FzfChild { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -pub struct Fzf { - child: Child, -} - -impl Fzf { - pub fn new(multiple: bool) -> Result { - const ERR_FZF_NOT_FOUND: &str = "could not find fzf, is it installed?"; - - // On Windows, CreateProcess implicitly searches the current working - // directory for the executable, which is a potential security issue. - // Instead, we resolve the path to the executable and then pass it to - // CreateProcess. - #[cfg(windows)] - let mut command = Command::new(which::which("fzf.exe").map_err(|_| anyhow!(ERR_FZF_NOT_FOUND))?); - #[cfg(not(windows))] - let mut command = Command::new("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() { - Ok(child) => child, - Err(e) if e.kind() == io::ErrorKind::NotFound => bail!(ERR_FZF_NOT_FOUND), - Err(e) => Err(e).context("could not launch fzf")?, - }; - - Ok(Fzf { child }) - } - - pub fn stdin(&mut self) -> &mut ChildStdin { - self.child.stdin.as_mut().unwrap() - } - - pub fn select(mut self) -> Result { - // Drop stdin to prevent deadlock. - mem::drop(self.child.stdin.take()); - - let mut stdout = self.child.stdout.take().unwrap(); - let mut output = String::new(); - stdout.read_to_string(&mut output).context("failed to read from fzf")?; - - let status = self.child.wait().context("wait failed on fzf")?; match status.code() { Some(0) => Ok(output), Some(1) => bail!("no match found"), @@ -250,7 +206,7 @@ fn tmpfile>(dir: P) -> Result<(File, PathBuf)> { // Atomically create the tmpfile. match OpenOptions::new().write(true).create_new(true).open(&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())); } @@ -258,25 +214,22 @@ fn tmpfile>(dir: P) -> Result<(File, PathBuf)> { } } -/// Similar to [`fs::rename`], but retries on Windows. +/// Similar to [`fs::rename`], but with retries on Windows. fn rename, Q: AsRef>(from: P, to: Q) -> Result<()> { - const MAX_ATTEMPTS: usize = 5; let from = from.as_ref(); let to = to.as_ref(); - if cfg!(windows) { - let mut attempts = 0; - loop { - attempts += 1; - match fs::rename(from, to) { - Err(e) if e.kind() == io::ErrorKind::PermissionDenied && attempts < MAX_ATTEMPTS => (), - result => break result, + const MAX_ATTEMPTS: usize = if cfg!(windows) { 5 } else { 1 }; + let mut attempts = 0; + + loop { + match fs::rename(from, to) { + Err(e) if e.kind() == io::ErrorKind::PermissionDenied && attempts < MAX_ATTEMPTS => attempts += 1, + 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>(path: &P) -> Result { @@ -392,7 +345,7 @@ pub fn resolve_path>(path: &P) -> Result { for component in components { match component { Component::Normal(_) => stack.push(component), - Component::CurDir => (), + Component::CurDir => {} Component::ParentDir => { if stack.last() != Some(&Component::RootDir) { stack.pop(); @@ -408,5 +361,9 @@ pub fn resolve_path>(path: &P) -> Result { /// Convert a string to lowercase, with a fast path for ASCII strings. pub fn to_lowercase>(s: S) -> String { 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() + } }