diff --git a/Cargo.lock b/Cargo.lock index 473fecc..f06dc41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,12 +14,70 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "bitflags" version = "2.9.0" @@ -41,6 +99,12 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "cairo-sys-rs" version = "0.18.2" @@ -60,6 +124,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-expr" version = "0.15.8" @@ -76,6 +146,32 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "dispatch2" version = "0.3.0" @@ -88,6 +184,17 @@ dependencies = [ "objc2", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dlib" version = "0.5.2" @@ -119,6 +226,21 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures" version = "0.3.31" @@ -238,6 +360,18 @@ dependencies = [ "system-deps", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "gio-sys" version = "0.18.1" @@ -302,6 +436,198 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -312,6 +638,34 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[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", + "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 = "js-sys" version = "0.3.77" @@ -324,9 +678,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.172" +version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "libloading" @@ -344,18 +698,77 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + [[package]] name = "objc2" version = "0.6.1" @@ -453,6 +866,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -480,6 +902,18 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -490,6 +924,7 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" name = "rfd" version = "0.17.2" dependencies = [ + "axum", "block2", "dispatch2", "futures", @@ -505,13 +940,16 @@ dependencies = [ "objc2-foundation", "percent-encoding", "pollster", - "raw-window-handle", + "raw-window-handle 0.6.2", + "tempfile", + "tokio", "wasm-bindgen", "wasm-bindgen-futures", "wayland-backend", "wayland-client", "wayland-protocols", "web-sys", + "webbrowser", "windows-sys 0.61.2", ] @@ -524,16 +962,44 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + [[package]] name = "rustversion" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -542,24 +1008,57 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -569,6 +1068,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "shlex" version = "1.3.0" @@ -590,6 +1101,22 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "syn" version = "2.0.101" @@ -601,6 +1128,23 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -620,6 +1164,74 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.8.22" @@ -654,18 +1266,109 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "version-compare" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -745,7 +1448,7 @@ checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" dependencies = [ "cc", "downcast-rs", - "rustix", + "rustix 0.38.44", "scoped-tls", "smallvec", "wayland-sys", @@ -758,7 +1461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" dependencies = [ "bitflags", - "rustix", + "rustix 0.38.44", "wayland-backend", "wayland-scanner", ] @@ -807,6 +1510,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webbrowser" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b" +dependencies = [ + "core-foundation", + "home", + "jni", + "log", + "ndk-context", + "objc", + "raw-window-handle 0.5.2", + "url", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" @@ -823,6 +1543,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -841,6 +1570,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[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]] name = "windows-sys" version = "0.59.0" @@ -850,6 +1588,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -859,6 +1606,21 @@ dependencies = [ "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]] name = "windows-targets" version = "0.52.6" @@ -892,6 +1654,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[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.52.6" @@ -904,6 +1672,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[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.52.6" @@ -916,6 +1690,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[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.52.6" @@ -940,6 +1720,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[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.52.6" @@ -952,6 +1738,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[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.52.6" @@ -964,6 +1756,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[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.52.6" @@ -976,6 +1774,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[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.52.6" @@ -996,3 +1800,98 @@ checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 6089f06..b98d4d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,6 +110,12 @@ web-sys = { version = "0.3.46", features = [ ] } wasm-bindgen-futures = "0.4.19" +[target.'cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd", target_arch = "wasm32")))'.dependencies] +webbrowser = "0.8" +tempfile = "3.8" +tokio = { version = "1.0", features = ["sync", "rt", "time", "net"] } +axum = "0.8" + [[example]] name = "simple" [[example]] diff --git a/README.md b/README.md index 6584cb5..f52e45a 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ It provides both asynchronous and synchronous APIs. Supported platforms: * macOS * Linux & BSDs (GTK3 or XDG Desktop Portal) * WASM32 (async only) + * Others (via web fallback) Refer to the [documentation](https://docs.rs/rfd) for more details. diff --git a/src/backend.rs b/src/backend.rs index a4edf24..d8af3cd 100755 --- a/src/backend.rs +++ b/src/backend.rs @@ -46,6 +46,18 @@ mod win_cid; ))] mod xdg_desktop_portal; +#[cfg(not(any( + target_os = "macos", + target_os = "windows", + target_os = "linux", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd", + target_arch = "wasm32" +)))] +mod web_fallback; + // // Sync // diff --git a/src/backend/web_fallback.rs b/src/backend/web_fallback.rs new file mode 100644 index 0000000..c8bdb09 --- /dev/null +++ b/src/backend/web_fallback.rs @@ -0,0 +1,1267 @@ +//! Web fallback backend for platforms without native file dialog support. +//! +//! This backend uses a local HTTP server (Axum) and the system's web browser to present +//! dialogs to the user. It's primarily intended for Android and other platforms +//! where native dialog APIs aren't available. + +use std::time::Instant; + +use crate::backend::{ + AsyncFilePickerDialogImpl, AsyncFileSaveDialogImpl, AsyncFolderPickerDialogImpl, + AsyncMessageDialogImpl, DialogFutureType, FilePickerDialogImpl, FileSaveDialogImpl, + FolderPickerDialogImpl, MessageDialogImpl, +}; +use crate::file_dialog::FileDialog; +use crate::message_dialog::{MessageButtons, MessageDialog, MessageDialogResult}; +use crate::FileHandle; +use axum::{ + body::Body, + extract::State, + http::{header, StatusCode}, + response::{Html, IntoResponse, Response}, + routing::{get, post}, + Router, +}; +use log::{debug, warn}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::{mpsc, Arc, Mutex}; +use std::time::Duration; +use tokio::sync::oneshot; + +/// Default timeout for waiting for user response (5 minutes) +const DIALOG_TIMEOUT_SECS: u64 = 300; + +// Type aliases to reduce complexity +type ResultSender = Arc>>>; +type ShutdownSender = Arc>>>; + +// ============================================================================ +// HTML Helpers +// ============================================================================ + +/// Simple HTML escaping for user-provided strings +fn html_escape(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + +/// Build an HTML accept attribute from file filters +fn build_accept(filters: &[crate::file_dialog::Filter]) -> String { + let mut exts: Vec = Vec::new(); + for filter in filters { + for ext in &filter.extensions { + if !ext.is_empty() { + exts.push(format!(".{}", ext)); + } + } + } + if exts.is_empty() { + String::new() + } else { + format!(r#"accept="{}""#, exts.join(",")) + } +} + +/// Common HTML page wrapper with basic styling +fn html_page(title: &str, body: &str) -> String { + format!( + r#" + + + + + {title} + + + + {body} + +"#, + title = html_escape(title), + body = body + ) +} + +/// Generate HTML buttons for message dialog +fn generate_buttons_html(buttons: &MessageButtons) -> String { + match buttons { + MessageButtons::Ok => { + r#""#.to_string() + } + MessageButtons::OkCancel => { + r#" + "# + .to_string() + } + MessageButtons::YesNo => { + r#" + "# + .to_string() + } + MessageButtons::YesNoCancel => { + r#" + + "# + .to_string() + } + MessageButtons::OkCustom(text) => { + format!( + r#""#, + t = html_escape(text) + ) + } + MessageButtons::OkCancelCustom(ok, cancel) => { + format!( + r#" + "#, + o = html_escape(ok), + c = html_escape(cancel) + ) + } + MessageButtons::YesNoCancelCustom(yes, no, cancel) => { + format!( + r#" + + "#, + y = html_escape(yes), + n = html_escape(no), + c = html_escape(cancel) + ) + } + } +} + +/// Parse a dialog result string into MessageDialogResult +fn parse_dialog_result(result: &str) -> MessageDialogResult { + match result { + "Ok" | "ok" | "OK" => MessageDialogResult::Ok, + "Cancel" | "cancel" | "CANCEL" => MessageDialogResult::Cancel, + "Yes" | "yes" | "YES" => MessageDialogResult::Yes, + "No" | "no" | "NO" => MessageDialogResult::No, + custom => MessageDialogResult::Custom(custom.to_string()), + } +} + +/// Parse URL-encoded form data +fn parse_urlencoded(body: &str) -> Vec<(String, String)> { + body.split('&') + .filter_map(|pair| { + let mut parts = pair.splitn(2, '='); + let key = parts.next()?; + let value = parts.next().unwrap_or(""); + Some((url_decode(key), url_decode(value))) + }) + .collect() +} + +/// Simple URL decoding +fn url_decode(s: &str) -> String { + let mut result = String::with_capacity(s.len()); + let mut chars = s.chars().peekable(); + + while let Some(c) = chars.next() { + match c { + '%' => { + let mut hex = String::with_capacity(2); + for _ in 0..2 { + if let Some(&h) = chars.peek() { + if h.is_ascii_hexdigit() { + hex.push(chars.next().unwrap()); + } + } + } + if hex.len() == 2 { + if let Ok(byte) = u8::from_str_radix(&hex, 16) { + result.push(byte as char); + } + } + } + '+' => result.push(' '), + _ => result.push(c), + } + } + result +} + +// ============================================================================ +// Multipart Parser +// ============================================================================ + +/// Parsed file from multipart form data +struct MultipartFile { + filename: String, + data: Vec, +} + +/// Parse multipart/form-data content +fn parse_multipart(body: &[u8], boundary: &str) -> Vec { + let mut files = Vec::new(); + + // Create boundary markers + let boundary_line = format!("--{}", boundary); + let end_boundary_line = format!("--{}--", boundary); + + // Convert to bytes for efficient searching + let boundary_bytes = boundary_line.as_bytes(); + let end_boundary_bytes = end_boundary_line.as_bytes(); + + let mut pos = 0; + let len = body.len(); + + while pos < len { + // Find the next boundary + let boundary_pos = if let Some(p) = find_subsequence(&body[pos..], boundary_bytes) { + pos + p + } else { + break; + }; + + // Check if this is the end boundary + if boundary_pos + boundary_bytes.len() < len + && &body[boundary_pos..boundary_pos + end_boundary_bytes.len()] == end_boundary_bytes + { + break; // End of multipart data + } + + // Move past the boundary + pos = boundary_pos + boundary_bytes.len(); + + // Skip CRLF after boundary + if pos + 1 < len && &body[pos..pos + 2] == b"\r\n" { + pos += 2; + } else if pos < len && body[pos] == b'\n' { + pos += 1; + } + + // Find the next boundary or end of data + let next_boundary_pos = find_subsequence(&body[pos..], boundary_bytes) + .map(|p| pos + p) + .unwrap_or(len); + + let part_end = if next_boundary_pos < len { + // Back up to before the next boundary + next_boundary_pos + } else { + len + }; + + if pos >= part_end { + break; + } + + let part = &body[pos..part_end]; + + // Parse this part + if let Some((filename, data)) = parse_multipart_part(part) { + if !filename.is_empty() { + files.push(MultipartFile { + filename, + data: data.to_vec(), + }); + } + } + + pos = part_end; + } + + files +} + +/// Parse a single multipart part +fn parse_multipart_part(part: &[u8]) -> Option<(String, &[u8])> { + // Find the header/body separator + let sep_pos = + find_subsequence(part, b"\r\n\r\n").or_else(|| find_subsequence(part, b"\n\n"))?; + + let headers = &part[..sep_pos]; + let body_start = sep_pos + + if part.get(sep_pos..sep_pos + 4) == Some(b"\r\n\r\n") { + 4 + } else { + 2 + }; + + // Remove trailing whitespace/newlines from body + let mut body_end = part.len(); + while body_end > body_start && (part[body_end - 1] == b'\r' || part[body_end - 1] == b'\n') { + body_end -= 1; + } + + let body = &part[body_start..body_end]; + + // Extract filename from headers + let filename = extract_filename_from_headers(headers)?; + + Some((filename, body)) +} + +/// Find subsequence in byte slice +fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option { + haystack + .windows(needle.len()) + .position(|window| window == needle) +} + +/// Extract filename from Content-Disposition header bytes +fn extract_filename_from_headers(headers: &[u8]) -> Option { + let headers_str = String::from_utf8_lossy(headers); + + for line in headers_str.lines() { + let line_trimmed = line.trim(); + if line_trimmed + .to_lowercase() + .starts_with("content-disposition:") + { + let disposition = &line_trimmed["content-disposition:".len()..].trim(); + + // Parse the disposition parameters + if let Some(params_start) = disposition.find(';') { + let params = &disposition[params_start + 1..]; + + // Look for filename parameter + for param in params.split(';') { + let param = param.trim(); + if let Some(filename_part) = param.strip_prefix("filename=") { + if filename_part.starts_with('"') + && filename_part.ends_with('"') + && filename_part.len() > 1 + { + // Quoted filename + return Some(filename_part[1..filename_part.len() - 1].to_string()); + } else if !filename_part.starts_with('"') { + // Unquoted filename + return Some(filename_part.to_string()); + } + } else if let Some(encoded) = param.strip_prefix("filename*=") { + // RFC 6266 encoded filename + if let Some(quote_pos) = encoded.find("''") { + let charset = &encoded[..quote_pos]; + let value = &encoded[quote_pos + 2..]; + if charset.to_uppercase() == "UTF-8" { + return Some(url_decode(value)); + } + } + } + } + } + } + } + + None +} + +/// Extract boundary from Content-Type header +fn extract_boundary(content_type: &str) -> Option { + let content_type = content_type.trim(); + + // Find the boundary parameter + for part in content_type.split(';') { + let part = part.trim(); + if part.to_lowercase().starts_with("boundary=") { + let boundary = part["boundary=".len()..].trim(); + + // Remove quotes if present + if boundary.starts_with('"') && boundary.ends_with('"') && boundary.len() > 1 { + return Some(boundary[1..boundary.len() - 1].to_string()); + } else if !boundary.is_empty() { + return Some(boundary.to_string()); + } + } + } + + None +} + +// ============================================================================ +// Server Runner Helper +// ============================================================================ + +/// Run an Axum server, open browser, and wait for result +fn run_server( + router: Router, + result_rx: mpsc::Receiver, + shutdown_rx: oneshot::Receiver<()>, +) -> Option { + // Create a tokio runtime for the server + let rt = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(rt) => rt, + Err(e) => { + warn!("Web fallback: Failed to create tokio runtime: {}", e); + return None; + } + }; + + let result = rt.block_on(async { + // Bind to a random available port + let listener = match tokio::net::TcpListener::bind("127.0.0.1:0").await { + Ok(l) => l, + Err(e) => { + warn!("Web fallback: Failed to bind server: {}", e); + return None; + } + }; + + let addr = match listener.local_addr() { + Ok(a) => a, + Err(e) => { + warn!("Web fallback: Failed to get local address: {}", e); + return None; + } + }; + + let url = format!("http://127.0.0.1:{}/", addr.port()); + debug!("Web fallback: Server listening on {}", url); + + // Open browser + if let Err(e) = webbrowser::open(&url) { + warn!("Web fallback: Failed to open browser: {}", e); + return None; + } + + // Run server with graceful shutdown + let server = axum::serve(listener, router).with_graceful_shutdown(async move { + let _ = shutdown_rx.await; + }); + + tokio::spawn(async move { + let _ = server.await; + }); + + // Wait for result with timeout + match tokio::time::timeout( + Duration::from_secs(DIALOG_TIMEOUT_SECS), + tokio::task::spawn_blocking(move || result_rx.recv()), + ) + .await + { + Ok(Ok(Ok(result))) => Some(result), + Ok(Ok(Err(_))) => { + debug!("Web fallback: Channel closed"); + None + } + Ok(Err(e)) => { + warn!("Web fallback: Task error: {}", e); + None + } + Err(_) => { + debug!("Web fallback: Timeout"); + None + } + } + }); + + result +} + +// ============================================================================ +// Message Dialog Implementation +// ============================================================================ + +#[derive(Clone)] +struct MessageDialogState { + title: String, + description: String, + buttons: MessageButtons, + result_tx: ResultSender, + shutdown_tx: ShutdownSender, +} + +async fn message_dialog_page(State(state): State) -> Html { + let buttons_html = generate_buttons_html(&state.buttons); + let body = format!( + r#"
+

{}

+

{}

+
+
+ {} +
+
+
"#, + html_escape(&state.title), + html_escape(&state.description), + buttons_html + ); + Html(html_page(&state.title, &body)) +} + +async fn message_dialog_submit( + State(state): State, + body: String, +) -> Html { + let params = parse_urlencoded(&body); + for (key, value) in params { + if key == "result" { + debug!("Web fallback: Message dialog result = {}", value); + if let Some(tx) = state.result_tx.lock().unwrap().take() { + let _ = tx.send(parse_dialog_result(&value)); + } + // Trigger shutdown + if let Some(tx) = state.shutdown_tx.lock().unwrap().take() { + let _ = tx.send(()); + } + break; + } + } + + let body = r#"
+

+

Done. You can close this tab.

+
"#; + Html(html_page("Done", body)) +} + +impl MessageDialogImpl for MessageDialog { + fn show(self) -> MessageDialogResult { + debug!("Web fallback: Starting message dialog"); + + let (result_tx, result_rx) = mpsc::channel(); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + + let state = MessageDialogState { + title: self.title.clone(), + description: self.description.clone(), + buttons: self.buttons.clone(), + result_tx: Arc::new(Mutex::new(Some(result_tx))), + shutdown_tx: Arc::new(Mutex::new(Some(shutdown_tx))), + }; + + let router = Router::new() + .route("/", get(message_dialog_page)) + .route("/submit", post(message_dialog_submit)) + .with_state(state); + + run_server(router, result_rx, shutdown_rx).unwrap_or(MessageDialogResult::Cancel) + } +} + +impl AsyncMessageDialogImpl for MessageDialog { + fn show_async(self) -> DialogFutureType { + let (tx, rx) = oneshot::channel(); + std::thread::spawn(move || { + let result = MessageDialogImpl::show(self); + let _ = tx.send(result); + }); + Box::pin(async move { rx.await.unwrap_or(MessageDialogResult::Cancel) }) + } +} + +// ============================================================================ +// File Picker Dialog Implementation +// ============================================================================ + +#[derive(Clone)] +struct FilePickerState { + title: String, + accept: String, + multiple: bool, + result_tx: ResultSender>>, + shutdown_tx: ShutdownSender, +} + +async fn file_picker_page(State(state): State) -> Html { + let multiple_attr = if state.multiple { "multiple" } else { "" }; + let body = format!( + r#"
+

{}

+
+ +
+ Cancel + +
+
+
"#, + html_escape(&state.title), + state.accept, + multiple_attr + ); + Html(html_page(&state.title, &body)) +} + +async fn file_picker_cancel(State(state): State) -> Html { + debug!("Web fallback: File picker cancelled"); + if let Some(tx) = state.result_tx.lock().unwrap().take() { + let _ = tx.send(None); + } + if let Some(tx) = state.shutdown_tx.lock().unwrap().take() { + let _ = tx.send(()); + } + + let body = r#"
+

Cancelled

+

You can close this tab.

+
"#; + Html(html_page("Cancelled", body)) +} + +async fn file_picker_submit( + State(state): State, + request: axum::http::Request, +) -> Response { + debug!("Web fallback: Processing file upload"); + + let content_type = request + .headers() + .get(header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + let boundary = match extract_boundary(content_type) { + Some(b) => b, + None => { + warn!("Web fallback: No boundary in content-type"); + if let Some(tx) = state.result_tx.lock().unwrap().take() { + let _ = tx.send(None); + } + if let Some(tx) = state.shutdown_tx.lock().unwrap().take() { + let _ = tx.send(()); + } + return (StatusCode::BAD_REQUEST, "Missing boundary").into_response(); + } + }; + + // Read the body with increased limit for many files + let read_start = Instant::now(); + let body_bytes = match axum::body::to_bytes(request.into_body(), 500 * 1024 * 1024).await { + Ok(b) => b, + Err(e) => { + warn!("Web fallback: Failed to read body: {}", e); + if let Some(tx) = state.result_tx.lock().unwrap().take() { + let _ = tx.send(None); + } + if let Some(tx) = state.shutdown_tx.lock().unwrap().take() { + let _ = tx.send(()); + } + return (StatusCode::BAD_REQUEST, "Failed to read body").into_response(); + } + }; + let read_duration = read_start.elapsed(); + debug!( + "Web fallback: Read {} bytes in {:?}", + body_bytes.len(), + read_duration + ); + + let parse_start = Instant::now(); + let files = parse_multipart(&body_bytes, &boundary); + let parse_duration = parse_start.elapsed(); + debug!( + "Web fallback: Parsed {} files in {:?}", + files.len(), + parse_duration + ); + + // Create one temp directory for all files + let temp_dir_start = Instant::now(); + let temp_dir = match tempfile::tempdir() { + Ok(dir) => dir.keep(), + Err(e) => { + warn!("Web fallback: Failed to create temp dir: {}", e); + if let Some(tx) = state.result_tx.lock().unwrap().take() { + let _ = tx.send(None); + } + if let Some(tx) = state.shutdown_tx.lock().unwrap().take() { + let _ = tx.send(()); + } + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to create temp directory", + ) + .into_response(); + } + }; + let temp_dir_duration = temp_dir_start.elapsed(); + debug!("Web fallback: Created temp dir in {:?}", temp_dir_duration); + + let save_start = Instant::now(); + let paths: Vec = files + .into_iter() + .filter_map( + |f| match create_temp_file_in_dir(&temp_dir, &f.filename, &f.data) { + Ok(path) => { + debug!("Web fallback: Saved file to {:?}", path); + Some(path) + } + Err(e) => { + warn!("Web fallback: Failed to save file {}: {}", f.filename, e); + None + } + }, + ) + .collect(); + let save_duration = save_start.elapsed(); + debug!( + "Web fallback: Saved {} files in {:?}", + paths.len(), + save_duration + ); + + let result = if paths.is_empty() { None } else { Some(paths) }; + + if let Some(tx) = state.result_tx.lock().unwrap().take() { + let _ = tx.send(result); + } + if let Some(tx) = state.shutdown_tx.lock().unwrap().take() { + let _ = tx.send(()); + } + + let body = r#"
+

