diff --git a/Cargo.lock b/Cargo.lock index f594145..f977dc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.1" @@ -204,6 +210,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + [[package]] name = "castaway" version = "0.2.4" @@ -345,6 +357,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -546,6 +568,17 @@ dependencies = [ "objc2", ] +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "document-features" version = "0.2.12" @@ -646,6 +679,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.12" @@ -687,12 +730,43 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + [[package]] name = "futures-task" version = "0.3.34" @@ -706,7 +780,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", + "futures-io", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -738,8 +815,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -761,9 +840,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", + "wasm-bindgen", ] [[package]] @@ -820,6 +901,104 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -844,12 +1023,116 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "image" version = "0.25.10" @@ -896,6 +1179,12 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1008,6 +1297,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + [[package]] name = "litrs" version = "1.0.0" @@ -1038,6 +1333,12 @@ dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mac_address" version = "1.1.8" @@ -1291,6 +1592,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -1514,6 +1821,15 @@ dependencies = [ "winreg", ] +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1541,6 +1857,62 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases 0.2.2", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.20", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases 0.2.2", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "quote" version = "1.0.47" @@ -1594,6 +1966,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "ratatui" version = "0.30.2" @@ -1706,22 +2087,27 @@ dependencies = [ "clap", "crossterm", "dirs", + "flate2", "gethostname", "libc", "memchr", "portable-pty", "ratatui", "regex", + "reqwest", "rusqlite", "serde", "serde_json", + "sha2", "signal-hook 0.4.4", "strip-ansi-escapes", "sysinfo", + "tar", "toml", "ulid", "unicode-width", "windows-sys 0.60.2", + "zip", "zstd", ] @@ -1774,6 +2160,60 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rsqlite-vfs" version = "0.1.1" @@ -1799,6 +2239,12 @@ dependencies = [ "sqlite-wasm-rs", ] +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1822,31 +2268,110 @@ dependencies = [ ] [[package]] -name = "rustversion" -version = "1.0.23" +name = "rustls" +version = "0.23.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] [[package]] -name = "ryu" -version = "1.0.23" +name = "rustls-native-certs" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "rustls-pki-types" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] [[package]] -name = "semver" -version = "1.0.28" +name = "rustls-webpki" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] [[package]] -name = "serde" +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" @@ -1897,6 +2422,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serial2" version = "0.2.38" @@ -2006,6 +2543,16 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "sqlite-wasm-rs" version = "0.5.5" @@ -2018,6 +2565,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2060,6 +2613,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -2093,11 +2652,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "sysinfo" -version = "0.39.6" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" +checksum = "fe840c5b1afe259a5657392a4dbb74473a14c8db999c3ec2f4ae812e028a94da" dependencies = [ "libc", "memchr", @@ -2107,6 +2686,17 @@ dependencies = [ "windows", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "termina" version = "0.3.3" @@ -2258,6 +2848,55 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "toml" version = "1.1.5+spec-1.1.0" @@ -2297,6 +2936,82 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "typenum" version = "1.20.1" @@ -2349,6 +3064,30 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2397,6 +3136,15 @@ dependencies = [ "utf8parse", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2425,6 +3173,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.128" @@ -2457,6 +3215,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -2668,13 +3436,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -2686,6 +3463,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -2693,14 +3486,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -2712,48 +3505,96 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" @@ -2781,6 +3622,12 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + [[package]] name = "x11rb" version = "0.13.2" @@ -2798,6 +3645,39 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.57" @@ -2818,6 +3698,80 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "crc32fast", + "flate2", + "indexmap", + "memchr", + "typed-path", + "zopfli", +] + [[package]] name = "zlib-rs" version = "0.6.7" @@ -2830,6 +3784,18 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.14.0" diff --git a/Cargo.toml b/Cargo.toml index 33b160f..41611d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,24 +11,29 @@ chrono = "0.4.45" clap = { version = "4.6.6", features = ["derive"] } crossterm = "0.29.0" dirs = "7.0.0" +flate2 = "1.1.9" gethostname = "1.1.0" memchr = "2.8.3" portable-pty = "0.9.0" ratatui = "0.30.2" regex = "1.13.1" +reqwest = { version = "0.12.28", default-features = false, features = ["blocking", "json", "rustls-tls-native-roots"] } rusqlite = { version = "0.40.2", features = ["bundled"] } serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" strip-ansi-escapes = "0.2.1" +sha2 = "0.10.9" +tar = "0.4.44" toml = "1.1.5" ulid = { version = "3.0.0", features = ["serde"] } unicode-width = "0.2.2" zstd = "0.14.0" +zip = { version = "8.2.0", default-features = false, features = ["deflate"] } [target.'cfg(unix)'.dependencies] libc = "0.2.182" signal-hook = "0.4.4" [target."cfg(windows)".dependencies] -sysinfo = { version = "0.39.6", default-features = false, features = ["system"] } +sysinfo = { version = "=0.38.0", default-features = false, features = ["system"] } windows-sys = { version = "0.60", features = ["Win32_Foundation", "Win32_System_Console"] } diff --git a/README.md b/README.md index 06908e9..45b6750 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,18 @@ install -Dm755 target/release/recall ~/.local/bin/recall # Linux/macOS On Windows the binary is `target\release\recall.exe`; copy it somewhere on `PATH`. Make sure the install directory is on `PATH`. +### Update + +```sh +recall update +``` + +`recall update` downloads the latest stable GitHub Release for the current +platform, shows download progress, verifies its SHA-256 checksum, and replaces +the running binary. Use `recall update --check` to check without installing. +The history TUI checks for stable updates at most once every 24 hours and shows +an available update in its status bar. + ### Uninstall ```sh diff --git a/README.zh-CN.md b/README.zh-CN.md index 9df0c49..b20e22c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -56,6 +56,16 @@ install -Dm755 target/release/recall ~/.local/bin/recall # Linux/macOS Windows 上二进制为 `target\release\recall.exe`,把它复制到 `PATH` 中的目录即可。 +### 更新 + +```sh +recall update +``` + +`recall update` 会下载当前平台的最新稳定版 GitHub Release,展示下载进度、校验 +SHA-256,然后替换正在运行的二进制。使用 `recall update --check` 仅检查更新。 +历史 TUI 每 24 小时最多检查一次稳定版更新,并在状态栏显示更新提示。 + ### 卸载 ```sh diff --git a/src/cli.rs b/src/cli.rs index 7fcb1c8..9556c43 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -26,6 +26,8 @@ pub enum Command { Doctor, /// Drop stored output older than the retention window. Prune, + /// Download and install the latest stable release. + Update(UpdateArgs), /// Show or locate the configuration file. Config(ConfigArgs), /// Print a fresh recall id. @@ -125,6 +127,13 @@ pub struct ConfigArgs { pub action: ConfigAction, } +#[derive(Debug, Args)] +pub struct UpdateArgs { + /// Check for an update without downloading it. + #[arg(long)] + pub check: bool, +} + #[derive(Debug, Subcommand)] pub enum ConfigAction { /// Print the configuration file path. diff --git a/src/commands/mod.rs b/src/commands/mod.rs index a8e0f78..e79315f 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -4,6 +4,7 @@ mod init; mod proxy; mod record; mod shell; +mod update; use anyhow::Result; @@ -26,6 +27,7 @@ pub fn run(cli: Cli) -> Result<()> { Some(Command::Import(args)) => import::run(args), Some(Command::Doctor) => doctor::run(), Some(Command::Prune) => prune(), + Some(Command::Update(args)) => update::run(args), Some(Command::Config(args)) => config(args), Some(Command::Uuid) => { println!("{}", new_id()); @@ -69,7 +71,13 @@ fn prune() -> Result<()> { fn search(args: SearchArgs) -> Result<()> { let config = Config::load()?; - let code = crate::tui::run(args, config)?; + let update_check = update::startup_check(); + let code = crate::tui::run( + args, + config, + update_check.initial_notice, + update_check.refreshed_notice, + )?; if code != 0 { std::process::exit(code); } diff --git a/src/commands/update.rs b/src/commands/update.rs new file mode 100644 index 0000000..34dda49 --- /dev/null +++ b/src/commands/update.rs @@ -0,0 +1,503 @@ +use std::fs::{self, File}; +use std::io::{self, IsTerminal, Read, Write}; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::{self, Receiver}; +use std::thread; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use anyhow::{Context, Result, bail}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::cli::UpdateArgs; + +const REPOSITORY: &str = "wendaining/recall"; +const CHECK_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); + +#[derive(Debug, Deserialize)] +struct Release { + tag_name: String, + draft: bool, + prerelease: bool, + assets: Vec, +} + +#[derive(Debug, Deserialize)] +struct Asset { + name: String, + browser_download_url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct UpdateCache { + checked_at: u64, + #[serde(default)] + latest: Option, +} + +pub struct StartupCheck { + pub initial_notice: Option, + pub refreshed_notice: Receiver>, +} + +pub fn run(args: UpdateArgs) -> Result<()> { + let release = latest_release()?; + if !is_newer(&release.tag_name, env!("CARGO_PKG_VERSION"))? { + println!("recall {} is up to date", env!("CARGO_PKG_VERSION")); + return Ok(()); + } + if args.check { + println!( + "update available: {} -> {}", + env!("CARGO_PKG_VERSION"), + release.tag_name + ); + return Ok(()); + } + + let target = target()?; + let archive_name = archive_name(&release.tag_name, target); + let archive = release + .assets + .iter() + .find(|asset| asset.name == archive_name) + .context("latest release does not include an archive for this platform")?; + let checksums = release + .assets + .iter() + .find(|asset| asset.name == "SHA256SUMS") + .context("latest release does not include SHA256SUMS")?; + let expected = checksum_for( + &download_text(&checksums.browser_download_url)?, + &archive_name, + )?; + + let executable = std::env::current_exe().context("locating the running recall binary")?; + let parent = executable + .parent() + .context("locating the binary directory")?; + let stage = parent.join(format!(".recall-update-{}", std::process::id())); + if stage.exists() { + fs::remove_dir_all(&stage).with_context(|| format!("clearing {}", stage.display()))?; + } + fs::create_dir(&stage).with_context(|| format!("creating {}", stage.display()))?; + let result = install_release(&stage, archive, &expected, &executable); + let _ = fs::remove_dir_all(&stage); + result +} + +pub fn startup_check() -> StartupCheck { + let (sender, receiver) = mpsc::channel(); + let Some(path) = cache_path() else { + return StartupCheck { + initial_notice: None, + refreshed_notice: receiver, + }; + }; + let cached = read_cache(&path); + let cached_latest = cached.as_ref().and_then(|cache| cache.latest.clone()); + let initial_notice = cached_latest.as_deref().and_then(newer_notice); + if !cache_due(cached.as_ref(), now_secs()) { + return StartupCheck { + initial_notice, + refreshed_notice: receiver, + }; + } + thread::spawn(move || { + let latest = latest_release().ok().map(|release| release.tag_name); + let cache = UpdateCache { + checked_at: now_secs(), + latest: cache_latest(latest, cached_latest), + }; + let _ = write_cache(&path, &cache); + let _ = sender.send(cache.latest.as_deref().and_then(newer_notice)); + }); + StartupCheck { + initial_notice, + refreshed_notice: receiver, + } +} + +fn install_release(stage: &Path, archive: &Asset, expected: &str, executable: &Path) -> Result<()> { + let archive_path = stage.join(&archive.name); + let actual = download_with_progress(&archive.browser_download_url, &archive_path)?; + if actual != expected { + bail!("checksum mismatch for {}", archive.name); + } + eprintln!("checksum verified"); + + let replacement = stage.join(executable_name()); + extract_binary(&archive_path, &replacement)?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&replacement, fs::Permissions::from_mode(0o755))?; + } + match replace_binary(&replacement, executable)? { + Replacement::Complete => println!( + "updated recall to {}", + latest_version_from_archive(&archive.name)? + ), + #[cfg(windows)] + Replacement::Pending => { + println!("update staged; close all recall sessions to finish replacing it") + } + } + Ok(()) +} + +fn latest_release() -> Result { + let url = format!("https://api.github.com/repos/{REPOSITORY}/releases/latest"); + let release: Release = client() + .get(url) + .header("Accept", "application/vnd.github+json") + .header("X-GitHub-Api-Version", "2022-11-28") + .send() + .context("checking the latest recall release")? + .error_for_status() + .context("checking the latest recall release")? + .json() + .context("reading the latest recall release")?; + if release.draft || release.prerelease { + bail!("latest release is not a stable release"); + } + parse_version(&release.tag_name)?; + Ok(release) +} + +fn client() -> reqwest::blocking::Client { + reqwest::blocking::Client::builder() + .timeout(Duration::from_secs(20)) + .user_agent("recall-updater") + .build() + .expect("valid HTTP client configuration") +} + +fn download_text(url: &str) -> Result { + client() + .get(url) + .send() + .with_context(|| format!("downloading {url}"))? + .error_for_status() + .with_context(|| format!("downloading {url}"))? + .text() + .with_context(|| format!("reading {url}")) +} + +fn download_with_progress(url: &str, destination: &Path) -> Result { + let mut response = client() + .get(url) + .send() + .with_context(|| format!("downloading {url}"))? + .error_for_status() + .with_context(|| format!("downloading {url}"))?; + let total = response.content_length(); + let mut file = + File::create(destination).with_context(|| format!("creating {}", destination.display()))?; + let mut hasher = Sha256::new(); + let mut bytes = 0_u64; + let mut buffer = [0_u8; 64 * 1024]; + loop { + let count = response + .read(&mut buffer) + .context("reading update download")?; + if count == 0 { + break; + } + file.write_all(&buffer[..count]) + .context("writing update download")?; + hasher.update(&buffer[..count]); + bytes += count as u64; + draw_progress(bytes, total); + } + clear_progress(); + Ok(format!("{:x}", hasher.finalize())) +} + +fn draw_progress(bytes: u64, total: Option) { + if !io::stderr().is_terminal() { + return; + } + let progress = total + .map(|total| bytes.saturating_mul(100) / total) + .unwrap_or(0); + let filled = (progress / 5).min(20) as usize; + eprint!( + "\r\x1b[36mupdate\x1b[0m [{}{}] {:>3}% {}", + "█".repeat(filled), + "·".repeat(20 - filled), + progress, + format_bytes(bytes) + ); + if let Some(total) = total { + eprint!(" / {}", format_bytes(total)); + } + let _ = io::stderr().flush(); +} + +fn clear_progress() { + if io::stderr().is_terminal() { + eprintln!(); + } +} + +fn extract_binary(archive: &Path, destination: &Path) -> Result<()> { + #[cfg(windows)] + { + let file = File::open(archive)?; + let mut zip = zip::ZipArchive::new(file).context("opening update archive")?; + let mut entry = zip + .by_name(executable_name()) + .context("archive does not contain recall.exe")?; + let mut output = File::create(destination)?; + io::copy(&mut entry, &mut output)?; + } + #[cfg(not(windows))] + { + let file = File::open(archive)?; + let decoder = flate2::read::GzDecoder::new(file); + let mut tar = tar::Archive::new(decoder); + let mut found = false; + for entry in tar.entries().context("opening update archive")? { + let mut entry = entry?; + if entry + .path()? + .file_name() + .is_some_and(|name| name == executable_name()) + { + let mut output = File::create(destination)?; + io::copy(&mut entry, &mut output)?; + found = true; + break; + } + } + if !found { + bail!("archive does not contain recall"); + } + } + Ok(()) +} + +#[cfg(unix)] +fn replace_binary(replacement: &Path, executable: &Path) -> Result { + fs::rename(replacement, executable) + .with_context(|| format!("replacing {}", executable.display()))?; + Ok(Replacement::Complete) +} + +#[cfg(windows)] +fn replace_binary(replacement: &Path, executable: &Path) -> Result { + let pending = executable.with_extension("exe.recall-new"); + fs::rename(replacement, &pending)?; + let script = std::env::temp_dir().join(format!("recall-update-{}.cmd", std::process::id())); + let pid = std::process::id(); + fs::write( + &script, + format!( + "@echo off\r\n:wait\r\ntasklist /FI \"PID eq {pid}\" /NH | find \"{pid}\" >nul\r\nif not errorlevel 1 (\r\n timeout /t 1 /nobreak >nul\r\n goto wait\r\n)\r\n:replace\r\nmove /Y \"{}\" \"{}\" >nul\r\nif errorlevel 1 (\r\n timeout /t 1 /nobreak >nul\r\n goto replace\r\n)\r\ndel \"%~f0\"\r\n", + pending.display(), + executable.display() + ), + )?; + std::process::Command::new("cmd") + .args(["/C", &script.to_string_lossy()]) + .spawn() + .context("starting the Windows update helper")?; + Ok(Replacement::Pending) +} + +enum Replacement { + Complete, + #[cfg(windows)] + Pending, +} + +fn checksum_for(text: &str, archive: &str) -> Result { + text.lines() + .find_map(|line| { + let (hash, name) = line.split_once(char::is_whitespace)?; + (name.trim_start_matches('*').trim() == archive).then(|| hash.to_ascii_lowercase()) + }) + .filter(|hash| hash.len() == 64 && hash.bytes().all(|byte| byte.is_ascii_hexdigit())) + .context("SHA256SUMS does not contain the update archive") +} + +fn archive_name(tag: &str, target: &str) -> String { + format!( + "recall-{tag}-{target}.{}", + if cfg!(windows) { "zip" } else { "tar.gz" } + ) +} + +fn executable_name() -> &'static str { + if cfg!(windows) { + "recall.exe" + } else { + "recall" + } +} + +fn target() -> Result<&'static str> { + match (std::env::consts::OS, std::env::consts::ARCH) { + ("macos", "aarch64") => Ok("aarch64-apple-darwin"), + ("macos", "x86_64") => Ok("x86_64-apple-darwin"), + ("linux", "aarch64") => Ok("aarch64-unknown-linux-gnu"), + ("linux", "x86_64") => Ok("x86_64-unknown-linux-gnu"), + ("windows", "x86_64") => Ok("x86_64-pc-windows-msvc"), + (os, arch) => bail!("unsupported update platform: {os}/{arch}"), + } +} + +fn parse_version(version: &str) -> Result> { + let version = version.strip_prefix('v').unwrap_or(version); + let parts = version + .split('.') + .map(str::parse::) + .collect::, _>>() + .context("release version is not valid semver")?; + if parts.len() != 3 { + bail!("release version is not valid semver"); + } + Ok(parts) +} + +fn is_newer(candidate: &str, current: &str) -> Result { + Ok(parse_version(candidate)? > parse_version(current)?) +} + +fn latest_version_from_archive(name: &str) -> Result<&str> { + name.strip_prefix("recall-") + .and_then(|name| name.split('-').next()) + .context("reading update version") +} + +fn cache_path() -> Option { + Some(dirs::cache_dir()?.join("recall").join("update.json")) +} + +fn cache_due(cache: Option<&UpdateCache>, now: u64) -> bool { + cache.is_none_or(|cache| now.saturating_sub(cache.checked_at) >= CHECK_INTERVAL.as_secs()) +} + +fn cache_latest(latest: Option, previous: Option) -> Option { + latest.or(previous) +} + +fn read_cache(path: &Path) -> Option { + serde_json::from_str(&fs::read_to_string(path).ok()?).ok() +} + +fn write_cache(path: &Path, cache: &UpdateCache) -> Result<()> { + let directory = path.parent().context("locating update cache directory")?; + fs::create_dir_all(directory)?; + fs::write(path, serde_json::to_vec(cache)?)?; + Ok(()) +} + +fn now_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + +fn newer_notice(latest: &str) -> Option { + is_newer(latest, env!("CARGO_PKG_VERSION")) + .ok() + .filter(|newer| *newer) + .map(|_| format!("update available: {latest} · run recall update")) +} + +fn format_bytes(bytes: u64) -> String { + format!("{:.1} MiB", bytes as f64 / 1024.0 / 1024.0) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_dir(name: &str) -> PathBuf { + let path = + std::env::temp_dir().join(format!("recall-update-{name}-{}", std::process::id())); + let _ = fs::remove_dir_all(&path); + fs::create_dir(&path).unwrap(); + path + } + + #[test] + fn compares_stable_versions() { + assert!(is_newer("v0.2.0", "0.1.0").unwrap()); + assert!(!is_newer("v0.1.0", "0.1.0").unwrap()); + assert!(!is_newer("v0.0.9", "0.1.0").unwrap()); + } + + #[test] + fn rejects_non_stable_versions() { + assert!(parse_version("v0.2.0-rc.1").is_err()); + assert!(parse_version("v0.2").is_err()); + } + + #[test] + fn finds_checksum_for_named_archive() { + let checksum = checksum_for( + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 recall-v1.2.3-aarch64-apple-darwin.tar.gz\n", + "recall-v1.2.3-aarch64-apple-darwin.tar.gz", + ) + .unwrap(); + assert_eq!( + checksum, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" + ); + } + + #[test] + fn refreshes_stale_update_cache_and_preserves_known_version_on_failure() { + let fresh = UpdateCache { + checked_at: 100, + latest: Some("v0.2.0".into()), + }; + assert!(!cache_due(Some(&fresh), 101)); + assert!(cache_due(Some(&fresh), 100 + CHECK_INTERVAL.as_secs())); + assert_eq!(cache_latest(None, fresh.latest), Some("v0.2.0".to_string())); + } + + #[cfg(not(windows))] + #[test] + fn extracts_recall_from_a_release_archive() { + let directory = test_dir("extract"); + let archive = directory.join("release.tar.gz"); + let payload = b"replacement binary"; + let encoder = flate2::write::GzEncoder::new( + File::create(&archive).unwrap(), + flate2::Compression::default(), + ); + let mut tar = tar::Builder::new(encoder); + let mut header = tar::Header::new_gnu(); + header.set_size(payload.len() as u64); + header.set_mode(0o755); + header.set_cksum(); + tar.append_data(&mut header, "recall", &payload[..]) + .unwrap(); + tar.into_inner().unwrap().finish().unwrap(); + + let extracted = directory.join("recall"); + extract_binary(&archive, &extracted).unwrap(); + assert_eq!(fs::read(&extracted).unwrap(), payload); + fs::remove_dir_all(directory).unwrap(); + } + + #[cfg(unix)] + #[test] + fn replaces_binary_atomically() { + let directory = test_dir("replace"); + let executable = directory.join("recall"); + let replacement = directory.join("recall.new"); + fs::write(&executable, b"old binary").unwrap(); + fs::write(&replacement, b"new binary").unwrap(); + + replace_binary(&replacement, &executable).unwrap(); + assert_eq!(fs::read(&executable).unwrap(), b"new binary"); + assert!(!replacement.exists()); + fs::remove_dir_all(directory).unwrap(); + } +} diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 6d2b677..b536356 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -2,6 +2,7 @@ mod app; mod ui; use std::io; +use std::sync::mpsc::{Receiver, TryRecvError}; use std::time::Duration; use anyhow::Result; @@ -31,11 +32,19 @@ type Backend = CrosstermBackend; /// /// Returns an exit code: 0 for a normal edit selection, 2 when the user asked /// to rerun the command. -pub fn run(args: SearchArgs, config: Config) -> Result { +pub fn run( + args: SearchArgs, + config: Config, + update_notice: Option, + update_result: Receiver>, +) -> Result { let mut app = App::new(config, args.cmd_only, args.query)?; + if let Some(notice) = update_notice { + app.set_status(notice); + } let mut guard = TerminalGuard::enter()?; - let result = event_loop(guard.terminal(), &mut app); + let result = event_loop(guard.terminal(), &mut app, update_result); guard.leave(); result?; @@ -52,7 +61,12 @@ pub fn run(args: SearchArgs, config: Config) -> Result { Ok(code) } -fn event_loop(terminal: &mut Terminal, app: &mut App) -> Result<()> { +fn event_loop( + terminal: &mut Terminal, + app: &mut App, + update_result: Receiver>, +) -> Result<()> { + let mut update_result = Some(update_result); loop { terminal.draw(|frame| ui::draw(frame, app))?; if event::poll(Duration::from_millis(200))? { @@ -61,6 +75,13 @@ fn event_loop(terminal: &mut Terminal, app: &mut App) -> Result<()> { _ => {} } } + if let Some(receiver) = &update_result { + match receiver.try_recv() { + Ok(Some(notice)) => app.set_status(notice), + Ok(None) | Err(TryRecvError::Disconnected) => update_result = None, + Err(TryRecvError::Empty) => {} + } + } if app.should_quit { break; }