From 95bf1766cef9424ea753238cc2b824e95b53a4b5 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 5 Mar 2026 11:13:53 +0300 Subject: [PATCH] stash: async db operations; make hashes deterministic Signed-off-by: NotAShelf Change-Id: Iccc9980fa13a752e0e6c9fb630c28ba96a6a6964 --- Cargo.lock | 541 +++++++++++++++++++++++++++++++----------- Cargo.toml | 2 + src/commands/watch.rs | 391 ++++++++++++++++-------------- src/db/mod.rs | 97 +++++--- src/db/nonblocking.rs | 141 +++++++++++ src/main.rs | 5 +- 6 files changed, 815 insertions(+), 362 deletions(-) create mode 100644 src/db/nonblocking.rs diff --git a/Cargo.lock b/Cargo.lock index f18e409..30d0945 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,9 +84,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "async-broadcast" @@ -114,9 +114,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", @@ -306,9 +306,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -343,15 +343,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "castaway" @@ -364,9 +364,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.53" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", @@ -520,7 +520,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crossterm_winapi", "derive_more", "document-features", @@ -563,12 +563,12 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.5.1" +version = "3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ "dispatch2", - "nix 0.30.1", + "nix 0.31.2", "windows-sys", ] @@ -676,11 +676,11 @@ dependencies = [ [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "libc", "objc2", @@ -753,9 +753,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" dependencies = [ "log", "regex", @@ -763,9 +763,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ "anstream", "anstyle", @@ -871,9 +871,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "finl_unicode" @@ -921,16 +921,52 @@ dependencies = [ ] [[package]] -name = "futures-core" -version = "0.3.31" +name = "futures" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -945,6 +981,46 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -974,10 +1050,23 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "gimli" version = "0.32.3" @@ -1118,6 +1207,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1165,6 +1260,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -1182,7 +1279,7 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crossterm", "dyn-clone", "unicode-segmentation", @@ -1225,9 +1322,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.18" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "log", @@ -1238,9 +1335,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.18" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", @@ -1249,9 +1346,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1280,6 +1377,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.182" @@ -1288,11 +1391,10 @@ checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.10.0", "libc", ] @@ -1313,7 +1415,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -1382,9 +1484,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmem" @@ -1450,7 +1552,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", @@ -1459,11 +1561,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.30.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", @@ -1539,9 +1641,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -1552,7 +1654,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", "objc2", ] @@ -1569,7 +1671,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "libc", "objc2", @@ -1634,9 +1736,9 @@ dependencies = [ [[package]] name = "owo-colors" -version = "4.2.3" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "parking" @@ -1675,9 +1777,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -1685,9 +1787,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -1695,9 +1797,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", @@ -1708,9 +1810,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", "sha2", @@ -1781,15 +1883,15 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -1818,15 +1920,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] @@ -1847,10 +1949,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] -name = "proc-macro-crate" -version = "3.4.0" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] @@ -1875,18 +1987,18 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1897,6 +2009,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -1932,7 +2050,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "compact_str", "hashbrown 0.16.1", "indoc", @@ -1984,7 +2102,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "hashbrown 0.16.1", "indoc", "instability", @@ -2003,7 +2121,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2031,9 +2149,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2042,9 +2160,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rsqlite-vfs" @@ -2062,7 +2180,7 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -2092,7 +2210,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -2107,9 +2225,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "scopeguard" @@ -2236,15 +2354,15 @@ dependencies = [ [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2292,6 +2410,7 @@ name = "stash-clipboard" version = "0.3.6" dependencies = [ "base64", + "blocking", "clap", "clap-verbosity-flag", "color-eyre", @@ -2299,6 +2418,7 @@ dependencies = [ "ctrlc", "dirs", "env_logger", + "futures", "humantime", "imagesize", "inquire", @@ -2406,7 +2526,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys", @@ -2441,7 +2561,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", "base64", - "bitflags 2.10.0", + "bitflags 2.11.0", "fancy-regex", "filedescriptor", "finl_unicode", @@ -2557,18 +2677,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap", "toml_datetime", @@ -2578,9 +2698,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] @@ -2663,13 +2783,13 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca" dependencies = [ "memoffset", "tempfile", - "winapi", + "windows-sys", ] [[package]] @@ -2701,6 +2821,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "url" version = "2.5.8" @@ -2727,12 +2853,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.19.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ "atomic", - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -2773,18 +2899,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -2795,9 +2930,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2805,9 +2940,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -2818,18 +2953,52 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] -name = "wayland-backend" -version = "0.3.12" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wayland-backend" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaa6143502b9a87f759cb6a649ca801a226f77740eb54f3951cba2227790afeb" dependencies = [ "cc", "downcast-rs", @@ -2840,11 +3009,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.12" +version = "0.31.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" +checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "log", "rustix", "wayland-backend", @@ -2853,11 +3022,11 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.10" +version = "0.32.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" +checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -2865,11 +3034,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" +checksum = "78248e4cc0eff8163370ba5c158630dcae1f3497a586b826eca2ef5f348d6235" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -2878,20 +3047,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.8" +version = "0.31.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" +checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" dependencies = [ "proc-macro2", - "quick-xml 0.38.4", + "quick-xml 0.39.2", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.8" +version = "0.31.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" +checksum = "81d2bd69b1dadd601d0e98ef2fc9339a1b1e00cec5ee7545a77b5a0f52a90394" dependencies = [ "pkg-config", ] @@ -3136,9 +3305,91 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "wl-clipboard-rs" @@ -3189,9 +3440,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.13.2" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" dependencies = [ "async-broadcast", "async-executor", @@ -3224,9 +3475,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.13.2" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3304,15 +3555,15 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.16" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zvariant" -version = "5.9.2" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" dependencies = [ "endi", "enumflags2", @@ -3324,9 +3575,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.9.2" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 0a6abd5..709673f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ path = "src/main.rs" [dependencies] base64 = "0.22.1" +blocking = "1.6.2" clap = { version = "4.5.60", features = [ "derive", "env" ] } clap-verbosity-flag = "3.0.4" color-eyre = "0.6.5" @@ -43,6 +44,7 @@ wayland-protocols-wlr = { version = "0.3.10", default-features = false, optional wl-clipboard-rs = "0.9.3" [dev-dependencies] +futures = "0.3.32" tempfile = "3.26.0" [features] diff --git a/src/commands/watch.rs b/src/commands/watch.rs index fbc7239..9ac82cc 100644 --- a/src/commands/watch.rs +++ b/src/commands/watch.rs @@ -1,9 +1,32 @@ -use std::{ - collections::{BinaryHeap, hash_map::DefaultHasher}, - hash::{Hash, Hasher}, - io::Read, - time::Duration, -}; +use std::{collections::BinaryHeap, io::Read, time::Duration}; + +/// FNV-1a hasher for deterministic hashing across process runs. +/// Unlike DefaultHasher (SipHash), this produces stable hashes. +struct Fnv1aHasher { + state: u64, +} + +impl Fnv1aHasher { + const FNV_OFFSET: u64 = 0xCBF29CE484222325; + const FNV_PRIME: u64 = 0x100000001B3; + + fn new() -> Self { + Self { + state: Self::FNV_OFFSET, + } + } + + fn write(&mut self, bytes: &[u8]) { + for byte in bytes { + self.state ^= *byte as u64; + self.state = self.state.wrapping_mul(Self::FNV_PRIME); + } + } + + fn finish(&self) -> u64 { + self.state + } +} use smol::Timer; use wl_clipboard_rs::{ @@ -17,7 +40,7 @@ use wl_clipboard_rs::{ }, }; -use crate::db::{ClipboardDb, SqliteClipboardDb}; +use crate::db::{SqliteClipboardDb, nonblocking::AsyncClipboardDb}; /// Wrapper to provide [`Ord`] implementation for `f64` by negating values. /// This allows [`BinaryHeap`], which is a max-heap, to function as a min-heap. @@ -97,6 +120,16 @@ impl ExpirationQueue { } expired } + + /// Check if the queue is empty + fn is_empty(&self) -> bool { + self.heap.is_empty() + } + + /// Get the number of entries in the queue + fn len(&self) -> usize { + self.heap.len() + } } /// Get clipboard contents using the source application's preferred MIME type. @@ -177,7 +210,7 @@ fn negotiate_mime_type( #[allow(clippy::too_many_arguments)] pub trait WatchCommand { - fn watch( + async fn watch( &self, max_dedupe_search: u64, max_items: u64, @@ -190,7 +223,7 @@ pub trait WatchCommand { } impl WatchCommand for SqliteClipboardDb { - fn watch( + async fn watch( &self, max_dedupe_search: u64, max_items: u64, @@ -200,207 +233,203 @@ impl WatchCommand for SqliteClipboardDb { min_size: Option, max_size: usize, ) { - smol::block_on(async { - log::info!( - "Starting clipboard watch daemon with MIME type preference: \ - {mime_type_preference}" - ); + let async_db = AsyncClipboardDb::new(self.db_path.clone()); + log::info!( + "Starting clipboard watch daemon with MIME type preference: \ + {mime_type_preference}" + ); - // Build expiration queue from existing entries - let mut exp_queue = ExpirationQueue::new(); - if let Ok(Some((expires_at, id))) = self.get_next_expiration() { - exp_queue.push(expires_at, id); - // Load remaining expirations (exclude already-marked expired entries) - let mut stmt = self - .conn - .prepare( - "SELECT expires_at, id FROM clipboard WHERE expires_at IS NOT \ - NULL AND (is_expired IS NULL OR is_expired = 0) ORDER BY \ - expires_at ASC", - ) - .ok(); - if let Some(ref mut stmt) = stmt { - let mut rows = stmt.query([]).ok(); - if let Some(ref mut rows) = rows { - while let Ok(Some(row)) = rows.next() { - if let (Ok(exp), Ok(row_id)) = - (row.get::<_, f64>(0), row.get::<_, i64>(1)) - { - // Skip first entry which is already added - if exp_queue - .heap - .iter() - .any(|(_, existing_id)| *existing_id == row_id) - { - continue; - } - exp_queue.push(exp, row_id); - } - } - } + // Build expiration queue from existing entries + let mut exp_queue = ExpirationQueue::new(); + + // Load all expirations from database asynchronously + match async_db.load_all_expirations().await { + Ok(expirations) => { + for (expires_at, id) in expirations { + exp_queue.push(expires_at, id); } - } - - // We use hashes for comparison instead of storing full contents - let mut last_hash: Option = None; - let mut buf = Vec::with_capacity(4096); - - // Helper to hash clipboard contents - let hash_contents = |data: &[u8]| -> u64 { - let mut hasher = DefaultHasher::new(); - data.hash(&mut hasher); - hasher.finish() - }; - - // Initialize with current clipboard using smart MIME negotiation - if let Ok((mut reader, _)) = negotiate_mime_type(mime_type_preference) { - buf.clear(); - if reader.read_to_end(&mut buf).is_ok() && !buf.is_empty() { - last_hash = Some(hash_contents(&buf)); + if !exp_queue.is_empty() { + log::info!("Loaded {} expirations from database", exp_queue.len()); } + }, + Err(e) => { + log::warn!("Failed to load expirations: {e}"); + }, + } + + // We use hashes for comparison instead of storing full contents + let mut last_hash: Option = None; + let mut buf = Vec::with_capacity(4096); + + // Helper to hash clipboard contents using FNV-1a (deterministic across + // runs) + let hash_contents = |data: &[u8]| -> u64 { + let mut hasher = Fnv1aHasher::new(); + hasher.write(data); + hasher.finish() + }; + + // Initialize with current clipboard using smart MIME negotiation + if let Ok((mut reader, _)) = negotiate_mime_type(mime_type_preference) { + buf.clear(); + if reader.read_to_end(&mut buf).is_ok() && !buf.is_empty() { + last_hash = Some(hash_contents(&buf)); } + } - loop { - // Process any pending expirations - if let Some(next_exp) = exp_queue.peek_next() { - let now = SqliteClipboardDb::now(); - if next_exp <= now { - // Expired entries to process - let expired_ids = exp_queue.pop_expired(now); - for id in expired_ids { - // Verify entry still exists and get its content_hash - let expired_hash: Option = self - .conn - .query_row( - "SELECT content_hash FROM clipboard WHERE id = ?1", - [id], - |row| row.get(0), - ) - .ok(); + let poll_interval = Duration::from_millis(500); - if let Some(stored_hash) = expired_hash { - // Mark as expired - self - .conn - .execute( - "UPDATE clipboard SET is_expired = 1 WHERE id = ?1", - [id], - ) - .ok(); + loop { + // Process any pending expirations that are due now + if let Some(next_exp) = exp_queue.peek_next() { + let now = SqliteClipboardDb::now(); + if next_exp <= now { + // Expired entries to process + let expired_ids = exp_queue.pop_expired(now); + for id in expired_ids { + // Verify entry still exists and get its content_hash + let expired_hash: Option = + match async_db.get_content_hash(id).await { + Ok(hash) => hash, + Err(e) => { + log::warn!("Failed to get content hash for entry {id}: {e}"); + None + }, + }; + + if let Some(stored_hash) = expired_hash { + // Mark as expired + if let Err(e) = async_db.mark_expired(id).await { + log::warn!("Failed to mark entry {id} as expired: {e}"); + } else { log::info!("Entry {id} marked as expired"); + } - // Check if this expired entry is currently in the clipboard - if let Ok((mut reader, _)) = - negotiate_mime_type(mime_type_preference) + // Check if this expired entry is currently in the clipboard + if let Ok((mut reader, _)) = + negotiate_mime_type(mime_type_preference) + { + let mut current_buf = Vec::new(); + if reader.read_to_end(&mut current_buf).is_ok() + && !current_buf.is_empty() { - let mut current_buf = Vec::new(); - if reader.read_to_end(&mut current_buf).is_ok() - && !current_buf.is_empty() - { - let current_hash = hash_contents(¤t_buf); - // Compare as i64 (database stores as i64) - if current_hash as i64 == stored_hash { - // Clear the clipboard since expired content is still - // there - let mut opts = Options::new(); - opts.clipboard( - wl_clipboard_rs::copy::ClipboardType::Regular, + let current_hash = hash_contents(¤t_buf); + // Convert stored i64 to u64 for comparison (preserves bit + // pattern) + if current_hash == stored_hash as u64 { + // Clear the clipboard since expired content is still + // there + let mut opts = Options::new(); + opts + .clipboard(wl_clipboard_rs::copy::ClipboardType::Regular); + if opts + .copy( + Source::Bytes(Vec::new().into()), + CopyMimeType::Autodetect, + ) + .is_ok() + { + log::info!( + "Cleared clipboard containing expired entry {id}" + ); + last_hash = None; // reset tracked hash + } else { + log::warn!( + "Failed to clear clipboard for expired entry {id}" ); - if opts - .copy( - Source::Bytes(Vec::new().into()), - CopyMimeType::Autodetect, - ) - .is_ok() - { - log::info!( - "Cleared clipboard containing expired entry {id}" - ); - last_hash = None; // reset tracked hash - } else { - log::warn!( - "Failed to clear clipboard for expired entry {id}" - ); - } } } } } } - } else { - // Sleep *precisely* until next expiration - let sleep_duration = next_exp - now; - Timer::after(Duration::from_secs_f64(sleep_duration)).await; - continue; // skip normal poll, process expirations first } } + } - // Normal clipboard polling - match negotiate_mime_type(mime_type_preference) { - Ok((mut reader, _mime_type)) => { - buf.clear(); - if let Err(e) = reader.read_to_end(&mut buf) { - log::error!("Failed to read clipboard contents: {e}"); - Timer::after(Duration::from_millis(500)).await; - continue; - } + // Normal clipboard polling (always run, even when expirations are + // pending) + match negotiate_mime_type(mime_type_preference) { + Ok((mut reader, _mime_type)) => { + buf.clear(); + if let Err(e) = reader.read_to_end(&mut buf) { + log::error!("Failed to read clipboard contents: {e}"); + Timer::after(Duration::from_millis(500)).await; + continue; + } - // Only store if changed and not empty - if !buf.is_empty() { - let current_hash = hash_contents(&buf); - if last_hash != Some(current_hash) { - match self.store_entry( - &buf[..], + // Only store if changed and not empty + if !buf.is_empty() { + let current_hash = hash_contents(&buf); + if last_hash != Some(current_hash) { + // Clone buf for the async operation since it needs 'static + let buf_clone = buf.clone(); + match async_db + .store_entry( + buf_clone, max_dedupe_search, max_items, - Some(excluded_apps), + Some(excluded_apps.to_vec()), min_size, max_size, - ) { - Ok(id) => { - log::info!("Stored new clipboard entry (id: {id})"); - last_hash = Some(current_hash); + ) + .await + { + Ok(id) => { + log::info!("Stored new clipboard entry (id: {id})"); + last_hash = Some(current_hash); - // Set expiration if configured - if let Some(duration) = expire_after { - let expires_at = - SqliteClipboardDb::now() + duration.as_secs_f64(); - self.set_expiration(id, expires_at).ok(); + // Set expiration if configured + if let Some(duration) = expire_after { + let expires_at = + SqliteClipboardDb::now() + duration.as_secs_f64(); + if let Err(e) = + async_db.set_expiration(id, expires_at).await + { + log::warn!( + "Failed to set expiration for entry {id}: {e}" + ); + } else { exp_queue.push(expires_at, id); } - }, - Err(crate::db::StashError::ExcludedByApp(_)) => { - log::info!("Clipboard entry excluded by app filter"); - last_hash = Some(current_hash); - }, - Err(crate::db::StashError::Store(ref msg)) - if msg.contains("Excluded by app filter") => - { - log::info!("Clipboard entry excluded by app filter"); - last_hash = Some(current_hash); - }, - Err(e) => { - log::error!("Failed to store clipboard entry: {e}"); - last_hash = Some(current_hash); - }, - } + } + }, + Err(crate::db::StashError::ExcludedByApp(_)) => { + log::info!("Clipboard entry excluded by app filter"); + last_hash = Some(current_hash); + }, + Err(crate::db::StashError::Store(ref msg)) + if msg.contains("Excluded by app filter") => + { + log::info!("Clipboard entry excluded by app filter"); + last_hash = Some(current_hash); + }, + Err(e) => { + log::error!("Failed to store clipboard entry: {e}"); + last_hash = Some(current_hash); + }, } } - }, - Err(e) => { - let error_msg = e.to_string(); - if !error_msg.contains("empty") { - log::error!("Failed to get clipboard contents: {e}"); - } - }, - } - - // Normal poll interval (only if no expirations pending) - if exp_queue.peek_next().is_none() { - Timer::after(Duration::from_millis(500)).await; - } + } + }, + Err(e) => { + let error_msg = e.to_string(); + if !error_msg.contains("empty") { + log::error!("Failed to get clipboard contents: {e}"); + } + }, } - }); + + // Calculate sleep time: min of poll interval and time until next + // expiration + let sleep_duration = if let Some(next_exp) = exp_queue.peek_next() { + let now = SqliteClipboardDb::now(); + let time_to_exp = (next_exp - now).max(0.0); + poll_interval.min(Duration::from_secs_f64(time_to_exp)) + } else { + poll_interval + }; + Timer::after(sleep_duration).await; + } } } diff --git a/src/db/mod.rs b/src/db/mod.rs index 61d3351..1f58cdf 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,14 +1,44 @@ use std::{ - collections::hash_map::DefaultHasher, env, fmt, fs, - hash::{Hash, Hasher}, io::{BufRead, BufReader, Read, Write}, + path::PathBuf, str, sync::OnceLock, }; +pub mod nonblocking; + +/// FNV-1a hasher for deterministic hashing across process runs. +/// Unlike DefaultHasher (SipHash with random seed), this produces stable +/// hashes. +pub struct Fnv1aHasher { + state: u64, +} + +impl Fnv1aHasher { + const FNV_OFFSET: u64 = 0xCBF29CE484222325; + const FNV_PRIME: u64 = 0x100000001B3; + + pub fn new() -> Self { + Self { + state: Self::FNV_OFFSET, + } + } + + pub fn write(&mut self, bytes: &[u8]) { + for byte in bytes { + self.state ^= *byte as u64; + self.state = self.state.wrapping_mul(Self::FNV_PRIME); + } + } + + pub fn finish(&self) -> u64 { + self.state + } +} + use base64::prelude::*; use log::{debug, error, info, warn}; use mime_sniffer::MimeTypeSniffer; @@ -210,11 +240,15 @@ impl fmt::Display for Entry { } pub struct SqliteClipboardDb { - pub conn: Connection, + pub conn: Connection, + pub db_path: PathBuf, } impl SqliteClipboardDb { - pub fn new(mut conn: Connection) -> Result { + pub fn new( + mut conn: Connection, + db_path: PathBuf, + ) -> Result { conn .pragma_update(None, "synchronous", "OFF") .map_err(|e| { @@ -449,7 +483,7 @@ impl SqliteClipboardDb { // focused window state. #[cfg(feature = "use-toplevel")] crate::wayland::init_wayland_state(); - Ok(Self { conn }) + Ok(Self { conn, db_path }) } } @@ -535,8 +569,8 @@ impl ClipboardDb for SqliteClipboardDb { } // Calculate content hash for deduplication - let mut hasher = DefaultHasher::new(); - buf.hash(&mut hasher); + let mut hasher = Fnv1aHasher::new(); + hasher.write(&buf); #[allow(clippy::cast_possible_wrap)] let content_hash = hasher.finish() as i64; @@ -940,20 +974,6 @@ impl SqliteClipboardDb { .map_err(|e| StashError::Trim(e.to_string().into())) } - /// Get the earliest expiration (timestamp, id) for heap initialization - pub fn get_next_expiration(&self) -> Result, StashError> { - match self.conn.query_row( - "SELECT expires_at, id FROM clipboard WHERE expires_at IS NOT NULL \ - ORDER BY expires_at ASC LIMIT 1", - [], - |row| Ok((row.get(0)?, row.get(1)?)), - ) { - Ok(result) => Ok(Some(result)), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(StashError::Store(e.to_string().into())), - } - } - /// Set expiration timestamp for an entry pub fn set_expiration( &self, @@ -1338,7 +1358,8 @@ mod tests { fn test_db() -> SqliteClipboardDb { let conn = Connection::open_in_memory().expect("Failed to open in-memory db"); - SqliteClipboardDb::new(conn).expect("Failed to create test database") + SqliteClipboardDb::new(conn, PathBuf::from(":memory:")) + .expect("Failed to create test database") } fn get_schema_version(conn: &Connection) -> rusqlite::Result { @@ -1369,7 +1390,8 @@ mod tests { let db_path = temp_dir.path().join("test_fresh.db"); let conn = Connection::open(&db_path).expect("Failed to open database"); - let db = SqliteClipboardDb::new(conn).expect("Failed to create database"); + let db = SqliteClipboardDb::new(conn, PathBuf::from(":memory:")) + .expect("Failed to create database"); assert_eq!( get_schema_version(&db.conn).expect("Failed to get schema version"), @@ -1419,7 +1441,8 @@ mod tests { assert_eq!(get_schema_version(&conn).expect("Failed to get version"), 0); - let db = SqliteClipboardDb::new(conn).expect("Failed to create database"); + let db = SqliteClipboardDb::new(conn, PathBuf::from(":memory:")) + .expect("Failed to create database"); assert_eq!( get_schema_version(&db.conn) @@ -1461,7 +1484,8 @@ mod tests { ) .expect("Failed to insert data"); - let db = SqliteClipboardDb::new(conn).expect("Failed to create database"); + let db = SqliteClipboardDb::new(conn, PathBuf::from(":memory:")) + .expect("Failed to create database"); assert_eq!( get_schema_version(&db.conn) @@ -1504,7 +1528,8 @@ mod tests { ) .expect("Failed to insert data"); - let db = SqliteClipboardDb::new(conn).expect("Failed to create database"); + let db = SqliteClipboardDb::new(conn, PathBuf::from(":memory:")) + .expect("Failed to create database"); assert_eq!( get_schema_version(&db.conn) @@ -1535,12 +1560,13 @@ mod tests { ) .expect("Failed to create table"); - let db = SqliteClipboardDb::new(conn).expect("Failed to create database"); + let db = SqliteClipboardDb::new(conn, PathBuf::from(":memory:")) + .expect("Failed to create database"); let version_after_first = get_schema_version(&db.conn).expect("Failed to get version"); - let db2 = - SqliteClipboardDb::new(db.conn).expect("Failed to create database again"); + let db2 = SqliteClipboardDb::new(db.conn, db.db_path) + .expect("Failed to create database again"); let version_after_second = get_schema_version(&db2.conn).expect("Failed to get version"); @@ -1553,7 +1579,8 @@ mod tests { let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); let db_path = temp_dir.path().join("test_store.db"); let conn = Connection::open(&db_path).expect("Failed to open database"); - let db = SqliteClipboardDb::new(conn).expect("Failed to create database"); + let db = SqliteClipboardDb::new(conn, PathBuf::from(":memory:")) + .expect("Failed to create database"); let test_data = b"Hello, World!"; let cursor = std::io::Cursor::new(test_data.to_vec()); @@ -1589,7 +1616,8 @@ mod tests { let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); let db_path = temp_dir.path().join("test_copy.db"); let conn = Connection::open(&db_path).expect("Failed to open database"); - let db = SqliteClipboardDb::new(conn).expect("Failed to create database"); + let db = SqliteClipboardDb::new(conn, PathBuf::from(":memory:")) + .expect("Failed to create database"); let test_data = b"Test content for copy"; let cursor = std::io::Cursor::new(test_data.to_vec()); @@ -1608,8 +1636,8 @@ mod tests { std::thread::sleep(std::time::Duration::from_millis(1100)); - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - test_data.hash(&mut hasher); + let mut hasher = Fnv1aHasher::new(); + hasher.write(test_data); let content_hash = hasher.finish() as i64; let now = std::time::SystemTime::now() @@ -1670,7 +1698,8 @@ mod tests { ) .expect("Failed to insert data"); - let db = SqliteClipboardDb::new(conn).expect("Failed to create database"); + let db = SqliteClipboardDb::new(conn, PathBuf::from(":memory:")) + .expect("Failed to create database"); assert_eq!( get_schema_version(&db.conn).expect("Failed to get version"), diff --git a/src/db/nonblocking.rs b/src/db/nonblocking.rs new file mode 100644 index 0000000..9640e26 --- /dev/null +++ b/src/db/nonblocking.rs @@ -0,0 +1,141 @@ +use std::path::PathBuf; + +use rusqlite::OptionalExtension; + +use crate::db::{ClipboardDb, SqliteClipboardDb, StashError}; + +/// Async wrapper for database operations that runs blocking operations +/// on a thread pool to avoid blocking the async runtime. +/// +/// Since rusqlite::Connection is not Send, we store the database path +/// and open a new connection for each operation. +pub struct AsyncClipboardDb { + db_path: PathBuf, +} + +impl AsyncClipboardDb { + pub fn new(db_path: PathBuf) -> Self { + Self { db_path } + } + + pub async fn store_entry( + &self, + data: Vec, + max_dedupe_search: u64, + max_items: u64, + excluded_apps: Option>, + min_size: Option, + max_size: usize, + ) -> Result { + let path = self.db_path.clone(); + blocking::unblock(move || { + let db = Self::open_db_internal(&path)?; + db.store_entry( + std::io::Cursor::new(data), + max_dedupe_search, + max_items, + excluded_apps.as_deref(), + min_size, + max_size, + ) + }) + .await + } + + pub async fn set_expiration( + &self, + id: i64, + expires_at: f64, + ) -> Result<(), StashError> { + let path = self.db_path.clone(); + blocking::unblock(move || { + let db = Self::open_db_internal(&path)?; + db.set_expiration(id, expires_at) + }) + .await + } + + pub async fn load_all_expirations( + &self, + ) -> Result, StashError> { + let path = self.db_path.clone(); + blocking::unblock(move || { + let db = Self::open_db_internal(&path)?; + let mut stmt = db + .conn + .prepare( + "SELECT expires_at, id FROM clipboard WHERE expires_at IS NOT NULL \ + AND (is_expired IS NULL OR is_expired = 0) ORDER BY expires_at ASC", + ) + .map_err(|e| StashError::ListDecode(e.to_string().into()))?; + + let mut rows = stmt + .query([]) + .map_err(|e| StashError::ListDecode(e.to_string().into()))?; + let mut expirations = Vec::new(); + + while let Some(row) = rows + .next() + .map_err(|e| StashError::ListDecode(e.to_string().into()))? + { + let exp = row + .get::<_, f64>(0) + .map_err(|e| StashError::ListDecode(e.to_string().into()))?; + let id = row + .get::<_, i64>(1) + .map_err(|e| StashError::ListDecode(e.to_string().into()))?; + expirations.push((exp, id)); + } + Ok(expirations) + }) + .await + } + + pub async fn get_content_hash( + &self, + id: i64, + ) -> Result, StashError> { + let path = self.db_path.clone(); + blocking::unblock(move || { + let db = Self::open_db_internal(&path)?; + let result: Option = db + .conn + .query_row( + "SELECT content_hash FROM clipboard WHERE id = ?1", + [id], + |row| row.get(0), + ) + .optional() + .map_err(|e| StashError::ListDecode(e.to_string().into()))?; + Ok(result) + }) + .await + } + + pub async fn mark_expired(&self, id: i64) -> Result<(), StashError> { + let path = self.db_path.clone(); + blocking::unblock(move || { + let db = Self::open_db_internal(&path)?; + db.conn + .execute("UPDATE clipboard SET is_expired = 1 WHERE id = ?1", [id]) + .map_err(|e| StashError::Store(e.to_string().into()))?; + Ok(()) + }) + .await + } + + fn open_db_internal(path: &PathBuf) -> Result { + let conn = rusqlite::Connection::open(path).map_err(|e| { + StashError::Store(format!("Failed to open database: {e}").into()) + })?; + SqliteClipboardDb::new(conn, path.clone()) + } +} + +impl Clone for AsyncClipboardDb { + fn clone(&self) -> Self { + Self { + db_path: self.db_path.clone(), + } + } +} diff --git a/src/main.rs b/src/main.rs index fd74b1f..2c2f6e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -228,7 +228,7 @@ fn main() -> color_eyre::eyre::Result<()> { } let conn = rusqlite::Connection::open(&db_path)?; - let db = db::SqliteClipboardDb::new(conn)?; + let db = db::SqliteClipboardDb::new(conn, db_path)?; match cli.command { Some(Command::Store) => { @@ -476,7 +476,8 @@ fn main() -> color_eyre::eyre::Result<()> { &mime_type, cli.min_size, cli.max_size, - ); + ) + .await; }, None => {