+

File(s) selected. You can close this tab.

+
"#; + Html(html_page("Done", body)).into_response() +} + +fn pick_files_impl(dialog: FileDialog, multiple: bool) -> Option> { + debug!("Web fallback: Starting file picker (multiple={})", multiple); + + let (result_tx, result_rx) = mpsc::channel(); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + + let state = FilePickerState { + title: dialog + .title + .clone() + .unwrap_or_else(|| "Select File".to_string()), + accept: build_accept(&dialog.filters), + multiple, + result_tx: Arc::new(Mutex::new(Some(result_tx))), + shutdown_tx: Arc::new(Mutex::new(Some(shutdown_tx))), + }; + + let router = Router::new() + .route("/", get(file_picker_page)) + .route("/cancel", get(file_picker_cancel)) + .route("/submit", post(file_picker_submit)) + .with_state(state); + + run_server(router, result_rx, shutdown_rx).flatten() +} + +impl FilePickerDialogImpl for FileDialog { + fn pick_file(self) -> Option { + pick_files_impl(self, false).and_then(|mut v| v.pop()) + } + + fn pick_files(self) -> Option> { + pick_files_impl(self, true) + } +} + +impl AsyncFilePickerDialogImpl for FileDialog { + fn pick_file_async(self) -> DialogFutureType> { + let (tx, rx) = oneshot::channel(); + std::thread::spawn(move || { + let result = FilePickerDialogImpl::pick_file(self); + let _ = tx.send(result); + }); + Box::pin(async move { rx.await.ok().flatten().map(FileHandle::from) }) + } + + fn pick_files_async(self) -> DialogFutureType>> { + let (tx, rx) = oneshot::channel(); + std::thread::spawn(move || { + let result = FilePickerDialogImpl::pick_files(self); + let _ = tx.send(result); + }); + Box::pin(async move { + rx.await + .ok() + .flatten() + .map(|v| v.into_iter().map(FileHandle::from).collect()) + }) + } +} + +// ============================================================================ +// Folder Picker Dialog Implementation +// ============================================================================ + +#[derive(Clone)] +struct FolderPickerState { + title: String, + multiple: bool, + result_tx: ResultSender>>, + shutdown_tx: ShutdownSender, +} + +async fn folder_picker_page(State(state): State) -> Html { + let multiple_attr = if state.multiple { "multiple" } else { "" }; + let body = format!( + r#"
+

