commit 4f3517b97b5389e1cbd5b4fd84793833c1b577dd Author: mixa Date: Wed Feb 11 20:37:44 2026 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3b66410 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "git.ignoreLimitWarning": true +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6610b43 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7015 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "annotate-snippets" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" +dependencies = [ + "anstyle", + "unicode-width", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] + +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64 0.22.1", + "http 1.4.0", + "log", + "url", +] + +[[package]] +name = "audiopus" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3743519567e9135cf6f9f1a509851cb0c8e4cb9d66feb286668afb1923bec458" +dependencies = [ + "audiopus_sys 0.1.8", +] + +[[package]] +name = "audiopus" +version = "0.3.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab55eb0e56d7c6de3d59f544e5db122d7725ec33be6a276ee8241f3be6473955" +dependencies = [ + "audiopus_sys 0.2.2", +] + +[[package]] +name = "audiopus_sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927791de46f70facea982dbfaf19719a41ce6064443403be631a85de6a58fff9" +dependencies = [ + "log", + "pkg-config", +] + +[[package]] +name = "audiopus_sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62314a1546a2064e033665d658e88c620a62904be945f8147e6b16c3db9f8651" +dependencies = [ + "cmake", + "log", + "pkg-config", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper 1.0.2", + "tokio", + "tokio-tungstenite 0.28.0", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "annotate-snippets", + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.114", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfg-expr" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cef5b5a1a6827c7322ae2a636368a573006b27cfa76c7ebd53e834daeaab6a" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" + +[[package]] +name = "cordyceps" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" +dependencies = [ + "loom 0.7.2", + "tracing", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.10.0", + "crossterm_winapi", + "futures-core", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.10.0", + "crossterm_winapi", + "derive_more 2.1.1", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.3", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "211f05e03c7d03754740fd9e585de910a095d6b99f8bcfffdef8319fa02a8331" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "crypto_secretbox" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" +dependencies = [ + "aead", + "cipher", + "generic-array", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f9200d1d13637f15a6acb71e758f64624048d85b31a5fdbfd8eca1e2687d0b7" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.11.0-rc.10", + "fiat-crypto", + "rand_core 0.9.5", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core 0.10.2", + "darling_macro 0.10.2", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.9.3", + "syn 1.0.109", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.114", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.114", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core 0.10.2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "der" +version = "0.8.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c1d73e9668ea6b6a28172aa55f3ebec38507131ce179051c8033b5c6037653" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" +dependencies = [ + "darling 0.10.2", + "derive_builder_core 0.9.0", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" +dependencies = [ + "darling 0.10.2", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core 0.20.2", + "syn 2.0.114", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.114", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.114", + "unicode-xid", +] + +[[package]] +name = "diatomic-waker" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa94b64bfc6549e6e4b5a3216f22593224174083da7a90db47e951c4fb31725" +dependencies = [ + "block-buffer 0.11.0", + "const-oid", + "crypto-common 0.2.0", +] + +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "discortp" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c793408a15d361754613fa68123ffa60424c2617fafdf82127b4bedf37d3f5d" +dependencies = [ + "pnet_macros", + "pnet_macros_support", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ed25519" +version = "3.0.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad207ed88a133091f83224265eac21109930db09bedcad05d5252f2af2de20a1" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.9.5", + "serde", + "sha2 0.11.0-rc.2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "enum-assoc" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed8956bd5c1f0415200516e78ff07ec9e16415ade83c056c230d7b7ea0d55b7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "euclid" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand 0.9.2", + "siphasher", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +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 = "from_map" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99f31122ab0445ff8cee420b805f24e07683073815de1dd276ee7d588d301700" +dependencies = [ + "hashmap_derive", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-buffered" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e0e1f38ec07ba4abbde21eed377082f17ccb988be9d988a5adbf4bafc118fd" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin 0.10.0", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset 0.5.7", + "futures-core", + "futures-lite", + "pin-project", + "smallvec", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generator" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashmap_derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb30bf173e72cc31b5265dac095423ca14e7789ff7c3b0e6096a37a996f12883" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin 0.9.8", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "bytes", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "h2 0.4.13", + "http 1.4.0", + "idna", + "ipnet", + "once_cell", + "rand 0.9.2", + "ring 0.17.14", + "rustls 0.23.36", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tokio-rustls 0.26.4", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.2", + "resolv-conf", + "rustls 0.23.36", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-rustls 0.26.4", + "tracing", +] + +[[package]] +name = "hls_m3u8" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b906521a5b0e6d2ec07ea0bb855d92a1db30b48812744a645a3b2a1405cb8159" +dependencies = [ + "derive_builder 0.9.0", + "derive_more 0.99.20", + "hex", + "shorthand", + "stable-vec", + "strum 0.17.1", + "thiserror 1.0.69", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.8.1", + "hyper-util", + "rustls 0.23.36", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", + "webpki-roots 1.0.6", +] + +[[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 1.4.0", + "http-body 1.0.1", + "hyper 1.8.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +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 = "identity-hash" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "igd-next" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" +dependencies = [ + "async-trait", + "attohttpc", + "bytes", + "futures", + "http 1.4.0", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "log", + "rand 0.9.2", + "tokio", + "url", + "xmltree", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instability" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +dependencies = [ + "darling 0.23.0", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "iroh" +version = "0.96.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5236da4d5681f317ec393c8fe2b7e3d360d31c6bb40383991d0b7429ca5ad117" +dependencies = [ + "backon", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more 2.1.1", + "ed25519-dalek", + "futures-util", + "getrandom 0.3.4", + "hickory-resolver", + "http 1.4.0", + "igd-next", + "iroh-base", + "iroh-metrics", + "iroh-quinn", + "iroh-quinn-proto", + "iroh-quinn-udp", + "iroh-relay", + "n0-error", + "n0-future", + "n0-watcher", + "netdev", + "netwatch", + "papaya", + "pin-project", + "pkarr", + "pkcs8", + "portmapper", + "rand 0.9.2", + "reqwest 0.12.28", + "rustc-hash", + "rustls 0.23.36", + "rustls-pki-types", + "rustls-webpki 0.103.9", + "serde", + "smallvec", + "strum 0.27.2", + "sync_wrapper 1.0.2", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", + "webpki-roots 1.0.6", +] + +[[package]] +name = "iroh-base" +version = "0.96.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c99d836a1c99e037e98d1bf3ef209c3a4df97555a00ce9510eb78eccdf5567" +dependencies = [ + "curve25519-dalek", + "data-encoding", + "derive_more 2.1.1", + "digest 0.11.0-rc.10", + "ed25519-dalek", + "n0-error", + "rand_core 0.9.5", + "serde", + "sha2 0.11.0-rc.2", + "url", + "zeroize", + "zeroize_derive", +] + +[[package]] +name = "iroh-gossip" +version = "0.96.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d04f83254c847ac61a9b2215b95a36d598d87af033ca12a546cd1c6a2e06dab" +dependencies = [ + "blake3", + "bytes", + "data-encoding", + "derive_more 2.1.1", + "ed25519-dalek", + "futures-concurrency", + "futures-lite", + "futures-util", + "hex", + "indexmap", + "iroh", + "iroh-base", + "iroh-metrics", + "irpc", + "n0-error", + "n0-future", + "postcard", + "rand 0.9.2", + "serde", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "iroh-metrics" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c946095f060e6e59b9ff30cc26c75cdb758e7fb0cde8312c89e2144654989fcb" +dependencies = [ + "iroh-metrics-derive", + "itoa", + "n0-error", + "postcard", + "ryu", + "serde", + "tracing", +] + +[[package]] +name = "iroh-metrics-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab063c2bfd6c3d5a33a913d4fdb5252f140db29ec67c704f20f3da7e8f92dbf" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "iroh-quinn" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034ed21f34c657a123d39525d948c885aacba59508805e4dd67d71f022e7151b" +dependencies = [ + "bytes", + "cfg_aliases", + "iroh-quinn-proto", + "iroh-quinn-udp", + "pin-project-lite", + "rustc-hash", + "rustls 0.23.36", + "socket2 0.6.2", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "web-time", +] + +[[package]] +name = "iroh-quinn-proto" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de99ad8adc878ee0e68509ad256152ce23b8bbe45f5539d04e179630aca40a9" +dependencies = [ + "bytes", + "derive_more 2.1.1", + "enum-assoc", + "fastbloom", + "getrandom 0.3.4", + "identity-hash", + "lru-slab", + "rand 0.9.2", + "ring 0.17.14", + "rustc-hash", + "rustls 0.23.36", + "rustls-pki-types", + "slab", + "sorted-index-buffer", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "iroh-quinn-udp" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f981dadd5a072a9e0efcd24bdcc388e570073f7e51b33505ceb1ef4668c80c86" +dependencies = [ + "cfg_aliases", + "libc", + "socket2 0.6.2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "iroh-relay" +version = "0.96.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd2b63e654b9dec799a73372cdc79b529ca6c7248c0c8de7da78a02e3a46f03c" +dependencies = [ + "blake3", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more 2.1.1", + "getrandom 0.3.4", + "hickory-resolver", + "http 1.4.0", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "iroh-base", + "iroh-metrics", + "iroh-quinn", + "iroh-quinn-proto", + "lru", + "n0-error", + "n0-future", + "num_enum", + "pin-project", + "pkarr", + "postcard", + "rand 0.9.2", + "reqwest 0.12.28", + "rustls 0.23.36", + "rustls-pki-types", + "serde", + "serde_bytes", + "strum 0.27.2", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util", + "tokio-websockets", + "tracing", + "url", + "vergen-gitcl", + "webpki-roots 1.0.6", + "ws_stream_wasm", + "z32", +] + +[[package]] +name = "irpc" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bbc84aaeab13a6d7502bae4f40f2517b643924842e0230ea0bf807477cc208" +dependencies = [ + "futures-util", + "irpc-derive", + "n0-error", + "n0-future", + "serde", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "irpc-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58148196d2230183c9679431ac99b57e172000326d664e8456fa2cd27af6505a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kasuari" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.181" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + +[[package]] +name = "libspa" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b8cfa2a7656627b4c92c6b9ef929433acd673d5ab3708cda1b18478ac00df4" +dependencies = [ + "bitflags 2.10.0", + "cc", + "convert_case 0.8.0", + "cookie-factory", + "libc", + "libspa-sys", + "nix 0.30.1", + "nom 8.0.0", + "system-deps", +] + +[[package]] +name = "libspa-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901049455d2eb6decf9058235d745237952f4804bc584c5fcb41412e6adcc6e0" +dependencies = [ + "bindgen", + "cc", + "system-deps", +] + +[[package]] +name = "line-clipping" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator 0.7.5", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator 0.8.8", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac-addr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix 0.29.0", + "winapi", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "n0-error" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4782b4baf92d686d161c15460c83d16ebcfd215918763903e9619842665cae" +dependencies = [ + "anyhow", + "n0-error-macros", + "spez", +] + +[[package]] +name = "n0-error-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03755949235714b2b307e5ae89dd8c1c2531fb127d9b8b7b4adf9c876cd3ed18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "n0-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" +dependencies = [ + "cfg_aliases", + "derive_more 2.1.1", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "n0-watcher" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38795f7932e6e9d1c6e989270ef5b3ff24ebb910e2c9d4bed2d28d8bae3007dc" +dependencies = [ + "derive_more 2.1.1", + "n0-error", + "n0-future", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "netdev" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc9815643a243856e7bd84524e1ff739e901e846cfb06ad9627cd2b6d59bd737" +dependencies = [ + "block2", + "dispatch2", + "dlopen2", + "ipnet", + "libc", + "mac-addr", + "netlink-packet-core", + "netlink-packet-route 0.25.1", + "netlink-sys", + "objc2-core-foundation", + "objc2-system-configuration", + "once_cell", + "plist", + "windows-sys 0.59.0", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec2f5b6839be2a19d7fa5aab5bc444380f6311c2b693551cb80f45caaa7b5ef" +dependencies = [ + "bitflags 2.10.0", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-packet-route" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" +dependencies = [ + "bitflags 2.10.0", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "netwatch" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "454b8c0759b2097581f25ed5180b4a1d14c324fde6d0734932a288e044d06232" +dependencies = [ + "atomic-waker", + "bytes", + "cfg_aliases", + "derive_more 2.1.1", + "iroh-quinn-udp", + "js-sys", + "libc", + "n0-error", + "n0-future", + "n0-watcher", + "netdev", + "netlink-packet-core", + "netlink-packet-route 0.28.0", + "netlink-proto", + "netlink-sys", + "objc2-core-foundation", + "objc2-system-configuration", + "pin-project-lite", + "serde", + "socket2 0.6.2", + "time", + "tokio", + "tokio-util", + "tracing", + "web-sys", + "windows 0.62.2", + "windows-result", + "wmi", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "no-std-compat" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df270209a7f04d62459240d890ecb792714d5db12c92937823574a09930276b4" + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "ntimestamp" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50f94c405726d3e0095e89e72f75ce7f6587b94a8bd8dc8054b73f65c0fd68c" +dependencies = [ + "base32", + "document-features", + "getrandom 0.2.17", + "httpdate", + "js-sys", + "once_cell", + "serde", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", + "block2", + "dispatch2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "p2p-chat" +version = "0.1.0" +dependencies = [ + "anyhow", + "audiopus 0.2.0", + "axum", + "bincode", + "bytes", + "chrono", + "clap", + "crossbeam-channel", + "crossterm 0.28.1", + "directories", + "futures", + "iroh", + "iroh-gossip", + "libspa", + "mime_guess", + "n0-future", + "pipewire", + "postcard", + "rand 0.8.5", + "ratatui", + "rust-embed", + "serde", + "serde_json", + "sha2 0.10.9", + "songbird", + "tokio", + "tokio-stream", + "toml 0.7.8", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "papaya" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92dd0b07c53a0a0c764db2ace8c541dc47320dad97c2200c2a637ab9dd2328f" +dependencies = [ + "equivalent", + "seize", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "patricia_tree" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f2f4539bffe53fc4b4da301df49d114b845b077bd5727b7fe2bd9d8df2ae68" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2 0.10.9", +] + +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pipewire" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9688b89abf11d756499f7c6190711d6dbe5a3acdb30c8fbf001d6596d06a8d44" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "libc", + "libspa", + "libspa-sys", + "nix 0.30.1", + "once_cell", + "pipewire-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "pipewire-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb028afee0d6ca17020b090e3b8fa2d7de23305aef975c7e5192a5050246ea36" +dependencies = [ + "bindgen", + "libspa-sys", + "system-deps", +] + +[[package]] +name = "pkarr" +version = "5.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d346b545765a0ef58b6a7e160e17ddaa7427f439b7b9a287df6c88c9e04bf2" +dependencies = [ + "async-compat", + "base32", + "bytes", + "cfg_aliases", + "document-features", + "dyn-clone", + "ed25519-dalek", + "futures-buffered", + "futures-lite", + "getrandom 0.3.4", + "log", + "lru", + "ntimestamp", + "reqwest 0.12.28", + "self_cell", + "serde", + "sha1_smol", + "simple-dns", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "wasm-bindgen-futures", +] + +[[package]] +name = "pkcs8" +version = "0.11.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b226d2cc389763951db8869584fd800cbbe2962bf454e2edeb5172b31ee99774" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "pnet_base" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cf6fb3ab38b68d01ab2aea03ed3d1132b4868fa4e06285f29f16da01c5f4c" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_macros" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688b17499eee04a0408aca0aa5cba5fc86401d7216de8a63fdf7a4c227871804" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.114", +] + +[[package]] +name = "pnet_macros_support" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea925b72f4bd37f8eab0f221bbe4c78b63498350c983ffa9dd4bcde7e030f56" +dependencies = [ + "pnet_base", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "portmapper" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d2a8825353ace3285138da3378b1e21860d60351942f7aa3b99b13b41f80318" +dependencies = [ + "base64 0.22.1", + "bytes", + "derive_more 2.1.1", + "futures-lite", + "futures-util", + "hyper-util", + "igd-next", + "iroh-metrics", + "libc", + "n0-error", + "netwatch", + "num_enum", + "rand 0.9.2", + "serde", + "smallvec", + "socket2 0.6.2", + "time", + "tokio", + "tokio-util", + "tower-layer", + "tracing", + "url", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "postcard-derive", + "serde", +] + +[[package]] +name = "postcard-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.36", + "socket2 0.6.2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring 0.17.14", + "rustc-hash", + "rustls 0.23.36", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "ratatui" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termwiz", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +dependencies = [ + "bitflags 2.10.0", + "compact_str", + "hashbrown 0.16.1", + "indoc", + "itertools 0.14.0", + "kasuari", + "lru", + "strum 0.27.2", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +dependencies = [ + "cfg-if", + "crossterm 0.29.0", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.16.1", + "indoc", + "instability", + "itertools 0.14.0", + "line-clipping", + "ratatui-core", + "strum 0.27.2", + "time", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "realfft" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" +dependencies = [ + "rustfft", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-rustls 0.24.2", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-rustls 0.24.1", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 0.25.4", + "winreg", +] + +[[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-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-rustls 0.27.7", + "hyper-util", + "js-sys", + "log", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.36", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 1.0.6", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[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 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "ringbuf" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c" +dependencies = [ + "crossbeam-utils", + "portable-atomic", + "portable-atomic-util", +] + +[[package]] +name = "rubato" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5d18b486e7d29a408ef3f825bc1327d8f87af091c987ca2f5b734625940e234" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "realfft", +] + +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.114", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "sha2 0.10.9", + "walkdir", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustfft" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.14", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring 0.17.14", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "log", + "once_cell", + "ring 0.17.14", + "rustls-pki-types", + "rustls-webpki 0.103.9", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring 0.17.14", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring 0.17.14", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty_pool" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed36cdb20de66d89a17ea04b8883fc7a386f2cf877aaedca5005583ce4876ff" +dependencies = [ + "crossbeam-channel", + "futures", + "futures-channel", + "futures-executor", + "num_cpus", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "seize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-aux" +version = "4.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207f67b28fe90fb596503a9bf0bf1ea5e831e21307658e177c5dfcdfc3ab8a0a" +dependencies = [ + "chrono", + "serde", + "serde-value", + "serde_json", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float 2.10.1", + "serde", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_cow" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7bbbec7196bfde255ab54b65e34087c0849629280028238e67ee25d6a4b7da" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +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 = "serenity" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bde37f42765dfdc34e2a039e0c84afbf79a3101c1941763b0beb816c2f17541" +dependencies = [ + "arrayvec", + "async-trait", + "base64 0.22.1", + "bitflags 2.10.0", + "bytes", + "flate2", + "futures", + "mime_guess", + "percent-encoding", + "reqwest 0.12.28", + "secrecy", + "serde", + "serde_cow", + "serde_json", + "time", + "tokio", + "tokio-tungstenite 0.21.0", + "tracing", + "typemap_rev", + "url", +] + +[[package]] +name = "serenity-voice-model" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790de0c27162611d3adbb98eb83abc295ecd0d608ad9c36653b3e5f2bd06dc22" +dependencies = [ + "bitflags 2.10.0", + "num-traits", + "serde", + "serde_json", + "serde_repr", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e3878ab0f98e35b2df35fe53201d088299b41a6bb63e3e34dada2ac4abd924" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0-rc.10", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shorthand" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "474f77f985d8212610f170332eaf173e768404c0c1d4deb041f32c297cf18931" +dependencies = [ + "from_map", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "3.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simple-dns" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "songbird" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30c6bde67a7ba20c8d9abefa35dfbb4f7abb570b344af73c201626033fb64703" +dependencies = [ + "aead", + "aes-gcm", + "async-trait", + "audiopus 0.3.0-rc.0", + "byteorder", + "bytes", + "chacha20poly1305", + "crypto_secretbox", + "dashmap", + "derivative", + "discortp", + "flume", + "futures", + "nohash-hasher", + "once_cell", + "parking_lot", + "pin-project", + "rand 0.8.5", + "reqwest 0.11.27", + "ringbuf", + "rubato", + "rusty_pool", + "serde", + "serde-aux", + "serde_json", + "serenity", + "serenity-voice-model", + "socket2 0.5.10", + "stream_lib", + "streamcatcher", + "symphonia", + "symphonia-core", + "tokio", + "tokio-tungstenite 0.21.0", + "tokio-util", + "tracing", + "tracing-futures", + "twilight-gateway", + "typemap_rev", + "typenum", + "url", + "uuid", +] + +[[package]] +name = "sorted-index-buffer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06" + +[[package]] +name = "spez" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "spki" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable-vec" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1dff32a2ce087283bec878419027cebd888760d8760b2941ad0843531dc9ec8" +dependencies = [ + "no-std-compat", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stream_lib" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa3f10eb5a7054e17abf61d310e4e29108187a847591c63c4c79b6a74898a5a7" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "hls_m3u8", + "patricia_tree", + "reqwest 0.11.27", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "streamcatcher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71664755c349abb0758fda6218fb2d2391ca2a73f9302c03b145491db4fcea29" +dependencies = [ + "crossbeam-utils", + "futures-util", + "loom 0.5.6", +] + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "530efb820d53b712f4e347916c5e7ed20deb76a4f0457943b3182fb889b06d2c" +dependencies = [ + "strum_macros 0.17.1", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6e163a520367c465f59e0a61a23cfae3b10b6546d78b6f672a382be79f7110" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "symphonia" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" +dependencies = [ + "lazy_static", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[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.114", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "7.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.9.12+spec-1.1.0", + "version-compare", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom 7.1.3", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bitflags 2.10.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset 0.4.2", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix 0.29.0", + "num-derive", + "num-traits", + "ordered-float 4.6.0", + "pest", + "pest_derive", + "phf", + "sha2 0.10.9", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "js-sys", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +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.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.9", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.36", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" +dependencies = [ + "futures-util", + "log", + "rustls 0.20.9", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.23.4", + "tungstenite 0.18.0", + "webpki", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "rustls 0.22.4", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tungstenite 0.21.0", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.28.0", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-websockets" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1b6348ebfaaecd771cecb69e832961d277f59845d4220a584701f72728152b7" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-sink", + "getrandom 0.3.4", + "http 1.4.0", + "httparse", + "rand 0.9.2", + "ring 0.17.14", + "rustls-pki-types", + "simdutf8", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.0.7+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1" +dependencies = [ + "winnow 0.7.14", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[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 1.0.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "http-range-header", + "httpdate", + "iri-string", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[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 = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes", + "http 0.2.12", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.20.9", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", + "webpki", +] + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.22.4", + "rustls-pki-types", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror 2.0.18", + "utf-8", +] + +[[package]] +name = "twilight-gateway" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30be5c7e2b13b4a59e0f93344c070c23404279a318a324eece1f4384ead47d86" +dependencies = [ + "bitflags 1.3.2", + "futures-util", + "rand 0.8.5", + "rustls 0.20.9", + "rustls-native-certs", + "serde", + "serde_json", + "tokio", + "tokio-tungstenite 0.18.0", + "tracing", + "twilight-gateway-queue", + "twilight-model", +] + +[[package]] +name = "twilight-gateway-queue" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3073747da8e1d09bc5383eed750451c9534021c8206a20092405b9855b3cb35a" +dependencies = [ + "tokio", + "tracing", +] + +[[package]] +name = "twilight-model" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276bd50f4817b3b421395afac89f5d7b61fdfd0f00a28b2a7db983e4878b4a1a" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde-value", + "serde_repr", + "time", +] + +[[package]] +name = "typemap_rev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74b08b0c1257381af16a5c3605254d529d3e7e109f3c62befc5d168968192998" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools 0.14.0", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +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 = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[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", + "serde_derive", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "atomic", + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vergen" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" +dependencies = [ + "anyhow", + "derive_builder 0.20.2", + "rustversion", + "vergen-lib 9.1.0", +] + +[[package]] +name = "vergen-gitcl" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9dfc1de6eb2e08a4ddf152f1b179529638bedc0ea95e6d667c014506377aefe" +dependencies = [ + "anyhow", + "derive_builder 0.20.2", + "rustversion", + "time", + "vergen", + "vergen-lib 0.1.6", +] + +[[package]] +name = "vergen-lib" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b07e6010c0f3e59fcb164e0163834597da68d1f864e2b8ca49f74de01e9c166" +dependencies = [ + "anyhow", + "derive_builder 0.20.2", + "rustversion", +] + +[[package]] +name = "vergen-lib" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" +dependencies = [ + "anyhow", + "derive_builder 0.20.2", + "rustversion", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2 0.10.9", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float 4.6.0", + "strsim 0.11.1", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[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.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +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 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "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]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "wmi" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "746791db82f029aaefc774ccbb8e61306edba18ef2c8998337cadccc0b8067f7" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror 2.0.18", + "windows 0.62.2", + "windows-core", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper", + "thiserror 2.0.18", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "z32" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f" + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ae79fec --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "p2p-chat" +version = "0.1.0" +edition = "2021" +description = "A peer-to-peer chat application with file transfer, built on QUIC via iroh" + +[dependencies] +# Networking +iroh = "0.96" +iroh-gossip = "0.96" +n0-future = "0.3" + +# Async runtime +tokio = { version = "1", features = ["full"] } + +# TUI +ratatui = "0.30" +crossterm = { version = "0.28", features = ["event-stream"] } + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" +bincode = "1" +postcard = { version = "1", features = ["alloc"] } + +# Utilities +bytes = "1" +sha2 = "0.10" +chrono = "0.4" +uuid = { version = "1", features = ["v4"] } +anyhow = "1" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +clap = { version = "4", features = ["derive"] } +rand = "0.8" + +# Configuration +toml = "0.7" +directories = "5.0" + +# Media +pipewire = "0.9" +libspa = "0.9" +songbird = { version = "0.4", features = ["builtin-queue"] } +audiopus = "0.2" +crossbeam-channel = "0.5" +axum = { version = "0.8.8", features = ["ws"] } +tokio-stream = "0.1.18" +rust-embed = "8.11.0" +futures = "0.3.31" +tower-http = { version = "0.6.8", features = ["fs", "cors"] } +mime_guess = "2.0.5" + +[profile.dev] +opt-level = 0 +debug = true + +[profile.release] +opt-level = 3 # Maximum optimization +lto = "fat" # Full link-time optimization across all crates +codegen-units = 1 # Single codegen unit for best optimization (slower compile) +panic = "abort" # Smaller binary, no unwinding overhead +strip = true # Strip debug symbols from binary +overflow-checks = false diff --git a/p2p-chat.log b/p2p-chat.log new file mode 100644 index 0000000..b402d4f --- /dev/null +++ b/p2p-chat.log @@ -0,0 +1,324 @@ +2026-02-11T12:16:43.446349Z INFO p2p_chat::web: Web interface listening on http://127.0.0.1:6969 +2026-02-11T12:17:01.401544Z ERROR p2p_chat::media::voice: Voice sender web error: Failed to connect for media stream +2026-02-11T16:36:56.002043Z INFO p2p_chat::web: Web interface listening on http://127.0.0.1:6969 +2026-02-11T16:37:08.175934Z INFO p2p_chat::web: Web interface listening on http://127.0.0.1:6970 +2026-02-11T16:37:21.007537Z WARN iroh_quinn_proto::connection: remote server configuration might cause nat traversal issues max_local_addresses=12 remote_cid_limit=5 +2026-02-11T16:37:21.235476Z WARN gossip{me=7674721a58}: iroh_quinn_proto::connection: remote server configuration might cause nat traversal issues max_local_addresses=12 remote_cid_limit=5 +2026-02-11T16:37:26.129541Z ERROR p2p_chat::media::voice: Voice sender web error: Failed to connect for media stream +2026-02-11T16:37:26.132439Z WARN iroh_quinn_proto::connection: remote server configuration might cause nat traversal issues max_local_addresses=12 remote_cid_limit=5 +2026-02-11T16:37:26.134817Z INFO p2p_chat::media: Accepted Audio stream from PublicKey(7674721a58e203eac4b4b44ce74c941e457c1686f0c3822347847a14e1951b57) +2026-02-11T16:37:26.134910Z INFO p2p_chat::media::voice: Incoming voice stream started (web) +2026-02-11T16:38:39.256197Z DEBUG p2p_chat::web: Received screen frame: 222545 bytes +2026-02-11T16:38:39.384580Z DEBUG p2p_chat::web: Received screen frame: 222486 bytes +2026-02-11T16:38:39.509008Z DEBUG p2p_chat::web: Received screen frame: 222475 bytes +2026-02-11T16:38:39.634092Z DEBUG p2p_chat::web: Received screen frame: 222448 bytes +2026-02-11T16:38:39.760878Z DEBUG p2p_chat::web: Received screen frame: 222521 bytes +2026-02-11T16:38:39.890677Z DEBUG p2p_chat::web: Received screen frame: 226127 bytes +2026-02-11T16:38:40.024723Z DEBUG p2p_chat::web: Received screen frame: 227887 bytes +2026-02-11T16:38:40.156064Z DEBUG p2p_chat::web: Received screen frame: 223115 bytes +2026-02-11T16:38:40.283115Z DEBUG p2p_chat::web: Received screen frame: 223053 bytes +2026-02-11T16:38:40.408418Z DEBUG p2p_chat::web: Received screen frame: 223251 bytes +2026-02-11T16:38:40.517284Z DEBUG p2p_chat::web: Received screen frame: 223140 bytes +2026-02-11T16:38:40.625172Z DEBUG p2p_chat::web: Received screen frame: 223228 bytes +2026-02-11T16:38:40.731547Z DEBUG p2p_chat::web: Received screen frame: 223358 bytes +2026-02-11T16:38:40.835317Z DEBUG p2p_chat::web: Received screen frame: 223179 bytes +2026-02-11T16:38:40.943378Z DEBUG p2p_chat::web: Received screen frame: 223265 bytes +2026-02-11T16:38:41.049721Z DEBUG p2p_chat::web: Received screen frame: 223353 bytes +2026-02-11T16:38:41.156372Z DEBUG p2p_chat::web: Received screen frame: 223370 bytes +2026-02-11T16:38:41.262222Z DEBUG p2p_chat::web: Received screen frame: 223301 bytes +2026-02-11T16:38:41.367052Z DEBUG p2p_chat::web: Received screen frame: 223244 bytes +2026-02-11T16:38:41.474297Z DEBUG p2p_chat::web: Received screen frame: 223282 bytes +2026-02-11T16:38:41.581267Z DEBUG p2p_chat::web: Received screen frame: 222884 bytes +2026-02-11T16:38:41.689764Z DEBUG p2p_chat::web: Received screen frame: 222714 bytes +2026-02-11T16:38:41.795363Z DEBUG p2p_chat::web: Received screen frame: 222364 bytes +2026-02-11T16:38:41.902893Z DEBUG p2p_chat::web: Received screen frame: 223055 bytes +2026-02-11T16:38:42.011299Z DEBUG p2p_chat::web: Received screen frame: 223174 bytes +2026-02-11T16:38:42.116650Z DEBUG p2p_chat::web: Received screen frame: 223249 bytes +2026-02-11T16:38:42.221509Z DEBUG p2p_chat::web: Received screen frame: 223302 bytes +2026-02-11T16:38:42.325697Z DEBUG p2p_chat::web: Received screen frame: 223158 bytes +2026-02-11T16:38:42.430021Z DEBUG p2p_chat::web: Received screen frame: 223260 bytes +2026-02-11T16:38:42.535410Z DEBUG p2p_chat::web: Received screen frame: 223076 bytes +2026-02-11T16:38:42.644657Z DEBUG p2p_chat::web: Received screen frame: 223279 bytes +2026-02-11T16:38:42.752129Z DEBUG p2p_chat::web: Received screen frame: 224782 bytes +2026-02-11T16:38:42.856602Z DEBUG p2p_chat::web: Received screen frame: 224218 bytes +2026-02-11T16:38:42.961267Z DEBUG p2p_chat::web: Received screen frame: 224178 bytes +2026-02-11T16:38:43.067158Z DEBUG p2p_chat::web: Received screen frame: 224327 bytes +2026-02-11T16:38:43.171276Z DEBUG p2p_chat::web: Received screen frame: 224313 bytes +2026-02-11T16:38:43.275128Z DEBUG p2p_chat::web: Received screen frame: 224345 bytes +2026-02-11T16:38:43.381958Z DEBUG p2p_chat::web: Received screen frame: 224276 bytes +2026-02-11T16:38:43.485921Z DEBUG p2p_chat::web: Received screen frame: 224357 bytes +2026-02-11T16:38:43.596004Z DEBUG p2p_chat::web: Received screen frame: 224329 bytes +2026-02-11T16:38:43.704652Z DEBUG p2p_chat::web: Received screen frame: 224368 bytes +2026-02-11T16:38:43.813300Z DEBUG p2p_chat::web: Received screen frame: 224194 bytes +2026-02-11T16:38:43.921742Z DEBUG p2p_chat::web: Received screen frame: 224228 bytes +2026-02-11T16:38:44.030558Z DEBUG p2p_chat::web: Received screen frame: 224261 bytes +2026-02-11T16:38:44.135797Z DEBUG p2p_chat::web: Received screen frame: 224345 bytes +2026-02-11T16:38:44.245682Z DEBUG p2p_chat::web: Received screen frame: 225272 bytes +2026-02-11T16:38:44.354876Z DEBUG p2p_chat::web: Received screen frame: 223359 bytes +2026-02-11T16:38:44.463893Z DEBUG p2p_chat::web: Received screen frame: 223360 bytes +2026-02-11T16:38:44.574700Z DEBUG p2p_chat::web: Received screen frame: 223058 bytes +2026-02-11T16:38:44.684199Z DEBUG p2p_chat::web: Received screen frame: 224573 bytes +2026-02-11T16:38:44.791637Z DEBUG p2p_chat::web: Received screen frame: 225211 bytes +2026-02-11T16:38:44.899143Z DEBUG p2p_chat::web: Received screen frame: 223344 bytes +2026-02-11T16:38:45.005228Z DEBUG p2p_chat::web: Received screen frame: 225761 bytes +2026-02-11T16:38:45.110952Z DEBUG p2p_chat::web: Received screen frame: 224395 bytes +2026-02-11T16:38:45.217118Z DEBUG p2p_chat::web: Received screen frame: 225173 bytes +2026-02-11T16:38:45.322489Z DEBUG p2p_chat::web: Received screen frame: 223001 bytes +2026-02-11T16:38:45.428021Z DEBUG p2p_chat::web: Received screen frame: 223232 bytes +2026-02-11T16:38:45.533755Z DEBUG p2p_chat::web: Received screen frame: 223341 bytes +2026-02-11T16:38:45.637161Z DEBUG p2p_chat::web: Received screen frame: 223286 bytes +2026-02-11T16:38:45.741755Z DEBUG p2p_chat::web: Received screen frame: 223342 bytes +2026-02-11T16:38:45.846054Z DEBUG p2p_chat::web: Received screen frame: 223327 bytes +2026-02-11T16:38:45.954329Z DEBUG p2p_chat::web: Received screen frame: 223295 bytes +2026-02-11T16:38:46.061481Z DEBUG p2p_chat::web: Received screen frame: 223320 bytes +2026-02-11T16:38:46.167641Z DEBUG p2p_chat::web: Received screen frame: 223257 bytes +2026-02-11T16:38:46.275054Z DEBUG p2p_chat::web: Received screen frame: 227212 bytes +2026-02-11T16:38:46.379678Z DEBUG p2p_chat::web: Received screen frame: 223506 bytes +2026-02-11T16:38:46.484220Z DEBUG p2p_chat::web: Received screen frame: 223686 bytes +2026-02-11T16:38:46.590779Z DEBUG p2p_chat::web: Received screen frame: 223376 bytes +2026-02-11T16:38:46.698925Z DEBUG p2p_chat::web: Received screen frame: 223371 bytes +2026-02-11T16:38:46.805201Z DEBUG p2p_chat::web: Received screen frame: 223196 bytes +2026-02-11T16:38:46.912272Z DEBUG p2p_chat::web: Received screen frame: 223109 bytes +2026-02-11T16:38:47.018848Z DEBUG p2p_chat::web: Received screen frame: 223121 bytes +2026-02-11T16:38:47.124929Z DEBUG p2p_chat::web: Received screen frame: 223138 bytes +2026-02-11T16:38:47.232210Z DEBUG p2p_chat::web: Received screen frame: 223095 bytes +2026-02-11T16:38:47.338134Z DEBUG p2p_chat::web: Received screen frame: 223483 bytes +2026-02-11T16:38:47.446152Z DEBUG p2p_chat::web: Received screen frame: 223366 bytes +2026-02-11T16:38:47.551007Z DEBUG p2p_chat::web: Received screen frame: 223578 bytes +2026-02-11T16:38:47.656538Z DEBUG p2p_chat::web: Received screen frame: 223513 bytes +2026-02-11T16:38:47.766616Z DEBUG p2p_chat::web: Received screen frame: 223500 bytes +2026-02-11T16:38:47.873598Z DEBUG p2p_chat::web: Received screen frame: 223570 bytes +2026-02-11T16:38:47.982245Z DEBUG p2p_chat::web: Received screen frame: 223564 bytes +2026-02-11T16:38:48.090719Z DEBUG p2p_chat::web: Received screen frame: 223610 bytes +2026-02-11T16:38:48.200733Z DEBUG p2p_chat::web: Received screen frame: 222575 bytes +2026-02-11T16:38:48.308074Z DEBUG p2p_chat::web: Received screen frame: 222637 bytes +2026-02-11T16:38:48.420652Z DEBUG p2p_chat::web: Received screen frame: 222616 bytes +2026-02-11T16:38:48.532925Z DEBUG p2p_chat::web: Received screen frame: 227065 bytes +2026-02-11T16:38:48.645722Z DEBUG p2p_chat::web: Received screen frame: 227947 bytes +2026-02-11T16:38:48.757247Z DEBUG p2p_chat::web: Received screen frame: 228164 bytes +2026-02-11T16:38:48.868776Z DEBUG p2p_chat::web: Received screen frame: 228168 bytes +2026-02-11T16:38:48.979834Z DEBUG p2p_chat::web: Received screen frame: 228115 bytes +2026-02-11T16:38:49.091226Z DEBUG p2p_chat::web: Received screen frame: 228213 bytes +2026-02-11T16:38:49.202653Z DEBUG p2p_chat::web: Received screen frame: 228192 bytes +2026-02-11T16:38:49.311287Z DEBUG p2p_chat::web: Received screen frame: 228162 bytes +2026-02-11T16:38:49.421010Z DEBUG p2p_chat::web: Received screen frame: 228112 bytes +2026-02-11T16:38:49.529169Z DEBUG p2p_chat::web: Received screen frame: 228200 bytes +2026-02-11T16:38:49.638545Z DEBUG p2p_chat::web: Received screen frame: 228172 bytes +2026-02-11T16:38:49.748120Z DEBUG p2p_chat::web: Received screen frame: 228248 bytes +2026-02-11T16:38:49.858933Z DEBUG p2p_chat::web: Received screen frame: 228258 bytes +2026-02-11T16:38:49.967826Z DEBUG p2p_chat::web: Received screen frame: 228445 bytes +2026-02-11T16:38:50.078356Z DEBUG p2p_chat::web: Received screen frame: 228484 bytes +2026-02-11T16:38:50.190910Z DEBUG p2p_chat::web: Received screen frame: 228473 bytes +2026-02-11T16:38:50.298529Z DEBUG p2p_chat::web: Received screen frame: 228502 bytes +2026-02-11T16:38:50.408287Z DEBUG p2p_chat::web: Received screen frame: 228482 bytes +2026-02-11T16:38:50.516729Z DEBUG p2p_chat::web: Received screen frame: 228508 bytes +2026-02-11T16:38:50.624651Z DEBUG p2p_chat::web: Received screen frame: 228371 bytes +2026-02-11T16:38:50.732204Z DEBUG p2p_chat::web: Received screen frame: 228398 bytes +2026-02-11T16:38:50.839703Z DEBUG p2p_chat::web: Received screen frame: 228451 bytes +2026-02-11T16:38:50.950365Z DEBUG p2p_chat::web: Received screen frame: 228052 bytes +2026-02-11T16:38:51.060473Z DEBUG p2p_chat::web: Received screen frame: 233933 bytes +2026-02-11T16:38:51.171401Z DEBUG p2p_chat::web: Received screen frame: 237132 bytes +2026-02-11T16:38:51.280639Z DEBUG p2p_chat::web: Received screen frame: 237284 bytes +2026-02-11T16:38:51.392789Z DEBUG p2p_chat::web: Received screen frame: 230703 bytes +2026-02-11T16:38:51.501068Z DEBUG p2p_chat::web: Received screen frame: 228509 bytes +2026-02-11T16:38:51.607607Z DEBUG p2p_chat::web: Received screen frame: 228483 bytes +2026-02-11T16:38:51.712440Z DEBUG p2p_chat::web: Received screen frame: 228551 bytes +2026-02-11T16:38:51.824267Z DEBUG p2p_chat::web: Received screen frame: 227428 bytes +2026-02-11T16:38:51.934584Z DEBUG p2p_chat::web: Received screen frame: 223474 bytes +2026-02-11T16:38:52.038889Z DEBUG p2p_chat::web: Received screen frame: 223377 bytes +2026-02-11T16:38:52.147456Z DEBUG p2p_chat::web: Received screen frame: 221754 bytes +2026-02-11T16:38:52.256093Z DEBUG p2p_chat::web: Received screen frame: 224925 bytes +2026-02-11T16:38:52.371056Z DEBUG p2p_chat::web: Received screen frame: 224316 bytes +2026-02-11T16:38:52.478515Z DEBUG p2p_chat::web: Received screen frame: 224235 bytes +2026-02-11T16:38:52.587715Z DEBUG p2p_chat::web: Received screen frame: 224444 bytes +2026-02-11T16:38:52.694790Z DEBUG p2p_chat::web: Received screen frame: 224315 bytes +2026-02-11T16:38:52.803360Z DEBUG p2p_chat::web: Received screen frame: 224471 bytes +2026-02-11T16:38:52.913055Z DEBUG p2p_chat::web: Received screen frame: 223702 bytes +2026-02-11T16:38:53.020693Z DEBUG p2p_chat::web: Received screen frame: 223369 bytes +2026-02-11T16:38:53.128885Z DEBUG p2p_chat::web: Received screen frame: 223241 bytes +2026-02-11T16:38:53.233586Z DEBUG p2p_chat::web: Received screen frame: 223139 bytes +2026-02-11T16:38:53.339499Z DEBUG p2p_chat::web: Received screen frame: 225770 bytes +2026-02-11T16:38:53.443997Z DEBUG p2p_chat::web: Received screen frame: 224180 bytes +2026-02-11T16:38:53.548434Z DEBUG p2p_chat::web: Received screen frame: 224226 bytes +2026-02-11T16:38:53.652763Z DEBUG p2p_chat::web: Received screen frame: 224160 bytes +2026-02-11T16:38:53.756895Z DEBUG p2p_chat::web: Received screen frame: 224134 bytes +2026-02-11T16:38:53.860821Z DEBUG p2p_chat::web: Received screen frame: 224146 bytes +2026-02-11T16:38:53.966535Z DEBUG p2p_chat::web: Received screen frame: 224138 bytes +2026-02-11T16:38:54.071237Z DEBUG p2p_chat::web: Received screen frame: 224233 bytes +2026-02-11T16:38:54.179349Z DEBUG p2p_chat::web: Received screen frame: 224316 bytes +2026-02-11T16:38:54.283245Z DEBUG p2p_chat::web: Received screen frame: 224213 bytes +2026-02-11T16:38:54.390859Z DEBUG p2p_chat::web: Received screen frame: 224262 bytes +2026-02-11T16:38:54.499830Z DEBUG p2p_chat::web: Received screen frame: 224254 bytes +2026-02-11T16:38:54.602734Z DEBUG p2p_chat::web: Received screen frame: 224190 bytes +2026-02-11T16:38:54.709369Z DEBUG p2p_chat::web: Received screen frame: 224727 bytes +2026-02-11T16:38:54.812778Z DEBUG p2p_chat::web: Received screen frame: 223280 bytes +2026-02-11T16:38:54.917755Z DEBUG p2p_chat::web: Received screen frame: 223293 bytes +2026-02-11T16:38:55.022347Z DEBUG p2p_chat::web: Received screen frame: 223164 bytes +2026-02-11T16:38:55.127888Z DEBUG p2p_chat::web: Received screen frame: 223305 bytes +2026-02-11T16:38:55.232778Z DEBUG p2p_chat::web: Received screen frame: 223270 bytes +2026-02-11T16:38:55.336328Z DEBUG p2p_chat::web: Received screen frame: 223192 bytes +2026-02-11T16:38:55.440921Z DEBUG p2p_chat::web: Received screen frame: 223254 bytes +2026-02-11T16:38:55.544606Z DEBUG p2p_chat::web: Received screen frame: 223275 bytes +2026-02-11T16:38:55.648711Z DEBUG p2p_chat::web: Received screen frame: 223226 bytes +2026-02-11T16:38:55.754063Z DEBUG p2p_chat::web: Received screen frame: 223143 bytes +2026-02-11T16:38:55.862396Z DEBUG p2p_chat::web: Received screen frame: 223255 bytes +2026-02-11T16:38:55.966488Z DEBUG p2p_chat::web: Received screen frame: 227921 bytes +2026-02-11T16:38:56.075393Z DEBUG p2p_chat::web: Received screen frame: 228623 bytes +2026-02-11T16:38:56.179639Z DEBUG p2p_chat::web: Received screen frame: 228803 bytes +2026-02-11T16:38:56.283775Z DEBUG p2p_chat::web: Received screen frame: 228904 bytes +2026-02-11T16:38:56.390177Z DEBUG p2p_chat::web: Received screen frame: 228838 bytes +2026-02-11T16:38:56.497795Z DEBUG p2p_chat::web: Received screen frame: 228918 bytes +2026-02-11T16:38:56.605318Z DEBUG p2p_chat::web: Received screen frame: 223439 bytes +2026-02-11T16:38:56.709567Z DEBUG p2p_chat::web: Received screen frame: 223461 bytes +2026-02-11T16:38:56.817819Z DEBUG p2p_chat::web: Received screen frame: 223306 bytes +2026-02-11T16:38:56.928778Z DEBUG p2p_chat::web: Received screen frame: 223303 bytes +2026-02-11T16:38:57.047827Z DEBUG p2p_chat::web: Received screen frame: 222813 bytes +2026-02-11T16:38:57.152119Z DEBUG p2p_chat::web: Received screen frame: 220986 bytes +2026-02-11T16:38:57.257610Z DEBUG p2p_chat::web: Received screen frame: 220399 bytes +2026-02-11T16:38:57.370956Z DEBUG p2p_chat::web: Received screen frame: 220273 bytes +2026-02-11T16:38:57.478844Z DEBUG p2p_chat::web: Received screen frame: 220062 bytes +2026-02-11T16:38:57.584093Z DEBUG p2p_chat::web: Received screen frame: 220201 bytes +2026-02-11T16:38:57.689024Z DEBUG p2p_chat::web: Received screen frame: 220054 bytes +2026-02-11T16:38:57.795894Z DEBUG p2p_chat::web: Received screen frame: 220189 bytes +2026-02-11T16:38:57.901412Z DEBUG p2p_chat::web: Received screen frame: 220246 bytes +2026-02-11T16:38:58.011714Z DEBUG p2p_chat::web: Received screen frame: 220235 bytes +2026-02-11T16:38:58.120831Z DEBUG p2p_chat::web: Received screen frame: 220235 bytes +2026-02-11T16:38:58.228052Z DEBUG p2p_chat::web: Received screen frame: 220235 bytes +2026-02-11T16:38:58.333874Z DEBUG p2p_chat::web: Received screen frame: 220334 bytes +2026-02-11T16:38:58.439102Z DEBUG p2p_chat::web: Received screen frame: 220329 bytes +2026-02-11T16:38:58.543380Z DEBUG p2p_chat::web: Received screen frame: 220254 bytes +2026-02-11T16:38:58.652113Z DEBUG p2p_chat::web: Received screen frame: 220271 bytes +2026-02-11T16:38:58.760926Z DEBUG p2p_chat::web: Received screen frame: 220271 bytes +2026-02-11T16:38:58.870651Z DEBUG p2p_chat::web: Received screen frame: 220271 bytes +2026-02-11T16:38:58.976952Z DEBUG p2p_chat::web: Received screen frame: 220271 bytes +2026-02-11T16:38:59.086685Z DEBUG p2p_chat::web: Received screen frame: 221787 bytes +2026-02-11T16:38:59.190153Z DEBUG p2p_chat::web: Received screen frame: 226379 bytes +2026-02-11T16:38:59.295261Z DEBUG p2p_chat::web: Received screen frame: 228569 bytes +2026-02-11T16:38:59.404905Z DEBUG p2p_chat::web: Received screen frame: 228835 bytes +2026-02-11T16:38:59.511518Z DEBUG p2p_chat::web: Received screen frame: 229108 bytes +2026-02-11T16:38:59.617118Z DEBUG p2p_chat::web: Received screen frame: 226242 bytes +2026-02-11T16:38:59.724048Z DEBUG p2p_chat::web: Received screen frame: 223787 bytes +2026-02-11T16:38:59.831684Z DEBUG p2p_chat::web: Received screen frame: 223500 bytes +2026-02-11T16:38:59.939542Z DEBUG p2p_chat::web: Received screen frame: 223421 bytes +2026-02-11T16:39:00.047819Z DEBUG p2p_chat::web: Received screen frame: 223416 bytes +2026-02-11T16:39:00.156037Z DEBUG p2p_chat::web: Received screen frame: 223364 bytes +2026-02-11T16:39:00.270226Z DEBUG p2p_chat::web: Received screen frame: 222894 bytes +2026-02-11T16:39:00.374427Z DEBUG p2p_chat::web: Received screen frame: 222680 bytes +2026-02-11T16:39:00.479065Z DEBUG p2p_chat::web: Received screen frame: 222787 bytes +2026-02-11T16:39:00.585488Z DEBUG p2p_chat::web: Received screen frame: 222662 bytes +2026-02-11T16:39:00.690629Z DEBUG p2p_chat::web: Received screen frame: 222649 bytes +2026-02-11T16:39:00.796456Z DEBUG p2p_chat::web: Received screen frame: 222761 bytes +2026-02-11T16:39:00.906250Z DEBUG p2p_chat::web: Received screen frame: 222722 bytes +2026-02-11T16:39:01.010822Z DEBUG p2p_chat::web: Received screen frame: 222634 bytes +2026-02-11T16:39:01.116146Z DEBUG p2p_chat::web: Received screen frame: 222649 bytes +2026-02-11T16:39:01.222085Z DEBUG p2p_chat::web: Received screen frame: 222517 bytes +2026-02-11T16:39:05.397912Z ERROR p2p_chat::media::capture: Video sender web error: Failed to connect for media stream +2026-02-11T16:39:05.400399Z WARN iroh_quinn_proto::connection: remote server configuration might cause nat traversal issues max_local_addresses=12 remote_cid_limit=5 +2026-02-11T16:39:05.402559Z INFO p2p_chat::media: Accepted Video stream from PublicKey(7674721a58e203eac4b4b44ce74c941e457c1686f0c3822347847a14e1951b57) +2026-02-11T16:39:05.402602Z INFO p2p_chat::media::capture: Starting Screen playback via Web Broadcast +2026-02-11T16:39:10.031877Z DEBUG p2p_chat::web: Received screen frame: 224143 bytes +2026-02-11T16:39:10.163878Z DEBUG p2p_chat::web: Received screen frame: 238165 bytes +2026-02-11T16:39:10.290992Z DEBUG p2p_chat::web: Received screen frame: 239833 bytes +2026-02-11T16:39:10.417287Z DEBUG p2p_chat::web: Received screen frame: 239899 bytes +2026-02-11T16:39:10.550698Z DEBUG p2p_chat::web: Received screen frame: 240068 bytes +2026-02-11T16:39:10.681492Z DEBUG p2p_chat::web: Received screen frame: 240501 bytes +2026-02-11T16:39:10.811916Z DEBUG p2p_chat::web: Received screen frame: 240964 bytes +2026-02-11T16:39:10.943473Z DEBUG p2p_chat::web: Received screen frame: 240948 bytes +2026-02-11T16:39:11.072369Z DEBUG p2p_chat::web: Received screen frame: 240886 bytes +2026-02-11T16:39:11.202679Z DEBUG p2p_chat::web: Received screen frame: 241000 bytes +2026-02-11T16:39:11.314576Z DEBUG p2p_chat::web: Received screen frame: 240971 bytes +2026-02-11T16:39:11.422556Z DEBUG p2p_chat::web: Received screen frame: 240887 bytes +2026-02-11T16:39:11.529221Z DEBUG p2p_chat::web: Received screen frame: 241007 bytes +2026-02-11T16:39:11.637948Z DEBUG p2p_chat::web: Received screen frame: 240946 bytes +2026-02-11T16:39:11.747607Z DEBUG p2p_chat::web: Received screen frame: 240991 bytes +2026-02-11T16:39:11.855580Z DEBUG p2p_chat::web: Received screen frame: 240964 bytes +2026-02-11T16:39:11.964208Z DEBUG p2p_chat::web: Received screen frame: 240885 bytes +2026-02-11T16:39:12.074572Z DEBUG p2p_chat::web: Received screen frame: 240821 bytes +2026-02-11T16:39:12.183808Z DEBUG p2p_chat::web: Received screen frame: 205533 bytes +2026-02-11T16:39:12.289266Z DEBUG p2p_chat::web: Received screen frame: 183402 bytes +2026-02-11T16:39:12.398940Z DEBUG p2p_chat::web: Received screen frame: 173981 bytes +2026-02-11T16:39:12.510199Z DEBUG p2p_chat::web: Received screen frame: 188848 bytes +2026-02-11T16:39:12.617482Z DEBUG p2p_chat::web: Received screen frame: 193525 bytes +2026-02-11T16:39:12.727529Z DEBUG p2p_chat::web: Received screen frame: 194740 bytes +2026-02-11T16:39:12.837601Z DEBUG p2p_chat::web: Received screen frame: 195227 bytes +2026-02-11T16:39:12.947246Z DEBUG p2p_chat::web: Received screen frame: 195045 bytes +2026-02-11T16:39:13.057826Z DEBUG p2p_chat::web: Received screen frame: 197200 bytes +2026-02-11T16:39:13.167514Z DEBUG p2p_chat::web: Received screen frame: 197880 bytes +2026-02-11T16:39:13.276403Z DEBUG p2p_chat::web: Received screen frame: 196031 bytes +2026-02-11T16:39:13.391202Z DEBUG p2p_chat::web: Received screen frame: 195397 bytes +2026-02-11T16:39:13.501947Z DEBUG p2p_chat::web: Received screen frame: 195887 bytes +2026-02-11T16:39:13.612095Z DEBUG p2p_chat::web: Received screen frame: 195327 bytes +2026-02-11T16:39:13.723780Z DEBUG p2p_chat::web: Received screen frame: 194351 bytes +2026-02-11T16:39:13.833604Z DEBUG p2p_chat::web: Received screen frame: 194112 bytes +2026-02-11T16:39:13.945154Z DEBUG p2p_chat::web: Received screen frame: 194037 bytes +2026-02-11T16:39:14.055624Z DEBUG p2p_chat::web: Received screen frame: 194300 bytes +2026-02-11T16:39:14.167014Z DEBUG p2p_chat::web: Received screen frame: 194850 bytes +2026-02-11T16:39:14.276563Z DEBUG p2p_chat::web: Received screen frame: 195111 bytes +2026-02-11T16:39:14.387088Z DEBUG p2p_chat::web: Received screen frame: 195110 bytes +2026-02-11T16:39:14.497698Z DEBUG p2p_chat::web: Received screen frame: 195130 bytes +2026-02-11T16:39:14.607940Z DEBUG p2p_chat::web: Received screen frame: 195102 bytes +2026-02-11T16:39:14.719830Z DEBUG p2p_chat::web: Received screen frame: 194979 bytes +2026-02-11T16:39:14.824272Z DEBUG p2p_chat::web: Received screen frame: 196605 bytes +2026-02-11T16:39:14.928138Z DEBUG p2p_chat::web: Received screen frame: 196693 bytes +2026-02-11T16:39:15.031800Z DEBUG p2p_chat::web: Received screen frame: 196729 bytes +2026-02-11T16:39:15.136021Z DEBUG p2p_chat::web: Received screen frame: 196773 bytes +2026-02-11T16:39:15.240535Z DEBUG p2p_chat::web: Received screen frame: 196762 bytes +2026-02-11T16:39:15.343535Z DEBUG p2p_chat::web: Received screen frame: 196796 bytes +2026-02-11T16:39:15.447840Z DEBUG p2p_chat::web: Received screen frame: 196740 bytes +2026-02-11T16:39:15.556055Z DEBUG p2p_chat::web: Received screen frame: 196648 bytes +2026-02-11T16:39:15.658166Z DEBUG p2p_chat::web: Received screen frame: 196665 bytes +2026-02-11T16:39:15.762218Z DEBUG p2p_chat::web: Received screen frame: 196719 bytes +2026-02-11T16:39:15.865760Z DEBUG p2p_chat::web: Received screen frame: 196807 bytes +2026-02-11T16:39:15.970619Z DEBUG p2p_chat::web: Received screen frame: 196632 bytes +2026-02-11T16:39:16.078141Z DEBUG p2p_chat::web: Received screen frame: 196625 bytes +2026-02-11T16:39:16.182434Z DEBUG p2p_chat::web: Received screen frame: 196632 bytes +2026-02-11T16:39:16.286185Z DEBUG p2p_chat::web: Received screen frame: 196756 bytes +2026-02-11T16:39:16.390383Z DEBUG p2p_chat::web: Received screen frame: 196670 bytes +2026-02-11T16:39:16.493955Z DEBUG p2p_chat::web: Received screen frame: 196580 bytes +2026-02-11T16:39:16.597849Z DEBUG p2p_chat::web: Received screen frame: 196640 bytes +2026-02-11T16:39:16.701687Z DEBUG p2p_chat::web: Received screen frame: 196640 bytes +2026-02-11T16:39:16.805453Z DEBUG p2p_chat::web: Received screen frame: 196816 bytes +2026-02-11T16:39:16.909458Z DEBUG p2p_chat::web: Received screen frame: 196678 bytes +2026-02-11T16:39:17.013447Z DEBUG p2p_chat::web: Received screen frame: 196690 bytes +2026-02-11T16:39:17.117657Z DEBUG p2p_chat::web: Received screen frame: 196706 bytes +2026-02-11T16:39:17.221270Z DEBUG p2p_chat::web: Received screen frame: 196670 bytes +2026-02-11T16:39:17.326489Z DEBUG p2p_chat::web: Received screen frame: 196790 bytes +2026-02-11T16:39:17.430832Z DEBUG p2p_chat::web: Received screen frame: 196646 bytes +2026-02-11T16:39:17.533849Z DEBUG p2p_chat::web: Received screen frame: 196761 bytes +2026-02-11T16:39:17.637874Z DEBUG p2p_chat::web: Received screen frame: 196659 bytes +2026-02-11T16:39:17.741621Z DEBUG p2p_chat::web: Received screen frame: 196620 bytes +2026-02-11T16:39:17.846016Z DEBUG p2p_chat::web: Received screen frame: 196572 bytes +2026-02-11T16:39:17.950199Z DEBUG p2p_chat::web: Received screen frame: 196698 bytes +2026-02-11T16:39:18.030957Z WARN gossip{me=7674721a58}: iroh_quinn_proto::connection: sent PATH_ABANDON after path was already discarded +2026-02-11T16:39:18.053172Z DEBUG p2p_chat::web: Received screen frame: 196633 bytes +2026-02-11T16:39:18.158287Z DEBUG p2p_chat::web: Received screen frame: 196661 bytes +2026-02-11T16:39:18.262001Z DEBUG p2p_chat::web: Received screen frame: 196633 bytes +2026-02-11T16:39:18.365864Z DEBUG p2p_chat::web: Received screen frame: 196644 bytes +2026-02-11T16:39:18.470708Z DEBUG p2p_chat::web: Received screen frame: 196648 bytes +2026-02-11T16:39:18.574270Z DEBUG p2p_chat::web: Received screen frame: 196553 bytes +2026-02-11T16:39:18.678087Z DEBUG p2p_chat::web: Received screen frame: 196625 bytes +2026-02-11T16:39:18.782939Z DEBUG p2p_chat::web: Received screen frame: 196716 bytes +2026-02-11T16:39:18.886037Z DEBUG p2p_chat::web: Received screen frame: 196731 bytes +2026-02-11T16:39:18.990368Z DEBUG p2p_chat::web: Received screen frame: 196602 bytes +2026-02-11T16:39:19.095809Z DEBUG p2p_chat::web: Received screen frame: 196629 bytes +2026-02-11T16:39:19.200459Z DEBUG p2p_chat::web: Received screen frame: 195918 bytes +2026-02-11T16:39:19.305117Z DEBUG p2p_chat::web: Received screen frame: 194321 bytes +2026-02-11T16:39:19.409039Z DEBUG p2p_chat::web: Received screen frame: 195261 bytes +2026-02-11T16:39:19.512643Z DEBUG p2p_chat::web: Received screen frame: 195586 bytes +2026-02-11T16:39:19.616201Z DEBUG p2p_chat::web: Received screen frame: 195655 bytes +2026-02-11T16:39:19.720491Z DEBUG p2p_chat::web: Received screen frame: 195665 bytes +2026-02-11T16:39:19.824103Z DEBUG p2p_chat::web: Received screen frame: 195673 bytes +2026-02-11T16:39:19.928319Z DEBUG p2p_chat::web: Received screen frame: 195640 bytes +2026-02-11T16:39:20.032662Z DEBUG p2p_chat::web: Received screen frame: 195635 bytes +2026-02-11T16:39:20.136998Z DEBUG p2p_chat::web: Received screen frame: 195668 bytes +2026-02-11T16:39:20.241713Z DEBUG p2p_chat::web: Received screen frame: 195635 bytes +2026-02-11T16:39:20.346111Z DEBUG p2p_chat::web: Received screen frame: 195630 bytes +2026-02-11T16:39:20.450563Z DEBUG p2p_chat::web: Received screen frame: 195638 bytes +2026-02-11T16:39:20.557656Z DEBUG p2p_chat::web: Received screen frame: 195622 bytes +2026-02-11T16:39:34.425339Z ERROR p2p_chat::media::voice: Voice sender web error: Failed to connect for media stream +2026-02-11T16:39:34.428904Z WARN iroh_quinn_proto::connection: remote server configuration might cause nat traversal issues max_local_addresses=12 remote_cid_limit=5 +2026-02-11T16:39:34.431196Z INFO p2p_chat::media: Accepted Audio stream from PublicKey(69c0253ed357440bf107a5eab12d790b0fe04346dfd4cf38cc4dc5be8ea60d7f) +2026-02-11T16:39:34.431287Z INFO p2p_chat::media::voice: Incoming voice stream started (web) +2026-02-11T17:17:42.956205Z INFO p2p_chat::web: Web interface listening on http://127.0.0.1:6969 +2026-02-11T17:18:06.892422Z WARN p2p_chat::media::voice: Failed to open voice stream to 5b21d1120a04f6c9647ab3354ad75ce305f4f4dc3b5cc9cb93eeae70778cd12f: Failed to connect for media stream +2026-02-11T17:18:06.893138Z WARN iroh_quinn_proto::connection: remote server configuration might cause nat traversal issues max_local_addresses=12 remote_cid_limit=5 diff --git a/src/chat/mod.rs b/src/chat/mod.rs new file mode 100644 index 0000000..bd582af --- /dev/null +++ b/src/chat/mod.rs @@ -0,0 +1,88 @@ +//! Chat module — manages chat history and message sending/receiving. + +use crate::protocol::{ChatMessage, GossipMessage}; +use crate::net::NetworkManager; +use anyhow::Result; + +/// Stored chat entry with display metadata. +#[derive(Debug, Clone)] +pub struct ChatEntry { + pub sender_name: String, + pub timestamp: u64, + pub text: String, + pub is_self: bool, + pub is_system: bool, +} + +/// Chat state: in-memory history and configuration. +pub struct ChatState { + pub history: Vec, + pub our_name: String, + pub max_history: usize, +} + +impl ChatState { + pub fn new(our_name: String) -> Self { + Self { + history: Vec::new(), + our_name, + max_history: 10_000, + } + } + + /// Send a chat message via gossip broadcast. + pub async fn send_message(&mut self, text: String, net: &NetworkManager) -> Result<()> { + let timestamp = chrono::Utc::now().timestamp_millis() as u64; + let msg = ChatMessage { + sender_name: self.our_name.clone(), + timestamp, + text: text.clone(), + }; + + // Broadcast via gossip + net.broadcast(&GossipMessage::Chat(msg)).await?; + + // Add to local history + self.add_entry(ChatEntry { + sender_name: self.our_name.clone(), + timestamp, + text, + is_self: true, + is_system: false, + }); + + Ok(()) + } + + /// Handle a received chat message from gossip. + pub fn receive_message(&mut self, msg: ChatMessage) { + self.add_entry(ChatEntry { + sender_name: msg.sender_name, + timestamp: msg.timestamp, + text: msg.text, + is_self: false, + is_system: false, + }); + } + + /// Add a system message (peer join/leave, file events, etc.). + pub fn add_system_message(&mut self, text: String) { + let timestamp = chrono::Utc::now().timestamp_millis() as u64; + self.add_entry(ChatEntry { + sender_name: String::from("SYSTEM"), + timestamp, + text, + is_self: false, + is_system: true, + }); + } + + fn add_entry(&mut self, entry: ChatEntry) { + self.history.push(entry); + // Trim if exceeding max history + if self.history.len() > self.max_history { + let drain = self.history.len() - self.max_history; + self.history.drain(..drain); + } + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..1bef01c --- /dev/null +++ b/src/config.rs @@ -0,0 +1,194 @@ +use std::path::PathBuf; +use std::fs; +use anyhow::Result; +use directories::ProjectDirs; +use serde::{Deserialize, Serialize}; +use ratatui::style::Color; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AppConfig { + #[serde(default)] + pub network: NetworkConfig, + #[serde(default)] + pub media: MediaConfig, + #[serde(default)] + pub ui: UiConfig, +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + network: NetworkConfig::default(), + media: MediaConfig::default(), + ui: UiConfig::default(), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct NetworkConfig { + pub topic: Option, +} + +impl Default for NetworkConfig { + fn default() -> Self { + Self { topic: None } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MediaConfig { + pub screen_resolution: String, + pub mic_name: Option, + pub speaker_name: Option, + #[serde(default = "default_bitrate")] + pub mic_bitrate: u32, +} + +fn default_bitrate() -> u32 { + 128000 +} + +impl Default for MediaConfig { + fn default() -> Self { + Self { + screen_resolution: "1280x720".to_string(), + mic_name: None, + speaker_name: None, + mic_bitrate: 128000, + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UiConfig { + pub border: String, + pub text: String, + pub self_name: String, + pub peer_name: String, + pub system_msg: String, + pub time: String, +} + +impl Default for UiConfig { + fn default() -> Self { + Self { + border: "cyan".to_string(), + text: "white".to_string(), + self_name: "green".to_string(), + peer_name: "magenta".to_string(), + system_msg: "yellow".to_string(), + time: "dark_gray".to_string(), + } + } +} + +impl AppConfig { + pub fn load() -> Result { + let config_path = Self::get_config_path(); + + if !config_path.exists() { + // Create default config if it doesn't exist + let default_config = Self::default(); + if let Some(parent) = config_path.parent() { + fs::create_dir_all(parent)?; + } + let toml = toml::to_string_pretty(&default_config)?; + fs::write(&config_path, toml)?; + return Ok(default_config); + } + + let content = fs::read_to_string(&config_path)?; + let config: AppConfig = toml::from_str(&content)?; + Ok(config) + } + + fn get_config_path() -> PathBuf { + if let Some(proj_dirs) = ProjectDirs::from("com", "p2p-chat", "p2p-chat") { + proj_dirs.config_dir().join("config.toml") + } else { + PathBuf::from("config.toml") + } + } + + /// Save the current configuration to disk. + pub fn save(&self) -> Result<()> { + let config_path = Self::get_config_path(); + if let Some(parent) = config_path.parent() { + fs::create_dir_all(parent)?; + } + let toml = toml::to_string_pretty(self)?; + fs::write(&config_path, toml)?; + Ok(()) + } +} + +// Helper to parse color strings +pub fn parse_color(color_str: &str) -> Color { + if color_str.starts_with('#') { + let hex = color_str.trim_start_matches('#'); + if let Ok(val) = u32::from_str_radix(hex, 16) { + let (r, g, b) = if hex.len() == 3 { + // #RGB -> #RRGGBB + let r = ((val >> 8) & 0xF) * 17; + let g = ((val >> 4) & 0xF) * 17; + let b = (val & 0xF) * 17; + (r as u8, g as u8, b as u8) + } else { + // #RRGGBB + let r = (val >> 16) & 0xFF; + let g = (val >> 8) & 0xFF; + let b = val & 0xFF; + (r as u8, g as u8, b as u8) + }; + return Color::Rgb(r, g, b); + } + } + + match color_str.to_lowercase().as_str() { + "black" => Color::Black, + "red" => Color::Red, + "green" => Color::Green, + "yellow" => Color::Yellow, + "blue" => Color::Blue, + "magenta" => Color::Magenta, + "cyan" => Color::Cyan, + "gray" => Color::Gray, + "dark_gray" | "darkgray" => Color::DarkGray, + "light_red" | "lightred" => Color::LightRed, + "light_green" | "lightgreen" => Color::LightGreen, + "light_yellow" | "lightyellow" => Color::LightYellow, + "light_blue" | "lightblue" => Color::LightBlue, + "light_magenta" | "lightmagenta" => Color::LightMagenta, + "light_cyan" | "lightcyan" => Color::LightCyan, + "white" => Color::White, + _ => { + // Try hex parsing if needed, but for now fallback to White + Color::White + } + } +} + +// Runtime Theme struct derived from config +#[derive(Debug, Clone)] +pub struct Theme { + pub border: Color, + pub text: Color, + pub self_name: Color, + pub peer_name: Color, + pub system_msg: Color, + pub time: Color, +} + +impl From for Theme { + fn from(cfg: UiConfig) -> Self { + Self { + border: parse_color(&cfg.border), + text: parse_color(&cfg.text), + self_name: parse_color(&cfg.self_name), + peer_name: parse_color(&cfg.peer_name), + system_msg: parse_color(&cfg.system_msg), + time: parse_color(&cfg.time), + } + } +} diff --git a/src/file_transfer/mod.rs b/src/file_transfer/mod.rs new file mode 100644 index 0000000..c288909 --- /dev/null +++ b/src/file_transfer/mod.rs @@ -0,0 +1,351 @@ +//! File transfer module — chunked file transfers over dedicated QUIC streams. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result}; +use sha2::{Sha256, Digest}; +use tokio::fs::File; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use crate::protocol::{ + decode_framed, write_framed, FileChunk, FileDone, FileId, FileOffer, + FileStreamMessage, FileAcceptReject, new_file_id, +}; + +/// Chunk size for file transfers (64 KB). +const CHUNK_SIZE: usize = 64 * 1024; + +/// State of a file transfer. +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum TransferState { + /// We offered a file, waiting for accept/reject. + Offering, + /// Transfer is in progress. + Transferring { + bytes_transferred: u64, + total_size: u64, + }, + /// Transfer completed successfully. + Complete, + /// Transfer was rejected by the peer. + Rejected, + /// Transfer failed with an error. + Failed(String), +} + +/// Information about a tracked file transfer. +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct TransferInfo { + pub file_id: FileId, + pub file_name: String, + pub file_size: u64, + pub state: TransferState, + pub is_outgoing: bool, +} + +use std::sync::{Arc, Mutex}; + +/// Manages file transfers. +#[derive(Clone)] +pub struct FileTransferManager { + pub transfers: Arc>>, + #[allow(dead_code)] + pub download_dir: PathBuf, +} + +impl FileTransferManager { + pub fn new(download_dir: PathBuf) -> Self { + Self { + transfers: Arc::new(Mutex::new(HashMap::new())), + download_dir, + } + } + + /// Initiate sending a file to a peer. + /// Returns the file ID and the file offer for broadcasting. + pub async fn prepare_send( + &self, + file_path: &Path, + ) -> Result<(FileId, FileOffer)> { + let file_name = file_path + .file_name() + .context("No filename")? + .to_string_lossy() + .to_string(); + + let metadata = tokio::fs::metadata(file_path) + .await + .context("Failed to read file metadata")?; + let file_size = metadata.len(); + + // Compute SHA-256 checksum + let mut file = File::open(file_path).await?; + let mut hasher = Sha256::new(); + let mut buf = vec![0u8; CHUNK_SIZE]; + loop { + let n = file.read(&mut buf).await?; + if n == 0 { + break; + } + hasher.update(&buf[..n]); + } + let checksum: [u8; 32] = hasher.finalize().into(); + + let file_id = new_file_id(); + + let offer = FileOffer { + file_id, + name: file_name.clone(), + size: file_size, + checksum, + }; + + { + let mut transfers = self.transfers.lock().unwrap(); + transfers.insert( + file_id, + TransferInfo { + file_id, + file_name, + file_size, + state: TransferState::Offering, + is_outgoing: true, + }, + ); + } + + Ok((file_id, offer)) + } + + /// Execute the sending side of a file transfer over a QUIC bi-stream. + #[allow(dead_code)] + pub async fn execute_send( + &self, + file_id: FileId, + file_path: &Path, + offer: FileOffer, + send: &mut iroh::endpoint::SendStream, + recv: &mut iroh::endpoint::RecvStream, + ) -> Result<()> { + // Send the offer + write_framed(send, &FileStreamMessage::Offer(offer)).await?; + + // Wait for accept or reject + let response: FileStreamMessage = decode_framed(recv).await?; + match response { + FileStreamMessage::Accept(_) => { + // Proceed with transfer + } + FileStreamMessage::Reject(_) => { + let mut transfers = self.transfers.lock().unwrap(); + if let Some(info) = transfers.get_mut(&file_id) { + info.state = TransferState::Rejected; + } + return Ok(()); + } + _ => { + anyhow::bail!("Unexpected response to file offer"); + } + } + + // Stream file chunks + let mut file = File::open(file_path).await?; + let mut offset: u64 = 0; + let total_size = tokio::fs::metadata(file_path).await?.len(); + let mut buf = vec![0u8; CHUNK_SIZE]; + + loop { + let n = file.read(&mut buf).await?; + if n == 0 { + break; + } + + let chunk = FileStreamMessage::Chunk(FileChunk { + file_id, + offset, + data: buf[..n].to_vec(), + }); + write_framed(send, &chunk).await?; + offset += n as u64; + + // Update progress + // Scope limit the lock + { + let mut transfers = self.transfers.lock().unwrap(); + if let Some(info) = transfers.get_mut(&file_id) { + info.state = TransferState::Transferring { + bytes_transferred: offset, + total_size, + }; + } + } + } + + // Send done + write_framed(send, &FileStreamMessage::Done(FileDone { file_id })).await?; + + { + let mut transfers = self.transfers.lock().unwrap(); + if let Some(info) = transfers.get_mut(&file_id) { + info.state = TransferState::Complete; + } + } + + Ok(()) + } + + /// Handle an incoming file offer for display. + #[allow(dead_code)] + /// Handle an incoming file offer for display. + #[allow(dead_code)] + pub fn register_incoming_offer(&self, offer: &FileOffer) { + let mut transfers = self.transfers.lock().unwrap(); + transfers.insert( + offer.file_id, + TransferInfo { + file_id: offer.file_id, + file_name: offer.name.clone(), + file_size: offer.size, + state: TransferState::Offering, + is_outgoing: false, + }, + ); + } + + /// Execute the receiving side of a file transfer over a QUIC bi-stream. + #[allow(dead_code)] + pub async fn execute_receive( + &self, + send: &mut iroh::endpoint::SendStream, + recv: &mut iroh::endpoint::RecvStream, + ) -> Result { + // Read the offer + let msg: FileStreamMessage = decode_framed(recv).await?; + let offer = match msg { + FileStreamMessage::Offer(o) => o, + _ => anyhow::bail!("Expected file offer"), + }; + + let file_id = offer.file_id; + self.register_incoming_offer(&offer); + + // Auto-accept for now + write_framed( + send, + &FileStreamMessage::Accept(FileAcceptReject { file_id }), + ) + .await?; + + // Receive chunks + let dest_path = self.download_dir.join(&offer.name); + + // Ensure download dir exists for safety (though main normally does this) + if let Some(parent) = dest_path.parent() { + let _ = tokio::fs::create_dir_all(parent).await; + } + + let mut file = tokio::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&dest_path) + .await?; + + let mut received: u64 = 0; + + loop { + let chunk_msg: FileStreamMessage = decode_framed(recv).await?; + match chunk_msg { + FileStreamMessage::Chunk(chunk) => { + file.write_all(&chunk.data).await?; + received += chunk.data.len() as u64; + + { + let mut transfers = self.transfers.lock().unwrap(); + if let Some(info) = transfers.get_mut(&file_id) { + info.state = TransferState::Transferring { + bytes_transferred: received, + total_size: offer.size, + }; + } + } + } + FileStreamMessage::Done(_) => { + break; + } + _ => { + anyhow::bail!("Unexpected message during file transfer"); + } + } + } + + file.flush().await?; + + { + let mut transfers = self.transfers.lock().unwrap(); + if let Some(info) = transfers.get_mut(&file_id) { + info.state = TransferState::Complete; + } + } + + Ok(file_id) + } + + /// Get a summary of active/recent transfers for display. + pub fn active_transfers(&self) -> Vec { + let transfers = self.transfers.lock().unwrap(); + transfers.values().cloned().collect() + } + + /// Format transfer progress as a human-readable string. + pub fn format_progress(info: &TransferInfo) -> String { + let direction = if info.is_outgoing { "↑" } else { "↓" }; + match &info.state { + TransferState::Offering => { + format!("{} {} (waiting...)", direction, info.file_name) + } + TransferState::Transferring { + bytes_transferred, + total_size, + } => { + let pct = if *total_size > 0 { + (*bytes_transferred as f64 / *total_size as f64 * 100.0) as u8 + } else { + 0 + }; + format!( + "{} {} {}% ({}/{})", + direction, + info.file_name, + pct, + format_bytes(*bytes_transferred), + format_bytes(*total_size) + ) + } + TransferState::Complete => { + format!("{} {} ✓ complete", direction, info.file_name) + } + TransferState::Rejected => { + format!("{} {} ✗ rejected", direction, info.file_name) + } + TransferState::Failed(e) => { + format!("{} {} ✗ {}", direction, info.file_name, e) + } + } + } +} + +fn format_bytes(bytes: u64) -> String { + if bytes < 1024 { + format!("{}B", bytes) + } else if bytes < 1024 * 1024 { + format!("{:.1}KiB", bytes as f64 / 1024.0) + } else if bytes < 1024 * 1024 * 1024 { + format!("{:.1}MiB", bytes as f64 / (1024.0 * 1024.0)) + } else { + format!("{:.1}GiB", bytes as f64 / (1024.0 * 1024.0 * 1024.0)) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..78f23c2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,10 @@ +//! P2P Chat — library crate exposing modules for integration tests. + +pub mod chat; +pub mod config; +pub mod file_transfer; +pub mod media; +pub mod net; +pub mod protocol; +pub mod tui; +pub mod web; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..95baf71 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,608 @@ +//! P2P Chat Application +//! +//! A Linux-only, terminal-based peer-to-peer communication app. +//! Chat is the primary feature, file transfer is first-class, +//! voice/camera/screen are optional, powered by PipeWire. + +mod chat; +mod config; +mod file_transfer; +mod media; +mod net; +mod protocol; +mod tui; +mod web; + +use std::io; +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use clap::Parser; +use crossterm::event::{Event, EventStream}; +use crossterm::terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, +}; +use crossterm::execute; +use iroh::EndpointId; +use n0_future::StreamExt; +use ratatui::backend::CrosstermBackend; +use ratatui::Terminal; +use tokio::sync::mpsc; + +use crate::chat::ChatState; +use crate::config::AppConfig; +use crate::file_transfer::FileTransferManager; +use crate::media::MediaState; +use crate::net::{NetEvent, NetworkManager, PeerInfo}; +use crate::protocol::{CapabilitiesMessage, GossipMessage, PeerAnnounce}; +use crate::tui::{App, TuiCommand}; + +/// P2P Chat — decentralized chat over QUIC +#[derive(Parser, Debug)] +#[command(name = "p2p-chat", about = "Peer-to-peer chat over QUIC")] +struct Cli { + /// Your display name + #[arg(short, long, default_value = "anon")] + name: String, + + /// Peer endpoint ID to join (hex string) + #[arg(short, long)] + join: Option, + + /// Topic room ID (32-byte hex). Peers on the same topic can chat. + #[arg(short, long)] + topic: Option, // Changed to Option to fallback to config + + /// Download directory for received files + #[arg(short, long, default_value = "~/Downloads")] + download_dir: String, + /// Screen resolution for sharing (e.g., 1280x720, 1920x1080) + #[arg(long)] + screen_resolution: Option, // Changed to Option to fallback to config +} + +#[tokio::main] +async fn main() -> Result<()> { + // ... tracing init ... + // Initialize tracing to file (not stdout, since we use TUI) + let _tracing_guard = tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive("p2p_chat=debug".parse()?) + .add_directive("iroh=warn".parse()?) + .add_directive("iroh_gossip=warn".parse()?), + ) + .with_writer(|| -> Box { + match std::fs::OpenOptions::new() + .create(true) + .append(true) + .open("p2p-chat.log") + { + Ok(f) => Box::new(f), + Err(_) => Box::new(io::sink()), + } + }) + .with_ansi(false) + .init(); + + // Load config + let config = AppConfig::load().unwrap_or_else(|e| { + eprintln!("Warning: Failed to load config: {}", e); + AppConfig::default() + }); + + let cli = Cli::parse(); + + // Resolution: CLI > Config > Default + let res_str = cli.screen_resolution.as_deref().unwrap_or(&config.media.screen_resolution); + let screen_res = parse_resolution(res_str).unwrap_or((1280, 720)); + + // Topic: CLI > Config > Default + let topic_str = cli.topic.as_deref().or(config.network.topic.as_deref()).unwrap_or("00000000000000000000000000000000"); + let topic_bytes = parse_topic(topic_str)?; + + // ... networking init ... + // Initialize networking + let (mut net_mgr, _net_tx, mut net_rx) = + NetworkManager::new(topic_bytes).await.context("Failed to start networking")?; + + let our_id = net_mgr.our_id; + let our_id_short = format!("{}", our_id) + .chars() + .take(8) + .collect::(); + + // Initialize application state + let mut chat = ChatState::new(cli.name.clone()); + + // Resolve download directory, handling ~ + let download_path = if cli.download_dir.starts_with("~/") { + if let Ok(home) = std::env::var("HOME") { + PathBuf::from(home).join(&cli.download_dir[2..]) + } else { + PathBuf::from(&cli.download_dir) + } + } else { + PathBuf::from(&cli.download_dir) + }; + // Ensure download directory exists + tokio::fs::create_dir_all(&download_path).await?; + + let file_mgr = FileTransferManager::new(download_path); + // Pass mic name from config if present + let mut media = MediaState::new( + screen_res, + config.media.mic_name.clone(), + config.media.speaker_name.clone(), + config.media.mic_bitrate, + ); + + // Initialize App with Theme + let theme = crate::config::Theme::from(config.ui.clone()); + let mut app = App::new(theme); + let mut connected = false; + + + // If a peer was specified, add it and bootstrap + let bootstrap_peers = if let Some(ref join_id) = cli.join { + let peer_id: EndpointId = join_id.parse().context( + "Invalid peer ID. Expected a hex-encoded endpoint ID.", + )?; + vec![peer_id] + } else { + vec![] + }; + + // We need a sender for the gossip event loop — but we already have net_rx + // Recreate the channel and use it for gossip + let (gossip_event_tx, mut gossip_event_rx) = mpsc::channel::(256); + + // Join the gossip topic + net_mgr + .join_gossip(bootstrap_peers, gossip_event_tx) + .await + .context("Failed to join gossip")?; + + // Announce ourselves + let announce = GossipMessage::PeerAnnounce(PeerAnnounce { + sender_name: cli.name.clone(), + }); + // Don't fail if no peers yet + let _ = net_mgr.broadcast(&announce).await; + + // Insert ourselves into the peer list + { + let mut peers = net_mgr.peers.lock().await; + peers.insert(our_id, PeerInfo { + id: our_id, + name: Some(cli.name.clone()), + capabilities: None, + is_self: true, + }); + } + + // Broadcast capabilities + let caps = GossipMessage::Capabilities(CapabilitiesMessage { + sender_name: cli.name.clone(), + ..Default::default() + }); + let _ = net_mgr.broadcast(&caps).await; + + chat.add_system_message(format!("Welcome, {}! Your ID: {}", cli.name, our_id_short)); + chat.add_system_message(format!("Full Endpoint ID: {}", our_id)); + if cli.join.is_some() { + chat.add_system_message("Connecting to peer...".to_string()); + } else { + chat.add_system_message( + "Waiting for peers. Share your Endpoint ID for others to join.".to_string(), + ); + } + + // Start Web Interface + // Start Web Interface + // Start Web Interface + tokio::spawn(crate::web::start_web_server( + media.broadcast_tx.clone(), + media.mic_broadcast.clone(), + media.cam_broadcast.clone(), + media.screen_broadcast.clone(), + )); + + // Setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // Main event loop + let mut our_name = cli.name.clone(); + let result = run_event_loop( + &mut terminal, + &mut app, + &mut chat, + file_mgr.clone(), + &mut media, + net_mgr.clone(), + &mut net_rx, + &mut gossip_event_rx, + &mut our_name, + &our_id_short, + &mut connected, + ) + .await; + + // Restore terminal + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + terminal.show_cursor()?; + + // Shutdown networking + let _ = net_mgr.shutdown().await; + + result +} + +async fn run_event_loop( + terminal: &mut Terminal>, + app: &mut App, + chat: &mut ChatState, + file_mgr: FileTransferManager, + media: &mut MediaState, + net: NetworkManager, + net_rx: &mut mpsc::Receiver, + gossip_rx: &mut mpsc::Receiver, + our_name: &mut String, + our_id_short: &str, + connected: &mut bool, +) -> Result<()> { + let mut event_stream = EventStream::new(); + + loop { + // Collect peers for rendering — self is always first + let peers: Vec<_> = { + let p = net.peers.lock().await; + let mut all: Vec<_> = p.values().cloned().collect(); + all.sort_by_key(|p| !p.is_self); // self first + all + }; + *connected = peers.iter().any(|p| !p.is_self); + + terminal.draw(|f| { + tui::render( + f, + app, + chat, + &file_mgr, + media, + &peers, + our_name, + our_id_short, + *connected, + ); + })?; + + // Wait for events + tokio::select! { + // Terminal/keyboard events + maybe_event = event_stream.next() => { + match maybe_event { + Some(Ok(Event::Key(key))) => { + let cmd = app.handle_key(key); + match cmd { + TuiCommand::SendMessage(text) => { + if let Err(e) = chat.send_message(text, &net).await { + chat.add_system_message(format!("Send error: {}", e)); + } + } + TuiCommand::SystemMessage(text) => { + chat.add_system_message(text); + } + TuiCommand::ToggleVoice => { + let status = media.toggle_voice(net.clone()).await; + chat.add_system_message(status.to_string()); + } + TuiCommand::ToggleCamera => { + let status = media.toggle_camera(net.clone()).await; + chat.add_system_message(status.to_string()); + } + TuiCommand::ToggleScreen => { + let status = media.toggle_screen(net.clone()).await; + chat.add_system_message(status.to_string()); + } + TuiCommand::Quit => { + // Broadcast disconnect to peers + let disconnect_msg = GossipMessage::Disconnect { + sender_name: our_name.clone(), + }; + let _ = net.broadcast(&disconnect_msg).await; + // Brief delay to let the message propagate + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + media.shutdown(); + return Ok(()); + } + TuiCommand::ChangeNick(new_nick) => { + let old = our_name.clone(); + *our_name = new_nick.clone(); + chat.our_name = new_nick.clone(); + // Update self in peer list + { + let mut peers = net.peers.lock().await; + if let Some(self_peer) = peers.get_mut(&net.our_id) { + self_peer.name = Some(new_nick.clone()); + } + } + chat.add_system_message( + format!("Nickname changed: {} → {}", old, new_nick), + ); + // Broadcast name change to all peers + let msg = GossipMessage::NameChange( + protocol::NameChange { + old_name: old, + new_name: new_nick, + }, + ); + let _ = net.broadcast(&msg).await; + } + TuiCommand::Connect(peer_id_str) => { + match peer_id_str.parse::() { + Ok(peer_id) => { + chat.add_system_message(format!("Connecting to {}...", peer_id)); + if let Err(e) = net.connect(peer_id).await { + chat.add_system_message(format!("Connection failed: {}", e)); + } else { + chat.add_system_message("Connection initiated.".to_string()); + } + } + Err(_) => { + chat.add_system_message(format!("Invalid peer ID: {}", peer_id_str)); + } + } + } + TuiCommand::SendFile(path) => { + chat.add_system_message(format!("Preparing to send file: {:?}", path)); + if !path.exists() { + chat.add_system_message(format!("File not found: {}", path.display())); + } else { + let file_mgr = file_mgr.clone(); + // Prepare send (hashing) - blocking for now, ideally async task + match file_mgr.prepare_send(&path).await { + Ok((file_id, offer)) => { + chat.add_system_message(format!("Offering file: {}", offer.name)); + let broadcast = protocol::FileOfferBroadcast { + sender_name: our_name.to_string(), + file_id, + file_name: offer.name.clone(), + file_size: offer.size, + }; + let msg = GossipMessage::FileOfferBroadcast(broadcast); + if let Err(e) = net.broadcast(&msg).await { + chat.add_system_message(format!("Failed to broadcast offer: {}", e)); + } + } + Err(e) => { + chat.add_system_message(format!("Failed to prepare file: {}", e)); + } + } + } + } + TuiCommand::Leave => { + chat.add_system_message("Leaving group chat...".to_string()); + media.shutdown(); + // Clear peer list (except self) + { + let mut peers = net.peers.lock().await; + peers.retain(|_, info| info.is_self); + } + chat.add_system_message("Session ended. Use /connect to start a new session.".to_string()); + // Do NOT exit the application + } + TuiCommand::SelectMic(node_name) => { + media.set_mic_name(Some(node_name.clone())); +// ... + } + TuiCommand::SetBitrate(bps) => { + media.set_bitrate(bps); + chat.add_system_message(format!("🎵 Bitrate set to {} kbps", bps / 1000)); + // Save to config + if let Ok(mut cfg) = crate::config::AppConfig::load() { + cfg.media.mic_bitrate = bps; + let _ = cfg.save(); + } + } + TuiCommand::SelectSpeaker(node_name) => { + media.set_speaker_name(Some(node_name.clone())); + chat.add_system_message(format!("🔊 Speaker set to: {}", node_name)); + // Save to config + if let Ok(mut cfg) = crate::config::AppConfig::load() { + cfg.media.speaker_name = Some(node_name); + if let Err(e) = cfg.save() { + tracing::warn!("Failed to save config: {}", e); + } + } + if media.voice_enabled() { + chat.add_system_message("Restart voice chat to apply changes.".to_string()); + } + } + TuiCommand::None => {} + } + } + Some(Ok(Event::Resize(_, _))) => { + // Terminal resize — just redraw on next iteration + } + Some(Err(e)) => { + tracing::error!("Terminal event error: {}", e); + } + None => { + return Ok(()); + } + _ => {} + } + } + + // Network events from file transfer acceptor + Some(event) = net_rx.recv() => { + handle_net_event(event, chat, file_mgr.clone(), media, &net).await; + } + + // Gossip events + Some(event) = gossip_rx.recv() => { + handle_net_event(event, chat, file_mgr.clone(), media, &net).await; + } + + // Signal handling (Ctrl+C / SIGTERM) + _ = tokio::signal::ctrl_c() => { + // Broadcast disconnect to peers + let disconnect_msg = GossipMessage::Disconnect { + sender_name: our_name.clone(), + }; + let _ = net.broadcast(&disconnect_msg).await; + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + media.shutdown(); + return Ok(()); + } + } + } +} + +async fn handle_net_event( + event: NetEvent, + chat: &mut ChatState, + file_mgr: FileTransferManager, + media: &mut MediaState, + net: &NetworkManager, +) { + match event { + NetEvent::GossipReceived { from, message } => match message { + GossipMessage::Chat(msg) => { + chat.receive_message(msg); + } + GossipMessage::Capabilities(caps) => { + let mut peers = net.peers.lock().await; + if let Some(peer) = peers.get_mut(&from) { + peer.name = Some(caps.sender_name.clone()); + peer.capabilities = Some(caps); + } + } + GossipMessage::PeerAnnounce(announce) => { + let mut peers = net.peers.lock().await; + if let Some(peer) = peers.get_mut(&from) { + peer.name = Some(announce.sender_name.clone()); + } + let short_id: String = format!("{}", from).chars().take(8).collect(); + chat.add_system_message(format!( + "{} ({}) joined", + announce.sender_name, short_id + )); + } + GossipMessage::FileOfferBroadcast(offer) => { + chat.add_system_message(format!( + "{} offers file: {} ({} bytes)", + offer.sender_name, offer.file_name, offer.file_size + )); + } + GossipMessage::NameChange(change) => { + // Update peer name in the peer list + let mut peers = net.peers.lock().await; + if let Some(peer) = peers.get_mut(&from) { + peer.name = Some(change.new_name.clone()); + } + chat.add_system_message(format!( + "✏️ {} is now known as {}", + change.old_name, change.new_name + )); + } + GossipMessage::Disconnect { sender_name } => { + // Remove peer from the list + { + let mut peers = net.peers.lock().await; + peers.remove(&from); + } + let short_id: String = format!("{}", from).chars().take(8).collect(); + chat.add_system_message(format!( + "👋 {} ({}) disconnected", + sender_name, short_id + )); + } + }, + NetEvent::PeerUp(peer_id) => { + let short_id: String = format!("{}", peer_id).chars().take(8).collect(); + chat.add_system_message(format!("Peer connected: {}", short_id)); + } + NetEvent::PeerDown(peer_id) => { + let short_id: String = format!("{}", peer_id).chars().take(8).collect(); + chat.add_system_message(format!("Peer disconnected: {}", short_id)); + } + NetEvent::IncomingFileStream { + from, + mut send, + mut recv, + } => { + chat.add_system_message("Incoming file transfer...".to_string()); + tracing::info!("Incoming file stream from {:?}", from); + + // Spawn task to handle transfer + tokio::spawn(async move { + if let Err(e) = file_mgr.execute_receive(&mut send, &mut recv).await { + tracing::error!("File receive failed: {}", e); + } + }); + } + NetEvent::IncomingMediaStream { + from, + kind, + send, + recv, + } => { + let short_id: String = format!("{}", from).chars().take(8).collect(); + chat.add_system_message(format!("📡 Incoming {:?} stream from {}", kind, short_id)); + media.handle_incoming_media(from, kind, send, recv); + } + } +} + +fn parse_topic(hex_str: &str) -> Result<[u8; 32]> { + // If it's all zeros (default), generate a deterministic topic + let hex_str = hex_str.trim(); + + if hex_str.len() != 32 && hex_str.len() != 64 { + // Treat as a room name — hash it to get 32 bytes + use sha2::{Sha256, Digest}; + let mut hasher = Sha256::new(); + hasher.update(hex_str.as_bytes()); + let result = hasher.finalize(); + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(&result); + return Ok(bytes); + } + + // Try parsing as hex + if hex_str.len() == 64 { + let mut bytes = [0u8; 32]; + for i in 0..32 { + bytes[i] = u8::from_str_radix(&hex_str[i * 2..i * 2 + 2], 16) + .context("Invalid hex in topic")?; + } + Ok(bytes) + } else { + // 32-char string — treat as room name + use sha2::{Sha256, Digest}; + let mut hasher = Sha256::new(); + hasher.update(hex_str.as_bytes()); + let result = hasher.finalize(); + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(&result); + Ok(bytes) + } +} + +fn parse_resolution(res: &str) -> Option<(u32, u32)> { + let parts: Vec<&str> = res.split('x').collect(); + if parts.len() == 2 { + let w = parts[0].parse().ok()?; + let h = parts[1].parse().ok()?; + Some((w, h)) + } else { + None + } +} diff --git a/src/media/capture.rs b/src/media/capture.rs new file mode 100644 index 0000000..ca048e3 --- /dev/null +++ b/src/media/capture.rs @@ -0,0 +1,389 @@ +//! Video capture and playback using FFmpeg and MPV. +//! +//! Captures video by spawning an `ffmpeg` process and reading its stdout. +//! Plays video by spawning `mpv` (or `vlc`) and writing to its stdin. + +use std::io::Read; +use std::process::{Command as StdCommand, Stdio as StdStdio}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread; + +use anyhow::{Context, Result}; +use tracing; + +use crate::protocol::{write_framed, decode_framed, MediaKind, MediaStreamMessage}; +use crate::media::WebMediaEvent; + +/// Manages a video capture session (camera or screen). +pub struct VideoCapture { + pub kind: MediaKind, // Make kind public or accessible + running: Arc, + capture_thread: Option>, + player_thread: Option>, // Keep it for now if handle_incoming_video uses it + tasks: Vec>, +} + +/// Chunk of video data from FFmpeg. +struct VideoChunk { + timestamp_ms: u64, + data: Vec, +} + +impl VideoCapture { + /// Start video capture with web input (broadcast receiver). + pub async fn start_web( + kind: MediaKind, + _local_peer_id: iroh::EndpointId, + peers: Vec, + network_manager: crate::net::NetworkManager, + input_rx: tokio::sync::broadcast::Sender>, + ) -> Result { + let running = Arc::new(AtomicBool::new(true)); + + // Spawn sender tasks + let mut tasks = Vec::new(); + for peer in peers { + let running = running.clone(); + let net = network_manager.clone(); + let rx = input_rx.subscribe(); + let kind = kind.clone(); + + let task = tokio::spawn(async move { + if let Err(e) = run_video_sender_web(net, peer, kind, rx, running).await { + tracing::error!("Video sender web error: {}", e); + } + }); + tasks.push(task); + } + + Ok(Self { + kind, // Added kind + running, + capture_thread: None, + player_thread: None, + tasks, // Added tasks + }) + } + + /// Start capture of the given `kind` and send video data to peers. + pub fn start( + kind: MediaKind, + peer_streams: Vec<(iroh::endpoint::SendStream, iroh::endpoint::RecvStream)>, + resolution: Option<(u32, u32)>, + ) -> Result { + let running = Arc::new(AtomicBool::new(true)); + // Use a bounded channel to backpressure ffmpeg if network is slow + let (chunk_tx, chunk_rx) = crossbeam_channel::bounded::(64); + + let capture_running = running.clone(); + let capture_thread = thread::Builder::new() + .name(format!("ffmpeg-{:?}", kind)) + .spawn(move || { + if let Err(e) = run_ffmpeg_capture(capture_running, chunk_tx, kind, resolution) { + tracing::error!("FFmpeg capture error: {}", e); + } + }) + .context("Failed to spawn ffmpeg capture thread")?; + + let mut tasks = Vec::new(); + // Parameters for signaling (mostly informative now, as ffmpeg controls generic stream) + let (width, height, fps) = match kind { + MediaKind::Camera => (640, 480, 30), + MediaKind::Screen => { + let (w, h) = resolution.unwrap_or((1920, 1080)); + (w, h, 30) + }, + _ => (0, 0, 0), + }; + + for (mut send, _recv) in peer_streams { + let chunk_rx = chunk_rx.clone(); + let running = running.clone(); + + let task = tokio::spawn(async move { + let start = MediaStreamMessage::VideoStart { + kind, + width, + height, + fps, + }; + if let Err(e) = write_framed(&mut send, &start).await { + tracing::error!("Failed to send VideoStart: {}", e); + return; + } + + let mut sequence: u64 = 0; + loop { + if !running.load(Ordering::Relaxed) { + break; + } + match chunk_rx.recv_timeout(std::time::Duration::from_millis(500)) { + Ok(chunk) => { + let msg = MediaStreamMessage::VideoFrame { + sequence, + timestamp_ms: chunk.timestamp_ms, + data: chunk.data, + }; + if let Err(e) = write_framed(&mut send, &msg).await { + tracing::debug!("Failed to send VideoFrame (peer disconnected?): {}", e); + break; + } + sequence += 1; + } + Err(crossbeam_channel::RecvTimeoutError::Timeout) => continue, + Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break, + } + } + + let stop = MediaStreamMessage::VideoStop { kind }; + let _ = write_framed(&mut send, &stop).await; + }); + + tasks.push(task); + } + + Ok(Self { + kind, + running, + capture_thread: Some(capture_thread), + player_thread: None, // Initialized to None for native capture + tasks, + }) + } + + /// Stop capture. + pub fn stop(&mut self) { + self.running.store(false, Ordering::Relaxed); + for task in self.tasks.drain(..) { + task.abort(); + } + if let Some(handle) = self.capture_thread.take() { + let _ = handle.join(); + } + if let Some(handle) = self.player_thread.take() { + let _ = handle.join(); + } + } + + /// Handle incoming video stream taking the first start message (Web Version). + pub async fn handle_incoming_video_web( + from: iroh::EndpointId, + message: MediaStreamMessage, + mut recv: iroh::endpoint::RecvStream, + broadcast_tx: tokio::sync::broadcast::Sender, + ) -> Result<()> { + let kind = match message { + MediaStreamMessage::VideoStart { kind, .. } => kind, + _ => anyhow::bail!("Invalid start message for video"), + }; + tracing::info!("Starting {:?} playback via Web Broadcast", kind); + + loop { + match decode_framed::(&mut recv).await { + Ok(msg) => match msg { + MediaStreamMessage::VideoFrame { data, .. } => { + // Send to web + let short_id: String = format!("{}", from).chars().take(8).collect(); + let _ = broadcast_tx.send(WebMediaEvent::Video { peer_id: short_id, kind, data }); + } + MediaStreamMessage::VideoStop { .. } => { + break; + } + _ => {} + }, + Err(_) => break, // Stream closed + } + } + Ok(()) + } + + /// Handle incoming video stream taking the first start message. + pub async fn handle_incoming_video( + message: MediaStreamMessage, + recv: iroh::endpoint::RecvStream, + ) -> Result<()> { + match message { + MediaStreamMessage::VideoStart { kind, .. } => { + tracing::info!("Starting {:?} playback via MPV/VLC", kind); + run_player_loop(recv).await + } + _ => anyhow::bail!("Invalid start message for video"), + } + } +} + +impl Drop for VideoCapture { + fn drop(&mut self) { + self.stop(); + } +} + +// --------------------------------------------------------------------------- +// FFmpeg Capture Logic +// --------------------------------------------------------------------------- + +async fn run_video_sender_web( + network_manager: crate::net::NetworkManager, + peer: iroh::EndpointId, + kind: MediaKind, + mut input_rx: tokio::sync::broadcast::Receiver>, + running: Arc, +) -> Result<()> { + let (mut send, _) = network_manager.open_media_stream(peer, kind.clone()).await?; + // For web, we assume fixed resolution and fps for now. + write_framed(&mut send, &MediaStreamMessage::VideoStart { kind, width: 640, height: 480, fps: 30 }).await?; + + while running.load(Ordering::Relaxed) { + match input_rx.recv().await { + Ok(data) => { + // Web sends MJPEG chunk (full frame) + let msg = MediaStreamMessage::VideoFrame { + sequence: 0, // Sequence not used for web input, set to 0 + timestamp_ms: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64, + data, + }; + if write_framed(&mut send, &msg).await.is_err() { + break; + } + } + Err(tokio::sync::broadcast::error::RecvError::Closed) => break, + Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { + tracing::warn!("Video sender lagged"); + } + } + } + + let _ = write_framed(&mut send, &MediaStreamMessage::VideoStop { kind }).await; + send.finish()?; + Ok(()) +} + +fn run_ffmpeg_capture( + running: Arc, + chunk_tx: crossbeam_channel::Sender, + kind: MediaKind, + resolution: Option<(u32, u32)>, +) -> Result<()> { + let mut child = match kind { + MediaKind::Camera => { + // Camera: Use ffmpeg directly with v4l2 + let mut cmd = StdCommand::new("ffmpeg"); + cmd.args(&[ + "-f", "v4l2", "-framerate", "30", "-video_size", "640x480", "-i", "/dev/video0", + "-c:v", "mjpeg", "-preset", "ultrafast", "-tune", "zerolatency", "-f", "mpegts", + "-", + ]); + cmd.stdout(StdStdio::piped()).stderr(StdStdio::null()); + cmd.spawn().context("Failed to spawn ffmpeg for camera")? + } + MediaKind::Screen => { + // Screen: Use pipewiresrc (via gst-launch) with MJPEG encoding + // User requested configurable resolution (default 720p). + let (w, h) = resolution.unwrap_or((1920, 1080)); + + // Pipeline: pipewiresrc -> videoscale -> jpegenc -> fdsink + let pipeline = format!( + "gst-launch-1.0 -q pipewiresrc do-timestamp=true \ + ! videoscale ! video/x-raw,width={},height={} \ + ! jpegenc quality=50 \ + ! fdsink", + w, h + ); + + let mut cmd = StdCommand::new("sh"); + cmd.args(&["-c", &pipeline]); + cmd.stdout(StdStdio::piped()).stderr(StdStdio::null()); + cmd.spawn().context("Failed to spawn pipewire capture pipeline")? + } + _ => anyhow::bail!("Unsupported media kind for capture"), + }; + + let mut stdout = child.stdout.take().context("Failed to open capture stdout")?; + let mut buffer = [0u8; 4096]; // 4KB chunks + + while running.load(Ordering::Relaxed) { + match stdout.read(&mut buffer) { + Ok(0) => break, // EOF + Ok(n) => { + let chunk = VideoChunk { + timestamp_ms: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64, + data: buffer[..n].to_vec(), + }; + if chunk_tx.send(chunk).is_err() { + break; // Receiver dropped + } + } + Err(e) => { + tracing::error!("Error reading capture stdout: {}", e); + break; + } + } + } + + let _ = child.kill(); + let _ = child.wait(); + Ok(()) +} + +// --------------------------------------------------------------------------- +// Player Logic (MPV/VLC) +// --------------------------------------------------------------------------- + +async fn run_player_loop(mut recv: iroh::endpoint::RecvStream) -> Result<()> { + use tokio::process::Command; + use tokio::io::AsyncWriteExt; + use std::process::Stdio; + + // Try spawning mpv + let mut cmd = Command::new("mpv"); + cmd.args(&["--no-cache", "--untimed", "--no-terminal", "--profile=low-latency", "-"]); + cmd.stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + // We must kill child on drop. + cmd.kill_on_drop(true); + + let mut child = match cmd.spawn() { + Ok(c) => c, + Err(e) => { + tracing::warn!("Failed to spawn mpv ({}), trying vlc...", e); + let mut vlc = Command::new("vlc"); + vlc.args(&["-", "--network-caching=300"]); + vlc.stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + vlc.kill_on_drop(true); + vlc.spawn().context("Failed to spawn vlc either")? + } + }; + + let mut stdin = child.stdin.take().context("Failed to open player stdin")?; + + loop { + match decode_framed::(&mut recv).await { + Ok(msg) => match msg { + MediaStreamMessage::VideoFrame { data, .. } => { + if let Err(_) = stdin.write_all(&data).await { + break; // Player closed + } + } + MediaStreamMessage::VideoStop { .. } => { + break; + } + _ => {} + }, + Err(_) => break, // Stream closed + } + } + + // Child is killed on drop (recv loop end) + Ok(()) +} diff --git a/src/media/mod.rs b/src/media/mod.rs new file mode 100644 index 0000000..5320e9b --- /dev/null +++ b/src/media/mod.rs @@ -0,0 +1,327 @@ +//! Media module — voice chat, camera, and screen sharing. +//! +//! Sub-modules: +//! - `voice`: mic capture + Opus codec + PipeWire playback +//! - `capture`: FFmpeg capture + MPV/VLC playback +//! +//! Each feature is runtime-toggleable and runs on dedicated threads/tasks. + +pub mod capture; +pub mod voice; + +use iroh::EndpointId; +use tracing; + +use std::sync::Arc; +use std::sync::atomic::{AtomicU32, Ordering}; +use crate::net::NetworkManager; +use crate::protocol::{MediaKind, MediaStreamMessage, decode_framed}; + +use self::capture::VideoCapture; +use self::voice::VoiceChat; + +/// Event sent to the web interface for playback. +#[derive(Debug, Clone)] +pub enum WebMediaEvent { + Audio { peer_id: String, data: Vec }, + Video { peer_id: String, kind: MediaKind, data: Vec }, +} + +/// Tracks all active media sessions. +pub struct MediaState { + /// Active voice chat session (if any). + voice: Option, + /// Active camera capture (if any). + camera: Option, + /// Active screen capture (if any). + screen: Option, + /// Playback task handles for incoming streams (voice/video). + /// Using a list to allow multiple streams (audio+video) from same or different peers. + incoming_media: Vec>, + /// Whether PipeWire is available on this system. + pipewire_available: bool, + /// Configured screen resolution (width, height). + #[allow(dead_code)] + screen_resolution: (u32, u32), + /// Configured microphone name (target). + mic_name: Option, + /// Configured speaker name (target). + speaker_name: Option, + /// Broadcast channel for web playback. + pub broadcast_tx: tokio::sync::broadcast::Sender, + // Input channels (from Web -> MediaState -> Peers) + pub mic_broadcast: tokio::sync::broadcast::Sender>, + pub cam_broadcast: tokio::sync::broadcast::Sender>, + pub screen_broadcast: tokio::sync::broadcast::Sender>, + pub mic_bitrate: Arc, +} + +impl MediaState { + pub fn new(screen_resolution: (u32, u32), mic_name: Option, speaker_name: Option, mic_bitrate: u32) -> Self { + let pipewire_available = check_pipewire(); + let (broadcast_tx, _) = tokio::sync::broadcast::channel(100); + let (mic_broadcast, _) = tokio::sync::broadcast::channel(100); + let (cam_broadcast, _) = tokio::sync::broadcast::channel(100); + let (screen_broadcast, _) = tokio::sync::broadcast::channel(100); + Self { + voice: None, + camera: None, + screen: None, + incoming_media: Vec::new(), + pipewire_available, + screen_resolution, + mic_name, + speaker_name, + broadcast_tx, + mic_broadcast, + cam_broadcast, + screen_broadcast, + mic_bitrate: Arc::new(AtomicU32::new(mic_bitrate)), + } + } + + pub fn set_bitrate(&self, bitrate: u32) { + self.mic_bitrate.store(bitrate, Ordering::Relaxed); + } + + /// Update the selected microphone name. + pub fn set_mic_name(&mut self, name: Option) { + self.mic_name = name; + } + + /// Update the selected speaker name. + pub fn set_speaker_name(&mut self, name: Option) { + self.speaker_name = name; + } + + // ----------------------------------------------------------------------- + // Public state queries + // ----------------------------------------------------------------------- + + pub fn voice_enabled(&self) -> bool { + self.voice.is_some() + } + + pub fn camera_enabled(&self) -> bool { + self.camera.is_some() + } + + pub fn screen_enabled(&self) -> bool { + self.screen.is_some() + } + + #[allow(dead_code)] + pub fn pipewire_available(&self) -> bool { + self.pipewire_available + } + + // ----------------------------------------------------------------------- + // Toggle methods — return a status message for the TUI + // ----------------------------------------------------------------------- + + /// Toggle voice chat. Opens media QUIC streams to all current peers. + pub async fn toggle_voice(&mut self, net: NetworkManager) -> &'static str { + if !self.pipewire_available { + return "Voice chat unavailable (PipeWire not found)"; + } + if self.voice.is_some() { + // Stop + if let Some(mut v) = self.voice.take() { + v.stop(); + } + "🎤 Voice chat stopped" + } else { + // Start — open media streams to all peers + // For web capture, we don't open streams here. start_web does it. + let peers = net.peers.lock().await; + + match VoiceChat::start_web( + net.clone(), + peers.keys().cloned().collect(), + self.mic_broadcast.subscribe(), // Subscribe to get new receiver! + self.broadcast_tx.clone(), + self.mic_bitrate.clone(), + ) { + Ok(vc) => { + self.voice = Some(vc); + "🎤 Voice chat started (Web)" + } + Err(e) => { + tracing::error!("Failed to start voice chat: {}", e); + "🎤 Failed to start voice chat" + } + } + } + } + /// Toggle camera capture. + pub async fn toggle_camera(&mut self, net: NetworkManager) -> &'static str { + // We use ffmpeg now, which doesn't strictly depend on pipewire crate, + // but likely requires pipewire daemon or v4l2. + // We kept pipewire check for consistency but it might be loose. + if self.camera.is_some() { + if let Some(mut c) = self.camera.take() { + c.stop(); + } + "📷 Camera stopped" + } else { + // Start + let peers = net.peers.lock().await; + match VideoCapture::start_web( + MediaKind::Camera, + net.our_id, + peers.keys().cloned().collect(), + net.clone(), + self.cam_broadcast.clone(), + ).await { + Ok(vc) => { + self.camera = Some(vc); + "📷 Camera started (Web)" + } + Err(e) => { + tracing::error!("Failed to start camera: {}", e); + "📷 Failed to start camera" + } + } + } + } + + /// Toggle screen sharing. + pub async fn toggle_screen(&mut self, net: NetworkManager) -> &'static str { + if self.screen.is_some() { + if let Some(mut s) = self.screen.take() { + s.stop(); + } + "🖥 Screen sharing stopped" + } else { + // Start + let peers = net.peers.lock().await; + match VideoCapture::start_web( + MediaKind::Screen, + net.our_id, + peers.keys().cloned().collect(), + net.clone(), + self.screen_broadcast.clone(), + ).await { + Ok(vc) => { + self.screen = Some(vc); + "🖥️ Screen share started (Web)" + } + Err(e) => { + tracing::error!("Failed to start screen share: {}", e); + "🖥️ Failed to start screen share" + } + } + } + } + + // ----------------------------------------------------------------------- + // Incoming media handling + // ----------------------------------------------------------------------- + + /// Handle an incoming media stream from a peer. + pub fn handle_incoming_media( + &mut self, + from: EndpointId, + kind: MediaKind, + _send: iroh::endpoint::SendStream, + mut recv: iroh::endpoint::RecvStream, + ) { + let speaker_name = self.speaker_name.clone(); + let broadcast_tx = self.broadcast_tx.clone(); + // Spawn a task to determine stream type and handle it + let handle = tokio::spawn(async move { + // Read first message to determine type. + // Note: We already know the kind from ALPN, but we still decode the start message. + match decode_framed::(&mut recv).await { + Ok(msg) => match msg { + MediaStreamMessage::AudioStart { .. } => { + if kind != MediaKind::Voice { + tracing::warn!("ALPN mismatch: expected Voice, got AudioStart"); + } + tracing::info!("Accepted Audio stream from {:?}", from); + if let Err(e) = VoiceChat::handle_incoming_audio_web(from, msg, recv, broadcast_tx).await { + tracing::error!("Audio web playback error: {}", e); + } + } + MediaStreamMessage::VideoStart { .. } => { + tracing::info!("Accepted Video stream from {:?}", from); + if let Err(e) = VideoCapture::handle_incoming_video_web(from, msg, recv, broadcast_tx).await { + tracing::error!("Video web playback error: {}", e); + } + } + _ => { + tracing::warn!("Unknown or unexpected start message from {:?}: {:?}", from, msg); + } + }, + Err(e) => { + tracing::warn!("Failed to decode initial media message from {:?}: {}", from, e); + } + } + }); + + // Store handle to allow cleanup on shutdown + // We clean up finished tasks periodically or on shutdown + self.incoming_media.push(handle); + // Clean up finished tasks + self.incoming_media.retain(|h| !h.is_finished()); + } + + // ----------------------------------------------------------------------- + // Status for TUI + // ----------------------------------------------------------------------- + + /// Get a status line for the TUI status bar. + pub fn status_line(&self) -> String { + let mic = if self.voice_enabled() { + "🎤 LIVE" + } else { + "🎤 off" + }; + let cam = if self.camera_enabled() { + "📷 LIVE" + } else { + "📷 off" + }; + let scr = if self.screen_enabled() { + "🖥 LIVE" + } else { + "🖥 off" + }; + format!("{} │ {} │ {}", mic, cam, scr) + } + + /// Shut down all active media. + pub fn shutdown(&mut self) { + if let Some(mut v) = self.voice.take() { + v.stop(); + } + if let Some(mut c) = self.camera.take() { + c.stop(); + } + if let Some(mut s) = self.screen.take() { + s.stop(); + } + for handle in self.incoming_media.drain(..) { + handle.abort(); + } + } +} + +impl Drop for MediaState { + fn drop(&mut self) { + self.shutdown(); + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Check if PipeWire is available on this system. +fn check_pipewire() -> bool { + // Try to initialize PipeWire — if it fails, it's not available + std::panic::catch_unwind(|| { + pipewire::init(); + }) + .is_ok() +} diff --git a/src/media/voice.rs b/src/media/voice.rs new file mode 100644 index 0000000..e2a7856 --- /dev/null +++ b/src/media/voice.rs @@ -0,0 +1,331 @@ +//! Voice capture and playback using PipeWire + Audiopus (via Songbird dependency). +//! +//! Architecture: +//! - Capture runs on a dedicated OS thread (PipeWire main loop). +//! - PipeWire process callback copies PCM → crossbeam channel. +//! - Async task reads from channel, encodes with Opus, sends over QUIC. +//! - Playback: receives Opus packets from QUIC, decodes, feeds to PipeWire output. + +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::Arc; +use std::thread; + +use anyhow::Result; +use crate::protocol::{write_framed, decode_framed, MediaStreamMessage}; +use crate::media::WebMediaEvent; +// Use audiopus types directly +use audiopus::{coder::Encoder as OpusEncoder, coder::Decoder as OpusDecoder, Channels, Application, SampleRate, Bitrate}; + +// Constants +const SAMPLE_RATE_VAL: i32 = 48000; +const FRAME_SIZE_MS: u32 = 20; // 20ms +const FRAME_SIZE_SAMPLES: usize = (SAMPLE_RATE_VAL as usize * FRAME_SIZE_MS as usize) / 1000; + +/// Represents an available audio device (source or sink). +#[derive(Debug, Clone)] +pub struct AudioDevice { + /// PipeWire node name (used as target.object) + pub node_name: String, + /// Human-readable description + pub description: String, +} + +/// List available audio input sources via `pw-dump`. +pub fn list_audio_sources() -> Vec { + list_audio_nodes("Audio/Source") +} + +/// List available audio output sinks via `pw-dump`. +pub fn list_audio_sinks() -> Vec { + list_audio_nodes("Audio/Sink") +} + +fn list_audio_nodes(filter_class: &str) -> Vec { + use std::process::Command; + + let output = match Command::new("pw-dump").output() { + Ok(o) => o, + Err(_) => return Vec::new(), + }; + + if !output.status.success() { + return Vec::new(); + } + + let json_str = match String::from_utf8(output.stdout) { + Ok(s) => s, + Err(_) => return Vec::new(), + }; + + let data: Vec = match serde_json::from_str(&json_str) { + Ok(d) => d, + Err(_) => return Vec::new(), + }; + + let mut sources = Vec::new(); + for obj in &data { + let props = match obj.get("info").and_then(|i| i.get("props")) { + Some(p) => p, + None => continue, + }; + + let media_class = props.get("media.class").and_then(|v| v.as_str()).unwrap_or(""); + // Match partial class, e.g. "Audio/Source", "Audio/Sink", "Audio/Duplex" + if !media_class.contains(filter_class) && !media_class.contains("Audio/Duplex") { + continue; + } + + let node_name = props.get("node.name").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let description = props + .get("node.description") + .or_else(|| props.get("node.nick")) + .and_then(|v| v.as_str()) + .unwrap_or(&node_name) + .to_string(); + + if !node_name.is_empty() { + sources.push(AudioDevice { node_name, description }); + } + } + + sources +} + +/// Main voice chat coordination. +pub struct VoiceChat { + running: Arc, + capture_thread: Option>, + tasks: Vec>, +} + +impl VoiceChat { + /// Start voice chat session (Web Version). + /// Uses browser for capture/playback handling implicitly via `MediaState` channels, + /// but here we handle the NETWORK side encoding/decoding. + pub fn start_web( + net: crate::net::NetworkManager, + peers: Vec, // Multiple peers + mic_rx: tokio::sync::broadcast::Receiver>, + _broadcast_tx: tokio::sync::broadcast::Sender, + mic_bitrate: Arc, + ) -> Result { + let running = Arc::new(AtomicBool::new(true)); + + let mut tasks = Vec::new(); + + // One sender task to iterate ALL peers? Or one task per peer? + // Since input is broadcast channel, spawning multiple tasks (one per peer) works well. + // Each task gets a copy of `mic_rx`. Wait, `broadcast::Receiver` can be cloned? + // No, `mic_rx` must be resubscribed? + // `broadcast::Receiver` is `Clone`? No, `Sender::subscribe` returns receiver. + // But we are passing `Receiver` in. + // We can't clone a receiver to get same stream easily without `resubscribe`. + // But `resubscribe` needs sender. + // We should probably spawn ONE task that reads from `mic_rx` and sends to ALL peers. + // That is more efficient (encode once, send N times). + + let sender_running = running.clone(); + let net_clone = net.clone(); + let mic_bitrate_clone = mic_bitrate.clone(); + let sender_task = tokio::spawn(async move { + if let Err(e) = run_opis_sender_web_multi(net_clone, peers, mic_rx, sender_running, mic_bitrate_clone).await { + tracing::error!("Voice sender failed: {}", e); + } + }); + + tasks.push(sender_task); + + Ok(Self { + running, + capture_thread: None, + tasks, + }) + } + + /// Stop voice chat. + pub fn stop(&mut self) { + self.running.store(false, Ordering::SeqCst); + for task in &self.tasks { + task.abort(); + } + self.tasks.clear(); + if let Some(t) = self.capture_thread.take() { + t.thread().unpark(); // Wake up if sleeping + let _ = t.join(); + } + } + + /// Handle incoming audio stream (Web Version). + pub async fn handle_incoming_audio_web( + from: iroh::EndpointId, + message: MediaStreamMessage, + mut recv: iroh::endpoint::RecvStream, + broadcast_tx: tokio::sync::broadcast::Sender, + ) -> Result<()> { + // Initialize Opus decoder + let mut decoder = OpusDecoder::new(SampleRate::Hz48000, Channels::Mono) + .map_err(|e| anyhow::anyhow!("Failed to create Opus decoder: {:?}", e))?; + + // Process start message + match message { + MediaStreamMessage::AudioStart { .. } => { + tracing::info!("Incoming voice stream started (web) from {}", from); + } + _ => anyhow::bail!("Expected AudioStart"), + } + + let mut decode_buf = vec![0f32; FRAME_SIZE_SAMPLES]; + + loop { + let msg: MediaStreamMessage = match decode_framed(&mut recv).await { + Ok(m) => m, + Err(_) => break, // EOF + }; + + match msg { + MediaStreamMessage::AudioData { opus_data, .. } => { // Removed `channels` field usage if it existed + match decoder.decode_float(Some(&opus_data), &mut decode_buf, false) { + Ok(len) => { + let samples = decode_buf[..len].to_vec(); + // Broadcast to web + let short_id: String = format!("{}", from).chars().take(8).collect(); + let _ = broadcast_tx.send(WebMediaEvent::Audio { peer_id: short_id, data: samples }); + } + Err(e) => { + tracing::warn!("Opus decode error: {:?}", e); + } + } + } + MediaStreamMessage::AudioStop => { + tracing::info!("Peer stopped audio"); + break; + } + _ => {} + } + } + Ok(()) + } +} + +// --------------------------------------------------------------------------- +// Opus sender — encodes PCM and sends over QUIC (Multi-Peer) +// --------------------------------------------------------------------------- + +async fn run_opis_sender_web_multi( + network_manager: crate::net::NetworkManager, + peers: Vec, + mut input_rx: tokio::sync::broadcast::Receiver>, + running: Arc, + mic_bitrate: Arc, +) -> Result<()> { + if peers.is_empty() { + return Ok(()); + } + + // Open streams to all peers + let mut streams = Vec::new(); + for peer in peers { + match network_manager.open_media_stream(peer, crate::protocol::MediaKind::Voice).await { + Ok((mut send, _)) => { + if let Err(e) = write_framed(&mut send, &MediaStreamMessage::AudioStart { + channels: 1, + sample_rate: 48000, + frame_size_ms: FRAME_SIZE_MS as u8, + }).await { + tracing::error!("Failed to send start to {}: {}", peer, e); + continue; + } + streams.push(send); + } + Err(e) => { + tracing::warn!("Failed to open voice stream to {}: {}", peer, e); + } + } + } + + if streams.is_empty() { + tracing::warn!("No peers connected for voice chat"); + } + + let mut encoder = OpusEncoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip) + .map_err(|e| anyhow::anyhow!("Failed to create Opus encoder: {:?}", e))?; + + // Set initial bitrate + let mut current_bitrate = mic_bitrate.load(Ordering::Relaxed); + encoder.set_bitrate(Bitrate::BitsPerSecond(current_bitrate as i32)) + .map_err(|e| anyhow::anyhow!("Failed to set bitrate: {:?}", e))?; + + // Opus frame size: 20ms at 48kHz = 960 samples + let frame_size = FRAME_SIZE_SAMPLES; + let mut pcm_buffer: Vec = Vec::with_capacity(frame_size * 2); + let mut opus_buffer = vec![0u8; 1500]; // MTU-ish + + while running.load(Ordering::Relaxed) { + // Check for bitrate change + let new_bitrate = mic_bitrate.load(Ordering::Relaxed); + if new_bitrate != current_bitrate { + if let Err(e) = encoder.set_bitrate(Bitrate::BitsPerSecond(new_bitrate as i32)) { + tracing::warn!("Failed to update bitrate to {}: {:?}", new_bitrate, e); + } else { + tracing::info!("Updated Opus bitrate to {} bps", new_bitrate); + current_bitrate = new_bitrate; + } + } + + // Receive PCM from Web + match input_rx.recv().await { + Ok(samples) => { + pcm_buffer.extend_from_slice(&samples); + + // Process 20ms chunks + while pcm_buffer.len() >= frame_size { + let chunk: Vec = pcm_buffer.drain(0..frame_size).collect(); + + match encoder.encode_float(&chunk, &mut opus_buffer) { + Ok(len) => { + let packet = opus_buffer[..len].to_vec(); + let msg = MediaStreamMessage::AudioData { + sequence: 0, // Should maintain seq counter? Protocol doesn't have it? + // Need to check Protocol definition. + // Assuming I need to match variants. + opus_data: packet.clone(), + }; + + // Send to all streams + // remove closed streams + // This is async inside a loop which makes ownership tricky for Vec + // but we can iterate. + let mut keep_indices = Vec::new(); + for (i, stream) in streams.iter_mut().enumerate() { + if let Err(e) = write_framed(stream, &msg).await { + tracing::error!("Failed to send audio packet: {}", e); + // mark for removal + } else { + keep_indices.push(i); + } + } + + // Naive removal (rebuild vec) if needed, or just warn. + // Ideally remove bad streams. + } + Err(e) => { + tracing::error!("Opus encode error: {:?}", e); + } + } + } + } + Err(tokio::sync::broadcast::error::RecvError::Closed) => break, + Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { + // Skip if lagged + } + } + } + + // Send Stop + for stream in &mut streams { + let _ = write_framed(stream, &MediaStreamMessage::AudioStop).await; + let _ = stream.finish(); // fire and forget + } + + Ok(()) +} diff --git a/src/net/mod.rs b/src/net/mod.rs new file mode 100644 index 0000000..2f266b3 --- /dev/null +++ b/src/net/mod.rs @@ -0,0 +1,340 @@ +//! Networking layer using iroh for QUIC connections and iroh-gossip for broadcast. +//! +//! This module manages: +//! - The iroh `Endpoint` and `Router` +//! - Gossip-based broadcast for chat messages +//! - Peer connection tracking +//! - File transfer stream handling + +use std::collections::HashMap; +use std::sync::Arc; + +use anyhow::{Context, Result}; +// use iroh::protocol::Router; +use iroh::Endpoint; +pub use iroh::EndpointId; +use iroh_gossip::Gossip; +use iroh_gossip::TopicId; +use n0_future::StreamExt; +use tokio::sync::{mpsc, Mutex}; + +use crate::protocol::{self, GossipMessage}; + +/// ALPN identifier for file transfers (direct peer-to-peer streams). +pub const FILE_TRANSFER_ALPN: &[u8] = b"p2p-chat/file/1"; + +/// ALPN identifier for voice chat streams. +pub const VOICE_ALPN: &[u8] = b"p2p-chat/voice/1"; +/// ALPN identifier for camera streams. +pub const CAMERA_ALPN: &[u8] = b"p2p-chat/camera/1"; +/// ALPN identifier for screen sharing streams. +pub const SCREEN_ALPN: &[u8] = b"p2p-chat/screen/1"; + +/// Network events sent to the application layer. +#[derive(Debug)] +pub enum NetEvent { + /// A gossip message was received from a peer. + GossipReceived { + from: EndpointId, + message: GossipMessage, + }, + /// A peer came online (joined the gossip swarm). + PeerUp(EndpointId), + /// A peer went offline (left the gossip swarm). + PeerDown(EndpointId), + /// An incoming file transfer stream was accepted. + IncomingFileStream { + from: EndpointId, + #[allow(dead_code)] + send: iroh::endpoint::SendStream, + #[allow(dead_code)] + recv: iroh::endpoint::RecvStream, + }, + /// An incoming media stream was accepted. + IncomingMediaStream { + from: EndpointId, + kind: protocol::MediaKind, + send: iroh::endpoint::SendStream, + recv: iroh::endpoint::RecvStream, + }, +} + +/// Information about a connected peer. +#[derive(Debug, Clone)] +pub struct PeerInfo { + pub id: EndpointId, + pub name: Option, + pub capabilities: Option, + pub is_self: bool, +} + +/// Manages the iroh networking stack. +#[derive(Clone)] +pub struct NetworkManager { + pub endpoint: Endpoint, + // We handle routing manually now + // router: Option, + pub gossip: Gossip, + topic: TopicId, + gossip_sender: Option, + pub peers: Arc>>, + pub our_id: EndpointId, +} + +impl NetworkManager { + /// Connect to a peer by ID. + /// Connect to a peer by ID. + pub async fn connect(&self, peer_id: EndpointId) -> Result<()> { + // 1. Establish transport connection + self.endpoint + .connect(peer_id, iroh_gossip::ALPN) + .await + .context("Failed to connect to peer")?; + + // 2. Add peer to gossip by subscribing with it as a bootstrap peer. + // We spawn a task to drain the new receiver so the subscription stays active. + // This ensures the gossip actor knows about this peer. + let (_sender, mut receiver) = self + .gossip + .subscribe(self.topic, vec![peer_id]) + .await + .context("Failed to subscribe with new peer")? + .split(); + + tokio::spawn(async move { + while let Some(_event) = receiver.next().await { + // Drain events to keep subscription active. + // We ignore them because the main subscription loop triggers + // the actual application logic (NetEvent). + } + }); + + Ok(()) + } + + /// Create a new NetworkManager and start the iroh endpoint. + pub async fn new(topic_bytes: [u8; 32]) -> Result<(Self, mpsc::Sender, mpsc::Receiver)> { + let (event_tx, event_rx) = mpsc::channel(256); + + // Create endpoint with file transfer ALPN and Gossip ALPN + let endpoint = Endpoint::builder() + .alpns(vec![ + FILE_TRANSFER_ALPN.to_vec(), + VOICE_ALPN.to_vec(), + CAMERA_ALPN.to_vec(), + SCREEN_ALPN.to_vec(), + iroh_gossip::ALPN.to_vec(), + ]) + .bind() + .await + .context("Failed to bind iroh endpoint")?; + + let our_id = endpoint.id(); + let topic = TopicId::from_bytes(topic_bytes); + + // Build gossip protocol + let gossip = Gossip::builder().spawn(endpoint.clone()); + + let mgr = Self { + endpoint, + gossip: gossip.clone(), + // router: None, + topic, + gossip_sender: None, + peers: Arc::new(Mutex::new(HashMap::new())), + our_id, + }; + + // Spawn unified connection acceptor + Self::spawn_acceptor(mgr.endpoint.clone(), gossip, event_tx.clone()); + + Ok((mgr, event_tx, event_rx)) + } + + /// Subscribe to the gossip topic and start receiving messages. + /// `bootstrap_peers` are the initial peers to connect to. + pub async fn join_gossip( + &mut self, + bootstrap_peers: Vec, + event_tx: mpsc::Sender, + ) -> Result<()> { + let (sender, mut receiver) = self + .gossip + .subscribe(self.topic, bootstrap_peers) + .await + .context("Failed to subscribe to gossip topic")? + .split(); + + self.gossip_sender = Some(sender); + + let peers = self.peers.clone(); + + // Spawn gossip event receiver + tokio::spawn(async move { + while let Some(event) = receiver.next().await { + match event { + Ok(iroh_gossip::api::Event::Received(msg)) => { + // Deserialize the gossip message + match postcard::from_bytes::(&msg.content) { + Ok(gossip_msg) => { + let _ = event_tx + .send(NetEvent::GossipReceived { + from: msg.delivered_from, + message: gossip_msg, + }) + .await; + } + Err(e) => { + tracing::warn!("Failed to deserialize gossip message: {}", e); + } + } + } + Ok(iroh_gossip::api::Event::NeighborUp(peer_id)) => { + { + let mut p = peers.lock().await; + p.entry(peer_id).or_insert_with(|| PeerInfo { + id: peer_id, + name: None, + capabilities: None, + is_self: false, + }); + } + let _ = event_tx.send(NetEvent::PeerUp(peer_id)).await; + } + Ok(iroh_gossip::api::Event::NeighborDown(peer_id)) => { + { + let mut p = peers.lock().await; + p.remove(&peer_id); + } + let _ = event_tx.send(NetEvent::PeerDown(peer_id)).await; + } + Ok(_) => {} + Err(e) => { + tracing::error!("Gossip receiver error: {}", e); + break; + } + } + } + }); + + Ok(()) + } + + /// Broadcast a gossip message to all peers in the topic. + pub async fn broadcast(&self, msg: &GossipMessage) -> Result<()> { + if let Some(sender) = &self.gossip_sender { + let data = postcard::to_allocvec(msg)?; + sender.broadcast(data.into()).await?; + } + Ok(()) + } + + /// Get our endpoint address for sharing with peers. + #[allow(dead_code)] + pub fn our_addr(&self) -> iroh::EndpointAddr { + self.endpoint.addr() + } + + /// Open a bi-directional stream to a specific peer for file transfer. + #[allow(dead_code)] + pub async fn open_file_stream( + &self, + peer_id: EndpointId, + ) -> Result<(iroh::endpoint::SendStream, iroh::endpoint::RecvStream)> { + let conn = self + .endpoint + .connect(peer_id, FILE_TRANSFER_ALPN) + .await + .context("Failed to connect for file transfer")?; + let (send, recv) = conn.open_bi().await?; + Ok((send, recv)) + } + + /// Open a bi-directional stream to a specific peer for media. + pub async fn open_media_stream( + &self, + peer_id: EndpointId, + kind: protocol::MediaKind, + ) -> Result<(iroh::endpoint::SendStream, iroh::endpoint::RecvStream)> { + let alpn = match kind { + protocol::MediaKind::Voice => VOICE_ALPN, + protocol::MediaKind::Camera => CAMERA_ALPN, + protocol::MediaKind::Screen => SCREEN_ALPN, + }; + let conn = self + .endpoint + .connect(peer_id, alpn) + .await + .context("Failed to connect for media stream")?; + let (send, recv) = conn.open_bi().await?; + Ok((send, recv)) + } + + /// Spawn a task that accepts incoming connections and dispatches them by ALPN. + fn spawn_acceptor(endpoint: Endpoint, gossip: Gossip, event_tx: mpsc::Sender) { + tokio::spawn(async move { + while let Some(incoming) = endpoint.accept().await { + let gossip = gossip.clone(); + let event_tx = event_tx.clone(); + + tokio::spawn(async move { + match incoming.await { + Ok(conn) => { + let alpn = conn.alpn().to_vec(); + let peer_id = conn.remote_id(); + + if alpn == iroh_gossip::ALPN { + // Dispatch to gossip + if let Err(e) = gossip.handle_connection(conn).await { + tracing::warn!("Gossip failed to handle connection: {}", e); + } + } else if alpn == FILE_TRANSFER_ALPN || alpn == VOICE_ALPN || alpn == CAMERA_ALPN || alpn == SCREEN_ALPN { + // Handle application protocols + match conn.accept_bi().await { + Ok((send, recv)) => { + let event = if alpn == FILE_TRANSFER_ALPN { + NetEvent::IncomingFileStream { + from: peer_id, + send, + recv, + } + } else { + let kind = if alpn == VOICE_ALPN { + protocol::MediaKind::Voice + } else if alpn == CAMERA_ALPN { + protocol::MediaKind::Camera + } else { + protocol::MediaKind::Screen + }; + NetEvent::IncomingMediaStream { + from: peer_id, + kind, + send, + recv, + } + }; + let _ = event_tx.send(event).await; + } + Err(e) => { + tracing::warn!("Failed to accept bi stream: {}", e); + } + } + } else { + tracing::warn!("Ignoring connection with unknown ALPN: {:?}", String::from_utf8_lossy(&alpn)); + } + } + Err(e) => { + tracing::warn!("Failed to establish connection: {}", e); + } + } + }); + } + }); + } + + /// Graceful shutdown. + pub async fn shutdown(self) -> Result<()> { + self.endpoint.close().await; + Ok(()) + } +} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs new file mode 100644 index 0000000..882f1dc --- /dev/null +++ b/src/protocol/mod.rs @@ -0,0 +1,225 @@ +//! Protocol message types for the P2P chat application. +//! +//! All messages are serialized with postcard and transmitted either via +//! iroh-gossip broadcast (chat, capabilities) or via dedicated QUIC streams (files). + +use serde::{Deserialize, Serialize}; + +/// Unique identifier for a file transfer. +pub type FileId = [u8; 16]; + +/// Generate a random file ID. +pub fn new_file_id() -> FileId { + let id = uuid::Uuid::new_v4(); + *id.as_bytes() +} + +// --------------------------------------------------------------------------- +// Gossip messages (broadcast to all peers) +// --------------------------------------------------------------------------- + +/// Top-level envelope for messages sent over the gossip broadcast channel. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum GossipMessage { + Chat(ChatMessage), + Capabilities(CapabilitiesMessage), + PeerAnnounce(PeerAnnounce), + FileOfferBroadcast(FileOfferBroadcast), + NameChange(NameChange), + /// Graceful disconnect notification. + Disconnect { sender_name: String }, +} + +/// A name change notification from a peer. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NameChange { + pub old_name: String, + pub new_name: String, +} + +/// A chat message from a peer. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChatMessage { + /// Display name of the sender. + pub sender_name: String, + /// Unix timestamp in milliseconds. + pub timestamp: u64, + /// UTF-8 message text. + pub text: String, +} + +/// Capabilities advertised by a peer. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CapabilitiesMessage { + pub sender_name: String, + pub chat: bool, + pub files: bool, + pub audio: bool, + pub camera: bool, + pub screen: bool, +} + +impl Default for CapabilitiesMessage { + fn default() -> Self { + Self { + sender_name: String::new(), + chat: true, + files: true, + audio: false, + camera: false, + screen: false, + } + } +} + +/// Announce a peer joining the swarm. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PeerAnnounce { + pub sender_name: String, +} + +/// Broadcast a file offer so the recipient can see it in chat. +/// Actual transfer happens over a dedicated stream. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileOfferBroadcast { + pub sender_name: String, + pub file_id: FileId, + pub file_name: String, + pub file_size: u64, +} + +// --------------------------------------------------------------------------- +// Stream messages (point-to-point over dedicated QUIC bi-streams) +// --------------------------------------------------------------------------- + +/// Messages sent over a dedicated file transfer QUIC stream. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(dead_code)] +pub enum FileStreamMessage { + Offer(FileOffer), + Accept(FileAcceptReject), + Reject(FileAcceptReject), + Chunk(FileChunk), + Done(FileDone), +} + +/// File offer sent at the start of a file transfer stream. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileOffer { + pub file_id: FileId, + pub name: String, + pub size: u64, + pub checksum: [u8; 32], +} + +/// Accept or reject a file offer. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(dead_code)] +pub struct FileAcceptReject { + pub file_id: FileId, +} + +/// A chunk of file data. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(dead_code)] +pub struct FileChunk { + pub file_id: FileId, + pub offset: u64, + pub data: Vec, +} + +/// Signal that a file transfer is complete. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(dead_code)] +pub struct FileDone { + pub file_id: FileId, +} + +// --------------------------------------------------------------------------- +// Media stream messages (point-to-point over dedicated QUIC bi-streams) +// --------------------------------------------------------------------------- + +/// Which kind of media a stream carries. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum MediaKind { + Voice, + Camera, + Screen, +} + +/// Messages sent over a dedicated media QUIC stream. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MediaStreamMessage { + /// Audio session start — tells the receiver the format. + AudioStart { + sample_rate: u32, + channels: u8, + frame_size_ms: u8, + }, + /// One Opus-encoded audio frame. + AudioData { + sequence: u64, + opus_data: Vec, + }, + /// Audio session ended. + AudioStop, + + /// Video session start (camera or screen). + VideoStart { + kind: MediaKind, + width: u32, + height: u32, + fps: u8, + }, + /// A video frame (MPEG-TS chunk). + VideoFrame { + sequence: u64, + timestamp_ms: u64, + data: Vec, + }, + /// Video session ended. + VideoStop { + kind: MediaKind, + }, +} + +// --------------------------------------------------------------------------- +// Serialization helpers (length-delimited framing) +// --------------------------------------------------------------------------- + +/// Serialize a message to bytes with a 4-byte big-endian length prefix. +#[allow(dead_code)] +pub fn encode_framed(msg: &T) -> anyhow::Result> { + let payload = postcard::to_allocvec(msg)?; + let len = payload.len() as u32; + let mut buf = Vec::with_capacity(4 + payload.len()); + buf.extend_from_slice(&len.to_be_bytes()); + buf.extend_from_slice(&payload); + Ok(buf) +} + +/// Read a length-delimited framed message from an async QUIC recv stream. +pub async fn decode_framed Deserialize<'de>>( + recv: &mut iroh::endpoint::RecvStream, +) -> anyhow::Result { + let mut len_buf = [0u8; 4]; + recv.read_exact(&mut len_buf).await?; + let len = u32::from_be_bytes(len_buf) as usize; + if len > 64 * 1024 * 1024 { + anyhow::bail!("Message too large: {} bytes", len); + } + let mut payload = vec![0u8; len]; + recv.read_exact(&mut payload).await?; + let msg: T = postcard::from_bytes(&payload)?; + Ok(msg) +} + +/// Write a length-delimited framed message to an async QUIC send stream. +pub async fn write_framed( + send: &mut iroh::endpoint::SendStream, + msg: &T, +) -> anyhow::Result<()> { + let data = encode_framed(msg)?; + send.write_all(&data).await?; + Ok(()) +} diff --git a/src/tui/chat_panel.rs b/src/tui/chat_panel.rs new file mode 100644 index 0000000..3ea2717 --- /dev/null +++ b/src/tui/chat_panel.rs @@ -0,0 +1,88 @@ +//! Chat panel widget — scrollable chat history. + +use ratatui::layout::Rect; +use ratatui::style::{Modifier, Style}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, Borders, List, ListItem}; +use ratatui::Frame; + +use crate::chat::{ChatEntry, ChatState}; + +use crate::tui::App; + +pub fn render(frame: &mut Frame, area: Rect, chat: &ChatState, app: &App) { + let block = Block::default() + .title(" 💬 Chat ") + .borders(Borders::ALL) + .border_style(Style::default().fg(app.theme.border)); + + let inner = block.inner(area); + frame.render_widget(block, area); + + if chat.history.is_empty() { + let empty = ratatui::widgets::Paragraph::new("No messages yet. Start typing!") + .style(Style::default().fg(app.theme.time)); + frame.render_widget(empty, inner); + return; + } + + let visible_height = inner.height as usize; + let total = chat.history.len(); + + // Calculate scroll position + let end = if app.scroll_offset >= total { + total + } else { + total - app.scroll_offset + }; + let start = end.saturating_sub(visible_height); + + let items: Vec = chat.history[start..end] + .iter() + .map(|entry| format_entry(entry, app)) + .collect(); + + let list = List::new(items); + frame.render_widget(list, inner); +} + +fn format_entry(entry: &ChatEntry, app: &App) -> ListItem<'static> { + let time = chrono::DateTime::from_timestamp_millis(entry.timestamp as i64) + .map(|dt| dt.format("%H:%M").to_string()) + .unwrap_or_default(); + + if entry.is_system { + let line = Line::from(vec![ + Span::styled( + format!("[{}] ", time), + Style::default().fg(app.theme.time), + ), + Span::styled( + format!("*** {} ***", entry.text), + Style::default() + .fg(app.theme.system_msg) + .add_modifier(Modifier::ITALIC), + ), + ]); + return ListItem::new(line); + } + + let name_color = if entry.is_self { + app.theme.self_name + } else { + app.theme.peer_name + }; + + let line = Line::from(vec![ + Span::styled(format!("[{}] ", time), Style::default().fg(app.theme.time)), + Span::styled( + format!("{}: ", entry.sender_name), + Style::default() + .fg(name_color) + .add_modifier(Modifier::BOLD), + ), + Span::styled(entry.text.clone(), Style::default().fg(app.theme.text)), + ]); + + ListItem::new(line) +} diff --git a/src/tui/file_panel.rs b/src/tui/file_panel.rs new file mode 100644 index 0000000..6bb3f9f --- /dev/null +++ b/src/tui/file_panel.rs @@ -0,0 +1,50 @@ +//! File transfer panel widget. + +use ratatui::layout::Rect; +use ratatui::style::{Color, Style}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, Borders, List, ListItem}; +use ratatui::Frame; + +use crate::file_transfer::{FileTransferManager, TransferState}; +use crate::tui::App; + +pub fn render(frame: &mut Frame, area: Rect, file_mgr: &FileTransferManager, app: &App) { + let transfers = file_mgr.active_transfers(); + + let block = Block::default() + .title(format!(" 📦 Transfers ({}) ", transfers.len())) + .borders(Borders::ALL) + .border_style(Style::default().fg(app.theme.border)); + + let inner = block.inner(area); + frame.render_widget(block, area); + + if transfers.is_empty() { + let empty = ratatui::widgets::Paragraph::new("No active transfers") + .style(Style::default().fg(app.theme.time)); + frame.render_widget(empty, inner); + return; + } + + let items: Vec = transfers + .iter() + .map(|info| { + let text = FileTransferManager::format_progress(info); + let color = match &info.state { + TransferState::Complete => Color::Green, + TransferState::Rejected | TransferState::Failed(_) => Color::Red, + TransferState::Transferring { .. } => Color::Cyan, + TransferState::Offering => Color::Yellow, + }; + // Note: Transfer colors are functional (state-based), so we keep them hardcoded for now or add to theme? + // "green", "red" etc are standard states. The user asked for "colour changing" which implies TUI theme. + // Let's keep functional state colors hardcoded as they convey specific meaning (success/error), + // but use theme for text/borders. + ListItem::new(Line::from(Span::styled(text, Style::default().fg(color)))) + }) + .collect(); + + let list = List::new(items); + frame.render_widget(list, inner); +} diff --git a/src/tui/input.rs b/src/tui/input.rs new file mode 100644 index 0000000..6306e2f --- /dev/null +++ b/src/tui/input.rs @@ -0,0 +1,64 @@ +//! Input bar widget with cursor. + +use ratatui::layout::Rect; +use ratatui::style::Style; +use ratatui::widgets::{Block, Borders, Paragraph}; +use ratatui::Frame; + +use super::{App, InputMode}; + +pub fn render(frame: &mut Frame, area: Rect, app: &App) { + let (title, border_color, content) = match app.input_mode { + InputMode::FilePrompt => ( + " 📁 File path (Enter to send, Esc to cancel) ", + app.theme.system_msg, + app.file_path_input.as_str(), + ), + InputMode::Editing => (" ✏ Message (Esc for commands) ", app.theme.self_name, app.input.as_str()), + InputMode::Normal => ( + " Press i/Enter to type, Q=quit, or use /help ", + app.theme.time, + app.input.as_str(), + ), + InputMode::MicSelect => ( + " 🎤 Selecting microphone... ", + app.theme.border, + "", + ), + InputMode::SpeakerSelect => ( + " 🔊 Selecting speaker... ", + app.theme.border, + "", + ), + }; + + let block = Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(Style::default().fg(border_color)); + + let paragraph = Paragraph::new(content.to_string()) + .style(Style::default().fg(app.theme.text)) + .block(block); + + frame.render_widget(paragraph, area); + + // Set cursor position when in editing/file mode + match app.input_mode { + InputMode::Editing => { + frame.set_cursor_position(( + area.x + 1 + app.cursor_position as u16, + area.y + 1, + )); + } + InputMode::FilePrompt => { + frame.set_cursor_position(( + area.x + 1 + app.file_path_input.len() as u16, + area.y + 1, + )); + } + InputMode::Normal => {} + InputMode::MicSelect => {} + InputMode::SpeakerSelect => {} + } +} diff --git a/src/tui/mod.rs b/src/tui/mod.rs new file mode 100644 index 0000000..35fdd1b --- /dev/null +++ b/src/tui/mod.rs @@ -0,0 +1,493 @@ +//! TUI module — terminal user interface using ratatui + crossterm. + +pub mod chat_panel; +pub mod peer_panel; +pub mod input; +pub mod status_bar; +pub mod file_panel; + +use std::path::PathBuf; + +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::style::{Modifier, Style}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, Borders, Clear, List, ListItem}; +use ratatui::Frame; + +use crate::chat::ChatState; +use crate::file_transfer::FileTransferManager; +use crate::media::MediaState; +use crate::media::voice::AudioDevice; +use crate::net::PeerInfo; + +/// Input mode for the TUI. +#[derive(Debug, Clone, PartialEq)] +pub enum InputMode { + Normal, + Editing, + #[allow(dead_code)] + FilePrompt, + MicSelect, + SpeakerSelect, +} + +/// Commands produced by TUI event handling. +#[derive(Debug)] +pub enum TuiCommand { + SendMessage(String), + /// Local-only system message (not broadcast to peers). + SystemMessage(String), + SendFile(PathBuf), + ChangeNick(String), + Connect(String), + ToggleVoice, + ToggleCamera, + ToggleScreen, + SelectMic(String), // node_name of selected mic + SelectSpeaker(String), // node_name of selected speaker + SetBitrate(u32), + Leave, + Quit, + None, +} + +use crate::config::Theme; +// ... imports ... + +/// Application state for the TUI. +pub struct App { + pub input: String, + pub input_mode: InputMode, + pub cursor_position: usize, + pub scroll_offset: usize, + #[allow(dead_code)] + pub show_file_panel: bool, + pub file_path_input: String, + pub theme: Theme, + // Device selection state (reused for Mic and Speaker) + pub audio_devices: Vec, + pub device_selected_index: usize, +} + +impl App { + pub fn new(theme: Theme) -> Self { + Self { + input: String::new(), + input_mode: InputMode::Editing, + cursor_position: 0, + scroll_offset: 0, + show_file_panel: true, + file_path_input: String::new(), + theme, + audio_devices: Vec::new(), + device_selected_index: 0, + } + } + + /// Open the mic selection screen. + pub fn open_mic_select(&mut self, sources: Vec) { + self.audio_devices = sources; + self.device_selected_index = 0; + self.input_mode = InputMode::MicSelect; + } + + /// Open the speaker selection screen. + pub fn open_speaker_select(&mut self, sinks: Vec) { + self.audio_devices = sinks; + self.device_selected_index = 0; + self.input_mode = InputMode::SpeakerSelect; + } + + /// Handle a key event and return a command. + pub fn handle_key(&mut self, key: KeyEvent) -> TuiCommand { + match self.input_mode { + InputMode::MicSelect => self.handle_device_select_key(key), + InputMode::SpeakerSelect => self.handle_device_select_key(key), + InputMode::FilePrompt => self.handle_file_prompt_key(key), + InputMode::Editing => self.handle_editing_key(key), + InputMode::Normal => self.handle_normal_key(key), + } + } + + fn handle_editing_key(&mut self, key: KeyEvent) -> TuiCommand { + match key.code { + KeyCode::Enter => { + if !self.input.is_empty() { + let text: String = self.input.drain(..).collect(); + self.cursor_position = 0; + + // Parse slash commands + if let Some(cmd) = text.strip_prefix('/') { + let parts: Vec<&str> = cmd.splitn(2, ' ').collect(); + match parts[0] { + "nick" | "name" => { + let new_name = parts.get(1).unwrap_or(&"").trim(); + if new_name.is_empty() { + return TuiCommand::SystemMessage( + "Usage: /nick ".to_string(), + ); + } + return TuiCommand::ChangeNick(new_name.to_string()); + } + "connect" | "join" => { + let peer_id = parts.get(1).unwrap_or(&"").trim(); + if peer_id.is_empty() { + return TuiCommand::SystemMessage( + "Usage: /connect ".to_string(), + ); + } + return TuiCommand::Connect(peer_id.to_string()); + } + "voice" => return TuiCommand::ToggleVoice, + "mic" | "microphone" => { + // Open mic selection screen + let sources = crate::media::voice::list_audio_sources(); + if sources.is_empty() { + return TuiCommand::SystemMessage( + "🎤 No audio sources found (is PipeWire running?)".to_string(), + ); + } + self.open_mic_select(sources); + return TuiCommand::None; + } + "speaker" | "output" => { + // Open speaker selection screen + let sinks = crate::media::voice::list_audio_sinks(); + if sinks.is_empty() { + return TuiCommand::SystemMessage( + "🔊 No audio outputs found (is PipeWire running?)".to_string(), + ); + } + self.open_speaker_select(sinks); + return TuiCommand::None; + } + "camera" | "cam" => return TuiCommand::ToggleCamera, + "screen" | "share" => return TuiCommand::ToggleScreen, + "file" | "send" => { + let path = parts.get(1).unwrap_or(&"").trim(); + if path.is_empty() { + // Open native file dialog + use std::process::Command; + let result = Command::new("zenity") + .args(["--file-selection", "--title=Select file to send"]) + .output() + .or_else(|_| { + Command::new("kdialog") + .args(["--getopenfilename", "."]) + .output() + }); + match result { + Ok(output) if output.status.success() => { + let chosen = String::from_utf8_lossy(&output.stdout) + .trim() + .to_string(); + if !chosen.is_empty() { + return TuiCommand::SendFile(PathBuf::from(chosen)); + } + return TuiCommand::None; // cancelled + } + _ => { + return TuiCommand::SystemMessage( + "No file dialog available. Use: /file ".to_string(), + ); + } + } + } + return TuiCommand::SendFile(PathBuf::from(path)); + } + "quit" | "q" => return TuiCommand::Quit, + "leave" => return TuiCommand::Leave, + "help" => { + return TuiCommand::SystemMessage( + "Commands: /nick , /connect , /voice, /bitrate , /mic, /camera, /screen, /file , /leave, /quit".to_string(), + ); + } + "bitrate" => { + let kbps_str = parts.get(1).unwrap_or(&"").trim(); + if let Ok(kbps) = kbps_str.parse::() { + return TuiCommand::SetBitrate(kbps * 1000); + } else { + return TuiCommand::SystemMessage("Usage: /bitrate (e.g. 128)".to_string()); + } + } + _ => { + return TuiCommand::SystemMessage( + format!("Unknown command: /{}. Type /help", parts[0]), + ); + } + } + } + + return TuiCommand::SendMessage(text); + } + TuiCommand::None + } + KeyCode::Char(c) => { + self.input.insert(self.cursor_position, c); + self.cursor_position += 1; + TuiCommand::None + } + KeyCode::Backspace => { + if self.cursor_position > 0 { + self.cursor_position -= 1; + self.input.remove(self.cursor_position); + } + TuiCommand::None + } + KeyCode::Delete => { + if self.cursor_position < self.input.len() { + self.input.remove(self.cursor_position); + } + TuiCommand::None + } + KeyCode::Left => { + if self.cursor_position > 0 { + self.cursor_position -= 1; + } + TuiCommand::None + } + KeyCode::Right => { + if self.cursor_position < self.input.len() { + self.cursor_position += 1; + } + TuiCommand::None + } + KeyCode::Home => { + self.cursor_position = 0; + TuiCommand::None + } + KeyCode::End => { + self.cursor_position = self.input.len(); + TuiCommand::None + } + KeyCode::Esc => { + self.input_mode = InputMode::Normal; + TuiCommand::None + } + KeyCode::Up => { + self.scroll_offset = self.scroll_offset.saturating_add(1); + TuiCommand::None + } + KeyCode::Down => { + self.scroll_offset = self.scroll_offset.saturating_sub(1); + TuiCommand::None + } + _ => TuiCommand::None, + } + } + + fn handle_normal_key(&mut self, key: KeyEvent) -> TuiCommand { + match key.code { + KeyCode::Char('q') | KeyCode::Char('Q') => TuiCommand::Quit, + KeyCode::Char('/') => { + self.input_mode = InputMode::Editing; + self.input.push('/'); + self.cursor_position = 1; + TuiCommand::None + } + KeyCode::Char('i') | KeyCode::Enter => { + self.input_mode = InputMode::Editing; + TuiCommand::None + } + KeyCode::Up => { + self.scroll_offset = self.scroll_offset.saturating_add(1); + TuiCommand::None + } + KeyCode::Down => { + self.scroll_offset = self.scroll_offset.saturating_sub(1); + TuiCommand::None + } + _ => TuiCommand::None, + } + } + + fn handle_file_prompt_key(&mut self, key: KeyEvent) -> TuiCommand { + match key.code { + KeyCode::Enter => { + if !self.file_path_input.is_empty() { + let path = PathBuf::from(self.file_path_input.drain(..).collect::()); + self.input_mode = InputMode::Editing; + return TuiCommand::SendFile(path); + } + self.input_mode = InputMode::Editing; + TuiCommand::None + } + KeyCode::Char(c) => { + self.file_path_input.push(c); + TuiCommand::None + } + KeyCode::Backspace => { + self.file_path_input.pop(); + TuiCommand::None + } + KeyCode::Esc => { + self.file_path_input.clear(); + self.input_mode = InputMode::Editing; + TuiCommand::None + } + _ => TuiCommand::None, + } + } + + fn handle_device_select_key(&mut self, key: KeyEvent) -> TuiCommand { + match key.code { + KeyCode::Up | KeyCode::Char('k') => { + if self.device_selected_index > 0 { + self.device_selected_index -= 1; + } + TuiCommand::None + } + KeyCode::Down | KeyCode::Char('j') => { + if self.device_selected_index + 1 < self.audio_devices.len() { + self.device_selected_index += 1; + } + TuiCommand::None + } + KeyCode::Enter => { + if let Some(dev) = self.audio_devices.get(self.device_selected_index) { + let node_name = dev.node_name.clone(); + let mode = self.input_mode.clone(); + self.input_mode = InputMode::Editing; + self.audio_devices.clear(); + + if mode == InputMode::MicSelect { + return TuiCommand::SelectMic(node_name); + } else { + return TuiCommand::SelectSpeaker(node_name); + } + } + self.input_mode = InputMode::Editing; + TuiCommand::None + } + KeyCode::Esc | KeyCode::Char('q') => { + self.input_mode = InputMode::Editing; + self.audio_devices.clear(); + TuiCommand::None + } + _ => TuiCommand::None, + } + } +} + +/// Render the full application UI. +pub fn render( + frame: &mut Frame, + app: &App, + chat: &ChatState, + file_mgr: &FileTransferManager, + media: &MediaState, + peers: &[PeerInfo], + our_name: &str, + our_id_short: &str, + connected: bool, +) { + let size = frame.area(); + + // Main layout: top (content) + bottom (status bar + input) + let main_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Min(5), // Content area + Constraint::Length(3), // Input bar + Constraint::Length(1), // Status bar + ]) + .split(size); + + // Content area: left (chat) + right (peers + files) + let content_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(75), // Chat panel + Constraint::Percentage(25), // Side panel + ]) + .split(main_chunks[0]); + + // Side panel: peers + file transfers + let side_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(50), // Peer list + Constraint::Percentage(50), // File transfers + ]) + .split(content_chunks[1]); + + // Render chat panel + chat_panel::render(frame, content_chunks[0], chat, app); + + // Render peer list + peer_panel::render(frame, side_chunks[0], peers, app); + + // Render file transfer panel + file_panel::render(frame, side_chunks[1], file_mgr, app); + + // Render input bar + input::render(frame, main_chunks[1], app); + + // Render status bar + status_bar::render( + frame, + main_chunks[2], + media, + our_name, + our_id_short, + connected, + &app.input_mode, + ); + + // Render device selection overlay if active + if app.input_mode == InputMode::MicSelect || app.input_mode == InputMode::SpeakerSelect { + render_device_overlay(frame, size, app); + } +} + +/// Render the device selection overlay (centered popup). +fn render_device_overlay(frame: &mut Frame, area: Rect, app: &App) { + let popup_width = 60u16.min(area.width.saturating_sub(4)); + let popup_height = (app.audio_devices.len() as u16 + 4).min(area.height.saturating_sub(4)); + + let x = (area.width.saturating_sub(popup_width)) / 2; + let y = (area.height.saturating_sub(popup_height)) / 2; + + let popup_area = Rect::new(x, y, popup_width, popup_height); + + // Clear the background + frame.render_widget(Clear, popup_area); + + let title = if app.input_mode == InputMode::MicSelect { + " 🎤 Select Microphone (↑↓ Enter Esc) " + } else { + " 🔊 Select Speaker (↑↓ Enter Esc) " + }; + + let block = Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(Style::default().fg(app.theme.border)); + + let inner = block.inner(popup_area); + frame.render_widget(block, popup_area); + + let items: Vec = app + .audio_devices + .iter() + .enumerate() + .map(|(i, dev)| { + let marker = if i == app.device_selected_index { "▶ " } else { " " }; + let style = if i == app.device_selected_index { + Style::default() + .fg(app.theme.self_name) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(app.theme.text) + }; + ListItem::new(Line::from(Span::styled( + format!("{}{}", marker, dev.description), + style, + ))) + }) + .collect(); + + let list = List::new(items); + frame.render_widget(list, inner); +} diff --git a/src/tui/peer_panel.rs b/src/tui/peer_panel.rs new file mode 100644 index 0000000..f895d41 --- /dev/null +++ b/src/tui/peer_panel.rs @@ -0,0 +1,83 @@ +//! Peer list panel widget. + +use ratatui::layout::Rect; +use ratatui::style::{Color, Modifier, Style}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, Borders, List, ListItem}; +use ratatui::Frame; + +use crate::net::PeerInfo; + +use crate::tui::App; + +pub fn render(frame: &mut Frame, area: Rect, peers: &[PeerInfo], app: &App) { + let block = Block::default() + .title(format!(" 👥 Peers ({}) ", peers.len())) + .borders(Borders::ALL) + .border_style(Style::default().fg(app.theme.border)); + + let inner = block.inner(area); + frame.render_widget(block, area); + + if peers.is_empty() { + let empty = ratatui::widgets::Paragraph::new("No peers connected") + .style(Style::default().fg(Color::DarkGray)); + frame.render_widget(empty, inner); + return; + } + + let items: Vec = peers + .iter() + .map(|peer| { + let name = peer + .name + .as_deref() + .unwrap_or("unknown"); + let id_short = format!("{}", peer.id).chars().take(8).collect::(); + + let caps = peer + .capabilities + .as_ref() + .map(|c| { + let mut s = String::new(); + if c.audio { + s.push_str("🎤"); + } + if c.camera { + s.push_str("📷"); + } + if c.screen { + s.push_str("🖥"); + } + s + }) + .unwrap_or_default(); + + let (marker, marker_color, name_suffix) = if peer.is_self { + ("★ ", app.theme.self_name, " (you)") + } else { + ("● ", app.theme.peer_name, "") + }; + + let line = Line::from(vec![ + Span::styled(marker, Style::default().fg(marker_color)), + Span::styled( + name.to_string(), + Style::default() + .fg(app.theme.text) + .add_modifier(Modifier::BOLD), + ), + Span::styled( + format!("{} ({})", name_suffix, id_short), + Style::default().fg(app.theme.time), + ), + Span::raw(format!(" {}", caps)), + ]); + + ListItem::new(line) + }) + .collect(); + + let list = List::new(items); + frame.render_widget(list, inner); +} diff --git a/src/tui/status_bar.rs b/src/tui/status_bar.rs new file mode 100644 index 0000000..568c785 --- /dev/null +++ b/src/tui/status_bar.rs @@ -0,0 +1,54 @@ +//! Status bar widget. + +use ratatui::layout::Rect; +use ratatui::style::{Color, Style}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::Paragraph; +use ratatui::Frame; + +use crate::media::MediaState; +use super::InputMode; + +pub fn render( + frame: &mut Frame, + area: Rect, + media: &MediaState, + our_name: &str, + our_id_short: &str, + connected: bool, + input_mode: &InputMode, +) { + let conn_status = if connected { + Span::styled("● Connected", Style::default().fg(Color::Green)) + } else { + Span::styled("○ Waiting for peers", Style::default().fg(Color::Yellow)) + }; + + let mode_span = match input_mode { + InputMode::Normal => Span::styled(" NORMAL ", Style::default().fg(Color::Black).bg(Color::Blue)), + InputMode::Editing => Span::styled(" INSERT ", Style::default().fg(Color::Black).bg(Color::Green)), + InputMode::FilePrompt => Span::styled(" FILE ", Style::default().fg(Color::Black).bg(Color::Yellow)), + InputMode::MicSelect => Span::styled(" MIC ", Style::default().fg(Color::Black).bg(Color::Magenta)), + InputMode::SpeakerSelect => Span::styled(" SPKR ", Style::default().fg(Color::Black).bg(Color::Cyan)), + }; + + let line = Line::from(vec![ + Span::raw(" "), + mode_span, + Span::raw(" "), + conn_status, + Span::styled( + format!(" │ {} ({})", our_name, our_id_short), + Style::default().fg(Color::Cyan), + ), + Span::styled( + format!(" │ {}", media.status_line()), + Style::default().fg(Color::DarkGray), + ), + ]); + + let paragraph = Paragraph::new(line) + .style(Style::default().bg(Color::Rgb(30, 30, 40))); + + frame.render_widget(paragraph, area); +} diff --git a/src/web/mod.rs b/src/web/mod.rs new file mode 100644 index 0000000..3573c47 --- /dev/null +++ b/src/web/mod.rs @@ -0,0 +1,166 @@ +use axum::{ + extract::{ + ws::{Message, WebSocket, WebSocketUpgrade}, + State, + }, + http::{header, StatusCode, Uri}, + body::Bytes, + response::IntoResponse, + routing::get, + Router, +}; +use rust_embed::RustEmbed; +use std::net::SocketAddr; + +use crate::media::WebMediaEvent; +use crate::protocol::MediaKind; +use tokio::sync::broadcast; +use futures::{SinkExt, StreamExt}; + +#[derive(Clone)] +struct AppState { + tx: broadcast::Sender, + mic_tx: broadcast::Sender>, + cam_tx: broadcast::Sender>, + screen_tx: broadcast::Sender>, +} + +#[derive(RustEmbed)] +#[folder = "web/"] +struct Assets; + +pub async fn start_web_server( + tx: broadcast::Sender, + mic_tx: broadcast::Sender>, + cam_tx: broadcast::Sender>, + screen_tx: broadcast::Sender>, +) { + let state = AppState { tx, mic_tx, cam_tx, screen_tx }; + let app = Router::new() + .route("/ws", get(ws_handler)) + .fallback(static_handler) + .with_state(state); + + let mut port = 6969; + let listener = loop { + let addr = SocketAddr::from(([127, 0, 0, 1], port)); + match tokio::net::TcpListener::bind(addr).await { + Ok(l) => { + tracing::info!("Web interface listening on http://{}", addr); + break l; + } + Err(_) => { + port += 1; + if port > 7000 { + tracing::error!("Failed to bind web interface to any port 6969-7000"); + return; + } + } + } + }; + axum::serve(listener, app).await.unwrap(); +} + +async fn ws_handler( + ws: WebSocketUpgrade, + State(state): State, +) -> impl IntoResponse { + ws.on_upgrade(move |socket| handle_socket(socket, state)) +} + +async fn handle_socket(socket: WebSocket, state: AppState) { + let (mut sender, mut receiver) = socket.split(); + let mut rx = state.tx.subscribe(); + + tokio::spawn(async move { + while let Ok(event) = rx.recv().await { + let msg = match event { + WebMediaEvent::Audio { peer_id, data: samples } => { + // 1 byte header (0) + 1 byte ID len + ID bytes + f32 bytes + let id_bytes = peer_id.as_bytes(); + let id_len = id_bytes.len() as u8; + let mut payload = Vec::with_capacity(1 + 1 + id_bytes.len() + samples.len() * 4); + payload.push(0u8); + payload.push(id_len); + payload.extend_from_slice(id_bytes); + for s in samples { + payload.extend_from_slice(&s.to_ne_bytes()); + } + Message::Binary(Bytes::from(payload)) + } + WebMediaEvent::Video { peer_id, kind, data } => { + // 1 byte header (1=Camera, 2=Screen) + 1 byte ID len + ID bytes + MJPEG data + let header = match kind { + MediaKind::Camera => 1u8, + MediaKind::Screen => 2u8, + _ => 1u8, + }; + let id_bytes = peer_id.as_bytes(); + let id_len = id_bytes.len() as u8; + + let mut payload = Vec::with_capacity(1 + 1 + id_bytes.len() + data.len()); + payload.push(header); + payload.push(id_len); + payload.extend_from_slice(id_bytes); + payload.extend_from_slice(&data); + Message::Binary(Bytes::from(payload)) + } + }; + + if sender.send(msg).await.is_err() { + break; + } + } + }); + + while let Some(msg) = receiver.next().await { + match msg { + Ok(Message::Binary(data)) => { + if data.is_empty() { continue; } + let header = data[0]; + let payload = &data[1..]; + + match header { + 3 => { // Mic (f32 PCM) + // integrity check + if payload.len() % 4 == 0 { + let samples: Vec = payload + .chunks_exact(4) + .map(|b| f32::from_ne_bytes([b[0], b[1], b[2], b[3]])) + .collect(); + // tracing::debug!("Received mic samples: {}", samples.len()); + let _ = state.mic_tx.send(samples); + } + } + 4 => { // Camera (MJPEG) + tracing::debug!("Received camera frame: {} bytes", payload.len()); + let _ = state.cam_tx.send(payload.to_vec()); + } + 5 => { // Screen (MJPEG) + tracing::debug!("Received screen frame: {} bytes", payload.len()); + let _ = state.screen_tx.send(payload.to_vec()); + } + _ => { + tracing::warn!("Unknown WS header: {}", header); + } + } + } + Ok(Message::Close(_)) => break, + Err(_) => break, + _ => {} + } + } +} + +async fn static_handler(uri: Uri) -> impl IntoResponse { + let path = uri.path().trim_start_matches('/'); + let path = if path.is_empty() { "index.html" } else { path }; + + match Assets::get(path) { + Some(content) => { + let mime = mime_guess::from_path(path).first_or_octet_stream(); + ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response() + } + None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(), + } +} diff --git a/tests/network_tests.rs b/tests/network_tests.rs new file mode 100644 index 0000000..dd6e2bd --- /dev/null +++ b/tests/network_tests.rs @@ -0,0 +1,180 @@ +use anyhow::{Context, Result}; +use p2p_chat::net::{NetEvent, NetworkManager}; +use p2p_chat::protocol::{ChatMessage, GossipMessage, MediaKind}; + +use tokio::time::{timeout, Duration}; + +const TEST_TIMEOUT: Duration = Duration::from_secs(10); + +async fn spawn_node(topic: [u8; 32]) -> Result<(NetworkManager, tokio::sync::mpsc::Sender, tokio::sync::mpsc::Receiver)> { + NetworkManager::new(topic).await +} + +#[tokio::test] +async fn test_p2p_connection_and_gossip() -> Result<()> { + // 0. Setup + let topic = [0u8; 32]; // Consistent topic for this test + let (mut node_a, tx_a, mut rx_a) = spawn_node(topic).await?; + let (mut node_b, tx_b, mut rx_b) = spawn_node(topic).await?; + + let addr_a = node_a.endpoint.addr(); + let id_a = addr_a.id; + let id_b = node_b.endpoint.id(); + + println!("Node A: {}", id_a); + println!("Node B: {}", id_b); + + // 1. Join Gossip + // Node A starts alone + node_a.join_gossip(vec![], tx_a.clone()).await?; + + // Node B joins, bootstrapping from Node A. + // We force a connection first so B knows A's address. + node_b.endpoint.connect(addr_a, iroh_gossip::ALPN).await?; + node_b.join_gossip(vec![id_a], tx_b.clone()).await?; + + // 2. Wait for connection (PeerUp) + // Both sides should see each other + let _peer_up_a = timeout(TEST_TIMEOUT, async { + while let Some(event) = rx_a.recv().await { + if let NetEvent::PeerUp(peer_id) = event { + if peer_id == id_b { return Ok(()); } + } + } + anyhow::bail!("Stream ended without PeerUp"); + }).await??; + + let _peer_up_b = timeout(TEST_TIMEOUT, async { + while let Some(event) = rx_b.recv().await { + if let NetEvent::PeerUp(peer_id) = event { + if peer_id == id_a { return Ok(()); } + } + } + anyhow::bail!("Stream ended without PeerUp"); + }).await??; + + // 3. Test Gossip Broadcast (Chat Message) + let chat_msg = GossipMessage::Chat(ChatMessage { + sender_name: "Node A".into(), + timestamp: 12345, + text: "Hello Node B!".into(), + }); + + node_a.broadcast(&chat_msg).await?; + + // Node B should receive it + timeout(TEST_TIMEOUT, async { + while let Some(event) = rx_b.recv().await { + if let NetEvent::GossipReceived { from, message } = event { + assert_eq!(from, id_a); + if let GossipMessage::Chat(msg) = message { + assert_eq!(msg.text, "Hello Node B!"); + assert_eq!(msg.sender_name, "Node A"); + return Ok(()); + } + } + } + anyhow::bail!("Stream ended without GossipReceived"); + }).await??; + + Ok(()) +} + +#[tokio::test] +async fn test_direct_file_transfer_stream() -> Result<()> { + // 0. Setup and Connect + let topic = [1u8; 32]; + let (mut node_a, tx_a, _rx_a) = spawn_node(topic).await?; + let (mut node_b, tx_b, mut rx_b) = spawn_node(topic).await?; + + let addr_a = node_a.endpoint.addr(); + let id_a = addr_a.id; + let id_b = node_b.endpoint.id(); + + node_a.join_gossip(vec![], tx_a).await?; + node_b.endpoint.connect(addr_a, iroh_gossip::ALPN).await?; + node_b.join_gossip(vec![id_a], tx_b).await?; + + // 1. Open direct stream from A to B + // node_b needs to accept the stream. In the actual app, this is handled by spawn_acceptor loop + // sending NetEvent::IncomingFileStream. + + // A opens stream + let (mut send_stream, mut _recv_stream) = node_a.open_file_stream(id_b).await?; + + // Send some data + let test_data = b"Hello direct stream"; + send_stream.write_all(test_data).await?; + send_stream.finish()?; + + // 2. B should receive IncomingFileStream event + let (mut b_recv_stream, from) = timeout(TEST_TIMEOUT, async { + while let Some(event) = rx_b.recv().await { + if let NetEvent::IncomingFileStream { from, recv, .. } = event { + return Ok((recv, from)); + } + } + anyhow::bail!("Stream ended without IncomingFileStream"); + }).await??; + + assert_eq!(from, id_a); + + // 3. Read data on B side + let mut buf = vec![0u8; test_data.len()]; + b_recv_stream.read_exact(&mut buf).await?; + assert_eq!(buf, test_data); + + Ok(()) +} + +#[tokio::test] +async fn test_media_stream_separation() -> Result<()> { + // 0. Setup and Connect + let topic = [2u8; 32]; + let (mut node_a, tx_a, _rx_a) = spawn_node(topic).await?; + let (mut node_b, tx_b, mut rx_b) = spawn_node(topic).await?; + + let addr_a = node_a.endpoint.addr(); + let id_a = addr_a.id; + let id_b = node_b.endpoint.id(); + + node_a.join_gossip(vec![], tx_a).await?; + node_b.endpoint.connect(addr_a, iroh_gossip::ALPN).await?; + node_b.join_gossip(vec![id_a], tx_b).await?; + + // Helper to verify stream type + async fn verify_stream( + node_sender: &NetworkManager, + peer_id: p2p_chat::net::EndpointId, + rx_receiver: &mut tokio::sync::mpsc::Receiver, + kind: MediaKind, + ) -> Result<()> { + // Sender opens stream + let (mut send, _recv) = node_sender.open_media_stream(peer_id, kind).await?; + send.finish()?; + + // Receiver should get IncomingMediaStream with correct kind + timeout(TEST_TIMEOUT, async { + while let Some(event) = rx_receiver.recv().await { + if let NetEvent::IncomingMediaStream { from: _, kind: k, .. } = event { + assert_eq!(k, kind); + return Ok(()); + } + } + anyhow::bail!("Stream ended without IncomingMediaStream for {:?}", kind); + }).await??; + + Ok(()) + } + + // 1. Verify Voice Stream + verify_stream(&node_a, id_b, &mut rx_b, MediaKind::Voice).await.context("Voice stream failed")?; + + // 2. Verify Camera Stream + verify_stream(&node_a, id_b, &mut rx_b, MediaKind::Camera).await.context("Camera stream failed")?; + + // 3. Verify Screen Stream + verify_stream(&node_a, id_b, &mut rx_b, MediaKind::Screen).await.context("Screen stream failed")?; + + Ok(()) +} diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..6b95aef --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,627 @@ +//! Integration tests for p2p-chat +//! +//! Covers: +//! - Protocol message serialization roundtrips +//! - Config defaults, TOML parsing, and color parsing +//! - TUI App command dispatch & key handling +//! - ChatState history management + +// ============================================================================ +// Protocol tests +// ============================================================================ +mod protocol_tests { + use p2p_chat::protocol::*; + + #[test] + fn chat_message_roundtrip() { + let msg = GossipMessage::Chat(ChatMessage { + sender_name: "alice".into(), + timestamp: 1234567890, + text: "Hello, world!".into(), + }); + let bytes = postcard::to_allocvec(&msg).unwrap(); + let decoded: GossipMessage = postcard::from_bytes(&bytes).unwrap(); + match decoded { + GossipMessage::Chat(m) => { + assert_eq!(m.sender_name, "alice"); + assert_eq!(m.timestamp, 1234567890); + assert_eq!(m.text, "Hello, world!"); + } + _ => panic!("Expected Chat variant"), + } + } + + #[test] + fn disconnect_message_roundtrip() { + let msg = GossipMessage::Disconnect { + sender_name: "bob".into(), + }; + let bytes = postcard::to_allocvec(&msg).unwrap(); + let decoded: GossipMessage = postcard::from_bytes(&bytes).unwrap(); + match decoded { + GossipMessage::Disconnect { sender_name } => { + assert_eq!(sender_name, "bob"); + } + _ => panic!("Expected Disconnect variant"), + } + } + + #[test] + fn name_change_roundtrip() { + let msg = GossipMessage::NameChange(NameChange { + old_name: "anon".into(), + new_name: "alice".into(), + }); + let bytes = postcard::to_allocvec(&msg).unwrap(); + let decoded: GossipMessage = postcard::from_bytes(&bytes).unwrap(); + match decoded { + GossipMessage::NameChange(nc) => { + assert_eq!(nc.old_name, "anon"); + assert_eq!(nc.new_name, "alice"); + } + _ => panic!("Expected NameChange variant"), + } + } + + #[test] + fn peer_announce_roundtrip() { + let msg = GossipMessage::PeerAnnounce(PeerAnnounce { + sender_name: "charlie".into(), + }); + let bytes = postcard::to_allocvec(&msg).unwrap(); + let decoded: GossipMessage = postcard::from_bytes(&bytes).unwrap(); + match decoded { + GossipMessage::PeerAnnounce(pa) => { + assert_eq!(pa.sender_name, "charlie"); + } + _ => panic!("Expected PeerAnnounce variant"), + } + } + + #[test] + fn capabilities_default() { + let caps = CapabilitiesMessage::default(); + assert!(caps.chat); + assert!(caps.files); + assert!(!caps.audio); + assert!(!caps.camera); + assert!(!caps.screen); + assert_eq!(caps.sender_name, ""); + } + + #[test] + fn file_offer_broadcast_roundtrip() { + let fid = new_file_id(); + let msg = GossipMessage::FileOfferBroadcast(FileOfferBroadcast { + sender_name: "dave".into(), + file_id: fid, + file_name: "test.txt".into(), + file_size: 42, + }); + let bytes = postcard::to_allocvec(&msg).unwrap(); + let decoded: GossipMessage = postcard::from_bytes(&bytes).unwrap(); + match decoded { + GossipMessage::FileOfferBroadcast(f) => { + assert_eq!(f.sender_name, "dave"); + assert_eq!(f.file_id, fid); + assert_eq!(f.file_name, "test.txt"); + assert_eq!(f.file_size, 42); + } + _ => panic!("Expected FileOfferBroadcast variant"), + } + } + + #[test] + fn encode_framed_produces_valid_length_prefix() { + let msg = ChatMessage { + sender_name: "test".into(), + timestamp: 0, + text: "hi".into(), + }; + let framed = encode_framed(&msg).unwrap(); + assert!(framed.len() > 4); + let len = u32::from_be_bytes([framed[0], framed[1], framed[2], framed[3]]) as usize; + assert_eq!(len, framed.len() - 4); + // Verify the payload can be deserialized + let decoded: ChatMessage = postcard::from_bytes(&framed[4..]).unwrap(); + assert_eq!(decoded.sender_name, "test"); + } + + #[test] + fn file_id_is_unique() { + let id1 = new_file_id(); + let id2 = new_file_id(); + assert_ne!(id1, id2); + } + + #[test] + fn media_kind_serialization() { + for kind in [MediaKind::Voice, MediaKind::Camera, MediaKind::Screen] { + let bytes = postcard::to_allocvec(&kind).unwrap(); + let decoded: MediaKind = postcard::from_bytes(&bytes).unwrap(); + assert_eq!(decoded, kind); + } + } + + #[test] + fn media_stream_message_roundtrip() { + let msgs = vec![ + MediaStreamMessage::AudioStart { + sample_rate: 48000, + channels: 1, + frame_size_ms: 20, + }, + MediaStreamMessage::AudioData { + sequence: 42, + opus_data: vec![0xDE, 0xAD], + }, + MediaStreamMessage::AudioStop, + MediaStreamMessage::VideoStart { + kind: MediaKind::Screen, + width: 1920, + height: 1080, + fps: 30, + }, + MediaStreamMessage::VideoFrame { + sequence: 1, + timestamp_ms: 1000, + data: vec![0x01, 0x02, 0x03], + }, + MediaStreamMessage::VideoStop { + kind: MediaKind::Camera, + }, + ]; + for msg in msgs { + let bytes = postcard::to_allocvec(&msg).unwrap(); + let _decoded: MediaStreamMessage = postcard::from_bytes(&bytes).unwrap(); + } + } +} + +// ============================================================================ +// Config tests +// ============================================================================ +mod config_tests { + use p2p_chat::config::*; + use ratatui::style::Color; + + #[test] + fn default_config_values() { + let config = AppConfig::default(); + assert_eq!(config.media.screen_resolution, "1280x720"); + assert!(config.media.mic_name.is_none()); + assert!(config.network.topic.is_none()); + } + + #[test] + fn config_toml_roundtrip() { + let config = AppConfig::default(); + let toml_str = toml::to_string_pretty(&config).unwrap(); + let parsed: AppConfig = toml::from_str(&toml_str).unwrap(); + assert_eq!(parsed.media.screen_resolution, config.media.screen_resolution); + assert_eq!(parsed.media.mic_name, config.media.mic_name); + assert_eq!(parsed.network.topic, config.network.topic); + } + + #[test] + fn config_partial_toml_uses_defaults() { + let toml_str = r#" +[media] +screen_resolution = "1920x1080" +"#; + let config: AppConfig = toml::from_str(toml_str).unwrap(); + assert_eq!(config.media.screen_resolution, "1920x1080"); + // network should use default + assert!(config.network.topic.is_none()); + } + + #[test] + fn parse_color_named_colors() { + assert_eq!(parse_color("red"), Color::Red); + assert_eq!(parse_color("green"), Color::Green); + assert_eq!(parse_color("blue"), Color::Blue); + assert_eq!(parse_color("cyan"), Color::Cyan); + assert_eq!(parse_color("magenta"), Color::Magenta); + assert_eq!(parse_color("yellow"), Color::Yellow); + assert_eq!(parse_color("white"), Color::White); + assert_eq!(parse_color("black"), Color::Black); + assert_eq!(parse_color("gray"), Color::Gray); + assert_eq!(parse_color("dark_gray"), Color::DarkGray); + assert_eq!(parse_color("darkgray"), Color::DarkGray); + } + + #[test] + fn parse_color_case_insensitive() { + assert_eq!(parse_color("RED"), Color::Red); + assert_eq!(parse_color("Green"), Color::Green); + assert_eq!(parse_color("BLUE"), Color::Blue); + } + + #[test] + fn parse_color_hex_6digit() { + assert_eq!(parse_color("#ff0000"), Color::Rgb(255, 0, 0)); + assert_eq!(parse_color("#00ff00"), Color::Rgb(0, 255, 0)); + assert_eq!(parse_color("#0000ff"), Color::Rgb(0, 0, 255)); + assert_eq!(parse_color("#ffffff"), Color::Rgb(255, 255, 255)); + assert_eq!(parse_color("#000000"), Color::Rgb(0, 0, 0)); + } + + #[test] + fn parse_color_hex_3digit() { + assert_eq!(parse_color("#f00"), Color::Rgb(255, 0, 0)); + assert_eq!(parse_color("#0f0"), Color::Rgb(0, 255, 0)); + assert_eq!(parse_color("#00f"), Color::Rgb(0, 0, 255)); + assert_eq!(parse_color("#fff"), Color::Rgb(255, 255, 255)); + } + + #[test] + fn parse_color_unknown_falls_back_to_white() { + assert_eq!(parse_color("nonexistent"), Color::White); + assert_eq!(parse_color(""), Color::White); + } + + #[test] + fn theme_from_ui_config() { + let ui = UiConfig::default(); + let theme: Theme = ui.into(); + assert_eq!(theme.border, Color::Cyan); + assert_eq!(theme.text, Color::White); + assert_eq!(theme.self_name, Color::Green); + assert_eq!(theme.peer_name, Color::Magenta); + assert_eq!(theme.system_msg, Color::Yellow); + assert_eq!(theme.time, Color::DarkGray); + } + + #[test] + fn ui_config_defaults() { + let ui = UiConfig::default(); + assert_eq!(ui.border, "cyan"); + assert_eq!(ui.text, "white"); + assert_eq!(ui.self_name, "green"); + assert_eq!(ui.peer_name, "magenta"); + assert_eq!(ui.system_msg, "yellow"); + assert_eq!(ui.time, "dark_gray"); + } +} + +// ============================================================================ +// TUI key handling tests +// ============================================================================ +mod tui_tests { + use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + use p2p_chat::config::{Theme, UiConfig}; + use p2p_chat::tui::{App, InputMode, TuiCommand}; + + fn make_app() -> App { + let theme: Theme = UiConfig::default().into(); + App::new(theme) + } + + fn key(code: KeyCode) -> KeyEvent { + KeyEvent::new(code, KeyModifiers::NONE) + } + + fn ctrl_key(code: KeyCode) -> KeyEvent { + KeyEvent::new(code, KeyModifiers::CONTROL) + } + + #[test] + fn initial_state() { + let app = make_app(); + assert_eq!(app.input_mode, InputMode::Editing); + assert_eq!(app.input, ""); + assert_eq!(app.cursor_position, 0); + assert_eq!(app.scroll_offset, 0); + } + + #[test] + fn esc_switches_to_normal_mode() { + let mut app = make_app(); + assert_eq!(app.input_mode, InputMode::Editing); + let cmd = app.handle_key(key(KeyCode::Esc)); + assert!(matches!(cmd, TuiCommand::None)); + assert_eq!(app.input_mode, InputMode::Normal); + } + + #[test] + fn i_switches_to_editing_mode() { + let mut app = make_app(); + app.input_mode = InputMode::Normal; + let cmd = app.handle_key(key(KeyCode::Char('i'))); + assert!(matches!(cmd, TuiCommand::None)); + assert_eq!(app.input_mode, InputMode::Editing); + } + + #[test] + fn enter_in_normal_switches_to_editing() { + let mut app = make_app(); + app.input_mode = InputMode::Normal; + let cmd = app.handle_key(key(KeyCode::Enter)); + assert!(matches!(cmd, TuiCommand::None)); + assert_eq!(app.input_mode, InputMode::Editing); + } + + #[test] + fn slash_in_normal_prefills_slash() { + let mut app = make_app(); + app.input_mode = InputMode::Normal; + let cmd = app.handle_key(key(KeyCode::Char('/'))); + assert!(matches!(cmd, TuiCommand::None)); + assert_eq!(app.input_mode, InputMode::Editing); + assert_eq!(app.input, "/"); + assert_eq!(app.cursor_position, 1); + } + + #[test] + fn q_in_normal_quits() { + let mut app = make_app(); + app.input_mode = InputMode::Normal; + let cmd = app.handle_key(key(KeyCode::Char('q'))); + assert!(matches!(cmd, TuiCommand::Quit)); + } + + #[test] + fn typing_characters_in_editing() { + let mut app = make_app(); + app.handle_key(key(KeyCode::Char('h'))); + app.handle_key(key(KeyCode::Char('i'))); + assert_eq!(app.input, "hi"); + assert_eq!(app.cursor_position, 2); + } + + #[test] + fn backspace_deletes_character() { + let mut app = make_app(); + app.handle_key(key(KeyCode::Char('a'))); + app.handle_key(key(KeyCode::Char('b'))); + app.handle_key(key(KeyCode::Char('c'))); + assert_eq!(app.input, "abc"); + app.handle_key(key(KeyCode::Backspace)); + assert_eq!(app.input, "ab"); + assert_eq!(app.cursor_position, 2); + } + + #[test] + fn enter_sends_message() { + let mut app = make_app(); + app.handle_key(key(KeyCode::Char('h'))); + app.handle_key(key(KeyCode::Char('i'))); + let cmd = app.handle_key(key(KeyCode::Enter)); + assert!(matches!(cmd, TuiCommand::SendMessage(ref s) if s == "hi")); + assert_eq!(app.input, ""); + assert_eq!(app.cursor_position, 0); + } + + #[test] + fn enter_on_empty_input_does_nothing() { + let mut app = make_app(); + let cmd = app.handle_key(key(KeyCode::Enter)); + assert!(matches!(cmd, TuiCommand::None)); + } + + #[test] + fn quit_command() { + let mut app = make_app(); + for c in "/quit".chars() { + app.handle_key(key(KeyCode::Char(c))); + } + let cmd = app.handle_key(key(KeyCode::Enter)); + assert!(matches!(cmd, TuiCommand::Quit)); + } + + #[test] + fn help_command_is_system_message() { + let mut app = make_app(); + for c in "/help".chars() { + app.handle_key(key(KeyCode::Char(c))); + } + let cmd = app.handle_key(key(KeyCode::Enter)); + assert!(matches!(cmd, TuiCommand::SystemMessage(_))); + } + + #[test] + fn nick_command_without_name_is_system_message() { + let mut app = make_app(); + for c in "/nick".chars() { + app.handle_key(key(KeyCode::Char(c))); + } + let cmd = app.handle_key(key(KeyCode::Enter)); + assert!(matches!(cmd, TuiCommand::SystemMessage(_))); + } + + #[test] + fn nick_command_with_name() { + let mut app = make_app(); + for c in "/nick alice".chars() { + app.handle_key(key(KeyCode::Char(c))); + } + let cmd = app.handle_key(key(KeyCode::Enter)); + assert!(matches!(cmd, TuiCommand::ChangeNick(ref s) if s == "alice")); + } + + #[test] + fn connect_command_without_id_is_system_message() { + let mut app = make_app(); + for c in "/connect".chars() { + app.handle_key(key(KeyCode::Char(c))); + } + let cmd = app.handle_key(key(KeyCode::Enter)); + assert!(matches!(cmd, TuiCommand::SystemMessage(_))); + } + + #[test] + fn connect_command_with_id() { + let mut app = make_app(); + for c in "/connect abc123".chars() { + app.handle_key(key(KeyCode::Char(c))); + } + let cmd = app.handle_key(key(KeyCode::Enter)); + assert!(matches!(cmd, TuiCommand::Connect(ref s) if s == "abc123")); + } + + #[test] + fn voice_command() { + let mut app = make_app(); + for c in "/voice".chars() { + app.handle_key(key(KeyCode::Char(c))); + } + let cmd = app.handle_key(key(KeyCode::Enter)); + assert!(matches!(cmd, TuiCommand::ToggleVoice)); + } + + #[test] + fn camera_command() { + let mut app = make_app(); + for c in "/camera".chars() { + app.handle_key(key(KeyCode::Char(c))); + } + let cmd = app.handle_key(key(KeyCode::Enter)); + assert!(matches!(cmd, TuiCommand::ToggleCamera)); + } + + #[test] + fn screen_command() { + let mut app = make_app(); + for c in "/screen".chars() { + app.handle_key(key(KeyCode::Char(c))); + } + let cmd = app.handle_key(key(KeyCode::Enter)); + assert!(matches!(cmd, TuiCommand::ToggleScreen)); + } + + #[test] + fn leave_command() { + let mut app = make_app(); + for c in "/leave".chars() { + app.handle_key(key(KeyCode::Char(c))); + } + let cmd = app.handle_key(key(KeyCode::Enter)); + assert!(matches!(cmd, TuiCommand::Leave)); + } + + #[test] + fn unknown_command_is_system_message() { + let mut app = make_app(); + for c in "/foobar".chars() { + app.handle_key(key(KeyCode::Char(c))); + } + let cmd = app.handle_key(key(KeyCode::Enter)); + assert!(matches!(cmd, TuiCommand::SystemMessage(_))); + } + + #[test] + fn file_command_with_path() { + let mut app = make_app(); + for c in "/file /tmp/test.txt".chars() { + app.handle_key(key(KeyCode::Char(c))); + } + let cmd = app.handle_key(key(KeyCode::Enter)); + match cmd { + TuiCommand::SendFile(path) => { + assert_eq!(path.to_str().unwrap(), "/tmp/test.txt"); + } + _ => panic!("Expected SendFile, got {:?}", cmd), + } + } + + #[test] + fn scroll_up_and_down_in_normal_mode() { + let mut app = make_app(); + app.input_mode = InputMode::Normal; + app.handle_key(key(KeyCode::Up)); + assert_eq!(app.scroll_offset, 1); + app.handle_key(key(KeyCode::Up)); + assert_eq!(app.scroll_offset, 2); + app.handle_key(key(KeyCode::Down)); + assert_eq!(app.scroll_offset, 1); + app.handle_key(key(KeyCode::Down)); + assert_eq!(app.scroll_offset, 0); + // Should not go below 0 + app.handle_key(key(KeyCode::Down)); + assert_eq!(app.scroll_offset, 0); + } + + #[test] + fn ctrl_c_not_handled_in_tui_layer() { + // Ctrl+C is handled at the signal level (tokio::signal::ctrl_c), + // not in TUI key handling. Crossterm delivers Char('c') with CONTROL modifier, + // which the TUI treats like any other char input. + let mut app = make_app(); + app.handle_key(key(KeyCode::Char('h'))); + app.handle_key(key(KeyCode::Char('i'))); + // Ctrl+C in editing mode inserts 'c' (crossterm behavior) + app.handle_key(ctrl_key(KeyCode::Char('c'))); + assert_eq!(app.input, "hic"); + } +} + +// ============================================================================ +// Chat state tests +// ============================================================================ +mod chat_tests { + use p2p_chat::chat::ChatState; + use p2p_chat::protocol::ChatMessage; + + #[test] + fn new_chat_state() { + let chat = ChatState::new("alice".into()); + assert_eq!(chat.our_name, "alice"); + assert!(chat.history.is_empty()); + } + + #[test] + fn add_system_message() { + let mut chat = ChatState::new("alice".into()); + chat.add_system_message("test message".into()); + assert_eq!(chat.history.len(), 1); + assert!(chat.history[0].is_system); + assert_eq!(chat.history[0].text, "test message"); + assert_eq!(chat.history[0].sender_name, "SYSTEM"); + } + + #[test] + fn receive_message() { + let mut chat = ChatState::new("alice".into()); + chat.receive_message(ChatMessage { + sender_name: "bob".into(), + timestamp: 1234567890, + text: "hello".into(), + }); + assert_eq!(chat.history.len(), 1); + assert!(!chat.history[0].is_self); + assert!(!chat.history[0].is_system); + assert_eq!(chat.history[0].sender_name, "bob"); + assert_eq!(chat.history[0].text, "hello"); + } + + #[test] + fn history_trimming() { + let mut chat = ChatState::new("alice".into()); + chat.max_history = 5; + for i in 0..10 { + chat.add_system_message(format!("msg {}", i)); + } + assert_eq!(chat.history.len(), 5); + // Should keep the last 5 messages + assert_eq!(chat.history[0].text, "msg 5"); + assert_eq!(chat.history[4].text, "msg 9"); + } + + #[test] + fn multiple_message_types() { + let mut chat = ChatState::new("alice".into()); + chat.add_system_message("welcome".into()); + chat.receive_message(ChatMessage { + sender_name: "bob".into(), + timestamp: 100, + text: "hi alice".into(), + }); + chat.add_system_message("bob left".into()); + + assert_eq!(chat.history.len(), 3); + assert!(chat.history[0].is_system); + assert!(!chat.history[1].is_system); + assert_eq!(chat.history[1].sender_name, "bob"); + assert!(chat.history[2].is_system); + } +} diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..c67f979 --- /dev/null +++ b/web/app.js @@ -0,0 +1,353 @@ +const toggleMicBtn = document.getElementById('toggle-mic'); +const toggleCamBtn = document.getElementById('toggle-cam'); +const toggleScreenBtn = document.getElementById('toggle-screen'); +const statusEl = document.getElementById('status'); +const remoteStreamsContainer = document.getElementById('remote-streams'); +const localVideo = document.getElementById('local-video'); + +// --- Local Media State --- +let micStream = null; +let micSource = null; +let camStream = null; +let screenStream = null; +let micScriptProcessor = null; +let audioCtx = null; +const SAMPLE_RATE = 48000; + +// --- Remote Peer State --- +// Map +// Note: We can use a single AudioContext for all peers, but need separate scheduler times. +const peers = new Map(); + +// Initialize shared AudioContext for playback +function getAudioContext() { + if (!audioCtx) { + audioCtx = new (window.AudioContext || window.webkitAudioContext)({ + sampleRate: SAMPLE_RATE, + }); + } + if (audioCtx.state === 'suspended') { + audioCtx.resume(); + } + return audioCtx; +} + +// --- WebSocket Setup --- +const ws = new WebSocket(`ws://${location.host}/ws`); +ws.binaryType = 'arraybuffer'; + +ws.onopen = () => { + statusEl.textContent = 'Connected'; + statusEl.style.color = '#4ade80'; +}; + +ws.onclose = () => { + statusEl.textContent = 'Disconnected'; + statusEl.style.color = '#f87171'; +}; + +ws.onmessage = (event) => { + const data = event.data; + if (data instanceof ArrayBuffer) { + // [Header(1)][ID_Len(1)][ID...][Data...] + const view = new DataView(data); + if (view.byteLength < 2) return; + + const header = view.getUint8(0); + const idLen = view.getUint8(1); + + if (view.byteLength < 2 + idLen) return; + + // Extract ID + const idBytes = new Uint8Array(data, 2, idLen); + const peerId = new TextDecoder().decode(idBytes); + + // Extract Payload + const payload = data.slice(2 + idLen); + + // Get or Create Peer + let peer = peers.get(peerId); + if (!peer) { + peer = createPeer(peerId); + peers.set(peerId, peer); + } + + if (header === 0) { // Audio + handleRemoteAudio(peer, payload); + } else if (header === 1) { // Id 1 = Camera + handleRemoteVideo(peer, payload, 'camera'); + } else if (header === 2) { // Id 2 = Screen + handleRemoteVideo(peer, payload, 'screen'); + } + } +}; + +function createPeer(peerId) { + // visual card + const card = document.createElement('div'); + card.className = 'peer-card'; + card.id = `peer-${peerId}`; + + const header = document.createElement('div'); + header.className = 'peer-header'; + header.innerHTML = `${peerId} `; + card.appendChild(header); + + // Container for multiple streams + const mediaContainer = document.createElement('div'); + mediaContainer.className = 'peer-media'; + card.appendChild(mediaContainer); + + remoteStreamsContainer.appendChild(card); + + return { + id: peerId, + mediaContainer: mediaContainer, + camImg: null, + screenImg: null, + nextStartTime: 0, + lastActivity: Date.now() + }; +} + +function handleRemoteAudio(peer, arrayBuffer) { + const ctx = getAudioContext(); + const float32Data = new Float32Array(arrayBuffer); + const buffer = ctx.createBuffer(1, float32Data.length, SAMPLE_RATE); + buffer.copyToChannel(float32Data, 0); + + const source = ctx.createBufferSource(); + source.buffer = buffer; + source.connect(ctx.destination); + + const now = ctx.currentTime; + + // Reset if behind + if (peer.nextStartTime < now) { + peer.nextStartTime = now + 0.02; // Reduced from 0.05 + } + + // Cap if too far ahead (latency reduction) + if (peer.nextStartTime > now + 0.5) { + console.warn("High latency detected, resetting playhead", peer.nextStartTime - now); + peer.nextStartTime = now + 0.02; + // Note: This overlaps/mixes with existing queued buffers, but helps catch up. + // Ideally we should stop previous sources, but we don't track them yet. + } + + source.start(peer.nextStartTime); + peer.nextStartTime += buffer.duration; + + // Visual indicator? + peer.lastActivity = Date.now(); + updatePeerStatus(peer, '🎤'); +} + +function handleRemoteVideo(peer, arrayBuffer, kind) { + const blob = new Blob([arrayBuffer], { type: 'image/jpeg' }); + const url = URL.createObjectURL(blob); + + let img = kind === 'camera' ? peer.camImg : peer.screenImg; + + if (!img) { + img = document.createElement('img'); + img.className = kind; // 'camera' or 'screen' + img.alt = `${kind} from ${peer.id}`; + peer.mediaContainer.appendChild(img); + if (kind === 'camera') peer.camImg = img; + else peer.screenImg = img; + } + + const prevUrl = img.src; + img.onload = () => { + if (prevUrl && prevUrl.startsWith('blob:')) { + URL.revokeObjectURL(prevUrl); + } + }; + img.src = url; + + peer.lastActivity = Date.now(); + updatePeerStatus(peer, kind === 'camera' ? '📷' : '🖥'); +} + +function updatePeerStatus(peer, icon) { + // optionally update status indicators in header +} + +// --- Local Capture Controls --- + +toggleMicBtn.addEventListener('click', async () => { + if (micStream) { + stopMic(); + toggleMicBtn.classList.remove('active'); + toggleMicBtn.textContent = 'Start Microphone'; + } else { + await startMic(); + toggleMicBtn.classList.add('active'); + toggleMicBtn.textContent = 'Stop Microphone'; + } +}); + +toggleCamBtn.addEventListener('click', async () => { + if (camStream) { + stopCam(); + toggleCamBtn.classList.remove('active'); + toggleCamBtn.textContent = 'Start Camera'; + localVideo.srcObject = null; + } else { + await startCam(); + toggleCamBtn.classList.add('active'); + toggleCamBtn.textContent = 'Stop Camera'; + } +}); + +toggleScreenBtn.addEventListener('click', async () => { + if (screenStream) { + stopScreen(); + toggleScreenBtn.classList.remove('active'); + toggleScreenBtn.textContent = 'Start Screen Share'; + localVideo.srcObject = null; + } else { + await startScreen(); + toggleScreenBtn.classList.add('active'); + toggleScreenBtn.textContent = 'Stop Screen Share'; + } +}); + +async function startMic() { + const ctx = getAudioContext(); + try { + micStream = await navigator.mediaDevices.getUserMedia({ audio: true }); + micSource = ctx.createMediaStreamSource(micStream); + // Use smaller buffer for lower latency (4096 -> 2048 or 1024) + micScriptProcessor = ctx.createScriptProcessor(2048, 1, 1); + + micScriptProcessor.onaudioprocess = (e) => { + if (!micStream) return; + if (ws.readyState === WebSocket.OPEN) { + const inputData = e.inputBuffer.getChannelData(0); + // Send: Header 3 (Mic) + Floats + // Optimize: direct Float32Array view + const payloadLen = inputData.length * 4; + const buffer = new ArrayBuffer(1 + payloadLen); + const view = new DataView(buffer); + view.setUint8(0, 3); + + // Fast copy using typed array constructor if possible, but we need byte offset 1 + // Just use loop for now but with reduced overhead? + // actually, we can create a Float32Array on the buffer at offset 4 (idx 1)? + // No, offset must be multiple of element size (4). 1 is not. + // So we have to copy. + // But we can use setFloat32 in loop (as before). + for (let i = 0; i < inputData.length; i++) { + view.setFloat32(1 + i * 4, inputData[i], true); + } + ws.send(buffer); + } + }; + + micSource.connect(micScriptProcessor); + // Mute local feedback + const mute = ctx.createGain(); + mute.gain.value = 0; + micScriptProcessor.connect(mute); + mute.connect(ctx.destination); + + } catch (err) { + console.error('Error starting mic:', err); + } +} + +function stopMic() { + if (micStream) { + micStream.getTracks().forEach(t => t.stop()); + micStream = null; + } + if (micSource) { + micSource.disconnect(); + micSource = null; + } + if (micScriptProcessor) { + micScriptProcessor.onaudioprocess = null; + micScriptProcessor.disconnect(); + micScriptProcessor = null; + } +} + +async function startCam() { + try { + camStream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } }); + localVideo.srcObject = camStream; + startVideoSender(camStream, 4); + } catch (err) { + console.error('Error starting camera:', err); + alert('Failed to access camera'); + } +} + +function stopCam() { + if (camStream) { + camStream.getTracks().forEach(t => t.stop()); + camStream = null; + } +} + +async function startScreen() { + try { + screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true }); + // Prioritize screen for local preview if both active? + localVideo.srcObject = screenStream; + + startVideoSender(screenStream, 5); + screenStream.getVideoTracks()[0].onended = () => { + stopScreen(); + toggleScreenBtn.classList.remove('active'); + toggleScreenBtn.textContent = 'Start Screen Share'; + }; + } catch (err) { + console.error('Error starting screen:', err); + } +} + +function stopScreen() { + if (screenStream) { + screenStream.getTracks().forEach(t => t.stop()); + screenStream = null; + } +} + +function startVideoSender(stream, headerByte) { + const video = document.createElement('video'); + video.srcObject = stream; + video.play(); + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + const sendFrame = () => { + if (!stream.active) return; + if (video.readyState === video.HAVE_ENOUGH_DATA) { + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + ctx.drawImage(video, 0, 0); + + canvas.toBlob((blob) => { + if (!blob) return; + const reader = new FileReader(); + reader.onloadend = () => { + if (ws.readyState === WebSocket.OPEN) { + const arrayBuffer = reader.result; + const buffer = new ArrayBuffer(1 + arrayBuffer.byteLength); + const view = new Uint8Array(buffer); + view[0] = headerByte; + view.set(new Uint8Array(arrayBuffer), 1); + ws.send(buffer); + } + }; + reader.readAsArrayBuffer(blob); + }, 'image/jpeg', 0.6); + } + setTimeout(sendFrame, 100); // 10 FPS + }; + sendFrame(); +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..38079c7 --- /dev/null +++ b/web/index.html @@ -0,0 +1,35 @@ + + + + + + P2P Chat Media + + + +
+
+

P2P Chat Media

+
Connecting...
+
+ +
+
+ +
+ +
+

Local Preview

+ +
+ +
+ + + +
+
+
+ + + diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..a76373f --- /dev/null +++ b/web/style.css @@ -0,0 +1,121 @@ +/* Base Styles */ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #1e1e1e; + color: #e0e0e0; + margin: 0; + padding: 20px; + display: flex; + justify-content: center; +} + +.container { + width: 100%; + max-width: 1200px; +} + +header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + border-bottom: 1px solid #333; + padding-bottom: 10px; +} + +.status { + font-weight: bold; + color: #fca5a5; /* Red-ish for disconnected */ +} + +/* Remote Streams Grid */ +.remote-streams { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.peer-card { + background-color: #2d2d2d; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 6px rgba(0,0,0,0.3); + display: flex; + flex-direction: column; +} + +.peer-header { + background-color: #333; + padding: 8px 12px; + font-size: 0.9em; + font-weight: bold; + color: #aaa; + display: flex; + justify-content: space-between; +} + +.peer-media { + position: relative; + width: 100%; + /* Aspect ratio placeholder or min-height */ + min-height: 200px; + background-color: #000; + display: flex; + justify-content: center; + align-items: center; +} + +.peer-media img { + width: 100%; + height: auto; + display: block; + max-height: 400px; + object-fit: contain; +} + +.peer-media .placeholder { + color: #555; + font-size: 0.8em; +} + +/* Local Preview */ +.local-preview { + margin-top: 20px; + border-top: 1px solid #333; + padding-top: 20px; + opacity: 0.6; +} + +.local-preview video { + width: 200px; + border-radius: 4px; + background: #000; +} + +/* Controls */ +.audio-controls { + display: flex; + gap: 15px; + margin-top: 20px; + justify-content: center; +} + +button { + padding: 10px 20px; + border: none; + border-radius: 4px; + background-color: #3b82f6; + color: white; + font-weight: bold; + cursor: pointer; + transition: background-color 0.2s; +} + +button:hover { + background-color: #2563eb; +} + +button.active { + background-color: #ef4444; /* Red for Stop */ +}