From 65b3f22daeb03d95c007ae26b1bb40b3c9cd56a6 Mon Sep 17 00:00:00 2001 From: mixa Date: Wed, 18 Feb 2026 11:51:00 +0300 Subject: [PATCH] Initial commit of p2p-chat GUI with native audio and screen sharing --- Cargo.lock | 4138 ++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 10 +- src/app_logic.rs | 181 +- src/chat/mod.rs | 2 +- src/config.rs | 20 + src/gui.rs | 610 ++++++ src/main.rs | 87 +- src/media/capture.rs | 693 ++++++- src/media/mod.rs | 280 ++- src/media/voice.rs | 653 +++++-- src/net/mod.rs | 5 +- src/tui/mod.rs | 116 +- src/tui/status_bar.rs | 4 +- src/web/mod.rs | 154 +- web/app.js | 299 ++- web/style.css | 2 +- 16 files changed, 6559 insertions(+), 695 deletions(-) create mode 100644 src/gui.rs diff --git a/Cargo.lock b/Cargo.lock index 17e345e..6dedcf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + [[package]] name = "adler2" version = "2.0.1" @@ -43,6 +59,30 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -52,12 +92,79 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "alsa" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3" +dependencies = [ + "alsa-sys", + "bitflags 2.10.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.10.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -67,6 +174,16 @@ 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 0.2.2", +] + [[package]] name = "anstream" version = "0.6.21" @@ -123,6 +240,44 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "anymap3" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170433209e817da6aae2c51aa0dd443009a613425dd041ebfb2492d1c4c11a25" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + [[package]] name = "arrayref" version = "0.3.9" @@ -138,6 +293,30 @@ dependencies = [ "serde", ] +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ash" +version = "0.37.3+1.3.251" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +dependencies = [ + "libloading 0.7.4", +] + [[package]] name = "ashpd" version = "0.8.1" @@ -153,7 +332,7 @@ dependencies = [ "serde", "serde_repr", "url", - "zbus", + "zbus 4.4.0", ] [[package]] @@ -369,6 +548,17 @@ dependencies = [ "url", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "audiopus" version = "0.2.0" @@ -414,6 +604,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + [[package]] name = "axum" version = "0.8.8" @@ -519,6 +752,25 @@ 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 2.1.1", + "shlex", + "syn 2.0.114", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -534,6 +786,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -546,6 +804,15 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + [[package]] name = "blake3" version = "1.8.3" @@ -584,13 +851,22 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + [[package]] name = "block2" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2", + "objc2 0.6.3", ] [[package]] @@ -606,17 +882,43 @@ dependencies = [ "piper", ] +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + [[package]] name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "bytemuck" version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] [[package]] name = "byteorder" @@ -624,6 +926,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -633,6 +941,57 @@ dependencies = [ "serde", ] +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.10.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.10.0", + "polling", + "rustix 1.1.3", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.4", + "rustix 1.1.3", + "wayland-backend", + "wayland-client", +] + [[package]] name = "castaway" version = "0.2.4" @@ -649,15 +1008,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[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.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "cfg_aliases" version = "0.2.1" @@ -699,7 +1091,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -713,6 +1105,33 @@ dependencies = [ "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 0.8.9", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "once_cell", + "strsim 0.10.0", + "termcolor", + "textwrap", +] + [[package]] name = "clap" version = "4.5.57" @@ -731,7 +1150,7 @@ checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ "anstream", "anstyle", - "clap_lex", + "clap_lex 0.7.7", "strsim 0.11.1", ] @@ -747,12 +1166,60 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clap_lex" version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "clipboard_macos" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f" +dependencies = [ + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "clipboard_wayland" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003f886bc4e2987729d10c1db3424e7f80809f3fc22dbc16c685738887cb37b8" +dependencies = [ + "smithay-clipboard", +] + +[[package]] +name = "clipboard_x11" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd63e33452ffdafd39924c4f05a5dd1e94db646c779c6bd59148a3d95fff5ad4" +dependencies = [ + "thiserror 2.0.18", + "x11rb", +] + [[package]] name = "cmake" version = "0.1.57" @@ -771,12 +1238,69 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width 0.1.14", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "com" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" +dependencies = [ + "com_macros", +] + +[[package]] +name = "com_macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" +dependencies = [ + "com_macros_support", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "com_macros_support" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "compact_str" version = "0.9.0" @@ -818,6 +1342,15 @@ 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" @@ -827,6 +1360,12 @@ 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" @@ -853,6 +1392,107 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "coreaudio-rs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" +dependencies = [ + "bitflags 1.3.2", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "cosmic-text" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2" +dependencies = [ + "bitflags 2.10.0", + "fontdb", + "log", + "rangemap", + "rayon", + "rustc-hash 1.1.0", + "rustybuzz", + "self_cell", + "swash", + "sys-locale", + "ttf-parser 0.21.1", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cpal" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb" +dependencies = [ + "alsa", + "coreaudio-rs", + "dasp_sample", + "jack", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "objc2 0.6.3", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.62.2", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -886,6 +1526,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-epoch" version = "0.9.18" @@ -911,7 +1561,7 @@ dependencies = [ "crossterm_winapi", "futures-core", "mio", - "parking_lot", + "parking_lot 0.12.5", "rustix 0.38.44", "signal-hook", "signal-hook-mio", @@ -929,7 +1579,7 @@ dependencies = [ "derive_more 2.1.1", "document-features", "mio", - "parking_lot", + "parking_lot 0.12.5", "rustix 1.1.3", "signal-hook", "signal-hook-mio", @@ -945,6 +1595,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -990,6 +1646,12 @@ dependencies = [ "phf", ] +[[package]] +name = "ctor-lite" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e162d0c2e2068eb736b71e5597eff0b9944e6b973cd9f37b6a288ab9bf20e300" + [[package]] name = "ctr" version = "0.9.2" @@ -999,6 +1661,12 @@ dependencies = [ "cipher", ] +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + [[package]] name = "curve25519-dalek" version = "5.0.0-pre.1" @@ -1028,6 +1696,33 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "d3d12" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" +dependencies = [ + "bitflags 2.10.0", + "libloading 0.8.9", + "winapi", +] + +[[package]] +name = "dark-light" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a76fa97167fa740dcdbfe18e8895601e1bc36525f09b044e00916e717c03a3c" +dependencies = [ + "dconf_rs", + "detect-desktop-environment", + "dirs", + "objc", + "rust-ini", + "web-sys", + "winreg 0.10.1", + "zbus 4.4.0", +] + [[package]] name = "darling" version = "0.10.2" @@ -1142,7 +1837,126 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core", + "parking_lot_core 0.9.12", +] + +[[package]] +name = "dasp" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7381b67da416b639690ac77c73b86a7b5e64a29e31d1f75fb3b1102301ef355a" +dependencies = [ + "dasp_envelope", + "dasp_frame", + "dasp_interpolate", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", + "dasp_signal", + "dasp_slice", + "dasp_window", +] + +[[package]] +name = "dasp_envelope" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ec617ce7016f101a87fe85ed44180839744265fae73bb4aa43e7ece1b7668b6" +dependencies = [ + "dasp_frame", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", +] + +[[package]] +name = "dasp_frame" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6" +dependencies = [ + "dasp_sample", +] + +[[package]] +name = "dasp_interpolate" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc975a6563bb7ca7ec0a6c784ead49983a21c24835b0bc96eea11ee407c7486" +dependencies = [ + "dasp_frame", + "dasp_ring_buffer", + "dasp_sample", +] + +[[package]] +name = "dasp_peak" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf88559d79c21f3d8523d91250c397f9a15b5fc72fbb3f87fdb0a37b79915bf" +dependencies = [ + "dasp_frame", + "dasp_sample", +] + +[[package]] +name = "dasp_ring_buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07d79e19b89618a543c4adec9c5a347fe378a19041699b3278e616e387511ea1" + +[[package]] +name = "dasp_rms" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c5dcb30b7e5014486e2822537ea2beae50b19722ffe2ed7549ab03774575aa" +dependencies = [ + "dasp_frame", + "dasp_ring_buffer", + "dasp_sample", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "dasp_signal" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa1ab7d01689c6ed4eae3d38fe1cea08cba761573fbd2d592528d55b421077e7" +dependencies = [ + "dasp_envelope", + "dasp_frame", + "dasp_interpolate", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", + "dasp_window", +] + +[[package]] +name = "dasp_slice" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1c7335d58e7baedafa516cb361360ff38d6f4d3f9d9d5ee2a2fc8e27178fa1" +dependencies = [ + "dasp_frame", + "dasp_sample", +] + +[[package]] +name = "dasp_window" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ded7b88821d2ce4e8b842c9f1c86ac911891ab89443cc1de750cae764c5076" +dependencies = [ + "dasp_sample", ] [[package]] @@ -1151,6 +1965,12 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "dconf_rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" + [[package]] name = "deltae" version = "0.3.2" @@ -1281,6 +2101,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "detect-desktop-environment" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" + [[package]] name = "diatomic-waker" version = "0.2.3" @@ -1314,7 +2140,27 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" dependencies = [ - "dirs-sys", + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", ] [[package]] @@ -1352,9 +2198,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.10.0", - "block2", + "block2 0.6.2", "libc", - "objc2", + "objc2 0.6.3", ] [[package]] @@ -1368,6 +2214,15 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.9", +] + [[package]] name = "dlopen2" version = "0.5.0" @@ -1379,6 +2234,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + [[package]] name = "document-features" version = "0.2.12" @@ -1388,12 +2249,77 @@ dependencies = [ "litrs", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e41459d99a9b529845f6d2c909eb9adf3b6d2f82635ae40be8de0601726e8b" +dependencies = [ + "drm-sys", + "rustix 0.38.44", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafb66c8dbc944d69e15cfcc661df7e703beffbaec8bd63151368b06c5f9858c" +dependencies = [ + "libc", + "linux-raw-sys 0.6.5", +] + [[package]] name = "dyn-clone" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "easyfft" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "767e39eef2ad8a3b6f1d733be3ec70364d21d437d06d4f18ea76ce08df20b75f" +dependencies = [ + "array-init", + "generic_singleton", + "num-complex", + "realfft", + "rustfft", +] + [[package]] name = "ed25519" version = "3.0.0-rc.4" @@ -1498,6 +2424,26 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1514,6 +2460,22 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + [[package]] name = "euclid" version = "0.22.13" @@ -1544,6 +2506,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fancy-regex" version = "0.11.0" @@ -1554,6 +2531,12 @@ dependencies = [ "regex", ] +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "fastbloom" version = "0.14.1" @@ -1572,6 +2555,35 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "fiat-crypto" version = "0.3.0" @@ -1647,6 +2659,65 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "font-types" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3971f9a5ca983419cdc386941ba3b9e1feba01a0ab888adf78739feb2798492" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.20.0", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1731,6 +2802,7 @@ dependencies = [ "futures-core", "futures-task", "futures-util", + "num_cpus", ] [[package]] @@ -1793,6 +2865,30 @@ dependencies = [ "slab", ] +[[package]] +name = "gbm" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce852e998d3ca5e4a97014fb31c940dc5ef344ec7d364984525fd11e8a547e6a" +dependencies = [ + "bitflags 2.10.0", + "drm", + "drm-fourcc", + "gbm-sys", + "libc", + "wayland-backend", + "wayland-server", +] + +[[package]] +name = "gbm-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13a5f2acc785d8fb6bf6b7ab6bfb0ef5dad4f4d97e8e70bb8e470722312f76f" +dependencies = [ + "libc", +] + [[package]] name = "generator" version = "0.7.5" @@ -1817,8 +2913,8 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link", - "windows-result", + "windows-link 0.2.1", + "windows-result 0.4.1", ] [[package]] @@ -1832,6 +2928,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "generic_singleton" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d5de0fc83987dac514f3b910c5d08392b220efe8cf72086c660029a197bf73" +dependencies = [ + "anymap3", + "lazy_static", + "parking_lot 0.12.5", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.3", + "windows-link 0.2.1", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1869,6 +2986,48 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gl" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glam" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" + +[[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" @@ -1881,6 +3040,89 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "glow" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8098adac955faa2d31079b65dc48841251f69efd3ac25477903fc424362ead" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.10.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "gpu-allocator" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "winapi", + "windows 0.52.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" +dependencies = [ + "bitflags 2.10.0", + "gpu-descriptor-types", + "hashbrown 0.14.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + [[package]] name = "h2" version = "0.3.27" @@ -1893,7 +3135,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1912,13 +3154,24 @@ dependencies = [ "futures-core", "futures-sink", "http 1.4.0", - "indexmap", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hash32" version = "0.2.1" @@ -1928,11 +3181,24 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", + "allocator-api2", +] [[package]] name = "hashbrown" @@ -1956,6 +3222,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "hassle-rs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +dependencies = [ + "bitflags 2.10.0", + "com", + "libc", + "libloading 0.8.9", + "thiserror 1.0.69", + "widestring", + "winapi", +] + [[package]] name = "heapless" version = "0.7.17" @@ -1985,6 +3266,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -1997,6 +3287,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + [[package]] name = "hickory-proto" version = "0.25.2" @@ -2039,7 +3335,7 @@ dependencies = [ "ipconfig", "moka", "once_cell", - "parking_lot", + "parking_lot 0.12.5", "rand 0.9.2", "resolv-conf", "rustls 0.23.36", @@ -2065,6 +3361,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + [[package]] name = "http" version = "0.2.12" @@ -2260,7 +3562,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -2272,6 +3574,184 @@ dependencies = [ "cc", ] +[[package]] +name = "iced" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88acfabc84ec077eaf9ede3457ffa3a104626d79022a9bf7f296093b1d60c73f" +dependencies = [ + "iced_core", + "iced_futures", + "iced_renderer", + "iced_widget", + "iced_winit", + "thiserror 1.0.69", +] + +[[package]] +name = "iced_core" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0013a238275494641bf8f1732a23a808196540dc67b22ff97099c044ae4c8a1c" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "dark-light", + "glam", + "log", + "num-traits", + "once_cell", + "palette", + "rustc-hash 2.1.1", + "smol_str", + "thiserror 1.0.69", + "web-time", +] + +[[package]] +name = "iced_futures" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c04a6745ba2e80f32cf01e034fd00d853aa4f4cd8b91888099cb7aaee0d5d7c" +dependencies = [ + "futures", + "iced_core", + "log", + "rustc-hash 2.1.1", + "wasm-bindgen-futures", + "wasm-timer", +] + +[[package]] +name = "iced_glyphon" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41c3bb56f1820ca252bc1d0994ece33d233a55657c0c263ea7cb16895adbde82" +dependencies = [ + "cosmic-text", + "etagere", + "lru 0.12.5", + "rustc-hash 2.1.1", + "wgpu", +] + +[[package]] +name = "iced_graphics" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba25a18cfa6d5cc160aca7e1b34f73ccdff21680fa8702168c09739767b6c66f" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "cosmic-text", + "half", + "iced_core", + "iced_futures", + "log", + "once_cell", + "raw-window-handle", + "rustc-hash 2.1.1", + "thiserror 1.0.69", + "unicode-segmentation", +] + +[[package]] +name = "iced_renderer" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73558208059f9e622df2bf434e044ee2f838ce75201a023cf0ca3e1244f46c2a" +dependencies = [ + "iced_graphics", + "iced_tiny_skia", + "iced_wgpu", + "log", + "thiserror 1.0.69", +] + +[[package]] +name = "iced_runtime" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348b5b2c61c934d88ca3b0ed1ed913291e923d086a66fa288ce9669da9ef62b5" +dependencies = [ + "bytes", + "iced_core", + "iced_futures", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "iced_tiny_skia" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c625d368284fcc43b0b36b176f76eff1abebe7959dd58bd8ce6897d641962a50" +dependencies = [ + "bytemuck", + "cosmic-text", + "iced_graphics", + "kurbo", + "log", + "rustc-hash 2.1.1", + "softbuffer", + "tiny-skia", +] + +[[package]] +name = "iced_wgpu" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15708887133671d2bcc6c1d01d1f176f43a64d6cdc3b2bf893396c3ee498295f" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "futures", + "glam", + "guillotiere", + "iced_glyphon", + "iced_graphics", + "log", + "once_cell", + "rustc-hash 2.1.1", + "thiserror 1.0.69", + "wgpu", +] + +[[package]] +name = "iced_widget" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81429e1b950b0e4bca65be4c4278fea6678ea782030a411778f26fa9f8983e1d" +dependencies = [ + "iced_renderer", + "iced_runtime", + "num-traits", + "once_cell", + "rustc-hash 2.1.1", + "thiserror 1.0.69", + "unicode-segmentation", +] + +[[package]] +name = "iced_winit" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44cd4e1c594b6334f409282937bf972ba14d31fedf03c23aa595d982a2fda28" +dependencies = [ + "iced_futures", + "iced_graphics", + "iced_runtime", + "log", + "rustc-hash 2.1.1", + "thiserror 1.0.69", + "tracing", + "wasm-bindgen-futures", + "web-sys", + "winapi", + "window_clipboard", + "winit", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -2407,6 +3887,56 @@ dependencies = [ "xmltree", ] +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core 0.5.1", + "zune-jpeg 0.5.12", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -2448,6 +3978,26 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "ipconfig" version = "0.3.2" @@ -2457,7 +4007,7 @@ dependencies = [ "socket2 0.5.10", "widestring", "windows-sys 0.48.0", - "winreg", + "winreg 0.50.0", ] [[package]] @@ -2484,7 +4034,7 @@ checksum = "5236da4d5681f317ec393c8fe2b7e3d360d31c6bb40383991d0b7429ca5ad117" dependencies = [ "backon", "bytes", - "cfg_aliases", + "cfg_aliases 0.2.1", "data-encoding", "derive_more 2.1.1", "ed25519-dalek", @@ -2511,7 +4061,7 @@ dependencies = [ "portmapper", "rand 0.9.2", "reqwest 0.12.28", - "rustc-hash", + "rustc-hash 2.1.1", "rustls 0.23.36", "rustls-pki-types", "rustls-webpki 0.103.9", @@ -2564,7 +4114,7 @@ dependencies = [ "futures-lite", "futures-util", "hex", - "indexmap", + "indexmap 2.13.0", "iroh", "iroh-base", "iroh-metrics", @@ -2613,11 +4163,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "034ed21f34c657a123d39525d948c885aacba59508805e4dd67d71f022e7151b" dependencies = [ "bytes", - "cfg_aliases", + "cfg_aliases 0.2.1", "iroh-quinn-proto", "iroh-quinn-udp", "pin-project-lite", - "rustc-hash", + "rustc-hash 2.1.1", "rustls 0.23.36", "socket2 0.6.2", "thiserror 2.0.18", @@ -2642,7 +4192,7 @@ dependencies = [ "lru-slab", "rand 0.9.2", "ring 0.17.14", - "rustc-hash", + "rustc-hash 2.1.1", "rustls 0.23.36", "rustls-pki-types", "slab", @@ -2659,7 +4209,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f981dadd5a072a9e0efcd24bdcc388e570073f7e51b33505ceb1ef4668c80c86" dependencies = [ - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", "socket2 0.6.2", "tracing", @@ -2674,7 +4224,7 @@ checksum = "cd2b63e654b9dec799a73372cdc79b529ca6c7248c0c8de7da78a02e3a46f03c" dependencies = [ "blake3", "bytes", - "cfg_aliases", + "cfg_aliases 0.2.1", "data-encoding", "derive_more 2.1.1", "getrandom 0.3.4", @@ -2687,7 +4237,7 @@ dependencies = [ "iroh-metrics", "iroh-quinn", "iroh-quinn-proto", - "lru", + "lru 0.16.3", "n0-error", "n0-future", "num_enum", @@ -2746,6 +4296,15 @@ 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" @@ -2761,6 +4320,65 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jack" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7811b07bcac5dafabf814ab52c4b0ca9b7948aa1e279f572f03aa6544d47d27" +dependencies = [ + "bitflags 2.10.0", + "jack-sys", + "lazy_static", + "libc", + "log", +] + +[[package]] +name = "jack-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6013b7619b95a22b576dfb43296faa4ecbe40abbdb97dfd22ead520775fc86ab" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "libc", + "libloading 0.7.4", + "log", + "pkg-config", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -2782,6 +4400,33 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading 0.8.9", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kurbo" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" +dependencies = [ + "arrayvec", + "smallvec", +] + [[package]] name = "lab" version = "0.11.0" @@ -2794,12 +4439,48 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libc" version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libm" version = "0.2.16" @@ -2814,6 +4495,56 @@ checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.10.0", "libc", + "redox_syscall 0.7.1", +] + +[[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 = "libwayshot-xcap" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558a3a7ca16a17a14adf8f051b3adcd7766d397532f5f6d6a48034db11e54c22" +dependencies = [ + "drm", + "gbm", + "gl", + "image", + "khronos-egl", + "memmap2", + "rustix 1.1.3", + "thiserror 2.0.18", + "tracing", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", ] [[package]] @@ -2831,6 +4562,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -2892,6 +4629,21 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" + [[package]] name = "lru" version = "0.16.3" @@ -2919,10 +4671,19 @@ version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" dependencies = [ - "nix", + "nix 0.29.0", "winapi", ] +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2947,12 +4708,31 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + [[package]] name = "memmem" version = "0.1.1" @@ -2968,6 +4748,21 @@ dependencies = [ "autocfg", ] +[[package]] +name = "metal" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" +dependencies = [ + "bitflags 2.10.0", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + [[package]] name = "mime" version = "0.3.17" @@ -3022,13 +4817,23 @@ dependencies = [ "crossbeam-epoch", "crossbeam-utils", "equivalent", - "parking_lot", + "parking_lot 0.12.5", "portable-atomic", "smallvec", "tagptr", "uuid", ] +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "n0-error" version = "0.1.3" @@ -3057,7 +4862,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" dependencies = [ - "cfg_aliases", + "cfg_aliases 0.2.1", "derive_more 2.1.1", "futures-buffered", "futures-lite", @@ -3083,6 +4888,26 @@ dependencies = [ "n0-future", ] +[[package]] +name = "naga" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843" +dependencies = [ + "bit-set", + "bitflags 2.10.0", + "codespan-reporting", + "hexf-parse", + "indexmap 2.13.0", + "log", + "num-traits", + "rustc-hash 1.1.0", + "spirv", + "termcolor", + "thiserror 1.0.69", + "unicode-xid", +] + [[package]] name = "nanorand" version = "0.7.0" @@ -3092,13 +4917,52 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.10.0", + "jni-sys", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + [[package]] name = "netdev" version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc9815643a243856e7bd84524e1ff739e901e846cfb06ad9627cd2b6d59bd737" dependencies = [ - "block2", + "block2 0.6.2", "dispatch2", "dlopen2", "ipnet", @@ -3182,7 +5046,7 @@ checksum = "454b8c0759b2097581f25ed5180b4a1d14c324fde6d0734932a288e044d06232" dependencies = [ "atomic-waker", "bytes", - "cfg_aliases", + "cfg_aliases 0.2.1", "derive_more 2.1.1", "iroh-quinn-udp", "js-sys", @@ -3206,10 +5070,16 @@ dependencies = [ "tracing", "web-sys", "windows 0.62.2", - "windows-result", + "windows-result 0.4.1", "wmi", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" version = "0.29.0" @@ -3218,11 +5088,39 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.10.0", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.2.1", "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 0.2.1", + "libc", +] + +[[package]] +name = "nnnoiseless" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "805d5964d1e7a0006a7fdced7dae75084d66d18b35f1dfe81bd76929b1f8da0c" +dependencies = [ + "anyhow", + "clap 3.2.25", + "dasp", + "dasp_interpolate", + "dasp_ring_buffer", + "easyfft", + "hound", + "once_cell", +] + [[package]] name = "no-std-compat" version = "0.2.0" @@ -3251,6 +5149,21 @@ dependencies = [ "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 = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "ntimestamp" version = "1.0.0" @@ -3275,6 +5188,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -3282,6 +5205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", + "serde", ] [[package]] @@ -3310,6 +5234,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3325,7 +5260,7 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi", + "hermit-abi 0.5.2", "libc", ] @@ -3367,6 +5302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", + "objc_exception", ] [[package]] @@ -3380,6 +5316,22 @@ dependencies = [ "objc_id", ] +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + [[package]] name = "objc2" version = "0.6.3" @@ -3389,6 +5341,171 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", + "objc2-cloud-kit 0.3.2", + "objc2-core-data 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.10.0", + "libc", + "objc2 0.6.3", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-av-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478ae33fcac9df0a18db8302387c666b8ef08a3e2d62b510ca4fc278a384b6c0" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "dispatch2", + "objc2 0.6.3", + "objc2-avf-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-image-io", + "objc2-media-toolbox", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2 0.6.3", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -3396,10 +5513,103 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.10.0", - "block2", + "block2 0.6.2", "dispatch2", "libc", - "objc2", + "objc2 0.6.3", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "dispatch2", + "libc", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-io-surface", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-media" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "dispatch2", + "objc2 0.6.3", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-core-video", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", + "objc2-metal 0.3.2", ] [[package]] @@ -3408,6 +5618,126 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-image-io" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b0446e98cf4a784cc7a0177715ff317eeaa8463841c616cfc78aa4f953c4ea" +dependencies = [ + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-media-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd9fdde720df3da7046bb9097811000c1e7ab5cd579fa89d96b27d56781fb30" +dependencies = [ + "objc2 0.6.3", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-core-media", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-security" version = "0.3.2" @@ -3415,10 +5745,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" dependencies = [ "bitflags 2.10.0", - "objc2", + "objc2 0.6.3", "objc2-core-foundation", ] +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-system-configuration" version = "0.3.2" @@ -3428,11 +5768,65 @@ dependencies = [ "bitflags 2.10.0", "dispatch2", "libc", - "objc2", + "objc2 0.6.3", "objc2-core-foundation", "objc2-security", ] +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit 0.2.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + [[package]] name = "objc_id" version = "0.1.1" @@ -3476,6 +5870,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "orbclient" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ad2c6bae700b7aa5d1cc30c59bdd3a1c180b09dbaea51e2ae2b8e1cf211fdd" +dependencies = [ + "libc", + "libredox", +] + [[package]] name = "ordered-float" version = "2.10.1" @@ -3494,6 +5898,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -3504,6 +5918,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser 0.25.1", +] + [[package]] name = "p2p-chat" version = "0.1.0" @@ -3514,20 +5943,27 @@ dependencies = [ "bincode", "bytes", "chrono", - "clap", + "clap 4.5.57", + "cpal", "crossbeam-channel", "crossterm 0.28.1", + "dashmap", "directories", "futures", "hex", + "iced", + "iced_futures", + "image", "iroh", "iroh-gossip", "mime_guess", "n0-future", + "nnnoiseless", "postcard", - "rand 0.8.5", + "rand 0.9.2", "ratatui", "rfd", + "ringbuf", "rust-embed", "serde", "serde_json", @@ -3535,11 +5971,36 @@ dependencies = [ "songbird", "tokio", "tokio-stream", - "toml", + "toml 0.7.8", "tower-http", "tracing", "tracing-subscriber", "uuid", + "xcap", +] + +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "palette_derive", + "phf", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.114", ] [[package]] @@ -3558,6 +6019,17 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -3565,7 +6037,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.12", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -3576,9 +6062,9 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -3587,6 +6073,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "patricia_tree" version = "0.8.0" @@ -3759,6 +6251,34 @@ dependencies = [ "futures-io", ] +[[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" @@ -3768,7 +6288,7 @@ dependencies = [ "async-compat", "base32", "bytes", - "cfg_aliases", + "cfg_aliases 0.2.1", "document-features", "dyn-clone", "ed25519-dalek", @@ -3776,7 +6296,7 @@ dependencies = [ "futures-lite", "getrandom 0.3.4", "log", - "lru", + "lru 0.16.3", "ntimestamp", "reqwest 0.12.28", "self_cell", @@ -3813,8 +6333,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap", - "quick-xml", + "indexmap 2.13.0", + "quick-xml 0.38.4", "serde", "time", ] @@ -3849,6 +6369,32 @@ dependencies = [ "pnet_base", ] +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -3857,7 +6403,7 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.5.2", "pin-project-lite", "rustix 1.1.3", "windows-sys 0.61.2", @@ -3986,6 +6532,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "primal-check" version = "0.3.4" @@ -4013,6 +6565,58 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -4029,11 +6633,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", - "cfg_aliases", + "cfg_aliases 0.2.1", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.1", "rustls 0.23.36", "socket2 0.6.2", "thiserror 2.0.18", @@ -4053,7 +6657,7 @@ dependencies = [ "lru-slab", "rand 0.9.2", "ring 0.17.14", - "rustc-hash", + "rustc-hash 2.1.1", "rustls 0.23.36", "rustls-pki-types", "slab", @@ -4069,7 +6673,7 @@ version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", "once_cell", "socket2 0.6.2", @@ -4151,6 +6755,18 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "range-alloc" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + [[package]] name = "ratatui" version = "0.30.0" @@ -4175,14 +6791,14 @@ dependencies = [ "compact_str", "hashbrown 0.16.1", "indoc", - "itertools", + "itertools 0.14.0", "kasuari", - "lru", + "lru 0.16.3", "strum 0.27.2", "thiserror 2.0.18", "unicode-segmentation", "unicode-truncate", - "unicode-width", + "unicode-width 0.2.2", ] [[package]] @@ -4227,13 +6843,63 @@ dependencies = [ "hashbrown 0.16.1", "indoc", "instability", - "itertools", + "itertools 0.14.0", "line-clipping", "ratatui-core", "strum 0.27.2", "time", "unicode-segmentation", - "unicode-width", + "unicode-width 0.2.2", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.2", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", ] [[package]] @@ -4242,6 +6908,36 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.22.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69aacb76b5c29acfb7f90155d39759a29496aebb49395830e928a9703d2eec2f" +dependencies = [ + "bytemuck", + "font-types", +] + [[package]] name = "realfft" version = "3.5.0" @@ -4251,6 +6947,24 @@ dependencies = [ "rustfft", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -4260,6 +6974,15 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -4300,6 +7023,12 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + [[package]] name = "reqwest" version = "0.11.27" @@ -4340,7 +7069,7 @@ dependencies = [ "wasm-streams", "web-sys", "webpki-roots 0.25.4", - "winreg", + "winreg 0.50.0", ] [[package]] @@ -4414,6 +7143,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" + [[package]] name = "ring" version = "0.16.20" @@ -4454,6 +7189,12 @@ dependencies = [ "portable-atomic-util", ] +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rubato" version = "0.15.0" @@ -4500,6 +7241,22 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -4690,6 +7447,23 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rustybuzz" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "libm", + "smallvec", + "ttf-parser 0.21.1", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.23" @@ -4745,6 +7519,19 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit 0.19.2", + "tiny-skia", +] + [[package]] name = "secrecy" version = "0.8.0" @@ -4921,6 +7708,15 @@ 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" @@ -5083,6 +7879,15 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -5104,18 +7909,109 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +[[package]] +name = "skrifa" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.10.0", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.10.0", + "calloop 0.14.4", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.3", + "thiserror 2.0.18", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.5.10" @@ -5136,6 +8032,38 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "drm", + "fastrand", + "js-sys", + "memmap2", + "ndk", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "raw-window-handle", + "redox_syscall 0.5.18", + "rustix 1.1.3", + "tiny-xlib", + "tracing", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.61.2", + "x11rb", +] + [[package]] name = "songbird" version = "0.4.6" @@ -5157,7 +8085,7 @@ dependencies = [ "futures", "nohash-hasher", "once_cell", - "parking_lot", + "parking_lot 0.12.5", "pin-project", "rand 0.8.5", "reqwest 0.11.27", @@ -5224,6 +8152,15 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "spki" version = "0.8.0-rc.4" @@ -5289,12 +8226,24 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "strsim" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -5349,6 +8298,23 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "swash" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd59f3f359ddd2c95af4758c18270eddd9c730dde98598023cdabff472c2ca2" +dependencies = [ + "skrifa", + "yazi", + "zeno", +] + [[package]] name = "symphonia" version = "0.5.5" @@ -5433,6 +8399,15 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -5454,12 +8429,31 @@ dependencies = [ "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 = "tempfile" version = "3.25.0" @@ -5473,6 +8467,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminfo" version = "0.9.0" @@ -5480,7 +8483,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" dependencies = [ "fnv", - "nom", + "nom 7.1.3", "phf", "phf_codegen", ] @@ -5512,7 +8515,7 @@ dependencies = [ "libc", "log", "memmem", - "nix", + "nix 0.29.0", "num-derive", "num-traits", "ordered-float 4.6.0", @@ -5536,6 +8539,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" + [[package]] name = "thiserror" version = "1.0.69" @@ -5585,6 +8594,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg 0.4.21", +] + [[package]] name = "time" version = "0.3.47" @@ -5619,6 +8642,45 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" +dependencies = [ + "as-raw-xcb-connection", + "ctor-lite", + "libloading 0.8.9", + "pkg-config", + "tracing", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -5653,7 +8715,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", + "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", "socket2 0.6.2", @@ -5813,11 +8875,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" dependencies = [ "serde", - "serde_spanned", + "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 2.13.0", + "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" @@ -5842,9 +8919,9 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.13.0", "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -5855,7 +8932,7 @@ version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ - "indexmap", + "indexmap 2.13.0", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow 0.7.14", @@ -5870,6 +8947,12 @@ 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" @@ -6014,6 +9097,24 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + [[package]] name = "tungstenite" version = "0.18.0" @@ -6151,12 +9252,48 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" + +[[package]] +name = "unicode-ccc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" + [[package]] name = "unicode-ident" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -6169,11 +9306,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ - "itertools", + "itertools 0.14.0", "unicode-segmentation", - "unicode-width", + "unicode-width 0.2.2", ] +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.2" @@ -6254,6 +9397,18 @@ dependencies = [ "atomic", "getrandom 0.3.4", "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", "wasm-bindgen", ] @@ -6311,6 +9466,12 @@ dependencies = [ "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" @@ -6432,6 +9593,171 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.3", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" +dependencies = [ + "bitflags 2.10.0", + "rustix 1.1.3", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.10.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5864c4b5b6064b06b1e8b74ead4a98a6c45a285fe7a0e784d24735f011fdb078" +dependencies = [ + "rustix 1.1.3", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791c58fdeec5406aa37169dd815327d1e47f334219b523444bc26d70ceb4c34e" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" +dependencies = [ + "proc-macro2", + "quick-xml 0.38.4", + "quote", +] + +[[package]] +name = "wayland-server" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9297ab90f8d1f597711d36455c5b1b2290eca59b8134485e377a296b80b118c9" +dependencies = [ + "bitflags 2.10.0", + "downcast-rs", + "rustix 1.1.3", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-sys" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" +dependencies = [ + "dlib", + "libc", + "log", + "memoffset", + "once_cell", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.85" @@ -6486,6 +9812,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "wezterm-bidi" version = "0.2.3" @@ -6558,6 +9890,113 @@ dependencies = [ "wezterm-dynamic", ] +[[package]] +name = "wgpu" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd7311dbd2abcfebaabf1841a2824ed7c8be443a0f29166e5d3c6a53a762c01" +dependencies = [ + "arrayvec", + "cfg-if", + "cfg_aliases 0.1.1", + "js-sys", + "log", + "naga", + "parking_lot 0.12.5", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b94525fc99ba9e5c9a9e24764f2bc29bad0911a7446c12f446a8277369bf3a" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.10.0", + "cfg_aliases 0.1.1", + "codespan-reporting", + "indexmap 2.13.0", + "log", + "naga", + "once_cell", + "parking_lot 0.12.5", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "web-sys", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfabcfc55fd86611a855816326b2d54c3b2fd7972c27ce414291562650552703" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.10.0", + "block", + "cfg_aliases 0.1.1", + "core-graphics-types", + "d3d12", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hassle-rs", + "js-sys", + "khronos-egl", + "libc", + "libloading 0.8.9", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "parking_lot 0.12.5", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "winapi", +] + +[[package]] +name = "wgpu-types" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b671ff9fb03f78b46ff176494ee1ebe7d603393f42664be55b64dc8d53969805" +dependencies = [ + "bitflags 2.10.0", + "js-sys", + "web-sys", +] + [[package]] name = "widestring" version = "1.2.1" @@ -6595,6 +10034,20 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "window_clipboard" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d692d46038c433f9daee7ad8757e002a4248c20b0a3fbc991d99521d3bcb6d" +dependencies = [ + "clipboard-win", + "clipboard_macos", + "clipboard_wayland", + "clipboard_x11", + "raw-window-handle", + "thiserror 1.0.69", +] + [[package]] name = "windows" version = "0.48.0" @@ -6604,16 +10057,48 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + [[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", + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", ] [[package]] @@ -6622,7 +10107,29 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-core", + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -6633,9 +10140,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", ] [[package]] @@ -6644,9 +10162,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "windows-core", - "windows-link", - "windows-threading", + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -6671,20 +10189,45 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[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", + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -6693,7 +10236,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -6702,7 +10254,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", ] [[package]] @@ -6747,7 +10308,22 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -6787,7 +10363,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -6798,15 +10374,30 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-threading" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -6825,6 +10416,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -6843,6 +10440,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -6873,6 +10476,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -6891,6 +10500,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -6909,6 +10524,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -6927,6 +10548,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -6945,6 +10572,58 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winit" +version = "0.30.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" +dependencies = [ + "ahash 0.8.12", + "android-activity", + "atomic-waker", + "bitflags 2.10.0", + "block2 0.5.1", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases 0.2.1", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit 0.19.2", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + [[package]] name = "winnow" version = "0.5.40" @@ -6963,6 +10642,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winreg" version = "0.50.0" @@ -6991,7 +10679,7 @@ dependencies = [ "serde", "thiserror 2.0.18", "windows 0.62.2", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -7019,6 +10707,87 @@ dependencies = [ "web-sys", ] +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading 0.8.9", + "once_cell", + "rustix 1.1.3", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b65b5b36b7abe56a64f6dd46e55e26ca1ed5937a84f57fc86e8f2dd37b136f1" +dependencies = [ + "dispatch2", + "image", + "lazy_static", + "libwayshot-xcap", + "log", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-av-foundation", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-media", + "objc2-core-video", + "objc2-foundation 0.3.2", + "percent-encoding", + "pipewire", + "rand 0.9.2", + "scopeguard", + "serde", + "thiserror 2.0.18", + "url", + "widestring", + "windows 0.61.3", + "xcb", + "zbus 5.13.2", +] + +[[package]] +name = "xcb" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4c580d8205abb0a5cf4eb7e927bd664e425b6c3263f9c5310583da96970cf6" +dependencies = [ + "bitflags 1.3.2", + "libc", + "quick-xml 0.30.0", +] + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + [[package]] name = "xdg-home" version = "1.3.0" @@ -7029,6 +10798,25 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.10.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + [[package]] name = "xml-rs" version = "0.8.28" @@ -7044,6 +10832,18 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yazi" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" + [[package]] name = "yoke" version = "0.8.1" @@ -7095,7 +10895,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.29.0", "ordered-stream", "rand 0.8.5", "serde", @@ -7106,9 +10906,44 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.3", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.14", + "zbus_macros 5.13.2", + "zbus_names 4.3.1", + "zvariant 5.9.2", ] [[package]] @@ -7121,7 +10956,22 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.114", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.114", + "zbus_names 4.3.1", + "zvariant 5.9.2", + "zvariant_utils 3.3.0", ] [[package]] @@ -7132,9 +10982,26 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 4.2.0", ] +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.14", + "zvariant 5.9.2", +] + +[[package]] +name = "zeno" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" + [[package]] name = "zerocopy" version = "0.8.39" @@ -7235,6 +11102,45 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" +dependencies = [ + "zune-core 0.5.1", +] + [[package]] name = "zvariant" version = "4.2.0" @@ -7246,7 +11152,21 @@ dependencies = [ "serde", "static_assertions", "url", - "zvariant_derive", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.14", + "zvariant_derive 5.9.2", + "zvariant_utils 3.3.0", ] [[package]] @@ -7259,7 +11179,20 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.114", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.114", + "zvariant_utils 3.3.0", ] [[package]] @@ -7272,3 +11205,16 @@ dependencies = [ "quote", "syn 2.0.114", ] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.114", + "winnow 0.7.14", +] diff --git a/Cargo.toml b/Cargo.toml index 644ea69..623b4fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ anyhow = "1" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } clap = { version = "4", features = ["derive"] } -rand = "0.8" +rand = "0.9" # Configuration toml = "0.7" @@ -42,6 +42,8 @@ directories = "5.0" songbird = { version = "0.4", features = ["builtin-queue"] } audiopus = "0.2" rfd = "0.14" +iced = "0.13" +iced_futures = "0.13" crossbeam-channel = "0.5" axum = { version = "0.8.8", features = ["ws"] } @@ -51,6 +53,12 @@ futures = "0.3.31" tower-http = { version = "0.6.8", features = ["fs", "cors"] } mime_guess = "2.0.5" hex = "0.4.3" +cpal = { version = "0.17.1", features = ["jack"] } +xcap = "0.8.2" +image = "0.25.9" +ringbuf = "0.4.8" +nnnoiseless = "0.5" +dashmap = "5" [profile.dev] opt-level = 0 diff --git a/src/app_logic.rs b/src/app_logic.rs index 0ddfd22..cf5a4f2 100644 --- a/src/app_logic.rs +++ b/src/app_logic.rs @@ -1,11 +1,64 @@ -use crate::chat::ChatState; +use crate::chat::{ChatEntry, ChatState}; use crate::config::AppConfig; use crate::file_transfer::FileTransferManager; use crate::media::MediaState; -use crate::net::{NetEvent, NetworkManager}; +use crate::net::{NetEvent, NetworkManager, PeerInfo}; use crate::protocol::{self, GossipMessage}; -use crate::tui::TuiCommand; use anyhow::Result; +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub struct FrontendState { + pub chat_history: Vec, + pub peers: Vec, + pub our_name: String, + pub our_id: String, + pub our_id_full: String, + pub media_status: String, + pub input_device_name: Option, + pub output_device_name: Option, + pub master_volume: f32, + pub noise_suppression: bool, +} + +impl Default for FrontendState { + fn default() -> Self { + Self { + chat_history: Vec::new(), + peers: Vec::new(), + our_name: "Unknown".to_string(), + our_id: "".to_string(), + our_id_full: "".to_string(), + media_status: "".to_string(), + input_device_name: None, + output_device_name: None, + master_volume: 1.0, + noise_suppression: true, + } + } +} + +#[derive(Debug)] +pub enum AppCommand { + SendMessage(String), + /// Local-only system message (not broadcast to peers). + SystemMessage(String), + SendFile(PathBuf), + AcceptFile(String), // file_id prefix + ChangeNick(String), + Connect(String), + ToggleVoice, + ToggleScreen, + SetInputDevice(String), + SetOutputDevice(String), + SetMasterVolume(f32), + ToggleNoiseCancel, + + SetBitrate(u32), + Leave, + Quit, + None, +} pub struct AppLogic { pub chat: ChatState, @@ -37,29 +90,60 @@ impl AppLogic { } } - pub async fn handle_tui_command(&mut self, cmd: TuiCommand) -> Result { + pub async fn handle_command(&mut self, cmd: AppCommand) -> Result { match cmd { - TuiCommand::SendMessage(text) => { + AppCommand::SendMessage(text) => { if let Err(e) = self.chat.send_message(text, &self.net).await { self.chat.add_system_message(format!("Send error: {}", e)); } } - TuiCommand::SystemMessage(text) => { + AppCommand::SystemMessage(text) => { self.chat.add_system_message(text); } - TuiCommand::ToggleVoice => { + AppCommand::ToggleVoice => { let status = self.media.toggle_voice(self.net.clone()).await; self.chat.add_system_message(status.to_string()); } - TuiCommand::ToggleCamera => { - let status = self.media.toggle_camera(self.net.clone()).await; - self.chat.add_system_message(status.to_string()); - } - TuiCommand::ToggleScreen => { + AppCommand::ToggleScreen => { let status = self.media.toggle_screen(self.net.clone()).await; self.chat.add_system_message(status.to_string()); } - TuiCommand::Quit => { + AppCommand::SetInputDevice(device_name) => { + self.media.set_input_device(device_name.clone()); + self.chat.add_system_message(format!("Microphone set to: {}", device_name)); + if let Ok(mut cfg) = AppConfig::load() { + cfg.media.input_device = Some(device_name); + let _ = cfg.save(); + } + } + AppCommand::SetOutputDevice(device_name) => { + self.media.set_output_device(device_name.clone()); + self.chat.add_system_message(format!("Output set to: {}", device_name)); + if let Ok(mut cfg) = AppConfig::load() { + cfg.media.output_device = Some(device_name); + let _ = cfg.save(); + } + } + AppCommand::SetMasterVolume(vol) => { + self.media.set_volume(vol); + if let Ok(mut cfg) = AppConfig::load() { + cfg.media.master_volume = vol; + let _ = cfg.save(); + } + } + AppCommand::ToggleNoiseCancel => { + if let Some(enabled) = self.media.toggle_denoise() { + let status = if enabled { "enabled" } else { "disabled" }; + self.chat.add_system_message(format!("Noise cancellation {}", status)); + if let Ok(mut cfg) = AppConfig::load() { + cfg.media.noise_suppression = enabled; + let _ = cfg.save(); + } + } else { + self.chat.add_system_message("Voice chat not active".to_string()); + } + } + AppCommand::Quit => { // Broadcast disconnect to peers let disconnect_msg = GossipMessage::Disconnect { sender_name: self.our_name.clone(), @@ -70,7 +154,7 @@ impl AppLogic { self.media.shutdown(); return Ok(true); // Signal to quit } - TuiCommand::ChangeNick(new_nick) => { + AppCommand::ChangeNick(new_nick) => { let old = self.our_name.clone(); self.our_name = new_nick.clone(); self.chat.our_name = new_nick.clone(); @@ -82,7 +166,7 @@ impl AppLogic { } } self.chat - .add_system_message(format!("Nickname changed: {} → {}", old, new_nick)); + .add_system_message(format!("Nickname changed: {} ➡️ {}", old, new_nick)); // Broadcast name change to all peers let msg = GossipMessage::NameChange(protocol::NameChange { old_name: old, @@ -90,7 +174,7 @@ impl AppLogic { }); let _ = self.net.broadcast(&msg).await; } - TuiCommand::Connect(peer_id_str) => { + AppCommand::Connect(peer_id_str) => { match peer_id_str.parse::() { Ok(peer_id) => { self.chat @@ -109,7 +193,7 @@ impl AppLogic { } } } - TuiCommand::SendFile(path) => { + AppCommand::SendFile(path) => { self.chat .add_system_message(format!("Preparing to send file: {:?}", path)); if !path.exists() { @@ -144,7 +228,7 @@ impl AppLogic { } } } - TuiCommand::Leave => { + AppCommand::Leave => { self.chat .add_system_message("Leaving group chat...".to_string()); self.media.shutdown(); @@ -158,7 +242,7 @@ impl AppLogic { ); } - TuiCommand::SetBitrate(bps) => { + AppCommand::SetBitrate(bps) => { self.media.set_bitrate(bps); self.chat .add_system_message(format!("🎵 Bitrate set to {} kbps", bps / 1000)); @@ -169,23 +253,25 @@ impl AppLogic { } } - TuiCommand::AcceptFile(prefix) => { + AppCommand::AcceptFile(prefix) => { // Find matching transfer - let transfers = self.file_mgr.transfers.lock().unwrap(); - let mut matched = None; - for (id, info) in transfers.iter() { - let id_str = hex::encode(id); - if id_str.starts_with(&prefix) { - if matched.is_some() { - self.chat - .add_system_message(format!("Ambiguous prefix '{}'", prefix)); - matched = None; - break; + let matched = { + let transfers = self.file_mgr.transfers.lock().unwrap(); + let mut matched = None; + for (id, info) in transfers.iter() { + let id_str = hex::encode(id); + if id_str.starts_with(&prefix) { + if matched.is_some() { + self.chat + .add_system_message(format!("Ambiguous prefix '{}'", prefix)); + matched = None; + break; + } + matched = Some((*id, info.clone())); } - matched = Some((*id, info.clone())); } - } - drop(transfers); + matched + }; if let Some((_id, info)) = matched { if let Some(peer_id) = info.peer { @@ -221,7 +307,7 @@ impl AppLogic { .add_system_message(format!("No transfer found matching '{}'", prefix)); } } - TuiCommand::None => {} + AppCommand::None => {} } Ok(false) // Do not quit } @@ -418,4 +504,31 @@ impl AppLogic { } } } + + pub async fn get_frontend_state(&self) -> FrontendState { + let peers_map = self.net.peers.lock().await; + let mut peers: Vec = peers_map.values().cloned().collect(); + peers.sort_by(|a, b| a.id.to_string().cmp(&b.id.to_string())); + + // Sync audio levels + let levels = self.media.get_peer_levels(); + for peer in &mut peers { + if let Some(level) = levels.get(&peer.id) { + peer.audio_level = *level; + } + } + + FrontendState { + chat_history: self.chat.history.clone(), + peers, + our_name: self.our_name.clone(), + our_id: self.our_id_short.clone(), + our_id_full: self.net.our_id.to_string(), + media_status: self.media.status_line(), + input_device_name: self.media.input_device.clone(), + output_device_name: self.media.output_device.clone(), + master_volume: self.media.get_volume(), + noise_suppression: self.media.is_denoise_enabled(), + } + } } diff --git a/src/chat/mod.rs b/src/chat/mod.rs index 126e75c..f63809a 100644 --- a/src/chat/mod.rs +++ b/src/chat/mod.rs @@ -5,7 +5,7 @@ use crate::protocol::{ChatMessage, GossipMessage}; use anyhow::Result; /// Stored chat entry with display metadata. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize)] pub struct ChatEntry { pub sender_name: String, pub timestamp: u64, diff --git a/src/config.rs b/src/config.rs index 19ab3e9..6db4fe2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -61,16 +61,36 @@ impl Default for NetworkConfig { pub struct MediaConfig { #[serde(default = "default_bitrate")] pub mic_bitrate: u32, + #[serde(default)] + pub input_device: Option, + #[serde(default)] + pub output_device: Option, + #[serde(default = "default_volume")] + pub master_volume: f32, + #[serde(default = "default_true")] + pub noise_suppression: bool, } fn default_bitrate() -> u32 { 128000 } +fn default_volume() -> f32 { + 1.0 +} + +fn default_true() -> bool { + true +} + impl Default for MediaConfig { fn default() -> Self { Self { mic_bitrate: 128000, + input_device: None, + output_device: None, + master_volume: 1.0, + noise_suppression: true, } } } diff --git a/src/gui.rs b/src/gui.rs new file mode 100644 index 0000000..b9c784f --- /dev/null +++ b/src/gui.rs @@ -0,0 +1,610 @@ +use iced::widget::{ + button, checkbox, column, container, pick_list, row, scrollable, slider, text, text_input, Column, +}; +use iced::{ + Alignment, Background, Border, Color, Element, Length, Subscription, Task, Theme, +}; +use std::sync::Arc; +use tokio::sync::{mpsc, Mutex}; +use futures::stream; + +use crate::app_logic::{AppCommand, FrontendState}; +use crate::net::PeerInfo; +use crate::chat::ChatEntry; +use chrono::{DateTime, Local, TimeZone, Utc}; + +// Discord-like Colors +const BG_DARK: Color = Color::from_rgb(0.21, 0.22, 0.25); // #36393f +const SIDEBAR_DARK: Color = Color::from_rgb(0.18, 0.19, 0.21); // #2f3136 +const INPUT_BG: Color = Color::from_rgb(0.25, 0.27, 0.29); // #40444b +const TEXT_COLOR: Color = Color::from_rgb(0.86, 0.86, 0.86); // #dcddde +const MUTED_TEXT: Color = Color::from_rgb(0.45, 0.46, 0.48); // #72767d + +pub struct ChatApp { + state: FrontendState, + input_value: String, + command_sender: mpsc::Sender, + // We keep the receiver in the struct to use it in subscription + state_receiver: Arc>>, + // Voice Chat State + input_devices: Vec, + selected_device: Option, + output_devices: Vec, + selected_output_device: Option, + is_in_voice: bool, + master_volume: f32, + noise_cancel_enabled: bool, +} + +#[derive(Debug, Clone)] +pub enum Message { + InputChanged(String), + SendMessage, + BackendUpdate(FrontendState), + // Voice + InputDeviceSelected(String), + OutputDeviceSelected(String), + ToggleVoice, + ToggleScreen, + RefreshDevices, + MasterVolumeChanged(f32), + ToggleNoiseCancel(bool), + CopyText(String), + NoOp, +} + +pub struct Flags { + pub initial_state: FrontendState, + pub command_sender: mpsc::Sender, + pub state_receiver: mpsc::Receiver, +} + +impl ChatApp { + pub fn new(flags: Flags) -> (Self, Task) { + let master_volume = flags.initial_state.master_volume; + let noise_cancel_enabled = flags.initial_state.noise_suppression; + ( + Self { + state: flags.initial_state, + input_value: String::new(), + command_sender: flags.command_sender, + state_receiver: Arc::new(Mutex::new(flags.state_receiver)), + input_devices: Vec::new(), + selected_device: None, + output_devices: Vec::new(), + selected_output_device: None, + is_in_voice: false, + master_volume, + noise_cancel_enabled, + }, + Task::perform(async {}, |_| Message::RefreshDevices), + ) + } + + pub fn title(&self) -> String { + format!("P2P Chat - {} ({})", self.state.our_name, self.state.our_id) + } + + pub fn update(&mut self, message: Message) -> Task { + match message { + Message::InputChanged(value) => { + self.input_value = value; + Task::none() + } + Message::SendMessage => { + let input_text = self.input_value.trim().to_string(); + if input_text.is_empty() { + return Task::none(); + } + + self.input_value.clear(); + + // Simple command parsing + let command = if input_text.starts_with('/') { + // ... same as before ... + let parts: Vec<&str> = input_text.split_whitespace().collect(); + match parts.as_slice() { + ["/nick", name] | ["/name", name] => { + Some(AppCommand::ChangeNick(name.to_string())) + } + ["/connect", id] | ["/join", id] => { + Some(AppCommand::Connect(id.to_string())) + } + ["/voice"] => Some(AppCommand::ToggleVoice), + ["/quit"] => Some(AppCommand::Quit), + _ => Some(AppCommand::SystemMessage(format!("Unknown command: {}", input_text))), + } + } else { + Some(AppCommand::SendMessage(input_text.clone())) + }; + + let sender = self.command_sender.clone(); + Task::perform( + async move { + if let Some(cmd) = command { + let _ = sender.send(cmd).await; + } + }, + |_| Message::InputChanged(String::new()), + ) + } + Message::BackendUpdate(new_state) => { + // Check if voice status changed to update UI state if needed + // Currently FrontendState doesn't explicitly have voice status bool, + // but we can infer or add it. For now, rely on local toggle state or messages. + // Actually FrontendState has `media_status` string. + if new_state.media_status.contains("🎤 LIVE") { + self.is_in_voice = true; + } else { + self.is_in_voice = false; + } + + // Update selected devices from backend state if they are set there + if let Some(dev) = &new_state.input_device_name { + if self.selected_device.as_ref() != Some(dev) { + self.selected_device = Some(dev.clone()); + } + } + if let Some(dev) = &new_state.output_device_name { + if self.selected_output_device.as_ref() != Some(dev) { + self.selected_output_device = Some(dev.clone()); + } + } + + self.state = new_state; + Task::none() + } + Message::InputDeviceSelected(device) => { + self.selected_device = Some(device.clone()); + let sender = self.command_sender.clone(); + Task::perform( + async move { + let _ = sender.send(AppCommand::SetInputDevice(device)).await; + }, + |_| Message::InputChanged(String::new()), // Dummy message + ) + } + Message::OutputDeviceSelected(device) => { + self.selected_output_device = Some(device.clone()); + let sender = self.command_sender.clone(); + Task::perform( + async move { + let _ = sender.send(AppCommand::SetOutputDevice(device)).await; + }, + |_| Message::InputChanged(String::new()), // Dummy message + ) + } + Message::ToggleVoice => { + let sender = self.command_sender.clone(); + Task::perform( + async move { + let _ = sender.send(AppCommand::ToggleVoice).await; + }, + |_| Message::InputChanged(String::new()), // Dummy + ) + } + Message::ToggleScreen => { + let sender = self.command_sender.clone(); + Task::perform( + async move { + let _ = sender.send(AppCommand::ToggleScreen).await; + }, + |_| Message::InputChanged(String::new()), // Dummy + ) + } + Message::MasterVolumeChanged(vol) => { + self.master_volume = vol; + let sender = self.command_sender.clone(); + Task::perform( + async move { + let _ = sender.send(AppCommand::SetMasterVolume(vol)).await; + }, + |_| Message::NoOp, + ) + } + Message::ToggleNoiseCancel(enabled) => { + self.noise_cancel_enabled = enabled; + let sender = self.command_sender.clone(); + Task::perform( + async move { + let _ = sender.send(AppCommand::ToggleNoiseCancel).await; + }, + |_| Message::NoOp, + ) + } + Message::RefreshDevices => { + // Use the improved device filtering from MediaState logic + + use cpal::traits::{HostTrait, DeviceTrait}; + + // Prioritize JACK if available + let available_hosts = cpal::available_hosts(); + let mut hosts = Vec::new(); + if available_hosts.contains(&cpal::HostId::Jack) { + hosts.push(cpal::host_from_id(cpal::HostId::Jack).unwrap()); + } + hosts.push(cpal::default_host()); + + let mut input_names = Vec::new(); + let mut output_names = Vec::new(); + + for host in &hosts { + if let Ok(devices) = host.input_devices() { + for device in devices { + if let Ok(name) = device.name() { + if name.contains("dmix") || name.contains("dsnoop") || name.contains("null") { + continue; + } + + let clean_name = if let Some(start) = name.find("CARD=") { + let rest = &name[start + 5..]; + let card_name = rest.split(',').next().unwrap_or(rest); + let prefix = name.split(':').next().unwrap_or("Unknown"); + format!("{} ({})", card_name, prefix) + } else if name.contains("HDA Intel PCH") { + name + } else { + name + }; + + input_names.push(clean_name); + } + } + } + if let Ok(devices) = host.output_devices() { + for device in devices { + if let Ok(name) = device.name() { + if name.contains("dmix") || name.contains("dsnoop") || name.contains("null") { + continue; + } + + let clean_name = if let Some(start) = name.find("CARD=") { + let rest = &name[start + 5..]; + let card_name = rest.split(',').next().unwrap_or(rest); + let prefix = name.split(':').next().unwrap_or("Unknown"); + format!("{} ({})", card_name, prefix) + } else { + name + }; + + output_names.push(clean_name); + } + } + } + } + + input_names.sort(); + input_names.dedup(); + output_names.sort(); + output_names.dedup(); + + self.input_devices = input_names; + self.output_devices = output_names; + + if self.selected_device.is_none() && !self.input_devices.is_empty() { + // Pre-select first + self.selected_device = Some(self.input_devices[0].clone()); + // We don't auto-send command to avoid loop, user must select or we wait for backend state + } + if self.selected_output_device.is_none() && !self.output_devices.is_empty() { + self.selected_output_device = Some(self.output_devices[0].clone()); + } + + Task::none() + } + Message::CopyText(text) => { + iced::clipboard::write(text) + } + Message::NoOp => Task::none(), + } + } + + pub fn view(&self) -> Element { + // Chat Area + let chat_content = self.state.chat_history.iter().fold( + Column::new().spacing(10).padding(20), + |column, entry| column.push(view_chat_entry(entry)), + ); + + let chat_scroll = scrollable(chat_content) + .height(Length::Fill) + .width(Length::Fill) + .id(scrollable::Id::new("chat_scroll")); + + // Input Area + let input = text_input("Message #general", &self.input_value) + .on_input(Message::InputChanged) + .on_submit(Message::SendMessage) + .padding(12) + .style(|_theme, status| { + text_input::Style { + background: Background::Color(INPUT_BG), + border: Border { + radius: 8.0.into(), + width: 0.0, + color: Color::TRANSPARENT, + }, + icon: Color::WHITE, + placeholder: MUTED_TEXT, + value: TEXT_COLOR, + selection: Color::from_rgb(0.4, 0.5, 0.8), + } + }); + + let input_container = container(input) + .padding(15) + .style(|_theme: &Theme| container::Style { + background: Some(Background::Color(BG_DARK)), + ..Default::default() + }); + + // Sidebar (Peers + Voice) + let identity_section = column![ + text("MY IDENTITY").size(12).style(|_theme: &Theme| text::Style { color: Some(MUTED_TEXT) }), + text(&self.state.our_name).size(16).style(|_theme: &Theme| text::Style { color: Some(TEXT_COLOR) }), + row![ + text_input("My ID", &self.state.our_id) + .padding(5) + .size(12) + .on_input(|_| Message::NoOp) + .style(|_theme, _status| text_input::Style { + background: Background::Color(Color::from_rgb(0.15, 0.16, 0.18)), + border: Border { radius: 4.0.into(), ..Default::default() }, + value: MUTED_TEXT, + placeholder: MUTED_TEXT, + selection: Color::from_rgb(0.4, 0.5, 0.8), + icon: Color::TRANSPARENT, + }), + button(text("Copy").size(12)) + .on_press(Message::CopyText(self.state.our_id_full.clone())) + .padding(5) + .style(|_theme, _status| button::Style { + background: Some(Background::Color(Color::from_rgb(0.3, 0.3, 0.35))), + text_color: Color::WHITE, + border: Border { radius: 4.0.into(), ..Default::default() }, + ..Default::default() + }) + ].spacing(5) + ].spacing(5).padding(10); + + let identity_container = container(identity_section) + .style(|_theme: &Theme| container::Style { + background: Some(Background::Color(Color::from_rgb(0.15, 0.16, 0.18))), + ..Default::default() + }); + + let peers_title = text("ONLINE").size(12).style(|_theme: &Theme| text::Style { color: Some(MUTED_TEXT) }); + + let peers_content = self.state.peers.iter().fold( + Column::new().spacing(5), + |column, peer| column.push(view_peer(peer)), + ); + + let voice_section = column![ + text("VOICE CONNECTED").size(12).style(|_theme: &Theme| text::Style { + color: Some(if self.is_in_voice { Color::from_rgb(0.4, 0.8, 0.4) } else { MUTED_TEXT }) + }), + text("Input Device").size(10).style(|_theme: &Theme| text::Style { color: Some(MUTED_TEXT) }), + pick_list( + self.input_devices.clone(), + self.selected_device.clone(), + Message::InputDeviceSelected + ).text_size(12).padding(5), + text("Output Device").size(10).style(|_theme: &Theme| text::Style { color: Some(MUTED_TEXT) }), + pick_list( + self.output_devices.clone(), + self.selected_output_device.clone(), + Message::OutputDeviceSelected + ).text_size(12).padding(5), + button( + text(if self.is_in_voice { "Disconnect" } else { "Join Voice" }).size(14) + ) + .on_press(Message::ToggleVoice) + .padding(8) + .style(move |_theme, _status| { + let bg = if self.is_in_voice { Color::from_rgb(0.8, 0.3, 0.3) } else { Color::from_rgb(0.3, 0.6, 0.4) }; + button::Style { + background: Some(Background::Color(bg)), + text_color: Color::WHITE, + border: Border { radius: 4.0.into(), ..Default::default() }, + ..Default::default() + } + }) + .width(Length::Fill), + button( + text(if self.state.media_status.contains("🖥 LIVE") { "Stop Screen" } else { "Share Screen" }).size(14) + ) + .on_press(Message::ToggleScreen) + .padding(8) + .style(move |_theme, _status| { + let is_sharing = self.state.media_status.contains("🖥 LIVE"); + let bg = if is_sharing { Color::from_rgb(0.8, 0.3, 0.3) } else { Color::from_rgb(0.3, 0.4, 0.6) }; + button::Style { + background: Some(Background::Color(bg)), + text_color: Color::WHITE, + border: Border { radius: 4.0.into(), ..Default::default() }, + ..Default::default() + } + }) + .width(Length::Fill), + + // Audio Controls + text("Master Volume").size(10).style(|_theme: &Theme| text::Style { color: Some(MUTED_TEXT) }), + slider(0.0..=2.0, self.master_volume, Message::MasterVolumeChanged).step(0.05), + + checkbox("Noise Cancellation", self.noise_cancel_enabled) + .on_toggle(Message::ToggleNoiseCancel) + .text_size(12) + .style(|_theme, _status| checkbox::Style { + background: Background::Color(INPUT_BG), + icon_color: Color::WHITE, + border: Border { radius: 4.0.into(), ..Default::default() }, + text_color: Some(TEXT_COLOR), + }), + + ].spacing(10).padding(10); + + let voice_panel = container(voice_section) + .style(|_theme: &Theme| container::Style { + background: Some(Background::Color(Color::from_rgb(0.15, 0.16, 0.18))), // Darker panel at bottom + ..Default::default() + }); + + let sidebar = container( + column![ + identity_container, + column![peers_title, peers_content].spacing(10).padding(10).height(Length::Fill), + voice_panel + ] + ) + .width(Length::Fixed(240.0)) + .height(Length::Fill) + .style(|_theme: &Theme| container::Style { + background: Some(Background::Color(SIDEBAR_DARK)), + ..Default::default() + }); + + // Main Layout + let main_content = column![chat_scroll, input_container] + .width(Length::Fill) + .height(Length::Fill); + + let layout = row![sidebar, main_content] + .width(Length::Fill) + .height(Length::Fill); + + container(layout) + .width(Length::Fill) + .height(Length::Fill) + .style(|_theme: &Theme| container::Style { + background: Some(Background::Color(BG_DARK)), + text_color: Some(TEXT_COLOR), + ..Default::default() + }) + .into() + } + + pub fn subscription(&self) -> Subscription { + struct BackendSubscription; + + let receiver = self.state_receiver.clone(); + + Subscription::run_with_id( + std::any::TypeId::of::(), + stream::unfold(receiver, |receiver| async move { + let mut guard = receiver.lock().await; + if let Some(state) = guard.recv().await { + Some((Message::BackendUpdate(state), receiver.clone())) + } else { + // Wait a bit if channel closed or empty to avoid hot loop if logic changes + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + Some((Message::BackendUpdate(FrontendState::default()), receiver.clone())) + } + }) + ) + } + + pub fn theme(&self) -> Theme { + Theme::Dark + } +} + +// ... run function ... +pub fn run(flags: Flags) -> iced::Result { + iced::application( + ChatApp::title, + ChatApp::update, + ChatApp::view + ) + .subscription(ChatApp::subscription) + .theme(ChatApp::theme) + .run_with(move || ChatApp::new(flags)) +} + +fn view_chat_entry(entry: &ChatEntry) -> Element { + let sender_color = if entry.is_self { + Color::from_rgb8(200, 200, 255) + } else if entry.is_system { + Color::from_rgb8(255, 100, 100) + } else { + Color::from_rgb8(100, 200, 100) + }; + + let sender = text(&entry.sender_name) + .style(move |_theme: &Theme| text::Style { color: Some(sender_color) }) + .font(iced::font::Font::DEFAULT) // Sans-serif + .size(15); + + let content = text(&entry.text) + .size(15) + .style(move |_theme: &Theme| text::Style { color: Some(TEXT_COLOR) }); + + let time = text(format_timestamp(entry.timestamp)) + .size(11) + .style(move |_theme: &Theme| text::Style { color: Some(MUTED_TEXT) }); + + let header = row![sender, time].spacing(8).align_y(Alignment::Center); + + // Rounded message bubble if needed, or just clean text like Discord + column![header, content].spacing(4).into() +} + +fn view_peer(peer: &PeerInfo) -> Element { + let name = peer.name.as_deref().unwrap_or("Unknown"); + + // Audio activity border + let (border_width, border_color) = if peer.audio_level > 0.01 { + (2.0, Color::from_rgb(0.2, 0.8, 0.2)) // Green + } else { + (0.0, Color::TRANSPARENT) + }; + + let peer_info = column![ + text(name).size(14).style(|_theme: &Theme| text::Style { color: Some(TEXT_COLOR) }), + row![ + text(if peer.audio_level > 0.01 { "🔊" } else { "🔇" }).size(12), + text(peer.id.to_string().chars().take(8).collect::()) + .size(10) + .style(|_theme: &Theme| text::Style { color: Some(MUTED_TEXT) }), + ].spacing(4) + ].spacing(2); + + let content = row![ + peer_info, + button(text("Copy").size(10)) + .on_press(Message::CopyText(peer.id.to_string())) + .padding(4) + .style(|_theme, _status| button::Style { + background: Some(Background::Color(Color::from_rgb(0.25, 0.26, 0.28))), + text_color: Color::WHITE, + border: Border { radius: 4.0.into(), ..Default::default() }, + ..Default::default() + }) + ] + .spacing(10) + .align_y(Alignment::Center); + + container(content) + .padding(5) + .style(move |_theme: &Theme| container::Style { + background: None, + border: Border { + width: border_width, + color: border_color, + radius: 4.0.into(), + }, + ..Default::default() + }) + .into() +} + +fn format_timestamp(ts: u64) -> String { + if ts == 0 { return "".to_string(); } + match Utc.timestamp_millis_opt(ts as i64) { + chrono::LocalResult::Single(dt) => { + let local: DateTime = DateTime::from(dt); + local.format("%H:%M").to_string() + } + _ => "".to_string(), + } +} diff --git a/src/main.rs b/src/main.rs index edf0b0b..19f92ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod app_logic; mod chat; mod config; mod file_transfer; +mod gui; mod media; mod net; mod protocol; @@ -57,6 +58,10 @@ struct Cli { /// Download directory for received files #[arg(short, long, default_value = "~/Downloads")] download_dir: String, + + /// Launch with GUI (Iced) instead of TUI + #[arg(long)] + gui: bool, } #[tokio::main] @@ -129,7 +134,13 @@ async fn main() -> Result<()> { let file_mgr = FileTransferManager::new(download_path); // Pass mic name from config if present // Pass mic name from config if present - let media = MediaState::new(config.media.mic_bitrate); + let media = MediaState::new( + config.media.mic_bitrate, + config.media.input_device.clone(), + config.media.output_device.clone(), + config.media.master_volume, + config.media.noise_suppression, + ); // Initialize App with Theme let theme = crate::config::Theme::from(config.ui.clone()); @@ -172,6 +183,7 @@ async fn main() -> Result<()> { name: Some(cli.name.clone()), capabilities: None, is_self: true, + audio_level: 0.0, }, ); } @@ -199,7 +211,6 @@ async fn main() -> Result<()> { tokio::spawn(crate::web::start_web_server( media.broadcast_tx.clone(), media.mic_broadcast.clone(), - media.cam_broadcast.clone(), media.screen_broadcast.clone(), )); @@ -213,6 +224,76 @@ async fn main() -> Result<()> { our_id_short, ); + if cli.gui { + use tokio::sync::mpsc; + + // Channel for GUI -> AppLogic commands + let (gui_cmd_tx, mut gui_cmd_rx) = mpsc::channel(100); + // Channel for AppLogic -> GUI state updates + let (gui_state_tx, gui_state_rx) = mpsc::channel(100); + + // Get initial state + let initial_state = app_logic.get_frontend_state().await; + + // Spawn AppLogic loop + tokio::spawn(async move { + let mut interval = tokio::time::interval(std::time::Duration::from_millis(100)); + + loop { + let mut state_changed = false; + tokio::select! { + _ = interval.tick() => { + app_logic.file_mgr.check_timeouts(); + } + Some(cmd) = gui_cmd_rx.recv() => { + match app_logic.handle_command(cmd).await { + Ok(true) => { // Quit command + break; + } + Ok(false) => { + state_changed = true; + } + Err(e) => { + tracing::error!("Command error: {}", e); + } + } + } + Some(event) = net_rx.recv() => { + app_logic.handle_net_event(event).await; + state_changed = true; + } + Some(event) = gossip_event_rx.recv() => { + app_logic.handle_net_event(event).await; + state_changed = true; + } + _ = tokio::signal::ctrl_c() => { + break; + } + } + + if state_changed { + let new_state = app_logic.get_frontend_state().await; + if gui_state_tx.send(new_state).await.is_err() { + break; + } + } + } + // Shutdown logic + let _ = app_logic.net.shutdown().await; + app_logic.media.shutdown(); + }); + + // Run GUI + let flags = crate::gui::Flags { + initial_state, + command_sender: gui_cmd_tx, + state_receiver: gui_state_rx, + }; + + crate::gui::run(flags)?; + return Ok(()); + } + // Setup terminal enable_raw_mode()?; let mut stdout = io::stdout(); @@ -287,7 +368,7 @@ async fn run_event_loop( match maybe_event { Some(Ok(Event::Key(key))) => { let cmd = app.handle_key(key); - if logic.handle_tui_command(cmd).await? { + if logic.handle_command(cmd).await? { return Ok(()); } } diff --git a/src/media/capture.rs b/src/media/capture.rs index 083556e..3c4e567 100644 --- a/src/media/capture.rs +++ b/src/media/capture.rs @@ -11,6 +11,11 @@ use tracing; use crate::media::WebMediaEvent; use crate::protocol::{decode_framed, write_framed, MediaKind, MediaStreamMessage}; +use std::process::Stdio; +use tokio::io::{AsyncReadExt, BufReader, AsyncWriteExt, AsyncBufReadExt}; +use tokio::process::Command; +use xcap::Monitor; + /// Manages a video capture session (camera or screen). pub struct VideoCapture { running: Arc, @@ -18,35 +23,114 @@ pub struct VideoCapture { } impl VideoCapture { - /// Start video capture with web input (broadcast receiver). - pub async fn start_web( - kind: MediaKind, - _local_peer_id: iroh::EndpointId, + /// Start web-based screen share (relay from Web to Peers). + pub async fn start_web_screen( peers: Vec, network_manager: crate::net::NetworkManager, - input_rx: tokio::sync::broadcast::Sender>, + source_tx: tokio::sync::broadcast::Sender>, ) -> Result { let running = Arc::new(AtomicBool::new(true)); - - // Spawn sender tasks let mut tasks = Vec::new(); + + // For each peer, spawn a sender task that subscribes to the source for peer in peers { let running = running.clone(); let net = network_manager.clone(); - let rx = input_rx.subscribe(); - let kind = kind.clone(); + let rx = source_tx.subscribe(); + let kind = MediaKind::Screen; - 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(tokio::spawn(async move { + if let Err(e) = run_video_sender_native(net, peer, kind, rx, running).await { + tracing::error!("Video sender web screen error: {}", e); } - }); - tasks.push(task); + })); } Ok(Self { running, - tasks, // Added tasks + tasks, + }) + } + + /// Start web-based camera share (relay from Web to Peers). + pub async fn start_web_camera( + peers: Vec, + network_manager: crate::net::NetworkManager, + source_tx: tokio::sync::broadcast::Sender>, + ) -> Result { + let running = Arc::new(AtomicBool::new(true)); + let mut tasks = Vec::new(); + + // For each peer, spawn a sender task that subscribes to the source + for peer in peers { + let running = running.clone(); + let net = network_manager.clone(); + let rx = source_tx.subscribe(); + let kind = MediaKind::Camera; + + tasks.push(tokio::spawn(async move { + if let Err(e) = run_video_sender_native(net, peer, kind, rx, running).await { + tracing::error!("Video sender web camera error: {}", e); + } + })); + } + + Ok(Self { + running, + tasks, + }) + } + + /// Start native video capture via FFmpeg. + pub async fn start_native( + kind: MediaKind, + _local_peer_id: iroh::EndpointId, + peers: Vec, + network_manager: crate::net::NetworkManager, + broadcast_tx: tokio::sync::broadcast::Sender, + ) -> Result { + let running = Arc::new(AtomicBool::new(true)); + + // Channel to distribute frames from FFmpeg to peer senders + let (frame_tx, _) = tokio::sync::broadcast::channel::>(100); + + let mut tasks = Vec::new(); + + // 1. Spawn Capture task + let capture_running = running.clone(); + let frame_tx_clone = frame_tx.clone(); + let broadcast_tx_clone = broadcast_tx.clone(); + let kind_clone = kind.clone(); + + tasks.push(tokio::spawn(async move { + let result = if matches!(kind_clone, MediaKind::Screen) { + run_xcap_capture(frame_tx_clone, broadcast_tx_clone, capture_running).await + } else { + run_ffmpeg_capture(kind_clone, frame_tx_clone, broadcast_tx_clone, capture_running).await + }; + + if let Err(e) = result { + tracing::error!("Capture error: {}", e); + } + })); + + // 2. Spawn peer sender tasks (reuse run_video_sender_web logic but with internal channel) + for peer in peers { + let running = running.clone(); + let net = network_manager.clone(); + let rx = frame_tx.subscribe(); + let kind = kind.clone(); + + tasks.push(tokio::spawn(async move { + if let Err(e) = run_video_sender_native(net, peer, kind, rx, running).await { + tracing::error!("Video sender native error: {}", e); + } + })); + } + + Ok(Self { + running, + tasks, }) } @@ -58,19 +142,48 @@ impl VideoCapture { } } - /// Handle incoming video stream from a peer (Web Version). - /// Receives video frames (e.g. H.264/VP9 encoded inside protocol messages) and forwards to frontend. - pub async fn handle_incoming_video_web( + /// Handle incoming video stream from a peer (Native MPV Version). + /// Receives video frames (e.g. H.264/HEVC encoded inside protocol messages) and pipes them to MPV. + pub async fn handle_incoming_video_native( from: iroh::EndpointId, message: MediaStreamMessage, mut recv: iroh::endpoint::RecvStream, - broadcast_tx: tokio::sync::broadcast::Sender, + _broadcast_tx: tokio::sync::broadcast::Sender, ) -> Result<()> { let kind = match message { MediaStreamMessage::VideoStart { kind, .. } => kind, _ => anyhow::bail!("Expected VideoStart"), }; - tracing::info!("Starting {:?} stream handler for {}", kind, from); + tracing::info!("Starting {:?} stream handler for {} (Native MPV)", kind, from); + + // Spawn mpv + let mut cmd = Command::new("mpv"); + cmd.args(&[ + "--no-terminal", + "--ontop", + "--profile=low-latency", + "--cache=no", + "--force-window", + "-", // Read from stdin + ]); + cmd.stdin(Stdio::piped()); + // We might want to quell stdout/stderr or log them + cmd.stdout(Stdio::null()); + cmd.stderr(Stdio::null()); + + // Ensure process is killed when this task drops + cmd.kill_on_drop(true); + + let mut child = match cmd.spawn() { + Ok(c) => c, + Err(e) => { + tracing::error!("Failed to spawn mpv: {}", e); + return Err(anyhow::anyhow!("Failed to spawn mpv: {}", e)); + } + }; + + let mut stdin = child.stdin.take().expect("Failed to open mpv stdin"); + use tokio::io::AsyncWriteExt; loop { let msg: MediaStreamMessage = match decode_framed(&mut recv).await { @@ -80,13 +193,20 @@ impl VideoCapture { match msg { MediaStreamMessage::VideoFrame { data, .. } => { - // Broadcast to web - let short_id: String = format!("{}", from).chars().take(8).collect(); - let _ = broadcast_tx.send(WebMediaEvent::Video { - peer_id: short_id, - kind: kind.clone(), - data, - }); + // Write directly to mpv stdin + // The data is already NAL units with start codes (from our capture logic) + // Note: 'data' from VideoFrame contains [1 byte type][NAL Unit including start code] + // We need to skip the first byte which is our protocol's frame type indicator (Key/Delta) + // Wait, let's check run_ffmpeg_capture. + // It does: payload.push(frame_type); payload.extend_from_slice(&nal_data); + // So yes, we need to skip the first byte. + + if data.len() > 1 { + if let Err(e) = stdin.write_all(&data[1..]).await { + tracing::error!("Failed to write to mpv: {}", e); + break; + } + } } MediaStreamMessage::VideoStop { .. } => { tracing::info!("Peer stopped video"); @@ -95,6 +215,8 @@ impl VideoCapture { _ => {} } } + + let _ = child.kill().await; Ok(()) } } @@ -105,11 +227,191 @@ impl Drop for VideoCapture { } } +// --------------------------------------------------------------------------- +// XCAP Capture Logic +// --------------------------------------------------------------------------- + +async fn run_xcap_capture( + frame_tx: tokio::sync::broadcast::Sender>, + broadcast_preview: tokio::sync::broadcast::Sender, + running: Arc, +) -> Result<()> { + // 1. Get monitors + let monitors = Monitor::all().map_err(|e| anyhow::anyhow!("Failed to list monitors: {}", e))?; + if monitors.is_empty() { + return Err(anyhow::anyhow!("No monitors found")); + } + + // Select first monitor for now + let monitor = &monitors[0]; + let width = monitor.width().map_err(|e| anyhow::anyhow!("Failed to get monitor width: {}", e))?; + let height = monitor.height().map_err(|e| anyhow::anyhow!("Failed to get monitor height: {}", e))?; + let name = monitor.name().unwrap_or_else(|_| "Unknown Monitor".to_string()); + + tracing::info!("Starting xcap capture on monitor: {} ({}x{})", name, width, height); + + // 2. Spawn FFmpeg to encode raw frames + // We feed raw RGBA frames to stdin + let mut cmd = Command::new("ffmpeg"); + cmd.kill_on_drop(true); + + cmd.args(&[ + "-f", "rawvideo", + "-pixel_format", "rgba", + "-video_size", &format!("{}x{}", width, height), + "-framerate", "30", + "-i", "-", // Read raw frames from stdin + ]); + + // Output args (same as before: HEVC NVENC/libx265 -> Raw stream) + cmd.args(&[ + "-vf", "scale=1280:720", // Force 720p resize + "-c:v", "hevc_nvenc", // Try hardware first + // Fallback or options... + "-b:v", "1M", // Lower bitrate to 1Mbps + "-g", "30", // Keyframe interval (GOP) 30 + "-zerolatency", "1", + "-preset", "p4", + "-f", "hevc", + "-", + ]); + + cmd.stdin(Stdio::piped()); + cmd.stdout(Stdio::piped()); + + // Log stderr + let stderr_file = std::fs::File::create("ffmpeg_xcap.log").unwrap(); + cmd.stderr(Stdio::from(stderr_file)); + + let mut child = cmd.spawn()?; + let mut stdin = child.stdin.take().expect("Failed to open ffmpeg stdin"); + let stdout = child.stdout.take().expect("Failed to open ffmpeg stdout"); + + // 3. Spawn thread/task to capture frames and write to FFmpeg stdin + // xcap is synchronous/blocking, so we should run it in a blocking task or separate thread + // But we need to write to async stdin. + + let running_clone = running.clone(); + let monitor_clone = monitor.clone(); // Monitor might not be cloneable easily? It is. + + // We use a channel to send frames from blocking capture to async writer + let (img_tx, mut img_rx) = tokio::sync::mpsc::channel::>(2); + + // Spawn blocking capture thread + std::thread::spawn(move || { + // Target 30fps + let frame_duration = std::time::Duration::from_millis(33); + + while running_clone.load(Ordering::Relaxed) { + let start = std::time::Instant::now(); + + match monitor_clone.capture_image() { + Ok(image) => { + // image is RgbaImage (Vec) + // We need raw bytes + let bytes = image.into_raw(); + if img_tx.blocking_send(bytes).is_err() { + break; + } + } + Err(e) => { + eprintln!("xcap capture failed: {}", e); + // Don't break immediately, maybe transient? + std::thread::sleep(std::time::Duration::from_millis(100)); + } + } + + // Sleep to maintain framerate + let elapsed = start.elapsed(); + if elapsed < frame_duration { + std::thread::sleep(frame_duration - elapsed); + } else { + // Even if we are slow, sleep a tiny bit to yield CPU + std::thread::sleep(std::time::Duration::from_millis(1)); + } + } + }); + + // 4. Async task to write frames to FFmpeg stdin + let stdin_task = tokio::spawn(async move { + while let Some(frame_data) = img_rx.recv().await { + if stdin.write_all(&frame_data).await.is_err() { + break; + } + } + }); + + // 5. Read FFmpeg stdout and distribute (Same logic as run_ffmpeg_capture) + let mut reader = BufReader::new(stdout); + let mut buffer = Vec::with_capacity(1024 * 1024); + let mut temp_buf = [0u8; 4096]; + + loop { + if !running.load(Ordering::Relaxed) { + break; + } + + let n = match reader.read(&mut temp_buf).await { + Ok(0) => break, // EOF + Ok(n) => n, + Err(_) => break, + }; + buffer.extend_from_slice(&temp_buf[0..n]); + + // Find NAL units + while let Some(start_idx) = find_start_code(&buffer) { + let end_idx = if let Some(next_start) = find_start_code_from(&buffer, start_idx + 4) { + next_start + } else { + break; // Wait for more data + }; + + let nal_data = buffer.drain(start_idx..end_idx).collect::>(); + + // Check if it's 3-byte or 4-byte start code + let start_code_len = if nal_data[2] == 1 { 3 } else { 4 }; + + // Construct payload + let mut payload: Vec = Vec::with_capacity(1 + nal_data.len()); + + // Check NAL type (HEVC/H.265) + // NAL header is after start code. + // data[start_code_len] is the NAL header. + let nal_header_byte = nal_data[start_code_len]; + + // Type is bits 1-6 (0x7E) shifted right by 1. + let nal_type = (nal_header_byte & 0x7E) >> 1; + + // HEVC Keyframes: + // 16-21: IRAP (BLA_W_LP, BLA_W_RADL, BLA_N_LP, IDR_W_RADL, IDR_N_LP, CRA_NUT) + // 32-34: VPS, SPS, PPS (Parameters - treat as critical/key) + let is_key = (nal_type >= 16 && nal_type <= 21) || (nal_type >= 32 && nal_type <= 34); + + let frame_type = if is_key { 0u8 } else { 1u8 }; + + payload.push(frame_type); + payload.extend_from_slice(&nal_data); + + let _ = frame_tx.send(payload.clone()); + + let _ = broadcast_preview.send(WebMediaEvent::Video { + peer_id: "local".to_string(), + kind: MediaKind::Screen, + data: payload, + }); + } + } + + let _ = child.kill().await; + stdin_task.abort(); + Ok(()) +} + // --------------------------------------------------------------------------- // FFmpeg Capture Logic // --------------------------------------------------------------------------- -async fn run_video_sender_web( +async fn run_video_sender_native( network_manager: crate::net::NetworkManager, peer: iroh::EndpointId, kind: MediaKind, @@ -119,13 +421,14 @@ async fn run_video_sender_web( let (mut send, _) = network_manager .open_media_stream(peer, kind.clone()) .await?; - // For web, we assume fixed resolution and fps for now. + + // Send Start message write_framed( &mut send, &MediaStreamMessage::VideoStart { kind, - width: 640, - height: 480, + width: 1280, // Target 720p + height: 720, fps: 30, }, ) @@ -134,9 +437,10 @@ async fn run_video_sender_web( while running.load(Ordering::Relaxed) { match input_rx.recv().await { Ok(data) => { - // Web sends WebP chunk (full frame) + // FFmpeg data is already [FrameType][VP8 Chunk], see run_ffmpeg_capture + // Just wrap in protocol message let msg = MediaStreamMessage::VideoFrame { - sequence: 0, // Sequence not used for web input, set to 0 + sequence: 0, timestamp_ms: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() @@ -147,18 +451,325 @@ async fn run_video_sender_web( break; } } - Err(tokio::sync::broadcast::error::RecvError::Closed) => break, - Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { - tracing::warn!("Video sender lagged"); - } + Err(_) => break, } } - let _ = write_framed(&mut send, &MediaStreamMessage::VideoStop { kind }).await; send.finish()?; Ok(()) } -// --------------------------------------------------------------------------- -// Player Logic (MPV/VLC) -// --------------------------------------------------------------------------- +async fn run_ffmpeg_capture( + kind: MediaKind, + frame_tx: tokio::sync::broadcast::Sender>, + broadcast_preview: tokio::sync::broadcast::Sender, + running: Arc, +) -> Result<()> { + let mut cmd = Command::new("ffmpeg"); + cmd.kill_on_drop(true); + + // Output args: Robust Encoder Selection + // Try: hevc_nvenc -> h264_nvenc -> libx264 + + struct EncoderConfig { + name: &'static str, + codec: &'static str, + opts: Vec<&'static str>, + format: &'static str, // "hevc" or "h264" (raw stream format) + filter: &'static str, // "hevc_mp4toannexb" or "h264_mp4toannexb" + pixel_format: Option<&'static str>, // Force pixel format if needed + } + + let encoders = vec![ + EncoderConfig { + name: "hevc_nvenc (Hardware)", + codec: "hevc_nvenc", + opts: vec!["-b:v", "1M", "-g", "30", "-zerolatency", "1", "-preset", "p4"], + format: "hevc", + filter: "hevc_mp4toannexb", + pixel_format: None, // NVENC usually handles formats well + }, + EncoderConfig { + name: "h264_nvenc (Hardware Fallback)", + codec: "h264_nvenc", + opts: vec!["-b:v", "1.5M", "-g", "30", "-zerolatency", "1", "-preset", "p4"], + format: "h264", + filter: "h264_mp4toannexb", + pixel_format: None, + }, + EncoderConfig { + name: "libx264 (Software Fallback)", + codec: "libx264", + opts: vec!["-preset", "ultrafast", "-tune", "zerolatency", "-b:v", "1M", "-g", "30"], + format: "h264", + filter: "h264_mp4toannexb", + pixel_format: Some("yuv420p"), // libx264 often needs yuv420p + }, + ]; + + let mut final_child = None; + let mut chosen_filter = ""; + // We need to keep the stdout/stderr open + + for enc in &encoders { + tracing::info!("Trying encoder: {}", enc.name); + + let mut cmd = Command::new("ffmpeg"); + cmd.kill_on_drop(true); + + // Input args (re-applied for each attempt) + // TODO: Detect platform/device better. For now assuming Linux/V4L2. + match kind { + MediaKind::Camera => { + cmd.args(&[ + "-f", "v4l2", + "-framerate", "30", + "-video_size", "1280x720", + "-i", "/dev/video0", + ]); + } + MediaKind::Screen => { + // Always use x11grab (works on X11 and XWayland) + let display_env = std::env::var("DISPLAY").unwrap_or_else(|_| ":0.0".to_string()); + tracing::info!("Using x11grab on display: {}", display_env); + + cmd.args(&[ + "-f", "x11grab", + "-framerate", "30", + "-video_size", "1920x1080", // Input size (assuming 1080p for now, but safer to autodect or be large) + "-i", &display_env, + "-vf", "scale=1280:720", // Force 720p resize + ]); + } + _ => return Ok(()), + } + + // Pixel format if needed + if let Some(pix_fmt) = enc.pixel_format { + cmd.args(&["-pix_fmt", pix_fmt]); + } + + // Encoder args + cmd.arg("-c:v").arg(enc.codec); + cmd.args(&enc.opts); + + // Bitstream filter to ensure Annex B (start codes) + cmd.arg("-bsf:v").arg(enc.filter); + + // Output format + cmd.arg("-f").arg(enc.format); + cmd.arg("-"); + + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + + match cmd.spawn() { + Ok(mut child) => { + // Wait a bit to see if it crashes immediately + // We sleep for 500ms to let ffmpeg initialize + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + if let Ok(Some(status)) = child.try_wait() { + tracing::warn!("Encoder {} failed immediately with status: {}", enc.name, status); + // Read stderr to see why + if let Some(mut stderr) = child.stderr.take() { + let mut err_buf = String::new(); + let _ = stderr.read_to_string(&mut err_buf).await; + tracing::warn!("FFmpeg stderr: {}", err_buf); + } + continue; // Try next + } + + // It seems to be running + tracing::info!("Selected encoder: {}", enc.name); + tracing::info!("Capture loop started"); + + // Redirect stderr to log file or tracing + if let Some(stderr) = child.stderr.take() { + // Spawn a task to log stderr line by line + let running_log = running.clone(); + tokio::spawn(async move { + let mut reader = BufReader::new(stderr); + let mut line = String::new(); + while running_log.load(Ordering::Relaxed) { + match reader.read_line(&mut line).await { + Ok(0) => break, // EOF + Ok(_) => { + // Log errors or critical warnings + if line.contains("Error") || line.contains("error") || line.contains("fail") { + tracing::error!("FFmpeg: {}", line.trim()); + } + line.clear(); + } + Err(_) => break, + } + } + }); + } + + final_child = Some(child); + chosen_filter = enc.filter; + break; + } + Err(e) => { + tracing::warn!("Failed to spawn encoder {}: {}", enc.name, e); + continue; + } + } + } + + let mut child = match final_child { + Some(c) => c, + None => { + tracing::error!("All encoders failed. Cannot start capture."); + return Err(anyhow::anyhow!("All encoders failed")); + } + }; + + let stdout = child.stdout.take().expect("Failed to open stdout"); + // We don't need BufReader for raw check if we just read blocks, but fine to use it or just AsyncRead + let mut reader = BufReader::new(stdout); + + // Raw H.264 parsing (Annex B) + // Stream is a sequence of NAL units, each starting with 00 00 00 01 (or 00 00 01) + // We need to buffer and split. + + let mut buffer = Vec::with_capacity(1024 * 1024); // 1MB buffer + let mut temp_buf = [0u8; 4096]; + + while running.load(Ordering::Relaxed) { + let n = match reader.read(&mut temp_buf).await { + Ok(0) => break, // EOF + Ok(n) => n, + Err(_) => break, + }; + buffer.extend_from_slice(&temp_buf[0..n]); + + // Find NAL units in buffer + // A NAL unit starts with 00 00 00 01 + // We look for start codes. + + while let Some(start_idx) = find_start_code(&buffer) { + // If we found a start code at index 0, we can't extract a frame yet + // unless we have another start code later. + // But actually, the buffer MIGHT have multiple NALs. + + // We need to find the NEXT start code to know where this one ends. + // Search from start_idx + 4 + + let end_idx = if let Some(next_start) = find_start_code_from(&buffer, start_idx + 4) { + next_start + } else { + // No next start code yet. + // If the buffer is getting huge, maybe we should just consume it? + // But for H.264 streaming we must be precise. + // Wait for more data. + break; + }; + + // Extract NAL unit (including start code? Browsers usually want it for AVC1/AnnexB) + // WebCodecs EncodedVideoChunk expects: + // "For 'avc1' (H.264), the chunk data must be an Annex B NAL unit." + // So we include the 00 00 00 01. + + let nal_data = buffer.drain(start_idx..end_idx).collect::>(); + + // Send NAL + // Frame Type detection for H.264: + // NAL type is in the first byte AFTER the start code. + // Start Code is 00 00 00 01 (4 bytes) or 00 00 01 (3 bytes) + // find_start_code finds 00 00 00 01. + + // Let's handle 3-byte start codes too? ffmpeg -f h264 usually sends 4-byte. + + // Check if it's 3-byte or 4-byte start code + let start_code_len = if nal_data[2] == 1 { 3 } else { 4 }; + + // Construct payload + let mut payload: Vec = Vec::with_capacity(1 + nal_data.len()); + + // Check NAL type + // nal_data[start_code_len] is the NAL header (first byte). + let nal_header_byte = nal_data[start_code_len]; + + let is_key = if chosen_filter.contains("hevc") { + // HEVC (H.265) + // Type is bits 1-6 (0x7E) shifted right by 1. + let nal_type = (nal_header_byte & 0x7E) >> 1; + + // HEVC Keyframes: + // 16-21: IRAP (BLA_W_LP, BLA_W_RADL, BLA_N_LP, IDR_W_RADL, IDR_N_LP, CRA_NUT) + // 32-34: VPS, SPS, PPS (Parameters - treat as critical/key) + (nal_type >= 16 && nal_type <= 21) || (nal_type >= 32 && nal_type <= 34) + } else { + // H.264 (AVC) + // Type is lower 5 bits (0x1F) + let nal_type = nal_header_byte & 0x1F; + + // H.264 Keyframes: + // 5: IDR (Instantaneous Decoding Refresh) - Keyframe + // 7: SPS (Sequence Parameter Set) + // 8: PPS (Picture Parameter Set) + nal_type == 5 || nal_type == 7 || nal_type == 8 + }; + + let frame_type = if is_key { 0u8 } else { 1u8 }; + + payload.push(frame_type); + payload.extend_from_slice(&nal_data); + + // Send to peers + let _ = frame_tx.send(payload.clone()); + + // Send to local web preview + let _ = broadcast_preview.send(WebMediaEvent::Video { + peer_id: "local".to_string(), + kind: kind.clone(), + data: payload, + }); + + // buffer now starts at what was end_idx (because of drain) + // drain removes items, so indexes shift. + // wait, drain(start..end) removes items. buffer automatically shrinks. + // so we loop again to see if there is ANOTHER start code at 0? + // Actually, `find_start_code` searches from 0. + // If we drained 0..end_idx, the next bytes are at 0. + } + } + + let _ = child.kill().await; + Ok(()) +} + +fn find_start_code(data: &[u8]) -> Option { + find_start_code_from(data, 0) +} + +fn find_start_code_from(data: &[u8], start: usize) -> Option { + if data.len() < 3 { return None; } + for i in start..data.len() - 2 { + // Look for 00 00 01 + if data[i] == 0 && data[i+1] == 0 && data[i+2] == 1 { + // Check if it's actually 00 00 00 01 (4 bytes) + // If i > 0 and data[i-1] == 0, then the start code might have been at i-1 + // But we iterate forward. + // If we find 00 00 01 at i, we return i. + // If there was a 0 before it, it would have been found as 00 00 01 at i-1? + // Wait. 00 00 00 01 contains 00 00 01 starting at offset 1. + // So if we have 00 00 00 01: + // i=0: 00 00 00 -> No. + // i=1: 00 00 01 -> Yes. Return 1. + // But the start code is at 0! + + // Correct logic: + // If we find 00 00 01 at i, check if i > 0 and data[i-1] == 0. + // If so, the start code is at i-1 (4 bytes). + // Return i-1. + if i > start && data[i-1] == 0 { + return Some(i-1); + } + return Some(i); + } + } + None +} diff --git a/src/media/mod.rs b/src/media/mod.rs index a71d77e..b923b18 100644 --- a/src/media/mod.rs +++ b/src/media/mod.rs @@ -17,6 +17,8 @@ use crate::protocol::{decode_framed, MediaKind, MediaStreamMessage}; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; +use cpal::traits::{DeviceTrait, HostTrait}; + use self::capture::VideoCapture; use self::voice::VoiceChat; @@ -38,8 +40,6 @@ pub enum WebMediaEvent { 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). @@ -49,27 +49,31 @@ pub struct MediaState { 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, + pub input_device: Option, + pub output_device: Option, + pub initial_master_volume: f32, + pub initial_noise_suppression: bool, } impl MediaState { - pub fn new(mic_bitrate: u32) -> Self { + pub fn new(mic_bitrate: u32, input_device: Option, output_device: Option, master_volume: f32, noise_suppression: bool) -> Self { 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(), broadcast_tx, mic_broadcast, - cam_broadcast, screen_broadcast, mic_bitrate: Arc::new(AtomicU32::new(mic_bitrate)), + input_device, + output_device, + initial_master_volume: master_volume, + initial_noise_suppression: noise_suppression, } } @@ -77,16 +81,150 @@ impl MediaState { self.mic_bitrate.store(bitrate, Ordering::Relaxed); } + pub fn set_input_device(&mut self, device_name: String) { + self.input_device = Some(device_name); + } + + pub fn set_output_device(&mut self, device_name: String) { + self.output_device = Some(device_name); + } + + pub fn get_input_devices(&self) -> Vec { + let mut names = Vec::new(); + + // Prioritize JACK if available, otherwise ALSA/Pulse/WASAPI + let available_hosts = cpal::available_hosts(); + let mut hosts = Vec::new(); + + // Push JACK first if available + if available_hosts.contains(&cpal::HostId::Jack) { + hosts.push(cpal::host_from_id(cpal::HostId::Jack).unwrap()); + } + + // Then default host + hosts.push(cpal::default_host()); + + for host in hosts { + if let Ok(devices) = host.input_devices() { + for device in devices { + if let Ok(name) = device.name() { + // Filter out common noise/unusable devices + if name.contains("dmix") || name.contains("dsnoop") || name.contains("null") { + continue; + } + + // Clean up ALSA names + // Example: "sysdefault:CARD=PCH" -> "PCH (sysdefault)" + // Example: "front:CARD=Microphone,DEV=0" -> "Microphone (front)" + let clean_name = if let Some(start) = name.find("CARD=") { + let rest = &name[start + 5..]; + let card_name = rest.split(',').next().unwrap_or(rest); + + let prefix = name.split(':').next().unwrap_or("Unknown"); + format!("{} ({})", card_name, prefix) + } else if name.contains("HDA Intel PCH") { + // Simplify generic Intel names if possible + name + } else { + name + }; + + names.push(clean_name); + } + } + } + } + + // Dedup and sort + names.sort(); + names.dedup(); + names + } + + pub fn get_output_devices(&self) -> Vec { + let mut names = Vec::new(); + + // Prioritize JACK if available + let available_hosts = cpal::available_hosts(); + let mut hosts = Vec::new(); + + if available_hosts.contains(&cpal::HostId::Jack) { + hosts.push(cpal::host_from_id(cpal::HostId::Jack).unwrap()); + } + hosts.push(cpal::default_host()); + + for host in hosts { + if let Ok(devices) = host.output_devices() { + for device in devices { + if let Ok(name) = device.name() { + if name.contains("dmix") || name.contains("dsnoop") || name.contains("null") { + continue; + } + + let clean_name = if let Some(start) = name.find("CARD=") { + let rest = &name[start + 5..]; + let card_name = rest.split(',').next().unwrap_or(rest); + let prefix = name.split(':').next().unwrap_or("Unknown"); + format!("{} ({})", card_name, prefix) + } else { + name + }; + + names.push(clean_name); + } + } + } + } + + names.sort(); + names.dedup(); + names + } + // ----------------------------------------------------------------------- // Public state queries // ----------------------------------------------------------------------- - pub fn voice_enabled(&self) -> bool { - self.voice.is_some() + pub fn set_volume(&self, volume: f32) { + if let Some(voice) = &self.voice { + voice.set_volume(volume); + } } - pub fn camera_enabled(&self) -> bool { - self.camera.is_some() + pub fn get_volume(&self) -> f32 { + if let Some(voice) = &self.voice { + voice.get_volume() + } else { + self.initial_master_volume + } + } + + pub fn is_denoise_enabled(&self) -> bool { + if let Some(voice) = &self.voice { + voice.is_denoise_enabled() + } else { + self.initial_noise_suppression + } + } + + pub fn toggle_denoise(&self) -> Option { + if let Some(voice) = &self.voice { + Some(voice.toggle_denoise()) + } else { + None + } + } + + pub fn get_peer_levels(&self) -> std::collections::HashMap { + if let Some(voice) = &self.voice { + voice.get_peer_levels() + } else { + std::collections::HashMap::new() + } + } + + pub fn voice_enabled(&self) -> bool { + self.voice.is_some() } pub fn screen_enabled(&self) -> bool { @@ -107,61 +245,32 @@ impl MediaState { "🎤 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( + // Use Native Capture + match VoiceChat::start_native( net.clone(), peers.keys().cloned().collect(), + self.mic_broadcast.clone(), self.mic_broadcast.subscribe(), // Subscribe to get new receiver! self.broadcast_tx.clone(), self.mic_bitrate.clone(), + self.input_device.clone(), + self.output_device.clone(), // Added output device + self.initial_master_volume, + self.initial_noise_suppression, ) { Ok(vc) => { self.voice = Some(vc); - "🎤 Voice chat started (Web)" + "🎤 Voice chat started (Native)" } Err(e) => { - tracing::error!("Failed to start voice chat: {}", e); + tracing::error!("Failed to start native 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() { @@ -172,22 +281,24 @@ impl MediaState { } else { // Start let peers = net.peers.lock().await; - match VideoCapture::start_web( + + // Use Native Capture (FFmpeg) + match VideoCapture::start_native( MediaKind::Screen, net.our_id, peers.keys().cloned().collect(), net.clone(), - self.screen_broadcast.clone(), + self.broadcast_tx.clone(), ) .await { Ok(vc) => { self.screen = Some(vc); - "🖥️ Screen share started (Web)" + "🖥️ Screen share started (Native FFmpeg)" } Err(e) => { tracing::error!("Failed to start screen share: {}", e); - "🖥️ Failed to start screen share" + "🖥️ Failed to start screen share (Install FFmpeg)" } } } @@ -201,36 +312,30 @@ impl MediaState { pub fn handle_incoming_media( &mut self, from: EndpointId, - kind: MediaKind, + _kind: MediaKind, _send: iroh::endpoint::SendStream, mut recv: iroh::endpoint::RecvStream, ) { 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); - } + // DEPRECATED in Native Datagram mode + tracing::warn!("Received Audio stream from {} (unexpected in datagram mode)", from); + // We could support stream fallback, but for now we ignore or log. + // Or we can close it. } MediaStreamMessage::VideoStart { .. } => { tracing::info!("Accepted Video stream from {:?}", from); if let Err(e) = - VideoCapture::handle_incoming_video_web(from, msg, recv, broadcast_tx) + VideoCapture::handle_incoming_video_native(from, msg, recv, broadcast_tx) .await { - tracing::error!("Video web playback error: {}", e); + tracing::error!("Video native playback error: {}", e); } } _ => { @@ -251,26 +356,25 @@ impl MediaState { } }); - // 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()); } /// Handle an incoming datagram (unreliable audio/video). pub fn handle_incoming_datagram(&mut self, from: EndpointId, data: bytes::Bytes) { - // We assume datagrams are for VOICE for now (simplification). - // Or we should add a prefix byte? - // Since we are optimizing audio, let's assume it's audio. - // But if we add video datagrams later... - - // For now, let's try to interpret as audio. - // Since VoiceChat expects `MediaStreamMessage`, we need to see how `postcard` serialized it. - // If sender sends serialized `AudioData`, we can deserialize it. - - if let Some(voice) = &mut self.voice { - voice.handle_datagram(from, data, self.broadcast_tx.clone()); + if data.is_empty() { return; } + + // Check first byte for type + match data[0] { + 1 => { // Audio + if let Some(voice) = &mut self.voice { + voice.handle_datagram(from, data); + } + }, + // 2 => Video? + _ => { + // tracing::trace!("Unknown datagram type: {}", data[0]); + } } } @@ -285,17 +389,12 @@ impl MediaState { } else { "🎤 off" }; - let cam = if self.camera_enabled() { - "📷 LIVE" - } else { - "📷 off" - }; let scr = if self.screen_enabled() { "🖥 LIVE" } else { "🖥 off" }; - format!("{} │ {} │ {}", mic, cam, scr) + format!("{} {} {}", mic, "I", scr) } /// Shut down all active media. @@ -303,9 +402,6 @@ impl MediaState { 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(); } @@ -320,7 +416,3 @@ impl Drop for MediaState { self.shutdown(); } } - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- diff --git a/src/media/voice.rs b/src/media/voice.rs index c52cda4..ac02f61 100644 --- a/src/media/voice.rs +++ b/src/media/voice.rs @@ -1,80 +1,184 @@ -//! Voice capture and playback using PipeWire + Audiopus (via Songbird dependency). +//! Voice capture and playback using cpal + Audiopus (Opus). //! -//! 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. +//! Native implementation using QUIC Datagrams. +use std::collections::HashMap; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::Arc; use std::thread; +use std::time::Duration; -use crate::media::WebMediaEvent; -use crate::protocol::{decode_framed, MediaStreamMessage}; -use anyhow::Result; -use postcard; -// Use audiopus types directly +use anyhow::{Result, anyhow}; +use dashmap::DashMap; +use nnnoiseless::DenoiseState; use audiopus::{ coder::Decoder as OpusDecoder, coder::Encoder as OpusEncoder, Application, Bitrate, Channels, SampleRate, }; +use bytes::Bytes; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use crossbeam_channel::{unbounded, Receiver, Sender}; +use iroh::EndpointId; +use ringbuf::{traits::*, HeapRb}; +use crate::media::WebMediaEvent; + +const PACKET_TYPE_AUDIO: u8 = 1; +const FRAME_SIZE_SAMPLES: usize = 960; // 20ms at 48kHz + +// Wrapper to make OpusDecoder Send + Sync +struct SendDecoder(OpusDecoder); +unsafe impl Send for SendDecoder {} +unsafe impl Sync for SendDecoder {} + +// Wrapper to make AudioProducer Sync +struct SyncAudioProducer(ringbuf::HeapProd); +unsafe impl Sync for SyncAudioProducer {} +unsafe impl Send for SyncAudioProducer {} // It is already Send, but for clarity + +impl std::ops::Deref for SyncAudioProducer { + type Target = ringbuf::HeapProd; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl std::ops::DerefMut for SyncAudioProducer { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +// Types for RingBuf +type AudioProducer = SyncAudioProducer; +type AudioConsumer = ringbuf::HeapCons; + -// 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; /// Main voice chat coordination. pub struct VoiceChat { running: Arc, - capture_thread: Option>, tasks: Vec>, - datagram_decoders: std::collections::HashMap, + + // Capture and Playback threads + capture_thread: Option>, + playback_thread: Option>, + + // Per-peer state: Decoder + Jitter Buffer Producer + peer_audio_sinks: HashMap, + + // Channel to notify playback thread of new peers + new_peer_tx: Sender<(EndpointId, AudioConsumer)>, + + // Audio processing controls + pub denoise_enabled: Arc, + pub output_volume: Arc, // stored as f32 bits + pub peer_levels: Arc>, } 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( + /// Start voice chat session (Native Version with CPAL + QUIC Datagrams). + pub fn start_native( net: crate::net::NetworkManager, - peers: Vec, // Multiple peers - mic_rx: tokio::sync::broadcast::Receiver>, + peers: Vec, + mic_tx: tokio::sync::broadcast::Sender>, + _mic_rx: tokio::sync::broadcast::Receiver>, _broadcast_tx: tokio::sync::broadcast::Sender, mic_bitrate: Arc, + input_device_name: Option, + output_device_name: Option, + initial_volume: f32, + initial_denoise: bool, ) -> Result { let running = Arc::new(AtomicBool::new(true)); + let denoise_enabled = Arc::new(AtomicBool::new(initial_denoise)); + let output_volume = Arc::new(AtomicU32::new(initial_volume.to_bits())); + let peer_levels = Arc::new(DashMap::new()); + tracing::info!("Starting Native Voice Chat..."); + + // 1. Setup Playback Thread (CPAL Output) + let (new_peer_tx, new_peer_rx) = unbounded::<(EndpointId, AudioConsumer)>(); + let playback_running = running.clone(); + let playback_device_name = output_device_name.clone(); + let playback_volume = output_volume.clone(); + let playback_levels = peer_levels.clone(); + + let playback_thread = thread::spawn(move || { + run_playback_loop(playback_running, new_peer_rx, playback_device_name, playback_volume, playback_levels); + }); + + // 2. Setup Capture Thread (CPAL Input) + let capture_running = running.clone(); + let mic_tx_capture = mic_tx.clone(); + let capture_device_name = input_device_name.clone(); + let capture_denoise = denoise_enabled.clone(); + + let capture_thread = thread::spawn(move || { + run_capture_loop(capture_running, mic_tx_capture, capture_device_name, capture_denoise); + }); + + // 3. Setup Network Sender Task (Opus -> Datagrams) let mut tasks = Vec::new(); - - // Spawn a single task to encode once and send to all peers. let sender_running = running.clone(); let net_clone = net.clone(); let mic_bitrate_clone = mic_bitrate.clone(); + let mic_rx_sender = mic_tx.subscribe(); // Subscribe to capture + let sender_task = tokio::spawn(async move { - if let Err(e) = run_opis_sender_web_multi( + run_network_sender( net_clone, peers, - mic_rx, + mic_rx_sender, sender_running, mic_bitrate_clone, - ) - .await - { - tracing::error!("Voice sender failed: {}", e); - } + ).await; }); - tasks.push(sender_task); Ok(Self { running, - capture_thread: None, tasks, - datagram_decoders: std::collections::HashMap::new(), + capture_thread: Some(capture_thread), + playback_thread: Some(playback_thread), + peer_audio_sinks: HashMap::new(), + new_peer_tx, + denoise_enabled, + output_volume, + peer_levels, }) } + + pub fn set_volume(&self, volume: f32) { + self.output_volume.store(volume.to_bits(), Ordering::Relaxed); + } + + pub fn get_volume(&self) -> f32 { + f32::from_bits(self.output_volume.load(Ordering::Relaxed)) + } + + pub fn is_denoise_enabled(&self) -> bool { + self.denoise_enabled.load(Ordering::Relaxed) + } + + pub fn toggle_denoise(&self) -> bool { + let current = self.denoise_enabled.load(Ordering::Relaxed); + self.denoise_enabled.store(!current, Ordering::Relaxed); + !current + } + + pub fn get_peer_levels(&self) -> HashMap { + self.peer_levels.iter().map(|entry| (*entry.key(), *entry.value())).collect() + } + + // Kept for compatibility but unused in Native mode + pub fn start_web( + _net: crate::net::NetworkManager, + _peers: Vec, + _mic_rx: tokio::sync::broadcast::Receiver>, + _broadcast_tx: tokio::sync::broadcast::Sender, + _mic_bitrate: Arc, + ) -> Result { + Err(anyhow!("Web voice not supported in this native build")) + } /// Stop voice chat. pub fn stop(&mut self) { @@ -83,211 +187,362 @@ impl VoiceChat { task.abort(); } self.tasks.clear(); + if let Some(t) = self.capture_thread.take() { - t.thread().unpark(); // Wake up if sleeping - let _ = t.join(); + t.thread().unpark(); + } + if let Some(t) = self.playback_thread.take() { + t.thread().unpark(); } } - /// 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"), + /// Handle incoming datagram from Network. + pub fn handle_datagram(&mut self, from: EndpointId, data: Bytes) { + // Packet format: [1 byte TYPE][Opus Data] + if data.len() < 2 { + return; } - 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; - } - _ => {} - } + // Check type (Audio = 1) + if data[0] != PACKET_TYPE_AUDIO { + return; } - Ok(()) - } - pub fn handle_datagram( - &mut self, - from: iroh::EndpointId, - data: bytes::Bytes, - broadcast_tx: tokio::sync::broadcast::Sender, - ) { - // tracing::info!("Received datagram from {} ({} bytes)", from, data.len()); - match postcard::from_bytes::(&data) { - Ok(MediaStreamMessage::AudioData { - opus_data, - sequence, - }) => { - if sequence % 50 == 0 { - tracing::info!("Received AudioData seq {} from {}", sequence, from); - } - let decoder = self.datagram_decoders.entry(from).or_insert_with(|| { - tracing::info!("Creating new OpusDecoder for {}", from); - OpusDecoder::new(SampleRate::Hz48000, Channels::Mono) - .expect("Failed to create decoder") - }); - // Max frame size is ~120ms (5760 samples). Use safe buffer. - let mut pcm = vec![0.0f32; 5760]; - match decoder.decode_float(Some(&opus_data), &mut pcm, false) { - Ok(len) => { - let samples = pcm[..len].to_vec(); - 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), - } + let opus_data = &data[1..]; + + // Get or create decoder/producer for this peer + let (decoder, producer) = self.peer_audio_sinks.entry(from).or_insert_with(|| { + tracing::info!("New voice peer detected: {}", from); + + // Create Jitter Buffer (RingBuf) + // 48kHz * 1s buffer + let rb = HeapRb::::new(48000); + let (prod, cons) = rb.split(); + + // Send consumer to playback thread + if let Err(e) = self.new_peer_tx.send((from, cons)) { + tracing::error!("Failed to send new peer to playback thread: {}", e); + } + + let decoder = OpusDecoder::new(SampleRate::Hz48000, Channels::Mono) + .expect("Failed to create Opus decoder"); + + (SendDecoder(decoder), SyncAudioProducer(prod)) + }); + + // Decode Opus -> PCM + // Max frame size for 120ms is 5760 samples. + let mut pcm = vec![0.0f32; 5760]; + match decoder.0.decode_float(Some(opus_data), &mut pcm, false) { + Ok(len) => { + let samples = &pcm[..len]; + producer.push_slice(samples); + } + Err(e) => { + tracing::warn!("Opus decode error: {:?}", e); } - Ok(_) => {} // Ignore non-audio datagrams - Err(e) => tracing::warn!("Failed to deserialize datagram: {}", e), } } } // --------------------------------------------------------------------------- -// Opus sender — encodes PCM and sends over QUIC (Multi-Peer) +// Loops // --------------------------------------------------------------------------- -async fn run_opis_sender_web_multi( - network_manager: crate::net::NetworkManager, - peers: Vec, - mut input_rx: tokio::sync::broadcast::Receiver>, +fn run_capture_loop( + running: Arc, + mic_tx: tokio::sync::broadcast::Sender>, + device_name: Option, + denoise_enabled: Arc, +) { + let host = cpal::default_host(); + + // Find device + let device = if let Some(ref name) = device_name { + host.input_devices().ok().and_then(|mut ds| ds.find(|d| d.name().map(|n| n == *name).unwrap_or(false))) + } else { + host.default_input_device() + }; + + let device = match device { + Some(d) => d, + None => { + tracing::error!("No input device found"); + return; + } + }; + tracing::info!("Mic opened: {:?}", device.name()); + + // Configure + let config = match device.default_input_config() { + Ok(c) => c, + Err(e) => { + tracing::error!("Failed to get default input config: {}", e); + return; + } + }; + + // We try to stick to default but standardise to 1 channel if possible. + let stream_config: cpal::StreamConfig = config.clone().into(); + + tracing::info!("Input config: {:?}", stream_config); + + // Initialize RNNoise + // RNNoise expects chunks of 480 samples (10ms at 48kHz). + let mut denoise_state = DenoiseState::new(); + let mut processing_buffer: Vec = Vec::with_capacity(480 * 2); + let mut out_buf = [0.0f32; DenoiseState::FRAME_SIZE]; + + let err_fn = |err| tracing::error!("Input stream error: {}", err); + + let stream = match config.sample_format() { + cpal::SampleFormat::F32 => { + let running_clone = running.clone(); + device.build_input_stream( + &stream_config, + move |data: &[f32], _: &_| { + if !running_clone.load(Ordering::Relaxed) { return; } + + // Convert to Mono + let channels = stream_config.channels as usize; + let mono_samples: Vec = if channels == 1 { + data.to_vec() + } else { + data.chunks(channels).map(|chunk| chunk.iter().sum::() / channels as f32).collect() + }; + + if !mono_samples.is_empty() { + let use_denoise = denoise_enabled.load(Ordering::Relaxed); + + if use_denoise { + processing_buffer.extend_from_slice(&mono_samples); + + while processing_buffer.len() >= DenoiseState::FRAME_SIZE { + let chunk: Vec = processing_buffer.drain(0..DenoiseState::FRAME_SIZE).collect(); + denoise_state.process_frame(&mut out_buf, &chunk); + let _ = mic_tx.send(out_buf.to_vec()); + } + } else { + // Pass through + let _ = mic_tx.send(mono_samples); + } + } + }, + err_fn, + None + ) + }, + _ => { + tracing::error!("Input device does not support F32 samples"); + return; + } + }; + + if let Ok(s) = stream { + if let Err(e) = s.play() { + tracing::error!("Failed to play input stream: {}", e); + } + tracing::info!("Voice started (Capture)"); + + // Keep thread alive + while running.load(Ordering::Relaxed) { + thread::sleep(Duration::from_millis(100)); + } + } else if let Err(e) = stream { + tracing::error!("Failed to build input stream: {}", e); + } +} + +fn run_playback_loop( + running: Arc, + new_peer_rx: Receiver<(EndpointId, AudioConsumer)>, + device_name: Option, + output_volume: Arc, + peer_levels: Arc>, +) { + let host = cpal::default_host(); + + let device = if let Some(ref name) = device_name { + host.output_devices().ok().and_then(|mut ds| ds.find(|d| d.name().map(|n| n == *name).unwrap_or(false))) + } else { + host.default_output_device() + }; + + let device = match device { + Some(d) => d, + None => { + tracing::error!("No output device found"); + return; + } + }; + tracing::info!("Speaker opened: {:?}", device.name()); + + let config = match device.default_output_config() { + Ok(c) => c, + Err(e) => { + tracing::error!("Failed to get default output config: {}", e); + return; + } + }; + let stream_config: cpal::StreamConfig = config.clone().into(); + + // Store consumers for mixing + let mut consumers: Vec<(EndpointId, AudioConsumer)> = Vec::new(); + + let err_fn = |err| tracing::error!("Output stream error: {}", err); + + let stream = match config.sample_format() { + cpal::SampleFormat::F32 => { + let running_clone = running.clone(); + device.build_output_stream( + &stream_config, + move |data: &mut [f32], _: &_| { + if !running_clone.load(Ordering::Relaxed) { return; } + + let master_vol = f32::from_bits(output_volume.load(Ordering::Relaxed)); + + // Check for new peers non-blocking + while let Ok((id, c)) = new_peer_rx.try_recv() { + tracing::info!("Adding peer {} to mix", id); + consumers.push((id, c)); + } + + // Mix + let channels = stream_config.channels as usize; + + // We assume we are filling interleaved buffer. + // Our ringbufs are Mono. We duplicate mono to all channels. + + // Pre-allocate level accumulators for this frame + // We'll calculate RMS over the whole buffer size for UI visualization + let mut peer_sums: HashMap = HashMap::new(); + let mut peer_counts: HashMap = HashMap::new(); + + // Iterate output buffer frame by frame (all channels per sample time) + for frame in data.chunks_mut(channels) { + let mut sum: f32 = 0.0; + + // Sum up all peers + for (id, c) in consumers.iter_mut() { + if let Some(sample) = c.try_pop() { + sum += sample; + + // Accumulate squared sample for RMS + *peer_sums.entry(*id).or_default() += sample * sample; + *peer_counts.entry(*id).or_default() += 1; + } + } + + // Apply master volume + sum *= master_vol; + + // Soft clip + let mixed = sum.clamp(-1.0, 1.0); + + // Assign to all channels + for sample in frame.iter_mut() { + *sample = mixed; + } + } + + // Update peer levels in shared map + for (id, sq_sum) in peer_sums { + let count = peer_counts.get(&id).unwrap_or(&1); + let rms = (sq_sum / *count as f32).sqrt(); + + // Smooth decay could be implemented here, but for now just raw RMS + peer_levels.insert(id, rms); + } + }, + err_fn, + None + ) + }, + _ => { + tracing::error!("Output device does not support F32 samples"); + return; + } + }; + + if let Ok(s) = stream { + if let Err(e) = s.play() { + tracing::error!("Failed to play output stream: {}", e); + } + + while running.load(Ordering::Relaxed) { + thread::sleep(Duration::from_millis(100)); + } + } else { + tracing::error!("Failed to build output stream"); + } +} + +async fn run_network_sender( + net: crate::net::NetworkManager, + peers: Vec, + mut mic_rx: tokio::sync::broadcast::Receiver>, running: Arc, mic_bitrate: Arc, -) -> Result<()> { - if peers.is_empty() { - return Ok(()); - } +) { + if peers.is_empty() { return; } - // Connect to all peers to get Connection handles + // Initialize connections let mut connections = Vec::new(); for peer in peers { - // We use VOICE_ALPN, but for datagrams ALPN matters for connection establishment. - - match network_manager - .endpoint - .connect(peer, crate::net::VOICE_ALPN) - .await - { - Ok(conn) => { - connections.push(conn); - } - Err(e) => { - tracing::warn!("Failed to connect to {}: {}", peer, e); - } + match net.endpoint.connect(peer, crate::net::VOICE_ALPN).await { + Ok(conn) => connections.push(conn), + Err(e) => tracing::warn!("Failed to connect to {}: {}", peer, e), } } if connections.is_empty() { tracing::warn!("No reachable peers for voice chat"); - return Ok(()); + return; } let mut encoder = OpusEncoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip) - .map_err(|e| anyhow::anyhow!("Failed to create Opus encoder: {:?}", e))?; + .expect("Failed to create Opus encoder"); + + // Initial bitrate + let _ = encoder.set_bitrate(Bitrate::BitsPerSecond(mic_bitrate.load(Ordering::Relaxed) as i32)); - // Set initial bitrate - let 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))?; + let mut pcm_buffer: Vec = Vec::with_capacity(FRAME_SIZE_SAMPLES * 2); + let mut opus_buffer = vec![0u8; 1500]; - // 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 - let mut sequence: u64 = 0; - - tracing::info!("Starting voice sender loop for {} peers", connections.len()); + tracing::info!("Voice Sender: Broadcasting to {} peers", connections.len()); while running.load(Ordering::Relaxed) { - // ... bitrate check ... + // Update bitrate + let bitrate = mic_bitrate.load(Ordering::Relaxed); + let _ = encoder.set_bitrate(Bitrate::BitsPerSecond(bitrate as i32)); - // Receive PCM from Web - match input_rx.recv().await { + match mic_rx.recv().await { Ok(samples) => { - // tracing::trace!("Received {} audio samples from web", samples.len()); 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(); - + while pcm_buffer.len() >= FRAME_SIZE_SAMPLES { + let chunk: Vec = pcm_buffer.drain(0..FRAME_SIZE_SAMPLES).collect(); + match encoder.encode_float(&chunk, &mut opus_buffer) { Ok(len) => { - let packet = opus_buffer[..len].to_vec(); - let msg = MediaStreamMessage::AudioData { - sequence, - opus_data: packet, - }; - sequence = sequence.wrapping_add(1); - - // Serialize for Datagram - match postcard::to_allocvec(&msg) { - Ok(data) => { - let bytes = bytes::Bytes::from(data); - let mut sent_count = 0; - for (_i, conn) in connections.iter_mut().enumerate() { - if let Err(e) = conn.send_datagram(bytes.clone()) { - tracing::debug!("Failed to send datagram: {}", e); - } else { - sent_count += 1; - } - } - if sent_count > 0 && sequence % 50 == 0 { - tracing::info!( - "Sent audio datagram seq {} to {} peers", - sequence, - sent_count - ); - } + let opus_packet = &opus_buffer[..len]; + + // Construct Datagram: [TYPE=1][OPUS] + let mut datagram = Vec::with_capacity(1 + len); + datagram.push(PACKET_TYPE_AUDIO); + datagram.extend_from_slice(opus_packet); + + let bytes = Bytes::from(datagram); + + // Send to all peers + for conn in &mut connections { + if let Err(e) = conn.send_datagram(bytes.clone()) { + // Don't log every failure for datagrams (spammy) + tracing::debug!("Datagram send error: {}", e); } - Err(e) => tracing::error!("Serialization error: {}", e), } } - Err(e) => { - tracing::error!("Opus encode error: {:?}", e); - } + Err(e) => tracing::error!("Opus encode error: {:?}", e), } } } @@ -297,6 +552,4 @@ async fn run_opis_sender_web_multi( } } } - - Ok(()) } diff --git a/src/net/mod.rs b/src/net/mod.rs index 938f558..cc5fbcb 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -65,12 +65,14 @@ pub enum NetEvent { } /// Information about a connected peer. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize)] pub struct PeerInfo { pub id: EndpointId, pub name: Option, pub capabilities: Option, pub is_self: bool, + #[serde(skip)] + pub audio_level: f32, } /// Manages the iroh networking stack. @@ -204,6 +206,7 @@ impl NetworkManager { name: None, capabilities: None, is_self: false, + audio_level: 0.0, }); } let _ = event_tx.send(NetEvent::PeerUp(peer_id)).await; diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 8d83e22..80991d1 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -15,6 +15,7 @@ use ratatui::Frame; use crate::chat::ChatState; use crate::file_transfer::FileTransferManager; +use crate::app_logic::AppCommand; use crate::media::MediaState; use crate::net::PeerInfo; @@ -27,26 +28,6 @@ pub enum InputMode { FilePrompt, } -/// 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), - AcceptFile(String), // file_id prefix - ChangeNick(String), - Connect(String), - ToggleVoice, - ToggleCamera, - ToggleScreen, - - SetBitrate(u32), - Leave, - Quit, - None, -} - use crate::config::Theme; /// Application state for the TUI. @@ -76,7 +57,7 @@ impl App { } /// Handle a key event and return a command. - pub fn handle_key(&mut self, key: KeyEvent) -> TuiCommand { + pub fn handle_key(&mut self, key: KeyEvent) -> AppCommand { match self.input_mode { InputMode::FilePrompt => self.handle_file_prompt_key(key), InputMode::Editing => self.handle_editing_key(key), @@ -84,7 +65,7 @@ impl App { } } - fn handle_editing_key(&mut self, key: KeyEvent) -> TuiCommand { + fn handle_editing_key(&mut self, key: KeyEvent) -> AppCommand { match key.code { KeyCode::Enter => { if !self.input.is_empty() { @@ -98,65 +79,64 @@ impl App { "nick" | "name" => { let new_name = parts.get(1).unwrap_or(&"").trim(); if new_name.is_empty() { - return TuiCommand::SystemMessage( + return AppCommand::SystemMessage( "Usage: /nick ".to_string(), ); } - return TuiCommand::ChangeNick(new_name.to_string()); + return AppCommand::ChangeNick(new_name.to_string()); } "connect" | "join" => { let peer_id = parts.get(1).unwrap_or(&"").trim(); if peer_id.is_empty() { - return TuiCommand::SystemMessage( + return AppCommand::SystemMessage( "Usage: /connect ".to_string(), ); } - return TuiCommand::Connect(peer_id.to_string()); + return AppCommand::Connect(peer_id.to_string()); } - "voice" => return TuiCommand::ToggleVoice, + "voice" => return AppCommand::ToggleVoice, // mic/speaker commands removed - "camera" | "cam" => return TuiCommand::ToggleCamera, - "screen" | "share" => return TuiCommand::ToggleScreen, + "screen" | "share" => return AppCommand::ToggleScreen, "file" | "send" => { let path = parts.get(1).unwrap_or(&"").trim(); if path.is_empty() { // Open native file dialog via rfd (cross-platform) if let Some(file) = rfd::FileDialog::new().pick_file() { - return TuiCommand::SendFile(file); + return AppCommand::SendFile(file); } - return TuiCommand::None; // cancelled + return AppCommand::None; // cancelled } - return TuiCommand::SendFile(PathBuf::from(path)); + return AppCommand::SendFile(PathBuf::from(path)); } "accept" | "a" => { let id_prefix = parts.get(1).unwrap_or(&"").trim(); if id_prefix.is_empty() { - return TuiCommand::SystemMessage( + return AppCommand::SystemMessage( "Usage: /accept ".to_string(), ); } - return TuiCommand::AcceptFile(id_prefix.to_string()); + return AppCommand::AcceptFile(id_prefix.to_string()); } - "quit" | "q" => return TuiCommand::Quit, - "leave" => return TuiCommand::Leave, + "quit" | "q" => return AppCommand::Quit, + "leave" => return AppCommand::Leave, "help" => { - return TuiCommand::SystemMessage( - "Commands: /nick , /connect , /voice, /camera, /screen, /file , /accept , /leave, /quit".to_string(), + return AppCommand::SystemMessage( + "Commands: /nick , /connect , /voice, /screen, /file , /accept , /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); + return AppCommand::SetBitrate(kbps * 1000); } else { - return TuiCommand::SystemMessage( + return AppCommand::SystemMessage( "Usage: /bitrate (e.g. 128)".to_string(), ); } } _ => { - return TuiCommand::SystemMessage(format!( + return AppCommand::SystemMessage(format!( "Unknown command: /{}. Type /help", parts[0] )); @@ -164,114 +144,114 @@ impl App { } } - return TuiCommand::SendMessage(text); + return AppCommand::SendMessage(text); } - TuiCommand::None + AppCommand::None } KeyCode::Char(c) => { self.input.insert(self.cursor_position, c); self.cursor_position += 1; - TuiCommand::None + AppCommand::None } KeyCode::Backspace => { if self.cursor_position > 0 { self.cursor_position -= 1; self.input.remove(self.cursor_position); } - TuiCommand::None + AppCommand::None } KeyCode::Delete => { if self.cursor_position < self.input.len() { self.input.remove(self.cursor_position); } - TuiCommand::None + AppCommand::None } KeyCode::Left => { if self.cursor_position > 0 { self.cursor_position -= 1; } - TuiCommand::None + AppCommand::None } KeyCode::Right => { if self.cursor_position < self.input.len() { self.cursor_position += 1; } - TuiCommand::None + AppCommand::None } KeyCode::Home => { self.cursor_position = 0; - TuiCommand::None + AppCommand::None } KeyCode::End => { self.cursor_position = self.input.len(); - TuiCommand::None + AppCommand::None } KeyCode::Esc => { self.input_mode = InputMode::Normal; - TuiCommand::None + AppCommand::None } KeyCode::Up => { self.scroll_offset = self.scroll_offset.saturating_add(1); - TuiCommand::None + AppCommand::None } KeyCode::Down => { self.scroll_offset = self.scroll_offset.saturating_sub(1); - TuiCommand::None + AppCommand::None } - _ => TuiCommand::None, + _ => AppCommand::None, } } - fn handle_normal_key(&mut self, key: KeyEvent) -> TuiCommand { + fn handle_normal_key(&mut self, key: KeyEvent) -> AppCommand { match key.code { - KeyCode::Char('q') | KeyCode::Char('Q') => TuiCommand::Quit, + KeyCode::Char('q') | KeyCode::Char('Q') => AppCommand::Quit, KeyCode::Char('/') => { self.input_mode = InputMode::Editing; self.input.push('/'); self.cursor_position = 1; - TuiCommand::None + AppCommand::None } KeyCode::Char('i') | KeyCode::Enter => { self.input_mode = InputMode::Editing; - TuiCommand::None + AppCommand::None } KeyCode::Up => { self.scroll_offset = self.scroll_offset.saturating_add(1); - TuiCommand::None + AppCommand::None } KeyCode::Down => { self.scroll_offset = self.scroll_offset.saturating_sub(1); - TuiCommand::None + AppCommand::None } - _ => TuiCommand::None, + _ => AppCommand::None, } } - fn handle_file_prompt_key(&mut self, key: KeyEvent) -> TuiCommand { + fn handle_file_prompt_key(&mut self, key: KeyEvent) -> AppCommand { 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); + return AppCommand::SendFile(path); } self.input_mode = InputMode::Editing; - TuiCommand::None + AppCommand::None } KeyCode::Char(c) => { self.file_path_input.push(c); - TuiCommand::None + AppCommand::None } KeyCode::Backspace => { self.file_path_input.pop(); - TuiCommand::None + AppCommand::None } KeyCode::Esc => { self.file_path_input.clear(); self.input_mode = InputMode::Editing; - TuiCommand::None + AppCommand::None } - _ => TuiCommand::None, + _ => AppCommand::None, } } } diff --git a/src/tui/status_bar.rs b/src/tui/status_bar.rs index b9318b7..dd4d0ba 100644 --- a/src/tui/status_bar.rs +++ b/src/tui/status_bar.rs @@ -45,11 +45,11 @@ pub fn render( Span::raw(" "), conn_status, Span::styled( - format!(" │ {} ({})", our_name, our_id_short), + format!(" I {} ({})", our_name, our_id_short), Style::default().fg(Color::Cyan), ), Span::styled( - format!(" │ {}", media.status_line()), + format!(" I {}", media.status_line()), Style::default().fg(Color::DarkGray), ), ]); diff --git a/src/web/mod.rs b/src/web/mod.rs index 998029b..7388fd0 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -21,7 +21,6 @@ use tokio::sync::broadcast; struct AppState { tx: broadcast::Sender, mic_tx: broadcast::Sender>, - cam_tx: broadcast::Sender>, screen_tx: broadcast::Sender>, } @@ -32,17 +31,16 @@ 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)) + .route("/ws/audio", get(ws_audio_handler)) + .route("/ws/screen", get(ws_screen_handler)) .fallback(static_handler) .with_state(state); @@ -66,103 +64,87 @@ pub async fn start_web_server( 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)) + +// --- AUDIO --- +async fn ws_audio_handler(ws: WebSocketUpgrade, State(state): State) -> impl IntoResponse { + ws.on_upgrade(move |socket| handle_audio_socket(socket, state)) } -async fn handle_socket(socket: WebSocket, state: AppState) { +async fn handle_audio_socket(socket: WebSocket, state: AppState) { let (mut sender, mut receiver) = socket.split(); let mut rx = state.tx.subscribe(); + // Outgoing (Server -> Browser) 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)) + if let WebMediaEvent::Audio { peer_id, data: samples } = event { + // Protocol: [IDLen] [ID] [f32...] + let id_bytes = peer_id.as_bytes(); + let id_len = id_bytes.len() as u8; + let mut payload = Vec::with_capacity(1 + id_bytes.len() + samples.len() * 4); + payload.push(id_len); + payload.extend_from_slice(id_bytes); + for s in samples { + payload.extend_from_slice(&s.to_ne_bytes()); } - WebMediaEvent::Video { - peer_id, - kind, - data, - } => { - // 1 byte header (1=Camera, 2=Screen) + 1 byte ID len + ID bytes + WebP 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(Message::Binary(Bytes::from(payload))).await.is_err() { + break; } - }; - - if sender.send(msg).await.is_err() { - break; } } }); + // Incoming (Browser -> Server) 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 (WebP) - tracing::debug!("Received camera frame: {} bytes", payload.len()); - let _ = state.cam_tx.send(payload.to_vec()); - } - 5 => { - // Screen (WebP) - tracing::debug!("Received screen frame: {} bytes", payload.len()); - let _ = state.screen_tx.send(payload.to_vec()); - } - _ => { - tracing::warn!("Unknown WS header: {}", header); - } - } + if let Ok(Message::Binary(data)) = msg { + // Protocol: [f32...] + // (We dropped the header byte 3) + if data.len() % 4 == 0 { + let samples: Vec = data + .chunks_exact(4) + .map(|b| f32::from_ne_bytes([b[0], b[1], b[2], b[3]])) + .collect(); + let _ = state.mic_tx.send(samples); } - Ok(Message::Close(_)) => break, - Err(_) => break, - _ => {} + } + } +} + +// --- SCREEN --- +async fn ws_screen_handler(ws: WebSocketUpgrade, State(state): State) -> impl IntoResponse { + ws.on_upgrade(move |socket| handle_screen_socket(socket, state)) +} + +async fn handle_screen_socket(socket: WebSocket, state: AppState) { + let (mut sender, mut receiver) = socket.split(); + let mut rx = state.tx.subscribe(); + + // Outgoing (Server -> Browser) + tokio::spawn(async move { + while let Ok(event) = rx.recv().await { + if let WebMediaEvent::Video { peer_id, kind, data } = event { + if matches!(kind, MediaKind::Screen) { + let id_bytes = peer_id.as_bytes(); + let id_len = id_bytes.len() as u8; + let mut payload = Vec::with_capacity(1 + id_bytes.len() + data.len()); + payload.push(id_len); + payload.extend_from_slice(id_bytes); + payload.extend_from_slice(&data); + + if sender.send(Message::Binary(Bytes::from(payload))).await.is_err() { + break; + } + } + } + } + }); + + // Incoming (Browser -> Server) + while let Some(msg) = receiver.next().await { + if let Ok(Message::Binary(data)) = msg { + // Protocol: [FrameType] [Data...] + tracing::debug!("Received screen frame: {} bytes", data.len()); + let _ = state.screen_tx.send(data.to_vec()); } } } diff --git a/web/app.js b/web/app.js index 617989f..6a21ef1 100644 --- a/web/app.js +++ b/web/app.js @@ -15,12 +15,18 @@ let micScriptProcessor = null; let audioCtx = null; const SAMPLE_RATE = 48000; +// Video Encoding State +let videoEncoder = null; +let screenEncoder = null; +let screenCanvasLoop = null; // Added +let frameCounter = 0; + // --- Remote Peer State --- // Map const peers = new Map(); @@ -69,7 +75,7 @@ ws.onmessage = (event) => { // Extract ID const idBytes = new Uint8Array(data, 2, idLen); - const peerId = new TextDecoder().decode(idBytes); + let peerId = new TextDecoder().decode(idBytes); // Extract Payload const payload = data.slice(2 + idLen); @@ -84,6 +90,7 @@ ws.onmessage = (event) => { screen: null }; peers.set(peerId, peer); + handlePeerConnected(peer); // Call new handler for peer connection } if (header === 0) { // Audio @@ -103,11 +110,11 @@ function getOrCreateCard(peer, type) { card.className = 'peer-card'; card.id = `peer-${peer.id}-${type}`; - // Video/Image element - const img = document.createElement('img'); - img.className = 'peer-video'; - img.alt = `${type} from ${peer.id}`; - card.appendChild(img); + // Video canvas element + const canvas = document.createElement('canvas'); + canvas.className = 'peer-video'; + // canvas.alt = `${type} from ${peer.id}`; + card.appendChild(canvas); // Overlay info const info = document.createElement('div'); @@ -123,9 +130,41 @@ function getOrCreateCard(peer, type) { videoGrid.appendChild(card); + // Initialize VideoDecoder + const decoder = new VideoDecoder({ + output: (frame) => { + // Draw frame to canvas + console.debug(`[Decoder] Frame decoded: ${frame.displayWidth}x${frame.displayHeight}`); + canvas.width = frame.displayWidth; + canvas.height = frame.displayHeight; + const ctx = canvas.getContext('2d'); + ctx.drawImage(frame, 0, 0); + frame.close(); + + updatePeerActivity(cardObj, false); + }, + error: (e) => { + console.error(`[Decoder] Error (${type}):`, e); + statusOverlay.style.display = 'flex'; + let statusText = `Decoding H.264 from ${peer.id}...`; + statusOverlay.querySelector('h2').textContent = `${statusText} Video Decoder Error: ${e.message}`; + } + }); + + console.log(`[Decoder] Configuring H.264 decoder for ${peer.id} (${type})`); + try { + decoder.configure({ + codec: 'avc1.42E01E', // H.264 Constrained Baseline + optimizeForLatency: true + }); + } catch (err) { + console.error(`[Decoder] Configuration failed:`, err); + } + const cardObj = { card: card, - imgElement: img, + canvas: canvas, + decoder: decoder, statusElement: info.querySelector('.peer-status'), activityTimeout: null }; @@ -166,18 +205,27 @@ function handleRemoteAudio(peer, arrayBuffer) { function handleRemoteVideo(peer, arrayBuffer, type) { const cardObj = getOrCreateCard(peer, type); - const blob = new Blob([arrayBuffer], { type: 'image/webp' }); - const url = URL.createObjectURL(blob); - - const prevUrl = cardObj.imgElement.src; - cardObj.imgElement.onload = () => { - if (prevUrl && prevUrl.startsWith('blob:')) { - URL.revokeObjectURL(prevUrl); + // Payload format: [1 byte frame type] [N bytes encoded chunk] + // Frame Type: 0 = Key, 1 = Delta + const view = new DataView(arrayBuffer); + const isKey = view.getUint8(0) === 0; + const chunkData = arrayBuffer.slice(1); + + const chunk = new EncodedVideoChunk({ + type: isKey ? 'key' : 'delta', + timestamp: performance.now() * 1000, // Use local time for now, or derive from seq + data: chunkData + }); + + try { + if (cardObj.decoder.state === 'configured') { + cardObj.decoder.decode(chunk); + } else { + console.warn(`[Decoder] Not configured yet, dropping chunk (Key: ${isKey})`); } - }; - cardObj.imgElement.src = url; - - updatePeerActivity(cardObj, false); + } catch (e) { + console.error("[Decoder] Decode exception:", e); + } } function updatePeerActivity(cardObj, isAudio) { @@ -191,6 +239,22 @@ function updatePeerActivity(cardObj, isAudio) { } } +function handlePeerConnected(peer) { + // Peer connected (or local preview) + console.log(`[App] Peer connected: ${peer.id}`); + + // Create card if not exists + let cardObj = getOrCreateCard(peer, 'cam'); // Assume cam card for general peer info + + // If it's local preview, update the status + if (peer.id === 'local') { + const statusDot = cardObj.card.querySelector('.peer-status'); + if (statusDot) statusDot.style.backgroundColor = '#3b82f6'; + const nameLabel = cardObj.card.querySelector('.peer-name'); // Updated selector + if (nameLabel) nameLabel.textContent = "Local Preview (H.264)"; + } +} + // --- Local Capture Controls --- function updateButton(btn, active, iconOn, iconOff) { @@ -294,12 +358,39 @@ function stopMic() { async function startCam() { try { - camStream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } }); - localVideo.srcObject = camStream; - startVideoSender(camStream, 4); // 4 = Camera + // Backend now handles capture. We just wait for "local" stream. + // But we might want to tell backend to start? + // Currently backend starts on TUI command /cam. + // Ideally we should have a /cam endpoint or message? + // For now, this button is purely cosmetic if backend is controlled via TUI, + // OR we implemented the /cam command in TUI. + // User asked to Encode on Backend. + // Let's assume user uses TUI /cam or we trigger it via existing mechanism? + // Wait, start_web_server doesn't listen to commands from web. + // The buttons in web UI were starting *browser* capture. + + // If we want the Web UI button to start backend capture, we need an endpoint. + // Since we don't have one easily, let's just show a message or assume TUI control. + // BUT, the existing implementation (VoiceChat::start_web) was triggered by TUI. + // The Web UI was just sending data. + + // Actually, previous flow was: + // 1. TUI /cam -> calls toggle_camera + // 2. toggle_camera -> calls VideoCapture::start_web -> spawns task receiving from channel + // 3. Web UI -> captures video -> sends to WS -> WS handler sends to channel + + // New flow: + // 1. TUI /cam -> calls toggle_camera -> calls Start Native -> Spawns ffmpeg -> sends to broadcast + // 2. Web UI -> receives broadcast -> renders + + // So the "Start Camera" button in Web UI is now USELESS/Misleading if it tries to do browser capture. + // It should probably be removed or replaced with "Status". + alert("Please use /cam in the terminal to start the camera (Backend Encoding)."); + } catch (err) { console.error('Error starting camera:', err); - alert('Camera access failed'); + alert('Failed to start camera'); + updateButton(toggleCamBtn, false, 'videocam', 'videocam_off'); } } @@ -308,62 +399,136 @@ function stopCam() { camStream.getTracks().forEach(t => t.stop()); camStream = null; } + if (videoEncoder) { + // We don't close the encoder, just stop feeding it? + // Or re-create it? Let's keep it but stop the reader loop which is tied to the track. + // Actually, send a config reset next time? + } +} + +//let screenStream = null; + +// Helper to read frames from the stream +async function readLoop(reader, encoder) { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (encoder.state === "configured") { + encoder.encode(value); + value.close(); + } else { + value.close(); + } + } } async function startScreen() { try { - screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true }); - localVideo.srcObject = screenStream; + // Hybrid Mode: Browser Capture + Backend Relay + screenStream = await navigator.mediaDevices.getDisplayMedia({ + video: { + cursor: "always" + }, + audio: false + }); + + const track = screenStream.getVideoTracks()[0]; + const { width, height } = track.getSettings(); - startVideoSender(screenStream, 5); // 5 = Screen - screenStream.getVideoTracks()[0].onended = () => { - stopScreen(); - updateButton(toggleScreenBtn, false, 'screen_share', 'screen_share'); - }; + // 1. Setup Local Preview (Draw to Canvas) + const localCardObj = getOrCreateCard({ id: 'local' }, 'screen'); + const canvas = localCardObj.canvas; + const ctx = canvas.getContext('2d'); + + // Create a temp video element to play the stream for drawing + const tempVideo = document.createElement('video'); + tempVideo.autoplay = true; + tempVideo.srcObject = screenStream; + tempVideo.muted = true; + await tempVideo.play(); + + // Canvas drawing loop + function drawLoop() { + if (tempVideo.paused || tempVideo.ended) return; + if (canvas.width !== tempVideo.videoWidth || canvas.height !== tempVideo.videoHeight) { + canvas.width = tempVideo.videoWidth; + canvas.height = tempVideo.videoHeight; + } + ctx.drawImage(tempVideo, 0, 0); + screenCanvasLoop = requestAnimationFrame(drawLoop); + } + drawLoop(); + + // 2. Encode and Send to Backend (for Peers) + + // Config H.264 Encoder + screenEncoder = new VideoEncoder({ + output: (chunk, metadata) => { + const buffer = new Uint8Array(chunk.byteLength); + chunk.copyTo(buffer); + + // Construct Header: [5 (Screen)] [FrameType] [Data] + // Chunk type: key=1, delta=0? No, EncodedVideoChunkType key/delta + const isKey = chunk.type === 'key'; + const frameType = isKey ? 0 : 1; + + const payload = new Uint8Array(1 + 1 + buffer.length); + payload[0] = 5; // Screen Header + payload[1] = frameType; + payload.set(buffer, 2); + + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(payload); + } + }, + error: (e) => console.error("Screen Encoder Error:", e) + }); + + screenEncoder.configure({ + codec: 'avc1.42E01E', // H.264 Baseline + width: width, + height: height, + bitrate: 3_000_000, // 3Mbps + framerate: 30 + }); + + // Reader + const processor = new MediaStreamTrackProcessor({ track }); + const reader = processor.readable.getReader(); + + readLoop(reader, screenEncoder); + + updateButton(toggleScreenBtn, true, 'stop_screen_share', 'stop_screen_share'); + + // Clean up on stop + track.onended = () => stopScreen(); + } catch (err) { console.error('Error starting screen:', err); + alert(`Failed to start screen share: ${err.message}. \n(Make sure to run /screen in terminal first!)`); + updateButton(toggleScreenBtn, false, 'screen_share', 'screen_share'); } } -function stopScreen() { +async function stopScreen() { if (screenStream) { screenStream.getTracks().forEach(t => t.stop()); screenStream = null; } + if (screenEncoder) { + screenEncoder.close(); + screenEncoder = null; + } + if (screenCanvasLoop) { + cancelAnimationFrame(screenCanvasLoop); + screenCanvasLoop = null; + } + const localCardObj = getOrCreateCard({ id: 'local' }, 'screen'); + const ctx = localCardObj.canvas.getContext('2d'); + ctx.clearRect(0, 0, localCardObj.canvas.width, localCardObj.canvas.height); + + updateButton(toggleScreenBtn, false, 'screen_share', 'screen_share'); } -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/webp', 0.6); - } - setTimeout(sendFrame, 100); // 10 FPS - }; - sendFrame(); -} + + diff --git a/web/style.css b/web/style.css index d91301a..b228017 100644 --- a/web/style.css +++ b/web/style.css @@ -83,7 +83,7 @@ body { align-items: center; } -.peer-card img, .peer-card video { +.peer-card img, .peer-card video, .peer-card canvas { width: 100%; height: 100%; object-fit: contain; /* or cover if preferred */