{}

+

Select a folder by choosing any file within it.

+
+ +
+ Cancel + +
+
+
"#, + html_escape(&state.title), + multiple_attr + ); + Html(html_page(&state.title, &body)) +} + +async fn folder_picker_cancel(State(state): State) -> Html { + debug!("Web fallback: Folder picker cancelled"); + if let Some(tx) = state.result_tx.lock().unwrap().take() { + let _ = tx.send(None); + } + if let Some(tx) = state.shutdown_tx.lock().unwrap().take() { + let _ = tx.send(()); + } + + let body = r#"
+

Cancelled

+

You can close this tab.

+
"#; + Html(html_page("Cancelled", body)) +} + +async fn folder_picker_submit( + State(state): State, + request: axum::http::Request, +) -> Response { + debug!("Web fallback: Processing folder selection"); + + let content_type = request + .headers() + .get(header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + let boundary = match extract_boundary(content_type) { + Some(b) => b, + None => { + if let Some(tx) = state.result_tx.lock().unwrap().take() { + let _ = tx.send(None); + } + if let Some(tx) = state.shutdown_tx.lock().unwrap().take() { + let _ = tx.send(()); + } + return (StatusCode::BAD_REQUEST, "Missing boundary").into_response(); + } + }; + + let read_start = Instant::now(); + let body_bytes = match axum::body::to_bytes(request.into_body(), 500 * 1024 * 1024).await { + Ok(b) => b, + Err(e) => { + warn!("Web fallback: Failed to read body: {}", e); + if let Some(tx) = state.result_tx.lock().unwrap().take() { + let _ = tx.send(None); + } + if let Some(tx) = state.shutdown_tx.lock().unwrap().take() { + let _ = tx.send(()); + } + return (StatusCode::BAD_REQUEST, "Failed to read body").into_response(); + } + }; + let read_duration = read_start.elapsed(); + debug!( + "Web fallback: Read {} bytes in {:?}", + body_bytes.len(), + read_duration + ); + + let parse_start = Instant::now(); + let files = parse_multipart(&body_bytes, &boundary); + let parse_duration = parse_start.elapsed(); + debug!( + "Web fallback: Parsed {} files in {:?}", + files.len(), + parse_duration + ); + + // For folder selection, we create a temp directory with the files + let result = if files.is_empty() { + None + } else { + let temp_dir_start = Instant::now(); + match tempfile::tempdir() { + Ok(dir) => { + let folder_path = dir.keep(); + let temp_dir_duration = temp_dir_start.elapsed(); + debug!("Web fallback: Created temp dir in {:?}", temp_dir_duration); + + let save_start = Instant::now(); + let file_count = files.len(); + // Save all files in the folder + for file in files { + if let Err(e) = + create_temp_file_in_dir(&folder_path, &file.filename, &file.data) + { + warn!("Web fallback: Failed to write {}: {}", file.filename, e); + } + } + let save_duration = save_start.elapsed(); + debug!( + "Web fallback: Saved {} files in {:?}", + file_count, save_duration + ); + debug!("Web fallback: Created folder at {:?}", folder_path); + Some(vec![folder_path]) + } + Err(e) => { + warn!("Web fallback: Failed to create temp dir: {}", e); + None + } + } + }; + + if let Some(tx) = state.result_tx.lock().unwrap().take() { + let _ = tx.send(result); + } + if let Some(tx) = state.shutdown_tx.lock().unwrap().take() { + let _ = tx.send(()); + } + + let body = r#"
+

