From e9e40bca36e49f2d1144a9fc3dd385799add87e3 Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 1 Oct 2026 23:12:21 +0300 Subject: [PATCH] feat: build carrier, a minimalist Iced smolmail client on fumi-core --- .gitignore | 7 + Cargo.lock | 4706 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 15 + README.md | 40 + devbox.json | 36 + devbox.lock | 255 +++ scripts/devbox-libs.sh | 52 + src/app.rs | 934 ++++++++ src/main.rs | 37 + src/ops.rs | 672 ++++++ src/theme.rs | 126 ++ src/view.rs | 818 +++++++ 12 files changed, 7698 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 devbox.json create mode 100644 devbox.lock create mode 100755 scripts/devbox-libs.sh create mode 100644 src/app.rs create mode 100644 src/main.rs create mode 100644 src/ops.rs create mode 100644 src/theme.rs create mode 100644 src/view.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..50dfd81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/target +result +result-* +*.key +*.db +*.db-wal +*.db-shm diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ed2977b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4706 @@ +# This file is automatically @generated by Cargo. +# 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "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 = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.2", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror 2.0.21", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[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 = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.5", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.5", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.5", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.2", + "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.13.2", + "polling", + "rustix 1.1.5", + "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.5", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "carrier" +version = "0.1.0" +dependencies = [ + "fumi-core", + "iced", + "rand_core", + "tempfile", + "tokio", +] + +[[package]] +name = "cc" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[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", + "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.21", + "x11rb", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[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 = "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 = "cosmic-text" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2" +dependencies = [ + "bitflags 2.13.2", + "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 = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf" +dependencies = [ + "dtor", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +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 = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "d3d12" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" +dependencies = [ + "bitflags 2.13.2", + "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", + "zbus", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "dconf_rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "detect-desktop-environment" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[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]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading 0.8.9", +] + +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + +[[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.13.2", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690" +dependencies = [ + "drm-sys", + "rustix 1.1.5", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8" +dependencies = [ + "libc", + "linux-raw-sys 0.9.4", +] + +[[package]] +name = "dtor" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[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.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "fumi-core" +version = "0.1.0" +dependencies = [ + "chacha20poly1305", + "data-encoding", + "ed25519-dalek", + "hkdf", + "hmac", + "rand_core", + "rusqlite", + "serde", + "serde_json", + "sha2", + "snow", + "x25519-dalek", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.5", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[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 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "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 = "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9" +dependencies = [ + "bitflags 2.13.2", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058" +dependencies = [ + "bitflags 2.13.2", +] + +[[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", +] + +[[package]] +name = "gpu-descriptor" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" +dependencies = [ + "bitflags 2.13.2", + "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.13.2", +] + +[[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 = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[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" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "hassle-rs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +dependencies = [ + "bitflags 2.13.2", + "com", + "libc", + "libloading 0.8.9", + "thiserror 1.0.69", + "widestring", + "winapi", +] + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +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 = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[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.13.2", + "bytes", + "dark-light", + "glam", + "log", + "num-traits", + "once_cell", + "palette", + "rustc-hash 2.1.3", + "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.3", + "tokio", + "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", + "rustc-hash 2.1.3", + "wgpu", +] + +[[package]] +name = "iced_graphics" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba25a18cfa6d5cc160aca7e1b34f73ccdff21680fa8702168c09739767b6c66f" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "cosmic-text", + "half", + "iced_core", + "iced_futures", + "log", + "lyon_path", + "once_cell", + "raw-window-handle", + "rustc-hash 2.1.3", + "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.3", + "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.13.2", + "bytemuck", + "futures", + "glam", + "guillotiere", + "iced_glyphon", + "iced_graphics", + "log", + "lyon", + "once_cell", + "rustc-hash 2.1.3", + "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", + "qrcode", + "rustc-hash 2.1.3", + "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.3", + "thiserror 1.0.69", + "tracing", + "wasm-bindgen-futures", + "web-sys", + "winapi", + "window_clipboard", + "winit", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[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 = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[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", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "bitflags 2.13.2", + "libc", + "plain", + "redox_syscall 0.9.4", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" + +[[package]] +name = "lyon" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0578bdecb7d6d88987b8b2b1e3a4e2f81df9d0ece1078623324a567904e7b7" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdfa8785f95e57914ddb35e3b59994aeba6f5e79e9cfd03da1c269f010f36009" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b8dcf906637ecef61b3c0740c7a4e7f27caeb31257cfac0cc579ce15be6005" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" +dependencies = [ + "bitflags 2.13.2", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "naga" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843" +dependencies = [ + "bit-set", + "bitflags 2.13.2", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "num-traits", + "rustc-hash 1.1.0", + "spirv", + "termcolor", + "thiserror 1.0.69", + "unicode-xid", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "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 0.3.1", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases 0.2.2", + "libc", + "memoffset", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +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.13.2", + "block2", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.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.13.2", + "block2", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +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.13.2", + "block2", + "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.13.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "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", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.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.13.2", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[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-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "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", + "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.13.2", + "block2", + "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 = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[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 = "palette" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64" +dependencies = [ + "approx", + "palette_derive", + "palette_math", + "phf", +] + +[[package]] +name = "palette_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "palette_math" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "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]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bee6c73da26345c729282832b60b0363cf3dd9f4bfd81d8551b7a1c889a113" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[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 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.5", + "windows-sys 0.61.2", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "qrcode" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "166f136dfdb199f98186f3649cf7a0536534a61417a1a30221b492b4fb60ce3f" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +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 = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rangemap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +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 = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_syscall" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.13.2", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "rustybuzz" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "libm", + "smallvec", + "ttf-parser 0.21.1", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "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 = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[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.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.13.2", + "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.13.2", + "calloop 0.14.4", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.5", + "thiserror 2.0.21", + "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 = "snow" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" +dependencies = [ + "aes-gcm", + "blake2", + "chacha20poly1305", + "curve25519-dalek", + "rand_core", + "rustc_version", + "sha2", + "subtle", +] + +[[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.4", + "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.5", + "tiny-xlib", + "tracing", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.61.2", + "x11rb", +] + +[[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.13.2", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "subtle" +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 = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.5", + "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 = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[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", + "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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90a0ca3ee6a69f2ad28fd11621a4c3f03b371f366be500b64df260c4ffbafb4" +dependencies = [ + "as-raw-xcb-connection", + "ctor", + "libloading 0.8.9", + "pkg-config", + "tracing", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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 = "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 = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[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.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[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.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[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.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.5", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix 1.1.5", + "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.13.2", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.5", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "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.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "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.13.2", + "cfg_aliases 0.1.1", + "codespan-reporting", + "indexmap", + "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.13.2", + "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.13.2", + "js-sys", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash 0.8.12", + "android-activity", + "atomic-waker", + "bitflags 2.13.2", + "block2", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases 0.2.2", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit", + "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 = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +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 = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[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.5", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "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.13.2", + "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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "yazi" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[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.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9e3dbc5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "carrier" +version = "0.1.0" +edition = "2021" +description = "A minimalist smolmail client built on fumi-core, with an Iced UI" +license = "Apache-2.0" + +[dependencies] +fumi-core = { path = "../fumi/core" } +iced = { version = "0.13", features = ["tokio", "qr_code"] } +tokio = { version = "1", features = ["rt"] } +rand_core = { version = "0.6", features = ["getrandom"] } + +[dev-dependencies] +tempfile = "3" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b0587a0 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# carrier + +A minimalist desktop client for [Smol Mail](../smolmail): one compact window — the list above, the article below — built on [fumi-core](../fumi), the reference CLI's own library, with an [Iced](https://iced.rs) UI. One 32-byte seed is the only secret; mail stays sealed at rest and opens on demand. + +## What it does + +- Create or restore an identity from a seed, shown once at creation time. +- Pin a server's key — paste the operator's, or connect and pin what it presents — then register a username or recall an existing address. +- Fetch (with cancel), read, reply and send sealed mail; the inbox, the requests tier and sent copies each keep their own folder. +- Accept or block a requester when their mail carried a self-certifying reply address; accept tokens attach to accepted contacts automatically. +- Contacts with verified/tofu marks and smol:// import; your own address as a URI, a QR code and a clipboard line. +- Key rotation with a confirm-first step, server pin management, a network timeout, and gsmol backup export/import that never contains the seed. + +## Layout + +``` +src/main.rs config directory, iced launch +src/app.rs state and update; blocking calls run on the worker pool +src/view.rs the compact split view: tab bar, list, article, status line +src/ops.rs fumi-core calls, key/read-mark files, and the offline tests +src/theme.rs the palette: navy surfaces, mint accent, monospace protocol values +``` + +## State + +Everything lives under `$XDG_CONFIG_HOME/carrier` (default `~/.config/carrier`): `carrier.key` is the master seed, written 0600; `carrier.db` is fumi-core's sealed store; `read.txt` is carrier's own read marks, one hex id per line. + +## Run and verify + +``` +devbox run run # build release and launch +devbox run check # fmt --check, build, tests +devbox run test # tests only +``` + +The tests run offline against an in-memory store: seed and key parsing, a sealed envelope listed and opened end to end, contact import, a full backup round-trip, pin labels and the date formatter. `scripts/devbox-libs.sh` puts the GUI stack's runtime libraries (wayland, xkbcommon, EGL, vulkan) on the library path inside the devbox shell — they are dlopened, so nothing that links the binary can provide them. + +## Status + +Work in progress: the UI is unexercised against a live server — the store, crypto and address paths are covered by tests, the network flows (register, fetch, send, rotate) are wired to fumi-core's operations but only smoke-tested as far as launching the window. diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..cc3a92f --- /dev/null +++ b/devbox.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.17.2/.schema/devbox.schema.json", + "packages": [ + "rustc@latest", + "cargo@latest", + "pkg-config@latest", + "rustfmt@latest", + "nixpkgs#wayland", + "nixpkgs#libxkbcommon", + "nixpkgs#mesa", + "nixpkgs#libGL", + "nixpkgs#vulkan-loader" + ], + "shell": { + "init_hook": [ + ". \"$PWD/scripts/devbox-libs.sh\"" + ], + "scripts": { + "build": [ + "cargo build --release" + ], + "run": [ + "cargo build --release && ./target/release/carrier" + ], + "dev": [ + "cargo build && ./target/debug/carrier" + ], + "test": [ + "cargo test" + ], + "check": [ + "cargo fmt --check && cargo build && cargo test" + ] + } + } +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 0000000..a0104f3 --- /dev/null +++ b/devbox.lock @@ -0,0 +1,255 @@ +{ + "lockfile_version": "1", + "packages": { + "cargo@latest": { + "last_modified": "2026-08-27T07:16:00Z", + "resolved": "github:NixOS/nixpkgs/c27cdad491a991b11ed731760aa2ef8db0cb0410#cargo", + "source": "devbox-search", + "version": "1.97.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/cvd2pzmgq4pr7kkzx2zmya47fqd70sqa-cargo-1.97.1", + "default": true + } + ], + "store_path": "/nix/store/cvd2pzmgq4pr7kkzx2zmya47fqd70sqa-cargo-1.97.1" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/z64fp7fjfy9zr3xn7prdidi4gww213d5-cargo-1.97.1", + "default": true + } + ], + "store_path": "/nix/store/z64fp7fjfy9zr3xn7prdidi4gww213d5-cargo-1.97.1" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/dv55qwamlq68hdiprg1v104ymwh1c27r-cargo-1.97.1", + "default": true + } + ], + "store_path": "/nix/store/dv55qwamlq68hdiprg1v104ymwh1c27r-cargo-1.97.1" + } + } + }, + "github:NixOS/nixpkgs/nixpkgs-unstable": { + "last_modified": "2026-09-29T03:29:29Z", + "resolved": "github:NixOS/nixpkgs/b6c8664de9b6cc07fe5666a29f91884ba81197c4?lastModified=1790652569&narHash=sha256-641r7xrlSOHYoktL9%2FaArPIYNgZJ9eM5URzSEOtPJDY%3D" + }, + "nixpkgs#libGL": { + "last_modified": "1970-01-01T00:00:00Z", + "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#libGL" + }, + "nixpkgs#libxkbcommon": { + "last_modified": "1970-01-01T00:00:00Z", + "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#libxkbcommon" + }, + "nixpkgs#mesa": { + "last_modified": "1970-01-01T00:00:00Z", + "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#mesa" + }, + "nixpkgs#vulkan-loader": { + "last_modified": "1970-01-01T00:00:00Z", + "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#vulkan-loader" + }, + "nixpkgs#wayland": { + "last_modified": "1970-01-01T00:00:00Z", + "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#wayland" + }, + "pkg-config@latest": { + "last_modified": "2025-11-23T21:50:36Z", + "resolved": "github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261#pkg-config", + "source": "devbox-search", + "version": "0.29.2", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/hygaaqwk9ylklp0ybwppqhw75nz8ya41-pkg-config-wrapper-0.29.2", + "default": true + }, + { + "name": "man", + "path": "/nix/store/9px0sji43x3r2w4zxl3j3idwsql7lwxx-pkg-config-wrapper-0.29.2-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/hqk44ra6qxw7iixardl6c3hdgb9kq6ns-pkg-config-wrapper-0.29.2-doc" + } + ], + "store_path": "/nix/store/hygaaqwk9ylklp0ybwppqhw75nz8ya41-pkg-config-wrapper-0.29.2" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/lbiigi8qbp7mzf1lpr7p982l1kyf01ql-pkg-config-wrapper-0.29.2", + "default": true + }, + { + "name": "man", + "path": "/nix/store/10060k24qggqyzlwdsfmni9y32zxcg0j-pkg-config-wrapper-0.29.2-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/0y4v51ndpyvkj09hwlfqkz0c3h17zfmc-pkg-config-wrapper-0.29.2-doc" + } + ], + "store_path": "/nix/store/lbiigi8qbp7mzf1lpr7p982l1kyf01ql-pkg-config-wrapper-0.29.2" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/vknadizq0q5kffvx6y4379p9gdry9zq3-pkg-config-wrapper-0.29.2", + "default": true + }, + { + "name": "man", + "path": "/nix/store/1nyspra675q22gfhf7hn2nmfpi6rgim5-pkg-config-wrapper-0.29.2-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/7lq1axxwrafwljs06n88bzyz9w523rkc-pkg-config-wrapper-0.29.2-doc" + } + ], + "store_path": "/nix/store/vknadizq0q5kffvx6y4379p9gdry9zq3-pkg-config-wrapper-0.29.2" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/8vdiwpbh0g4avsd6x5v4s0di32vcl3dp-pkg-config-wrapper-0.29.2", + "default": true + }, + { + "name": "man", + "path": "/nix/store/j9xfpnrygg3v37svc5pfin9q5bm49r94-pkg-config-wrapper-0.29.2-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/x3bypxdxaq20kykybhkf21x4jczsiy8y-pkg-config-wrapper-0.29.2-doc" + } + ], + "store_path": "/nix/store/8vdiwpbh0g4avsd6x5v4s0di32vcl3dp-pkg-config-wrapper-0.29.2" + } + } + }, + "rustc@latest": { + "last_modified": "2026-08-27T07:16:00Z", + "plugin_version": "0.0.1", + "resolved": "github:NixOS/nixpkgs/c27cdad491a991b11ed731760aa2ef8db0cb0410#rustc", + "source": "devbox-search", + "version": "1.97.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/swrk83krgf2iaqd7a1bs0pg1sinlz5ds-rustc-wrapper-1.97.1", + "default": true + }, + { + "name": "man", + "path": "/nix/store/grw7ifdixkdr4ya8bw5x0psjyfr5azva-rustc-wrapper-1.97.1-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/8qkha38sic70l51g4l7dmjrxnzqjrr6w-rustc-wrapper-1.97.1-doc" + } + ], + "store_path": "/nix/store/swrk83krgf2iaqd7a1bs0pg1sinlz5ds-rustc-wrapper-1.97.1" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/d6bnbpk6jr95frzmmksda4r7l3v8d3m6-rustc-wrapper-1.97.1", + "default": true + }, + { + "name": "man", + "path": "/nix/store/hn340n5pi9p30j4bcxgzqgq8lmclg1by-rustc-wrapper-1.97.1-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/avbwjkw896rb3nl28kh8qv6kxyq5b9v2-rustc-wrapper-1.97.1-doc" + } + ], + "store_path": "/nix/store/d6bnbpk6jr95frzmmksda4r7l3v8d3m6-rustc-wrapper-1.97.1" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/yaa1sq5g4jar7rvlspy7kk2wd98nj6qh-rustc-wrapper-1.97.1", + "default": true + }, + { + "name": "man", + "path": "/nix/store/an2hvf5i7mq52mqdad9rad8mxkpksmq2-rustc-wrapper-1.97.1-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/8svfp79q0gx10gcrsyxf1pw04q8jf0lp-rustc-wrapper-1.97.1-doc" + } + ], + "store_path": "/nix/store/yaa1sq5g4jar7rvlspy7kk2wd98nj6qh-rustc-wrapper-1.97.1" + } + } + }, + "rustfmt@latest": { + "last_modified": "2026-08-27T07:16:00Z", + "resolved": "github:NixOS/nixpkgs/c27cdad491a991b11ed731760aa2ef8db0cb0410#rustfmt", + "source": "devbox-search", + "version": "1.97.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/l7xlkqk1bz4w75rwf5dh7pn2br04n59l-rustfmt-1.97.1", + "default": true + } + ], + "store_path": "/nix/store/l7xlkqk1bz4w75rwf5dh7pn2br04n59l-rustfmt-1.97.1" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/rl3j1za9dpdri1vr52vl69y1qczjb5zv-rustfmt-1.97.1", + "default": true + } + ], + "store_path": "/nix/store/rl3j1za9dpdri1vr52vl69y1qczjb5zv-rustfmt-1.97.1" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/47k26k98v32x56rxiqawdk5qk0r0b6w1-rustfmt-1.97.1", + "default": true + } + ], + "store_path": "/nix/store/47k26k98v32x56rxiqawdk5qk0r0b6w1-rustfmt-1.97.1" + } + } + } + } +} diff --git a/scripts/devbox-libs.sh b/scripts/devbox-libs.sh new file mode 100755 index 0000000..685a74e --- /dev/null +++ b/scripts/devbox-libs.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env sh +# Devbox installs library packages as their -dev outputs, which carry no +# shared objects; the outs they reference do. Put the GUI stack's real lib +# dirs on LD_LIBRARY_PATH — wayland, xkbcommon, EGL and vulkan are dlopened +# at runtime, so neither rpath nor a DT_NEEDED scan can cover them. Only +# matching package names are picked up: a blanket list would drag glibc and +# friends onto the path for every process in the shell. +set -e + +if [ -z "$DEVBOX_PACKAGES_DIR" ]; then + return 0 2>/dev/null || exit 0 +fi + +MANIFEST="$DEVBOX_PACKAGES_DIR/manifest.json" +[ -f "$MANIFEST" ] || return 0 2>/dev/null || exit 0 + +libs=$(python3 - "$MANIFEST" <<'EOF' +import json, os, subprocess, sys + +wanted = ("wayland", "libxkbcommon", "libglvnd", "vulkan-loader", "mesa") +manifest = json.load(open(sys.argv[1])) +seen, dirs = set(), [] +paths = [ + path + for name, el in manifest["elements"].items() + if any(w in name for w in wanted) + for path in el.get("storePaths", []) +] +for path in paths: + candidates = [path] + try: + candidates += subprocess.run( + ["nix-store", "-q", "--references", path], + capture_output=True, text=True, + ).stdout.split() + except OSError: + pass + for cand in candidates: + lib = os.path.join(cand, "lib") + base = os.path.basename(cand) + if lib in seen or not os.path.isdir(lib): + continue + if not any(w in base for w in wanted): + continue + if any(name.endswith(".so") or ".so." in name for name in os.listdir(lib)): + seen.add(lib) + dirs.append(lib) +print(":".join(dirs)) +EOF +) + +export LD_LIBRARY_PATH="${libs}:${DEVBOX_PACKAGES_DIR}/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..59910b9 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,934 @@ +//! Application state and update. Every fumi-core call that can block runs in +//! `spawn_blocking` and reports back through a `Message`; purely local reads +//! (opening one sealed message, marking it read) happen inline in `update`. + +use std::collections::HashSet; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +use fumi_core::address::Address; +use fumi_core::crypto::{unb32, KEY_LEN}; +use fumi_core::store::Store; +use iced::widget::qr_code; +use iced::widget::text_editor::{self, Action}; +use iced::{clipboard, Task}; + +use crate::ops::{self, Folder, OpenedMsg, Row, Snapshot}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Page { + Onboard, + Signup, + Mail, + Contacts, + Settings, +} + +/// Read marks are carrier's own state, so they live beside the store as one +/// hex id per line — an append-only file that a torn tail can only cost a +/// re-read mark, never a lost one. +pub struct ReadMarks { + path: PathBuf, + ids: HashSet, +} + +impl ReadMarks { + pub fn load(path: PathBuf) -> ReadMarks { + let ids = std::fs::read_to_string(&path) + .map(|text| text.lines().map(str::to_string).collect()) + .unwrap_or_default(); + ReadMarks { path, ids } + } + + pub fn snapshot(&self) -> HashSet { + self.ids.clone() + } + + pub fn mark(&mut self, id: String) { + if self.ids.insert(id.clone()) { + use std::io::Write; + if let Ok(mut file) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&self.path) + { + let _ = writeln!(file, "{id}"); + } + } + } +} + +/// Onboarding: create a master or restore one from a pasted seed. +pub struct Onboard { + pub mode: OnboardMode, + pub seed: String, + pub seed_shown: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OnboardMode { + Start, + Created, + Restore, +} + +/// Signup: bind the identity to a username, or recall an existing address. +pub struct Signup { + pub user: String, + pub host: String, + pub invite: String, + /// The server's base32 static key, pasted from what the operator + /// published; pinned before the registration connects. + pub key: String, + /// True when onboarding just wrote the key file, so Back is offered. + pub fresh: bool, +} + +/// The compose view: `to` takes a short address or a smol:// URI. +pub struct Compose { + pub to: String, + pub subject: String, + pub body: text_editor::Content, + /// The message this replies to, as 64 hex characters. + pub reply_to: Option, +} + +#[derive(Debug, Clone)] +pub enum Dialog { + /// Delete locally, and on the server too when `everywhere` is chosen. + Delete { + id: String, + folder: Folder, + kept: bool, + }, + /// Rotation only moves the index forward, so the action asks first. + Rotate, +} + +#[derive(Debug, Clone)] +pub enum Message { + // Onboarding + OnboardCreate, + OnboardRestore, + OnboardBack, + OnboardSeedEdited(String), + OnboardRestoreFromSeed, + OnboardSeedSaved, + + // Signup + SignupUserEdited(String), + SignupHostEdited(String), + SignupInviteEdited(String), + SignupKeyEdited(String), + SignupBack, + SignupLearnKey, + SignupRegister, + SignupRecall, + + // Pin offer (signup and settings share it) + PinOffered(Result, String>), + PinAccept, + PinDiscard, + Pinned(Result<(), String>), + + // Mail + MessageSelected(Folder, String), + FetchPressed, + FetchCancelled, + ComposePressed, + ComposeToEdited(String), + ComposeSubjectEdited(String), + ComposeBody(Action), + ComposeClosed, + SendPressed, + ReplyPressed, + DeletePressed, + DeleteConfirmed(bool), + DeleteCancelled, + AcceptPressed, + BlockPressed, + + // Contacts + ContactImportEdited(String), + ContactImportPressed, + ContactWrite(String), + ContactAccept(String, String), + ContactBlock(String), + ContactUnblock(String), + CopyOwnAddress, + + // Settings + SettingsLearnKey, + TimeoutEdited(String), + RotatePressed, + RotateConfirmed, + RotateCancelled, + UnpinPressed(String), + ExportPressed, + Exported(Result<(String, String), String>), + CopyExport, + ImportBody(Action), + ImportPressed, + + // Navigation and results + GotoMail(Folder), + Goto(Page), + SnapshotLoaded(Result), + Generic(Result), +} + +pub struct Carrier { + pub dir: PathBuf, + pub store: Arc, + pub master: Option<[u8; KEY_LEN]>, + pub marks: ReadMarks, + pub page: Page, + pub status: Option, + pub status_error: bool, + pub busy: Option, + pub cancel: Arc, + pub timeout: u64, + pub timeout_text: String, + pub onboard: Onboard, + pub signup: Signup, + pub folder: Folder, + pub lists: Vec>, + pub open: Option<(Folder, OpenedMsg)>, + pub compose: Option, + pub snapshot: Option, + pub pin_offer: Option<(Address, [u8; KEY_LEN])>, + pub backup_text: Option, + pub import_body: text_editor::Content, + pub contact_import: String, + pub dialog: Option, + /// The own-address QR, rebuilt whenever a snapshot lands. + pub share_qr: Option, +} + +impl Carrier { + pub fn boot(dir: PathBuf) -> (Carrier, Task) { + let store = match Store::open(&dir.join("carrier.db")) { + Ok(store) => Arc::new(store), + Err(e) => { + eprintln!("cannot open {}: {e}", dir.join("carrier.db").display()); + std::process::exit(1); + } + }; + let master = ops::load_key(&dir.join("carrier.key")).ok(); + let registered = + master.is_some() && store.account().map(|addr| addr.is_some()).unwrap_or(false); + let page = match (master.is_some(), registered) { + (false, _) => Page::Onboard, + (true, false) => Page::Signup, + (true, true) => Page::Mail, + }; + let mut app = Carrier { + marks: ReadMarks::load(dir.join("read.txt")), + dir, + store, + master, + page, + status: None, + status_error: false, + busy: None, + cancel: Arc::new(AtomicBool::new(false)), + timeout: 30, + timeout_text: "30".to_string(), + onboard: Onboard { + mode: OnboardMode::Start, + seed: String::new(), + seed_shown: String::new(), + }, + signup: Signup { + user: String::new(), + host: String::new(), + invite: String::new(), + key: String::new(), + fresh: false, + }, + folder: Folder::Inbox, + lists: vec![Vec::new(), Vec::new(), Vec::new()], + open: None, + compose: None, + snapshot: None, + pin_offer: None, + backup_text: None, + import_body: text_editor::Content::new(), + contact_import: String::new(), + dialog: None, + share_qr: None, + }; + let task = if registered { + app.refresh() + } else { + Task::none() + }; + (app, task) + } + + fn master(&self) -> [u8; KEY_LEN] { + self.master.unwrap_or([0u8; KEY_LEN]) + } + + fn key_path(&self) -> PathBuf { + self.dir.join("carrier.key") + } + + fn note(&mut self, text: String) { + self.status = Some(text); + self.status_error = false; + } + + fn fail(&mut self, text: String) { + self.status = Some(text); + self.status_error = true; + } + + /// One operation at a time: a second action while one runs is ignored, + /// except `FetchCancelled`, which raises the token the fetch checks. + fn start(&mut self, label: &str) -> bool { + if self.busy.is_some() { + return false; + } + self.busy = Some(label.to_string()); + self.cancel.store(false, Ordering::Relaxed); + true + } + + fn done(&mut self) { + self.busy = None; + } + + /// One blocking pass over the store, bringing every page's data home. + fn refresh(&mut self) -> Task { + if !self.start("refreshing") { + return Task::none(); + } + let store = self.store.clone(); + let master = self.master(); + let read = self.marks.snapshot(); + Task::perform( + async move { + tokio::task::spawn_blocking(move || ops::snapshot(&store, &master, &read)) + .await + .unwrap_or_else(|e| Err(format!("refreshing failed: {e}"))) + }, + Message::SnapshotLoaded, + ) + } + + fn refresh_after(&mut self, note: String) -> Task { + self.note(note); + self.refresh() + } + + /// Runs one blocking operation on the worker pool, mapping its result to + /// `map` — the label doubles as the busy indicator and the panic message. + fn run_with(&mut self, label: &str, map: M, work: F) -> Task + where + T: Send + 'static, + F: FnOnce(Arc, [u8; KEY_LEN]) -> Result + Send + 'static, + M: Fn(Result) -> Message + Send + 'static, + { + if !self.start(label) { + return Task::none(); + } + let label = label.to_string(); + let store = self.store.clone(); + let master = self.master(); + Task::perform( + async move { + tokio::task::spawn_blocking(move || work(store, master)) + .await + .unwrap_or_else(|e| Err(format!("{label} failed: {e}"))) + }, + map, + ) + } + + fn run(&mut self, label: &str, work: F) -> Task + where + F: FnOnce(Arc, [u8; KEY_LEN]) -> Result + Send + 'static, + { + self.run_with(label, Message::Generic, work) + } + + /// `user@host` as typed on the signup page, defaulting the port. + fn signup_address(&self) -> Option
{ + let user = self.signup.user.trim(); + let host = self.signup.host.trim(); + if user.is_empty() || host.is_empty() { + return None; + } + Address::parse(&format!("{user}@{host}")).ok() + } + + /// The account address, for the settings page's learn-key action. + fn account_address(&self) -> Option
{ + self.store.account().ok().flatten() + } + + pub fn update(&mut self, message: Message) -> Task { + match message { + // Text editor actions must survive the busy guard: typing in the + // compose body while a fetch runs is ordinary behavior. + Message::ComposeBody(action) => { + if let Some(compose) = &mut self.compose { + compose.body.perform(action); + } + Task::none() + } + Message::ImportBody(action) => { + self.import_body.perform(action); + Task::none() + } + Message::FetchCancelled => { + if self.busy.is_some() { + self.cancel.store(true, Ordering::Relaxed); + } + Task::none() + } + _ if self.busy.is_some() => Task::none(), + Message::OnboardCreate => { + let master = ops::new_master(); + if let Err(e) = ops::save_key(&self.key_path(), &master) { + self.fail(e); + return Task::none(); + } + self.master = Some(master); + self.onboard.seed_shown = ops::seed_text(&master); + self.onboard.mode = OnboardMode::Created; + self.note("Identity created. Write the seed down; it is the only secret.".into()); + Task::none() + } + Message::OnboardRestore => { + self.onboard.mode = OnboardMode::Restore; + Task::none() + } + Message::OnboardBack => { + self.onboard.mode = OnboardMode::Start; + Task::none() + } + Message::OnboardSeedEdited(seed) => { + self.onboard.seed = seed; + Task::none() + } + Message::OnboardRestoreFromSeed => match ops::parse_seed(&self.onboard.seed) { + Ok(master) => { + if let Err(e) = ops::save_key(&self.key_path(), &master) { + self.fail(e); + return Task::none(); + } + self.master = Some(master); + self.signup.fresh = true; + self.page = Page::Signup; + self.note("Seed restored. Bind a username or recall an address.".into()); + Task::none() + } + Err(e) => { + self.fail(e); + Task::none() + } + }, + Message::OnboardSeedSaved => { + self.signup.fresh = true; + self.page = Page::Signup; + Task::none() + } + Message::SignupUserEdited(user) => { + self.signup.user = user; + Task::none() + } + Message::SignupHostEdited(host) => { + self.signup.host = host; + Task::none() + } + Message::SignupInviteEdited(invite) => { + self.signup.invite = invite; + Task::none() + } + Message::SignupKeyEdited(key) => { + self.signup.key = key; + Task::none() + } + Message::SignupBack => { + self.page = Page::Onboard; + Task::none() + } + Message::SignupLearnKey => { + let Some(addr) = self.signup_address() else { + self.fail("enter user and host first".into()); + return Task::none(); + }; + self.run_with("connecting", Message::PinOffered, move |store, _| { + ops::learn_key(&store, &addr, 30).map(|key| key.map(|key| (addr.clone(), key))) + }) + } + Message::SettingsLearnKey => { + let Some(addr) = self.account_address() else { + return Task::none(); + }; + self.run_with("connecting", Message::PinOffered, move |store, _| { + ops::learn_key(&store, &addr, 30).map(|key| key.map(|key| (addr.clone(), key))) + }) + } + Message::PinDiscard => { + self.pin_offer = None; + Task::none() + } + Message::PinOffered(result) => { + self.done(); + match result { + Ok(Some((addr, key))) => { + self.pin_offer = Some((addr, key)); + self.note( + "The server presented this key. Pin it only if the fingerprint \ + matches what the operator published." + .into(), + ); + Task::none() + } + Ok(None) => { + self.note("This server's key is already pinned.".into()); + Task::none() + } + Err(e) => { + self.fail(e); + Task::none() + } + } + } + Message::PinAccept => { + let Some((addr, key)) = self.pin_offer.take() else { + return Task::none(); + }; + self.run_with("pinning", Message::Pinned, move |store, _| { + ops::pin(&store, &addr, &key) + }) + } + Message::Pinned(result) => { + self.done(); + match result { + Ok(()) => self.note("server key pinned".into()), + Err(e) => self.fail(e), + } + Task::none() + } + Message::SignupRegister => { + let Some(addr) = self.signup_address() else { + self.fail("enter user and host".into()); + return Task::none(); + }; + let invite = if self.signup.invite.is_empty() { + None + } else { + Some(self.signup.invite.clone()) + }; + // A pasted operator key is pinned first: registration + // connects with the pin required. + let key = self.signup.key.trim().to_string(); + let timeout = self.timeout; + self.run("registering", move |store, master| { + if !key.is_empty() { + let key = ops::parse_key(&key)?; + ops::pin(&store, &addr, &key)?; + } + ops::register(&store, &master, &addr, invite.as_deref(), timeout) + .map(|_| "registered".to_string()) + }) + } + Message::SignupRecall => { + let Some(addr) = self.signup_address() else { + self.fail("enter user and host".into()); + return Task::none(); + }; + let timeout = self.timeout; + self.run("restoring", move |store, master| { + ops::restore(&store, &master, &addr, timeout) + .map(|index| format!("restored at rotation {index}")) + }) + } + Message::GotoMail(folder) => { + if self.page == Page::Mail && self.folder == folder { + return Task::none(); + } + self.page = Page::Mail; + self.folder = folder; + self.open = None; + Task::none() + } + Message::Goto(page) => { + self.page = page; + self.open = None; + self.compose = None; + if page == Page::Mail || page == Page::Contacts || page == Page::Settings { + return self.refresh(); + } + Task::none() + } + Message::MessageSelected(folder, id) => { + self.folder = folder; + self.open = None; + self.marks.mark(id.clone()); + // Opening one envelope and re-listing is local and fast, so + // it happens inline; the refresh keeps the unread dots honest. + let master = self.master(); + let read = self.marks.snapshot(); + match ops::open(&self.store, &master, &id, folder) { + Ok(msg) => { + self.open = Some((folder, msg)); + self.status = None; + } + Err(e) => self.fail(e), + } + if let Ok(lists) = ops::lists(&self.store, &master, &read) { + self.lists = lists; + } + Task::none() + } + Message::FetchPressed => { + if !self.start("fetching") { + return Task::none(); + } + let store = self.store.clone(); + let master = self.master(); + let cancel = self.cancel.clone(); + let timeout = self.timeout; + Task::perform( + async move { + tokio::task::spawn_blocking(move || { + ops::fetch(&store, &master, cancel, timeout) + }) + .await + .unwrap_or_else(|e| Err(format!("fetching failed: {e}"))) + }, + Message::Generic, + ) + } + Message::ComposePressed => { + self.compose = Some(Compose { + to: String::new(), + subject: String::new(), + body: text_editor::Content::new(), + reply_to: None, + }); + Task::none() + } + Message::ReplyPressed => { + let Some((_, msg)) = &self.open else { + return Task::none(); + }; + // A fingerprint-only sender left no address to reply to. + let to = if let Some(uri) = &msg.reply_to { + uri.clone() + } else if msg.from.starts_with('<') { + self.fail("this sender left no address to reply to".into()); + return Task::none(); + } else { + msg.from.clone() + }; + let subject = if msg.subject.is_empty() || msg.subject.starts_with("Re:") { + msg.subject.clone() + } else { + format!("Re: {}", msg.subject) + }; + self.compose = Some(Compose { + to, + subject, + body: text_editor::Content::new(), + reply_to: Some(msg.id.clone()), + }); + Task::none() + } + Message::ComposeToEdited(to) => { + if let Some(compose) = &mut self.compose { + compose.to = to; + } + Task::none() + } + Message::ComposeSubjectEdited(subject) => { + if let Some(compose) = &mut self.compose { + compose.subject = subject; + } + Task::none() + } + Message::ComposeClosed => { + self.compose = None; + Task::none() + } + Message::SendPressed => { + let Some(compose) = &self.compose else { + return Task::none(); + }; + if compose.to.trim().is_empty() { + self.fail("enter a recipient".into()); + return Task::none(); + } + let to = compose.to.trim().to_string(); + let subject = compose.subject.trim().to_string(); + let body = compose.body.text(); + let reply_to = compose.reply_to.clone(); + self.compose = None; + let timeout = self.timeout; + self.run("sending", move |store, master| { + ops::send( + &store, + &master, + &to, + if subject.is_empty() { + None + } else { + Some(subject.as_str()) + }, + &body, + reply_to.as_deref(), + timeout, + ) + }) + } + Message::DeletePressed => { + let Some((folder, msg)) = &self.open else { + return Task::none(); + }; + self.dialog = Some(Dialog::Delete { + id: msg.id.clone(), + folder: *folder, + kept: msg.kept, + }); + Task::none() + } + Message::DeleteCancelled => { + self.dialog = None; + Task::none() + } + Message::DeleteConfirmed(everywhere) => { + let Some(Dialog::Delete { id, folder, kept }) = self.dialog.take() else { + return Task::none(); + }; + let scope = everywhere && kept; + let timeout = self.timeout; + self.run("deleting", move |store, master| { + ops::delete(&store, &master, &id, folder, scope, timeout) + .map(|_| "deleted".to_string()) + }) + } + Message::AcceptPressed => { + let Some((folder, msg)) = &self.open else { + return Task::none(); + }; + if *folder != Folder::Requests { + return Task::none(); + } + let Some(uri) = msg.reply_to.clone() else { + self.fail("this sender left no address to accept".into()); + return Task::none(); + }; + let Ok(addr) = Address::parse(&uri) else { + self.fail("the sender's reply address does not parse".into()); + return Task::none(); + }; + let Ok(identity) = + <[u8; KEY_LEN]>::try_from(unb32(&msg.sender).unwrap_or_default()) + else { + self.fail("the sender's key does not parse".into()); + return Task::none(); + }; + let address = addr.short(); + let timeout = self.timeout; + self.run("accepting", move |store, master| { + ops::accept_contact(&store, &master, &address, &identity, timeout) + .map(|_| format!("accepted {address}")) + }) + } + Message::BlockPressed => { + let Some((_, msg)) = &self.open else { + return Task::none(); + }; + let Some(uri) = msg.reply_to.clone() else { + self.fail("this sender left no address to block".into()); + return Task::none(); + }; + let Ok(addr) = Address::parse(&uri) else { + self.fail("the sender's reply address does not parse".into()); + return Task::none(); + }; + let address = addr.short(); + let timeout = self.timeout; + self.run("blocking", move |store, master| { + ops::block_contact(&store, &master, &address, timeout) + .map(|_| format!("blocked {address}")) + }) + } + Message::ContactImportEdited(text) => { + self.contact_import = text; + Task::none() + } + Message::ContactImportPressed => { + let uri = self.contact_import.trim().to_string(); + if uri.is_empty() { + return Task::none(); + } + self.run("importing", move |store, _| { + ops::import_contact(&store, &uri) + .map(|(address, _)| format!("imported {address} (verified)")) + }) + } + Message::ContactWrite(address) => { + let key = self + .snapshot + .as_ref() + .and_then(|snap| { + snap.contacts + .iter() + .find(|c| c.address == address) + .map(|c| c.key.clone()) + }) + .unwrap_or_default(); + self.compose = Some(Compose { + // Self-certifying: the sealed copy goes to the key we hold. + to: format!("smol://{address}/{key}"), + subject: String::new(), + body: text_editor::Content::new(), + reply_to: None, + }); + Task::none() + } + Message::ContactAccept(address, key) => { + let Ok(identity) = <[u8; KEY_LEN]>::try_from(unb32(&key).unwrap_or_default()) + else { + return Task::none(); + }; + let timeout = self.timeout; + self.run("accepting", move |store, master| { + ops::accept_contact(&store, &master, &address, &identity, timeout) + .map(|_| format!("accepted {address}")) + }) + } + Message::ContactBlock(address) => { + let timeout = self.timeout; + self.run("blocking", move |store, master| { + ops::block_contact(&store, &master, &address, timeout) + .map(|_| format!("blocked {address}")) + }) + } + Message::ContactUnblock(address) => { + // A block keeps the row, so unblocking re-accepts it. + let key = self + .snapshot + .as_ref() + .and_then(|snap| { + snap.contacts + .iter() + .find(|c| c.address == address) + .map(|c| c.key.clone()) + }) + .unwrap_or_default(); + let timeout = self.timeout; + self.run("unblocking", move |store, master| { + let Ok(identity) = <[u8; KEY_LEN]>::try_from(unb32(&key).unwrap_or_default()) + else { + return Err("contact key does not parse".to_string()); + }; + ops::accept_contact(&store, &master, &address, &identity, timeout) + .map(|_| format!("unblocked {address}")) + }) + } + Message::CopyOwnAddress => { + let Some(snap) = &self.snapshot else { + return Task::none(); + }; + clipboard::write(format!("smol://{}/{}", snap.who.address, snap.who.key)) + } + Message::CopyExport => match &self.backup_text { + Some(text) => clipboard::write(text.clone()), + None => Task::none(), + }, + Message::TimeoutEdited(text) => { + if let Ok(timeout) = text.trim().parse::() { + self.timeout = timeout; + } + self.timeout_text = text; + Task::none() + } + Message::RotatePressed => { + self.dialog = Some(Dialog::Rotate); + Task::none() + } + Message::RotateCancelled => { + self.dialog = None; + Task::none() + } + Message::RotateConfirmed => { + self.dialog = None; + let timeout = self.timeout; + self.run("rotating", move |store, master| { + ops::rotate(&store, &master, timeout).map(|(index, key)| { + format!("rotated to index {index}; new key {}…", &key[..12]) + }) + }) + } + Message::UnpinPressed(label) => self.run("unpinning", move |store, _| { + ops::unpin(&store, &label).map(|_| format!("unpinned {label}")) + }), + Message::ExportPressed => { + let dir = self.dir.clone(); + self.run_with("exporting", Message::Exported, move |store, master| { + ops::export_backup(&store, &master, &dir) + .map(|(path, text)| (format!("backup written to {}", path.display()), text)) + }) + } + Message::Exported(result) => { + self.done(); + match result { + Ok((note, text)) => { + self.backup_text = Some(text); + self.note(note); + Task::none() + } + Err(e) => { + self.fail(e); + Task::none() + } + } + } + Message::ImportPressed => { + let text = self.import_body.text(); + if text.trim().is_empty() { + return Task::none(); + } + self.run("importing", move |store, master| { + ops::import_backup(&store, &master, &text) + }) + } + Message::SnapshotLoaded(result) => { + self.done(); + match result { + Ok(snap) => { + self.share_qr = qr_code::Data::new( + format!("smol://{}/{}", snap.who.address, snap.who.key).as_bytes(), + ) + .ok(); + self.lists = snap.lists.clone(); + self.snapshot = Some(snap); + Task::none() + } + Err(e) => { + self.fail(e); + Task::none() + } + } + } + Message::Generic(result) => { + self.done(); + match result { + Ok(note) => self.refresh_after(note), + Err(e) => { + self.fail(e); + Task::none() + } + } + } + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..70cb40a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,37 @@ +//! carrier — a minimalist smolmail client built on fumi-core, with an Iced +//! UI. State lives in the config directory: `carrier.key` (the master, +//! 0600), `carrier.db` (fumi-core's sealed store) and `read.txt` (read +//! marks, carrier's own sidecar). + +mod app; +mod ops; +mod theme; +mod view; + +use std::path::PathBuf; + +use app::Carrier; + +fn main() -> iced::Result { + let dir = config_dir(); + if let Err(e) = std::fs::create_dir_all(&dir) { + eprintln!("cannot create {}: {e}", dir.display()); + std::process::exit(1); + } + iced::application("carrier", Carrier::update, view::view) + .theme(|_| theme::theme()) + .window_size(iced::Size::new(1080.0, 640.0)) + .run_with(move || { + let (state, task) = Carrier::boot(dir); + (state, task) + }) +} + +fn config_dir() -> PathBuf { + match std::env::var("XDG_CONFIG_HOME") { + Ok(xdg) if !xdg.is_empty() => PathBuf::from(xdg).join("carrier"), + _ => PathBuf::from(std::env::var("HOME").unwrap_or_default()) + .join(".config") + .join("carrier"), + } +} diff --git a/src/ops.rs b/src/ops.rs new file mode 100644 index 0000000..f115b5f --- /dev/null +++ b/src/ops.rs @@ -0,0 +1,672 @@ +//! Blocking fumi-core operations and the local files around them. Everything +//! here runs on a worker thread; the UI state only ever sees the plain +//! values these functions return. + +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; + +use fumi_core::address::{Address, DEFAULT_PORT}; +use fumi_core::client::{FetchOptions, TrustChange}; +use fumi_core::crypto::{b32, fingerprint, unb32, KEY_LEN}; +use fumi_core::error::Error; +use fumi_core::store::Store; +use fumi_core::{account, client, export, store}; + +/// One message in a folder list, already opened for its metadata. +#[derive(Debug, Clone)] +pub struct Row { + pub id: String, + pub from: String, + pub subject: String, + pub at: i64, + pub unread: bool, +} + +/// A fully opened message. +#[derive(Debug, Clone)] +pub struct OpenedMsg { + pub id: String, + pub from: String, + pub subject: String, + pub time: i64, + pub body: String, + /// The sender's public key, base32 — the identity `accept` freezes. + pub sender: String, + /// The sender's self-certifying address, when the payload carried one. + pub reply_to: Option, + pub kept: bool, +} + +#[derive(Debug, Clone)] +pub struct Contact { + pub address: String, + pub key: String, + pub verified: bool, + /// Some(true) accepted, Some(false) blocked, None never decided. + pub accepted: Option, +} + +#[derive(Debug, Clone)] +pub struct Who { + pub address: String, + pub key: String, + pub fingerprint: String, + pub rotations: u32, +} + +/// Everything the main pages display, refreshed in one blocking pass. +#[derive(Debug, Clone)] +pub struct Snapshot { + pub lists: Vec>, + pub contacts: Vec, + pub pins: Vec<(String, [u8; KEY_LEN])>, + pub who: Who, +} + +pub fn snapshot( + store: &Store, + master: &[u8; KEY_LEN], + read: &HashSet, +) -> Result { + Ok(Snapshot { + lists: lists(store, master, read)?, + contacts: contacts(store)?, + pins: pins(store)?, + who: whoami(store, master)?, + }) +} + +/// The three folders, in sidebar order. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Folder { + Inbox, + Requests, + Sent, +} + +impl Folder { + pub const ALL: [Folder; 3] = [Folder::Inbox, Folder::Requests, Folder::Sent]; + + fn table(self) -> &'static str { + match self { + Folder::Inbox => "inbox", + Folder::Requests => "requests", + Folder::Sent => "sent", + } + } +} + +fn explain(e: Error) -> String { + e.to_string() +} + +pub fn new_master() -> [u8; KEY_LEN] { + use rand_core::RngCore; + let mut master = [0u8; KEY_LEN]; + rand_core::OsRng.fill_bytes(&mut master); + master +} + +pub fn seed_text(master: &[u8; KEY_LEN]) -> String { + b32(master) +} + +/// A seed as base32 or 64 hex characters, the two forms a paste can carry. +pub fn parse_seed(text: &str) -> Result<[u8; KEY_LEN], String> { + let text = text.trim(); + let bytes = if text.len() == 64 && text.chars().all(|c| c.is_ascii_hexdigit()) { + let mut raw = Vec::with_capacity(32); + for pair in text.as_bytes().chunks(2) { + let hi = (pair[0] as char).to_digit(16).ok_or("bad hex")?; + let lo = (pair[1] as char).to_digit(16).ok_or("bad hex")?; + raw.push((hi * 16 + lo) as u8); + } + raw + } else { + unb32(text).map_err(|e| explain(e))? + }; + bytes + .try_into() + .map_err(|_| "a seed is 32 bytes".to_string()) +} + +pub fn load_key(path: &Path) -> Result<[u8; KEY_LEN], String> { + let raw = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?; + raw.try_into() + .map_err(|_| format!("{}: not a 32-byte master", path.display())) +} + +pub fn save_key(path: &Path, master: &[u8; KEY_LEN]) -> Result<(), String> { + #[cfg(unix)] + let err = { + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + let mut file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(path) + .map_err(|e| e.to_string())?; + file.write_all(master).map_err(|e| e.to_string()) + }; + #[cfg(not(unix))] + let err = std::fs::write(path, master).map_err(|e| e.to_string()); + err +} + +pub fn account(store: &Store, master: &[u8; KEY_LEN]) -> Result { + let index = store.rotations().map_err(explain)?; + account::Account::new(*master, index).map_err(explain) +} + +pub fn parse_address(text: &str) -> Result { + Address::parse(text).map_err(explain) +} + +/// A base32 public key, the form the operator publishes and pins are shown in. +pub fn parse_key(text: &str) -> Result<[u8; KEY_LEN], String> { + unb32(text.trim()) + .map_err(explain)? + .try_into() + .map_err(|_| "keys are 32 bytes of base32".to_string()) +} + +pub fn register( + store: &Store, + master: &[u8; KEY_LEN], + addr: &Address, + invite: Option<&str>, + timeout: u64, +) -> Result<(), String> { + let me = account(store, master)?; + client::register(store, addr, &me, invite, timeout).map_err(explain) +} + +pub fn restore( + store: &Store, + master: &[u8; KEY_LEN], + addr: &Address, + timeout: u64, +) -> Result { + client::restore(store, master, addr, timeout).map_err(explain) +} + +/// Connects without requiring a pin and reports the static key the server +/// presented, so the user can decide to pin it (sec 4 trust on first use). +/// None when a pin already covers this host. +pub fn learn_key( + store: &Store, + addr: &Address, + timeout: u64, +) -> Result, String> { + if store + .server_pin(&addr.host, addr.port) + .map_err(explain)? + .is_some() + { + return Ok(None); + } + let mut session = client::connect(store, addr, false, timeout).map_err(explain)?; + let offered = session.unpinned_static; + session.close(); + Ok(offered) +} + +pub fn pin(store: &Store, addr: &Address, key: &[u8; KEY_LEN]) -> Result<(), String> { + store + .pin_server(&addr.host, addr.port, key) + .map_err(explain) +} + +/// Splits a stored pin label (`host` or `host:port`) back into its parts. +fn label_parts(label: &str) -> (String, u16) { + match label.rsplit_once(':') { + Some((host, port)) => (host.to_string(), port.parse().unwrap_or(DEFAULT_PORT)), + None => (label.to_string(), DEFAULT_PORT), + } +} + +pub fn unpin(store: &Store, label: &str) -> Result<(), String> { + let (host, port) = label_parts(label); + store.unpin_server(&host, port).map_err(explain) +} + +pub fn pins(store: &Store) -> Result, String> { + store.pins().map_err(explain) +} + +/// Fetches with the default cursor policy, returning a one-line summary. +pub fn fetch( + store: &Store, + master: &[u8; KEY_LEN], + cancel: Arc, + timeout: u64, +) -> Result { + let me = account(store, master)?; + let opts = FetchOptions { + keep: false, + reset: false, + timeout, + dial: None, + cancel: Some(&cancel), + }; + let fetched = client::fetch_with(store, &me, opts).map_err(explain)?; + let mut note = format!("{} fetched, {} stored", fetched.total, fetched.stored); + if !fetched.rejected.is_empty() { + note.push_str(&format!(", {} rejected", fetched.rejected.len())); + } + if fetched.cancelled { + note.push_str(" (cancelled)"); + } + Ok(note) +} + +pub fn send( + store: &Store, + master: &[u8; KEY_LEN], + to: &str, + subject: Option<&str>, + body: &str, + reply_to: Option<&str>, + timeout: u64, +) -> Result { + let addr = parse_address(to)?; + let me = account(store, master)?; + let draft = client::SendDraft { + address: &addr, + text: body.to_string(), + subject, + reply_to, + headers: &[], + anonymous: false, + no_pad: false, + }; + let sent = client::send(store, &me, &draft, timeout).map_err(explain)?; + let change = match sent.change { + TrustChange::New { unverified: true } => { + "; new contact learned from an unpinned server (unverified)" + } + TrustChange::New { unverified: false } => "; new contact learned (trust on first use)", + TrustChange::Rotated => "; contact rotated to a new key (chain verified)", + TrustChange::None => "", + }; + let mut note = format!("sent to {} ({} bytes)", addr.short(), sent.bytes); + if sent.token_used { + note.push_str(", accept token attached"); + } + note.push_str(change); + if let Some(warning) = sent.warning { + note.push_str(&format!("; {warning}")); + } + Ok(note) +} + +/// Removes one message locally, and on the server when `everywhere` is set. +pub fn delete( + store: &Store, + master: &[u8; KEY_LEN], + id: &str, + folder: Folder, + everywhere: bool, + timeout: u64, +) -> Result<(), String> { + let raw = hex32(id)?; + store.delete_local(folder.table(), &raw).map_err(explain)?; + if everywhere { + let me = account(store, master)?; + client::delete(store, &me, &[raw], None, timeout).map_err(explain)?; + } + Ok(()) +} + +/// Admits a correspondent to the main tier and pushes the changed token set. +pub fn accept_contact( + store: &Store, + master: &[u8; KEY_LEN], + address: &str, + identity: &[u8; KEY_LEN], + timeout: u64, +) -> Result<(), String> { + store.accept(address, identity).map_err(explain)?; + let me = account(store, master)?; + client::push_tokens(store, &me, None, timeout).map_err(explain)?; + Ok(()) +} + +/// Blocks a correspondent and pushes the changed token set. +pub fn block_contact( + store: &Store, + master: &[u8; KEY_LEN], + address: &str, + timeout: u64, +) -> Result { + let blocked = store.block(address).map_err(explain)?; + let me = account(store, master)?; + client::push_tokens(store, &me, None, timeout).map_err(explain)?; + Ok(blocked) +} + +/// Adds a contact from a self-certifying `smol://` address. +pub fn import_contact(store: &Store, uri: &str) -> Result<(String, String), String> { + let addr = parse_address(uri)?; + let key = addr + .identity + .ok_or("import needs a self-certifying smol:// address carrying a key")?; + store + .save_contact(&addr.short(), &key, true) + .map_err(explain)?; + Ok((addr.short(), b32(&key))) +} + +pub fn contacts(store: &Store) -> Result, String> { + Ok(store + .contact_rows() + .map_err(explain)? + .into_iter() + .map(|(address, identity, verified, accepted)| Contact { + address, + key: b32(&identity), + verified, + accepted: accepted.map(|state| state == 1), + }) + .collect()) +} + +pub fn rotate( + store: &Store, + master: &[u8; KEY_LEN], + timeout: u64, +) -> Result<(u32, String), String> { + let me = account(store, master)?; + let rotated = client::rotate(store, &me, None, timeout).map_err(explain)?; + Ok((store.rotations().map_err(explain)?, b32(&rotated.new_pk))) +} + +/// Writes the gsmol backup beside the store and returns it for the clipboard. +pub fn export_backup( + store: &Store, + master: &[u8; KEY_LEN], + dir: &Path, +) -> Result<(PathBuf, String), String> { + let text = export::export(store, master).map_err(explain)?; + let path = dir.join(format!("carrier-backup-{}.gsmol", store::now())); + std::fs::write(&path, &text).map_err(|e| format!("{}: {e}", path.display()))?; + Ok((path, text)) +} + +pub fn import_backup(store: &Store, master: &[u8; KEY_LEN], text: &str) -> Result { + let summary = export::import(store, master, text).map_err(explain)?; + Ok(summary.to_string()) +} + +fn hex32(text: &str) -> Result<[u8; KEY_LEN], String> { + let raw = if text.len() == 64 && text.chars().all(|c| c.is_ascii_hexdigit()) { + (0..32) + .map(|i| u8::from_str_radix(&text[i * 2..i * 2 + 2], 16)) + .collect::, _>>() + .map_err(|e| e.to_string())? + } else { + return Err("message ids are 64 hex characters".to_string()); + }; + raw.try_into() + .map_err(|_| "message ids are 32 bytes".to_string()) +} + +/// Lists one folder as rows, marking unread against the local read set. +fn rows(store: &Store, me: &account::Account, folder: Folder, read: &HashSet) -> Vec { + store + .mail(folder.table()) + .unwrap_or_default() + .into_iter() + .filter_map(|stored| { + let id = b32_hex(&stored.id); + let described = client::describe(store, me, &stored).ok()?; + let from = match folder { + Folder::Sent => stored.recipient.clone().unwrap_or_default(), + _ => described.from, + }; + let unread = !read.contains(&id); + Some(Row { + id, + from, + subject: described.subject, + at: described.time, + unread, + }) + }) + .collect() +} + +/// All three folders at once, so the sidebar counts never go stale. +pub fn lists( + store: &Store, + master: &[u8; KEY_LEN], + read: &HashSet, +) -> Result>, String> { + let me = account(store, master)?; + Ok(Folder::ALL + .iter() + .map(|folder| rows(store, &me, *folder, read)) + .collect()) +} + +pub fn open( + store: &Store, + master: &[u8; KEY_LEN], + id: &str, + folder: Folder, +) -> Result { + let raw = hex32(id)?; + let me = account(store, master)?; + let stored = store + .mail(folder.table()) + .map_err(explain)? + .into_iter() + .find(|m| m.id == raw) + .ok_or_else(|| "message not found".to_string())?; + let described = client::describe(store, &me, &stored).map_err(explain)?; + Ok(OpenedMsg { + id: id.to_string(), + from: match folder { + Folder::Sent => stored.recipient.clone().unwrap_or_default(), + _ => described.from, + }, + subject: described.subject, + time: described.time, + body: described.text, + sender: b32(&described.sender), + reply_to: described.fields.get("reply-to").cloned(), + kept: stored.kept, + }) +} + +pub fn whoami(store: &Store, master: &[u8; KEY_LEN]) -> Result { + let me = account(store, master)?; + let address = store + .account() + .map_err(explain)? + .map(|addr| addr.short()) + .unwrap_or_default(); + Ok(Who { + address, + key: b32(&me.me().pk()), + fingerprint: fingerprint(&me.me().pk()), + rotations: me.index(), + }) +} + +fn b32_hex(raw: &[u8; KEY_LEN]) -> String { + raw.iter().map(|b| format!("{b:02x}")).collect() +} + +/// UTC stamp for an epoch-second timestamp; mail is not minute-critical and +/// the writer's zone is unknown, so UTC is the honest display. +pub fn when(at: i64) -> String { + let days = at.div_euclid(86400); + let secs = at.rem_euclid(86400); + let (year, month, day) = civil_from_days(days); + format!( + "{year:04}-{month:02}-{day:02} {:02}:{:02}", + secs / 3600, + (secs % 3600) / 60 + ) +} + +/// Days since 1970-01-01 to a civil date (Howard Hinnant's algorithm). +fn civil_from_days(z: i64) -> (i64, u32, u32) { + let z = z + 719468; + let era = if z >= 0 { z } else { z - 146096 } / 146097; + let doe = z - era * 146097; + let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + let year = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let day = (doy - (153 * mp + 2) / 5 + 1) as u32; + let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32; + (if month <= 2 { year + 1 } else { year }, month, day) +} + +#[cfg(test)] +mod tests { + use super::*; + use fumi_core::message::{build_frontmatter, message_id, seal}; + use fumi_core::store::now; + + fn master() -> [u8; KEY_LEN] { + let mut seed = [0u8; KEY_LEN]; + for (i, b) in seed.iter_mut().enumerate() { + *b = i as u8; + } + seed + } + + fn setup() -> Store { + Store::open_in_memory().expect("in-memory store") + } + + #[test] + fn seed_round_trips_through_base32_and_hex() { + let m = master(); + assert_eq!(parse_seed(&seed_text(&m)).unwrap(), m); + let hex: String = m.iter().map(|b| format!("{b:02x}")).collect(); + assert_eq!(parse_seed(&hex).unwrap(), m); + assert!(parse_seed("not a seed").is_err()); + assert!(parse_seed(&hex[..63]).is_err()); + } + + #[test] + fn key_parsing_round_trips() { + let m = master(); + assert_eq!(parse_key(&b32(&m)).unwrap(), m); + assert!(parse_key("short").is_err()); + } + + /// A sealed self-copy in the store surfaces as a row, opens with its + /// frontmatter split, and flips to read against the marks set. + #[test] + fn list_and_open_round_trip() { + let store = setup(); + let m = master(); + let me = account(&store, &m).unwrap(); + let addr = Address::parse("me@localhost").unwrap(); + store.set_account(&addr).unwrap(); + + let body = build_frontmatter( + &[("Subject", "hello"), ("Reply-To", &addr.uri(&me.me().pk()))], + "the body text", + ); + let envelope = seal(me.me(), &me.me().pk(), body.as_bytes(), now(), true).unwrap(); + let id = message_id(&envelope); + store + .store_inbox(&id, &envelope, now(), false, false) + .unwrap(); + let id_hex: String = id.iter().map(|b| format!("{b:02x}")).collect(); + + let empty = HashSet::new(); + let folders = lists(&store, &m, &empty).unwrap(); + let inbox = &folders[0]; + assert_eq!(inbox.len(), 1); + assert_eq!(inbox[0].subject, "hello"); + assert_eq!(inbox[0].from, "me@localhost"); + assert!(inbox[0].unread); + + let opened = open(&store, &m, &id_hex, Folder::Inbox).unwrap(); + assert_eq!(opened.subject, "hello"); + assert_eq!(opened.body, "the body text"); + assert!(opened.reply_to.is_some()); + + let mut read = HashSet::new(); + read.insert(id_hex); + let folders = lists(&store, &m, &read).unwrap(); + assert!(!folders[0][0].unread); + } + + #[test] + fn import_contact_saves_verified() { + let store = setup(); + let me = account(&store, &master()).unwrap(); + let addr = Address::parse("me@localhost").unwrap(); + let uri = addr.uri(&me.me().pk()); + import_contact(&store, &uri).unwrap(); + let rows = contacts(&store).unwrap(); + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].address, "me@localhost"); + assert!(rows[0].verified); + // A short form carries no key, so importing it is an error. + assert!(import_contact(&store, "me@localhost").is_err()); + } + + #[test] + fn backup_round_trips() { + let dir = tempfile::tempdir().unwrap(); + let store = setup(); + let m = master(); + let me = account(&store, &m).unwrap(); + let addr = Address::parse("me@localhost").unwrap(); + store.set_account(&addr).unwrap(); + + let body = build_frontmatter(&[("Subject", "kept")], "sealed body"); + let envelope = seal(me.me(), &me.me().pk(), body.as_bytes(), now(), true).unwrap(); + let id = message_id(&envelope); + store + .store_inbox(&id, &envelope, now(), false, false) + .unwrap(); + + let (path, text) = export_backup(&store, &m, dir.path()).unwrap(); + assert!(path.exists()); + assert!(!text.is_empty()); + + let fresh = setup(); + let summary = import_backup(&fresh, &m, &text).unwrap(); + assert!(summary.contains("1 messages")); + let empty = HashSet::new(); + let folders = lists(&fresh, &m, &empty).unwrap(); + assert_eq!(folders[0].len(), 1); + assert_eq!(folders[0][0].subject, "kept"); + } + + #[test] + fn unpin_splits_the_label() { + let store = setup(); + let addr = Address::parse("me@localhost:1962").unwrap(); + let key = master(); + store.pin_server(&addr.host, addr.port, &key).unwrap(); + assert_eq!(pin(&store, &addr, &key), Ok(())); + let all = pins(&store).unwrap(); + assert_eq!(all.len(), 1); + assert_eq!(all[0].0, "localhost:1962"); + unpin(&store, "localhost:1962").unwrap(); + assert!(pins(&store).unwrap().is_empty()); + } + + #[test] + fn stamps_are_utc_civil_dates() { + assert_eq!(when(0), "1970-01-01 00:00"); + assert_eq!(when(1_759_279_361), "2025-10-01 00:42"); + // Negative stays well-defined (pre-1970). + assert_eq!(when(-1), "1969-12-31 23:59"); + } +} diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..8fc62c9 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,126 @@ +//! The one palette the whole UI is built from: dark navy surfaces, light +//! text, mint accents — the same scheme kirakira ships as its dark theme. + +use iced::border; +use iced::color; +use iced::theme::Palette; +use iced::widget::{button, container}; +use iced::{Border, Color, Theme}; + +pub const BACKGROUND: Color = color!(0x01111d); +pub const PANEL: Color = color!(0x0a1c2e); +pub const CARD: Color = color!(0x10263b); +pub const ACCENT: Color = color!(0x3ddc97); +pub const TEXT: Color = color!(0xdce7f2); +pub const MUTED: Color = color!(0x7a90a5); +pub const DANGER: Color = color!(0xf0564a); + +pub fn theme() -> Theme { + Theme::custom( + "carrier".to_string(), + Palette { + background: BACKGROUND, + text: TEXT, + primary: ACCENT, + success: ACCENT, + danger: DANGER, + }, + ) +} + +/// The surface of the navigation sidebar. +pub fn panel(_theme: &Theme) -> container::Style { + container::Style { + background: Some(PANEL.into()), + text_color: Some(TEXT), + ..container::Style::default() + } +} + +/// A card slightly lighter than its panel. +pub fn card(_theme: &Theme) -> container::Style { + container::Style { + background: Some(CARD.into()), + text_color: Some(TEXT), + border: border::rounded(6), + ..container::Style::default() + } +} + +/// The status line at the bottom of the window. +pub fn status(error: bool) -> impl Fn(&Theme) -> container::Style { + move |_theme| container::Style { + background: Some(PANEL.into()), + text_color: Some(if error { DANGER } else { MUTED }), + ..container::Style::default() + } +} + +/// The primary action button: mint on navy. +pub fn accent() -> impl Fn(&Theme, button::Status) -> button::Style { + move |_theme, status| { + let hovered = matches!(status, button::Status::Hovered); + button::Style { + background: Some(if hovered { color!(0x54e8a8) } else { ACCENT }.into()), + text_color: BACKGROUND, + border: border::rounded(4), + ..button::Style::default() + } + } +} + +/// A quiet secondary button: filled with the panel color, no border. +pub fn quiet() -> impl Fn(&Theme, button::Status) -> button::Style { + move |_theme, status| { + let hovered = matches!(status, button::Status::Hovered); + button::Style { + background: Some(if hovered { CARD } else { PANEL }.into()), + text_color: TEXT, + border: border::rounded(4), + ..button::Style::default() + } + } +} + +/// A destructive action, red text on a quiet surface. +pub fn danger() -> impl Fn(&Theme, button::Status) -> button::Style { + move |_theme, status| { + let hovered = matches!(status, button::Status::Hovered); + button::Style { + background: Some(if hovered { color!(0x2a1420) } else { PANEL }.into()), + text_color: DANGER, + border: border::rounded(4), + ..button::Style::default() + } + } +} + +/// A single list row; the selected row carries the accent border. +pub fn row_style(selected: bool) -> impl Fn(&Theme, button::Status) -> button::Style { + move |_theme, status| { + let hovered = matches!(status, button::Status::Hovered); + button::Style { + background: Some( + if selected { + CARD + } else if hovered { + color!(0x0d2136) + } else { + PANEL + } + .into(), + ), + text_color: TEXT, + border: if selected { + Border { + color: ACCENT, + width: 1.0, + radius: 4.0.into(), + } + } else { + border::rounded(4) + }, + ..button::Style::default() + } + } +} diff --git a/src/view.rs b/src/view.rs new file mode 100644 index 0000000..31eaa54 --- /dev/null +++ b/src/view.rs @@ -0,0 +1,818 @@ +//! The view: a compact horizontal split — a tab bar, the full-width list +//! above, the article below, and a status line at the bottom edge. Protocol +//! values (addresses, keys, ids, dates) are monospace; prose stays sans. + +use iced::widget::text::Wrapping; +use iced::widget::{ + button, column, container, qr_code, row, rule, scrollable, text, text_editor, text_input, Space, +}; +use iced::{Alignment, Element, Font, Length, Padding}; + +use fumi_core::crypto::{b32, fingerprint}; + +use crate::app::{Carrier, Dialog, Message, OnboardMode, Page}; +use crate::ops::{self, Contact, Folder, Row}; +use crate::theme::{self, ACCENT, MUTED, TEXT}; + +/// Column widths of the list, in pixels; monospace keeps them aligned. +const DATE_W: f32 = 104.0; +const FLAG_W: f32 = 24.0; +const FROM_W: f32 = 248.0; +const ADDR_W: f32 = 260.0; +const KEY_W: f32 = 248.0; +const STATE_W: f32 = 88.0; + +pub fn view(app: &Carrier) -> Element<'_, Message> { + let content: Element<'_, Message> = if app.compose.is_some() { + compose(app) + } else { + match app.page { + Page::Onboard => onboard(app), + Page::Signup => signup(app), + Page::Mail => mail(app), + Page::Contacts => contacts(app), + Page::Settings => settings(app), + } + }; + column![content, dialog_banner(app), status(app)] + .spacing(0) + .height(Length::Fill) + .width(Length::Fill) + .into() +} + +// --- shared pieces --------------------------------------------------------- + +/// One line of monospace UI text. +fn mono<'a>(content: impl std::fmt::Display) -> iced::widget::Text<'a> { + text(content.to_string()) + .font(Font::MONOSPACE) + .size(12) + .color(TEXT) +} + +fn muted<'a>(content: impl std::fmt::Display) -> iced::widget::Text<'a> { + text(content.to_string()) + .font(Font::MONOSPACE) + .size(12) + .color(MUTED) +} + +fn accent<'a>(content: impl std::fmt::Display) -> iced::widget::Text<'a> { + text(content.to_string()) + .font(Font::MONOSPACE) + .size(12) + .color(ACCENT) +} + +/// A fixed-width cell so every row lines up like a table column. +fn cell<'a>(content: iced::widget::Text<'a>, width: f32) -> Element<'a, Message> { + container(content) + .width(width) + .align_x(Alignment::Start) + .into() +} + +fn action<'a>(label: &str, msg: Message) -> Element<'a, Message> { + button(text(label.to_string()).size(12)) + .on_press(msg) + .style(theme::accent()) + .padding([3, 10]) + .into() +} + +fn action_disabled<'a>(label: &str) -> Element<'a, Message> { + button(text(label.to_string()).size(12)) + .padding([3, 10]) + .style(theme::quiet()) + .into() +} + +fn quiet_action<'a>(label: &str, msg: Message) -> Element<'a, Message> { + button(text(label.to_string()).size(12)) + .on_press(msg) + .style(theme::quiet()) + .padding([3, 10]) + .into() +} + +fn danger_action<'a>(label: &str, msg: Message) -> Element<'a, Message> { + button(text(label.to_string()).size(12)) + .on_press(msg) + .style(theme::danger()) + .padding([3, 10]) + .into() +} + +/// A tab entry: accent text when active, muted otherwise. +fn tab<'a>(label: impl std::fmt::Display, active: bool, msg: Message) -> Element<'a, Message> { + let label = label.to_string(); + button( + text(label) + .font(Font::MONOSPACE) + .size(12) + .color(if active { ACCENT } else { MUTED }), + ) + .on_press(msg) + .padding([3, 6]) + .into() +} + +/// The bar every main page shares: identity, folder tabs, page tabs, actions. +fn top_bar<'a>(app: &'a Carrier) -> Element<'a, Message> { + let address = app + .snapshot + .as_ref() + .map(|snap| snap.who.address.clone()) + .unwrap_or_default(); + let unread = |folder: usize| { + app.lists + .get(folder) + .map(|rows| rows.iter().filter(|r| r.unread).count()) + .unwrap_or(0) + }; + let fetching = app.busy.as_deref() == Some("fetching"); + let mut tabs = row![].spacing(2); + for (folder, label) in [ + (Folder::Inbox, "Inbox"), + (Folder::Requests, "Requests"), + (Folder::Sent, "Sent"), + ] { + let count = unread(folder as usize); + let label = if count > 0 { + format!("{label} {count}") + } else { + label.to_string() + }; + tabs = tabs.push(tab( + label, + app.folder == folder && app.page == Page::Mail, + Message::GotoMail(folder), + )); + } + tabs = tabs + .push(tab( + "Contacts", + app.page == Page::Contacts, + Message::Goto(Page::Contacts), + )) + .push(tab( + "Settings", + app.page == Page::Settings, + Message::Goto(Page::Settings), + )); + let fetch = if fetching { + action_disabled("Fetching…") + } else if app.busy.is_some() { + action_disabled("Fetch") + } else { + action("Fetch", Message::FetchPressed) + }; + let write = if app.busy.is_some() { + action_disabled("Write") + } else { + action("Write", Message::ComposePressed) + }; + container( + row![ + accent("carrier"), + mono(address), + Space::with_width(Length::Fill), + tabs, + Space::with_width(Length::Fill), + fetch, + write, + ] + .spacing(10) + .align_y(Alignment::Center), + ) + .style(theme::panel) + .padding([6, 10]) + .width(Length::Fill) + .into() +} + +/// The one status line: the busy label with its cancel button while an +/// operation runs, otherwise the last result or error. +fn status<'a>(app: &'a Carrier) -> Element<'a, Message> { + let mut line = row![].spacing(10).align_y(Alignment::Center); + match (&app.busy, &app.status) { + (Some(busy), _) => { + line = line.push(muted(format!("{busy}…"))); + if busy == "fetching" { + line = line.push(quiet_action("Cancel", Message::FetchCancelled)); + } + } + (None, Some(status)) => { + let piece = if app.status_error { + text(status.clone()).font(Font::MONOSPACE).size(12) + } else { + muted(status.clone()) + }; + line = line.push(piece); + } + (None, None) => {} + } + container(line) + .style(theme::status(app.status_error && app.busy.is_none())) + .padding([4, 10]) + .width(Length::Fill) + .into() +} + +/// Inline confirmations: a slim banner between the content and the status. +fn dialog_banner<'a>(app: &'a Carrier) -> Element<'a, Message> { + let Some(dialog) = &app.dialog else { + return row![].into(); + }; + let mut line = row![].spacing(8).align_y(Alignment::Center); + match dialog { + Dialog::Delete { kept, .. } => { + line = line.push(muted("delete this message:")); + line = line.push(danger_action("here", Message::DeleteConfirmed(false))); + if *kept { + line = line.push(danger_action("everywhere", Message::DeleteConfirmed(true))); + } + line = line.push(quiet_action("cancel", Message::DeleteCancelled)); + } + Dialog::Rotate => { + line = line.push(muted( + "rotate the identity? the old key stays derivable; the index never moves back", + )); + line = line.push(action("rotate", Message::RotateConfirmed)); + line = line.push(quiet_action("cancel", Message::RotateCancelled)); + } + } + container(line) + .style(theme::status(false)) + .padding([4, 10]) + .width(Length::Fill) + .into() +} + +fn pin_offer_banner<'a>(app: &'a Carrier) -> Element<'a, Message> { + let Some((_, key)) = &app.pin_offer else { + return row![].into(); + }; + row![ + muted("server key offered:"), + mono(fingerprint(key)), + mono(short(&b32(key), 24)), + action("pin", Message::PinAccept), + quiet_action("discard", Message::PinDiscard), + ] + .spacing(8) + .align_y(Alignment::Center) + .into() +} + +// --- mail ------------------------------------------------------------------ + +fn mail<'a>(app: &'a Carrier) -> Element<'a, Message> { + column![ + top_bar(app), + container(list_pane(app)).height(Length::FillPortion(38)), + rule::Rule::horizontal(1.0), + container(detail_pane(app)).height(Length::FillPortion(62)), + ] + .spacing(0) + .height(Length::Fill) + .into() +} + +fn list_pane<'a>(app: &'a Carrier) -> Element<'a, Message> { + let rows = app + .lists + .get(app.folder as usize) + .cloned() + .unwrap_or_default(); + let header = row![ + cell(muted("date"), DATE_W), + cell(muted("f"), FLAG_W), + cell(muted("from/to"), FROM_W), + muted("subject"), + ] + .spacing(10) + .padding(padding()); + let mut list = column![header].spacing(1); + if rows.is_empty() { + let hint = match app.folder { + Folder::Inbox => "inbox is empty — fetch to check the mailbox", + Folder::Requests => "no requests", + Folder::Sent => "nothing sent yet", + }; + list = list.push(container(muted(hint)).padding(Padding::new(10.0))); + } + for row in &rows { + let selected = app + .open + .as_ref() + .is_some_and(|(folder, msg)| *folder == app.folder && msg.id == row.id); + list = list.push(list_row(app.folder, row, selected)); + } + scrollable(list).height(Length::Fill).into() +} + +/// One compact row: date, flags, from/to, subject — a flat button so the +/// whole line is clickable like a mu4e header. +fn list_row<'a>(folder: Folder, row: &Row, selected: bool) -> Element<'a, Message> { + let flag = if row.unread { + accent("●") + } else { + muted("·") + }; + let date = ops::when(row.at); + button( + row![ + cell(mono(date), DATE_W), + cell(flag, FLAG_W), + cell(mono(truncate(&row.from, 32)), FROM_W), + text(truncate(&row.subject, 64)) + .font(Font::MONOSPACE) + .size(12) + .color(if row.unread { TEXT } else { MUTED }) + .wrapping(Wrapping::None), + ] + .spacing(10) + .align_y(Alignment::Center), + ) + .on_press(Message::MessageSelected(folder, row.id.clone())) + .style(theme::row_style(selected)) + .padding(padding()) + .width(Length::Fill) + .into() +} + +fn padding() -> Padding { + Padding::new(3.0).right(10.0).left(10.0) +} + +/// A `label: value` header line for the article view. +fn header_line<'a>(label: &str, value: impl std::fmt::Display) -> Element<'a, Message> { + row![muted(format!("{label:<9}")), mono(value),] + .spacing(4) + .into() +} + +fn detail_pane<'a>(app: &'a Carrier) -> Element<'a, Message> { + let Some((folder, msg)) = &app.open else { + return container(muted("select a message to read it")) + .center(Length::Fill) + .into(); + }; + let mut headers = column![].spacing(1); + let to_label = if *folder == Folder::Sent { + "to" + } else { + "from" + }; + headers = headers.push(header_line(to_label, &msg.from)); + headers = headers.push(header_line( + "subject", + if msg.subject.is_empty() { + "(no subject)".to_string() + } else { + msg.subject.clone() + }, + )); + headers = headers.push(header_line("date", format!("{} UTC", ops::when(msg.time)))); + headers = headers.push(header_line("id", short(&msg.id, 16))); + if msg.kept { + headers = headers.push(header_line("server", "a copy still exists there")); + } + let mut actions = row![].spacing(8); + actions = actions.push(quiet_action("reply", Message::ReplyPressed)); + if *folder == Folder::Requests { + if msg.reply_to.is_some() { + actions = actions.push(action("accept", Message::AcceptPressed)); + actions = actions.push(danger_action("block", Message::BlockPressed)); + } else { + actions = actions.push(muted("no reply address: cannot accept or block")); + } + } + actions = actions.push(danger_action("delete", Message::DeletePressed)); + column![ + container(headers).padding([8, 10]), + rule::Rule::horizontal(1.0), + scrollable(column![container( + text(msg.body.clone()) + .font(Font::MONOSPACE) + .size(12) + .color(TEXT) + ) + .padding([8, 10])]) + .height(Length::Fill), + rule::Rule::horizontal(1.0), + container(actions).padding([6, 10]), + ] + .spacing(0) + .height(Length::Fill) + .into() +} + +// --- compose --------------------------------------------------------------- + +/// Compose takes the lower pane, keeping the list visible above — writing is +/// the article pane's other job, as in the layout it comes from. +fn compose<'a>(app: &'a Carrier) -> Element<'a, Message> { + let Some(compose) = &app.compose else { + unreachable!("compose view without a draft") + }; + let label = if compose.reply_to.is_some() { + "reply" + } else { + "write" + }; + column![ + top_bar(app), + container( + column![ + row![ + muted("to"), + text_input("alice@example.org or smol://…", &compose.to) + .on_input(Message::ComposeToEdited) + .size(12) + .width(Length::Fill), + ] + .spacing(8), + row![ + muted("subject"), + text_input("subject", &compose.subject) + .on_input(Message::ComposeSubjectEdited) + .size(12) + .width(Length::Fill), + ] + .spacing(8), + text_editor(&compose.body) + .on_action(Message::ComposeBody) + .font(Font::MONOSPACE) + .size(12) + .height(Length::Fill), + row![ + action(label, Message::SendPressed), + quiet_action("cancel", Message::ComposeClosed), + ] + .spacing(8), + ] + .spacing(6) + ) + .padding(10) + .height(Length::Fill), + ] + .spacing(0) + .height(Length::Fill) + .into() +} + +// --- contacts --------------------------------------------------------------- + +fn contacts<'a>(app: &'a Carrier) -> Element<'a, Message> { + let Some(snap) = &app.snapshot else { + return column![top_bar(app)].into(); + }; + let own = format!("smol://{}/{}", snap.who.address, snap.who.key); + let mut share = row![ + muted("you:"), + mono(own), + quiet_action("copy", Message::CopyOwnAddress), + ] + .spacing(8) + .align_y(Alignment::Center); + if let Some(data) = &app.share_qr { + share = share.push(container(qr_code(data).cell_size(3)).padding(0)); + } + let header = row![ + cell(muted("address"), ADDR_W), + cell(muted("key"), KEY_W), + cell(muted("state"), STATE_W), + muted(""), + ] + .spacing(10) + .padding(padding()); + let mut table = column![header].spacing(1); + for contact in &snap.contacts { + table = table.push(contact_row(contact)); + } + if snap.contacts.is_empty() { + table = table.push( + container(muted("no contacts yet — import a smol:// address")) + .padding(Padding::new(10.0)), + ); + } + column![ + top_bar(app), + container(share).style(theme::panel).padding([6, 10]), + container( + row![ + text_input("paste a smol:// address to import", &app.contact_import) + .on_input(Message::ContactImportEdited) + .on_submit(Message::ContactImportPressed) + .size(12) + .width(Length::Fill), + action("import", Message::ContactImportPressed), + ] + .spacing(8) + ) + .padding([6, 10]), + scrollable(table).height(Length::Fill), + ] + .spacing(0) + .height(Length::Fill) + .into() +} + +fn contact_row<'a>(contact: &Contact) -> Element<'a, Message> { + let state = match contact.accepted { + Some(true) => "accepted", + Some(false) => "blocked", + None => { + if contact.verified { + "verified" + } else { + "tofu" + } + } + }; + let mut actions = row![quiet_action( + "write", + Message::ContactWrite(contact.address.clone()), + )] + .spacing(6); + match contact.accepted { + Some(true) => { + actions = actions.push(danger_action( + "block", + Message::ContactBlock(contact.address.clone()), + )); + } + Some(false) => { + actions = actions.push(action( + "unblock", + Message::ContactUnblock(contact.address.clone()), + )); + } + None => { + actions = actions.push(action( + "accept", + Message::ContactAccept(contact.address.clone(), contact.key.clone()), + )); + } + } + container( + row![ + cell(accent(&contact.address), ADDR_W), + cell(mono(short(&contact.key, 32)), KEY_W), + cell(muted(state), STATE_W), + actions, + ] + .spacing(10) + .align_y(Alignment::Center), + ) + .style(theme::card) + .padding(padding()) + .width(Length::Fill) + .into() +} + +// --- settings --------------------------------------------------------------- + +fn settings<'a>(app: &'a Carrier) -> Element<'a, Message> { + let Some(snap) = &app.snapshot else { + return column![top_bar(app)].into(); + }; + let who = &snap.who; + let identity = column![ + header_line("address", &who.address), + header_line("key", &who.key), + header_line("fingerprint", &who.fingerprint), + header_line("rotations", who.rotations), + row![ + action("rotate", Message::RotatePressed), + quiet_action("learn key", Message::SettingsLearnKey), + ] + .spacing(8), + ] + .spacing(1); + + let mut pins = column![].spacing(1); + for (label, key) in &snap.pins { + pins = pins.push( + row![ + cell(mono(label), ADDR_W), + cell(mono(short(&b32(key), 32)), KEY_W), + danger_action("unpin", Message::UnpinPressed(label.clone())), + ] + .spacing(10) + .align_y(Alignment::Center), + ); + } + if snap.pins.is_empty() { + pins = pins.push(muted("no server keys pinned")); + } + + // Pinned to the app borrow: the column mixes owned rows with the + // `text_editor` that borrows `app.import_body`. + let mut export: iced::widget::Column<'_, Message> = column![].spacing(4); + export = export.push( + row![ + action("export", Message::ExportPressed), + quiet_action("copy", Message::CopyExport), + ] + .spacing(8), + ); + if let Some(text) = &app.backup_text { + export = export.push(scrollable(mono(short(text, 4000))).height(Length::Fixed(80.0))); + } + export = export.push(muted("import a backup (gsmol): paste it below")); + export = export.push( + text_editor(&app.import_body) + .on_action(Message::ImportBody) + .font(Font::MONOSPACE) + .size(12) + .height(Length::Fixed(100.0)), + ); + export = export.push(action("import", Message::ImportPressed)); + + let sections = column![ + section("identity", identity.into()), + section("server keys", pins.into()), + section("backup", export.into()), + section( + "network", + row![ + muted("timeout"), + text_input("30", &app.timeout_text) + .on_input(Message::TimeoutEdited) + .size(12) + .width(80), + muted("seconds"), + ] + .spacing(8) + .align_y(Alignment::Center) + .into(), + ), + ] + .spacing(10); + + column![ + top_bar(app), + pin_offer_banner(app), + scrollable(container(sections).padding(10)).height(Length::Fill), + ] + .spacing(0) + .height(Length::Fill) + .into() +} + +fn section<'a>(title: &str, body: Element<'a, Message>) -> Element<'a, Message> { + container( + column![container(accent(title)).padding([2, 0]), body] + .spacing(6) + .max_width(720), + ) + .style(theme::card) + .padding(10) + .width(Length::Fill) + .into() +} + +// --- onboarding and signup --------------------------------------------------- + +fn centered<'a>(body: Element<'a, Message>) -> Element<'a, Message> { + container(body) + .center(Length::Fill) + .max_width(520) + .padding(20) + .into() +} + +fn onboard<'a>(app: &'a Carrier) -> Element<'a, Message> { + let body = match app.onboard.mode { + OnboardMode::Start => column![ + text("carrier").font(Font::MONOSPACE).size(24).color(ACCENT), + muted("smolmail: one seed, sealed mail, five operations"), + row![ + action("create an identity", Message::OnboardCreate), + quiet_action("restore from a seed", Message::OnboardRestore), + ] + .spacing(8) + .padding([12, 0]), + ] + .spacing(10) + .align_x(Alignment::Start), + OnboardMode::Created => column![ + muted("this seed is the only secret — write it down now; it is shown once"), + container( + text_input("", &app.onboard.seed_shown) + .font(Font::MONOSPACE) + .size(12) + .width(Length::Fill) + ) + .style(theme::card) + .padding(6), + row![ + action("i saved it", Message::OnboardSeedSaved), + quiet_action("back", Message::OnboardBack), + ] + .spacing(8), + ] + .spacing(10), + OnboardMode::Restore => column![ + muted("paste the 32-byte seed, base32 or hex"), + text_input("seed", &app.onboard.seed) + .on_input(Message::OnboardSeedEdited) + .on_submit(Message::OnboardRestoreFromSeed) + .size(12) + .width(Length::Fill), + row![ + action("restore", Message::OnboardRestoreFromSeed), + quiet_action("back", Message::OnboardBack), + ] + .spacing(8), + ] + .spacing(10), + }; + centered(body.into()) +} + +fn signup<'a>(app: &'a Carrier) -> Element<'a, Message> { + let mut body = column![ + accent("set up the address"), + row![ + muted("user"), + text_input("alice", &app.signup.user) + .on_input(Message::SignupUserEdited) + .size(12) + .width(Length::Fill), + ] + .spacing(8), + row![ + muted("host"), + text_input("example.org", &app.signup.host) + .on_input(Message::SignupHostEdited) + .size(12) + .width(Length::Fill), + ] + .spacing(8), + row![ + muted("invite"), + text_input("optional token", &app.signup.invite) + .on_input(Message::SignupInviteEdited) + .size(12) + .width(Length::Fill), + ] + .spacing(8), + row![ + muted("key"), + text_input("server key, if the operator published one", &app.signup.key) + .on_input(Message::SignupKeyEdited) + .size(12) + .width(Length::Fill), + ] + .spacing(8), + ] + .spacing(8); + if app.signup.key.is_empty() { + body = body.push( + row![ + muted("or"), + quiet_action("learn the server key", Message::SignupLearnKey), + muted("and pin what it presents"), + ] + .spacing(8) + .align_y(Alignment::Center), + ); + } + body = body.push(pin_offer_banner(app)); + body = body.push( + row![ + action("register", Message::SignupRegister), + quiet_action("i already have this address", Message::SignupRecall), + ] + .spacing(8) + .padding([8, 0]), + ); + if app.signup.fresh { + body = body.push(quiet_action("back", Message::SignupBack)); + } + centered(body.into()) +} + +// --- small helpers ----------------------------------------------------------- + +fn truncate(text: &str, max: usize) -> String { + if text.chars().count() <= max { + text.to_string() + } else { + let cut: String = text.chars().take(max - 1).collect(); + format!("{cut}…") + } +} + +fn short(text: &str, max: usize) -> String { + if text.len() <= max { + text.to_string() + } else { + format!("{}…{}", &text[..max / 2], &text[text.len() - 3..]) + } +}