+

Folder selected. You can close this tab.

+
"#; + Html(html_page("Done", body)).into_response() +} + +fn pick_folder_impl(dialog: FileDialog, multiple: bool) -> Option> { + debug!( + "Web fallback: Starting folder picker (multiple={})", + multiple + ); + + let (result_tx, result_rx) = mpsc::channel(); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + + let state = FolderPickerState { + title: dialog + .title + .clone() + .unwrap_or_else(|| "Select Folder".to_string()), + multiple, + result_tx: Arc::new(Mutex::new(Some(result_tx))), + shutdown_tx: Arc::new(Mutex::new(Some(shutdown_tx))), + }; + + let router = Router::new() + .route("/", get(folder_picker_page)) + .route("/cancel", get(folder_picker_cancel)) + .route("/submit", post(folder_picker_submit)) + .with_state(state); + + run_server(router, result_rx, shutdown_rx).flatten() +} + +impl FolderPickerDialogImpl for FileDialog { + fn pick_folder(self) -> Option { + pick_folder_impl(self, false).and_then(|mut v| v.pop()) + } + + fn pick_folders(self) -> Option> { + pick_folder_impl(self, true) + } +} + +impl AsyncFolderPickerDialogImpl for FileDialog { + fn pick_folder_async(self) -> DialogFutureType> { + let (tx, rx) = oneshot::channel(); + std::thread::spawn(move || { + let result = FolderPickerDialogImpl::pick_folder(self); + let _ = tx.send(result); + }); + Box::pin(async move { rx.await.ok().flatten().map(FileHandle::from) }) + } + + fn pick_folders_async(self) -> DialogFutureType>> { + let (tx, rx) = oneshot::channel(); + std::thread::spawn(move || { + let result = FolderPickerDialogImpl::pick_folders(self); + let _ = tx.send(result); + }); + Box::pin(async move { + rx.await + .ok() + .flatten() + .map(|v| v.into_iter().map(FileHandle::from).collect()) + }) + } +} + +// ============================================================================ +// File Save Dialog Implementation +// ============================================================================ + +#[derive(Clone)] +struct FileSaveState { + title: String, + default_name: String, + result_tx: ResultSender>, + shutdown_tx: ShutdownSender, +} + +async fn file_save_page(State(state): State) -> Html { + let body = format!( + r#"
+

{}

+
+ + +
+ Cancel + +
+
+
"#, + html_escape(&state.title), + html_escape(&state.default_name) + ); + Html(html_page(&state.title, &body)) +} + +async fn file_save_cancel(State(state): State) -> Html { + debug!("Web fallback: Save dialog cancelled"); + if let Some(tx) = state.result_tx.lock().unwrap().take() { + let _ = tx.send(None); + } + if let Some(tx) = state.shutdown_tx.lock().unwrap().take() { + let _ = tx.send(()); + } + + let body = r#"
+

Cancelled

+

You can close this tab.

+
"#; + Html(html_page("Cancelled", body)) +} + +async fn file_save_submit(State(state): State, body: String) -> Html { + let params = parse_urlencoded(&body); + let mut result = None; + + for (key, value) in params { + if key == "filename" { + let filename = value.trim(); + if !filename.is_empty() { + // Basic validation to prevent path traversal: disallow separators and ".." + if filename.contains('/') || filename.contains('\\') || filename.contains("..") { + warn!( + "Web fallback: Rejected unsafe filename from save dialog: {:?}", + filename + ); + } else { + // Create a path in a temp directory with the chosen filename + match tempfile::tempdir() { + Ok(dir) => { + let path = dir.keep().join(filename); + debug!("Web fallback: Save path = {:?}", path); + result = Some(path); + } + Err(e) => { + warn!("Web fallback: Failed to create temp dir: {}", e); + } + } + } + } + break; + } + } + + if let Some(tx) = state.result_tx.lock().unwrap().take() { + let _ = tx.send(result); + } + if let Some(tx) = state.shutdown_tx.lock().unwrap().take() { + let _ = tx.send(()); + } + + let body = r#"
+

+

Save location selected. You can close this tab.

+
"#; + Html(html_page("Done", body)) +} + +impl FileSaveDialogImpl for FileDialog { + fn save_file(self) -> Option { + debug!("Web fallback: Starting save file dialog"); + + let (result_tx, result_rx) = mpsc::channel(); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + + let state = FileSaveState { + title: self + .title + .clone() + .unwrap_or_else(|| "Save File".to_string()), + default_name: self + .file_name + .clone() + .unwrap_or_else(|| "untitled".to_string()), + result_tx: Arc::new(Mutex::new(Some(result_tx))), + shutdown_tx: Arc::new(Mutex::new(Some(shutdown_tx))), + }; + + let router = Router::new() + .route("/", get(file_save_page)) + .route("/cancel", get(file_save_cancel)) + .route("/submit", post(file_save_submit)) + .with_state(state); + + run_server(router, result_rx, shutdown_rx).flatten() + } +} + +impl AsyncFileSaveDialogImpl for FileDialog { + fn save_file_async(self) -> DialogFutureType> { + let (tx, rx) = oneshot::channel(); + std::thread::spawn(move || { + let result = FileSaveDialogImpl::save_file(self); + let _ = tx.send(result); + }); + Box::pin(async move { rx.await.ok().flatten().map(FileHandle::from) }) + } +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/// Create a temporary file in the given base directory with the filename and content +fn create_temp_file_in_dir( + base_dir: &Path, + filename: &str, + data: &[u8], +) -> std::io::Result { + // Sanitize the provided filename to prevent path traversal outside `base_dir`. + let filename_path = Path::new(filename); + + // Reject absolute paths and any use of `..`, root, or platform-specific prefixes. + if filename_path.is_absolute() + || filename_path.components().any(|c| { + matches!( + c, + std::path::Component::ParentDir + | std::path::Component::RootDir + | std::path::Component::Prefix(_) + ) + }) + { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "invalid filename", + )); + } + + // Use the base directory with the sanitized relative path + let path = base_dir.join(filename_path); + // Create parent directories if the filename contains path separators + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + + let mut file = std::fs::File::create(&path)?; + file.write_all(data)?; + Ok(path) +} + +/// Create a temporary file with the given filename and content, preserving the original filename +/// (legacy function for single files) +#[allow(dead_code)] +fn create_temp_file(filename: &str, data: &[u8]) -> std::io::Result { + // Create a temp directory to hold the file + let temp_dir = tempfile::tempdir()?; + let dir_path = temp_dir.keep(); + + create_temp_file_in_dir(&dir_path, filename, data) +} diff --git a/src/lib.rs b/src/lib.rs index 535429c..3d2abff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,25 +108,25 @@ //! | ------------- | //! | 🚧 | //! -//! | Feature | Linux | Windows | MacOS | Wasm32 | -//! | ------------ | ----- | ------- | --------- | ------ | -//! | SingleFile | ✔ | ✔ | ✔ | ✔ | -//! | MultipleFile | ✔ | ✔ | ✔ | ✔ | -//! | PickFolder | ✔ | ✔ | ✔ | ✖ | -//! | SaveFile | ✔ | ✔ | ✔ | ✖ | -//! | Filters | ✔ | ✔ | ✔ | ✔ | -//! | StartingPath | ✔ | ✔ | ✔ | ✖ | -//! | Async | ✔ | ✔ | ✔ | ✔ | +//! | Feature | Linux | Windows | MacOS | Wasm32 | Others (Web) | +//! | ------------ | ----- | ------- | --------- | ------ | ------------ | +//! | SingleFile | ✔ | ✔ | ✔ | ✔ | ✔ | +//! | MultipleFile | ✔ | ✔ | ✔ | ✔ | ✔ | +//! | PickFolder | ✔ | ✔ | ✔ | ✖ | ✔ | +//! | SaveFile | ✔ | ✔ | ✔ | ✖ | ✔ | +//! | Filters | ✔ | ✔ | ✔ | ✔ | ✔ | +//! | StartingPath | ✔ | ✔ | ✔ | ✖ | ✔ | +//! | Async | ✔ | ✔ | ✔ | ✔ | ✔ | //! //! # rfd-extras //! //! AKA features that are not file related //! -//! | Feature | Linux | Windows | MacOS | Wasm32 | -//! | ------------- | ----- | ------- | ----- | ------ | -//! | MessageDialog | ✔ | ✔ | ✔ | ✔ | -//! | PromptDialog | | | | | -//! | ColorPicker | | | | | +//! | Feature | Linux | Windows | MacOS | Wasm32 | Others (Web) | +//! | ------------- | ----- | ------- | ----- | ------ | ------------ | +//! | MessageDialog | ✔ | ✔ | ✔ | ✔ | ✔ | +//! | PromptDialog | | | | | | +//! | ColorPicker | | | | | | mod backend; diff --git a/src/oneshot.rs b/src/oneshot.rs index d9383f5..afeabfd 100644 --- a/src/oneshot.rs +++ b/src/oneshot.rs @@ -7,13 +7,16 @@ use std::{ }; #[must_use = "futures do nothing unless you `.await` or poll them"] +#[allow(dead_code)] pub struct Receiver(Arc>); impl Unpin for Receiver {} +#[allow(dead_code)] pub struct Sender(Arc>); impl Unpin for Sender {} #[derive(Default)] +#[allow(dead_code)] enum State { Ready(T), #[default] @@ -21,13 +24,16 @@ enum State { Canceled, } +#[allow(dead_code)] struct Inner { rx_waker: Option, state: State, } +#[allow(dead_code)] struct Channel(Mutex>); +#[allow(dead_code)] pub fn channel() -> (Sender, Receiver) { let inner = Arc::new(Channel::new()); (Sender(inner.clone()), Receiver(inner)) @@ -41,6 +47,7 @@ impl Channel { })) } + #[allow(dead_code)] fn send(&self, t: T) -> Result<(), T> { let Ok(mut inner) = self.0.lock() else { debug_assert!(false, "Lock poisoned"); @@ -94,6 +101,7 @@ impl Channel { } impl Sender { + #[allow(dead_code)] pub fn send(self, t: T) -> Result<(), T> { let res = self.0.send(t); drop(self);