From 86c3b9641358b1b8aefde9f3fffdb3800bace770 Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 14 Oct 2025 09:12:55 +0800 Subject: [PATCH 01/74] add crates --- Cargo.lock | 3177 ++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 6 + cargo/licenses.json | 40 + 3 files changed, 3150 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e9c74f6ea..418e49730 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -38,6 +49,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -50,7 +76,7 @@ version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6" dependencies = [ - "cssparser", + "cssparser 0.35.0", "html5ever 0.35.0", "maplit", "tendril", @@ -63,6 +89,23 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -99,7 +142,7 @@ dependencies = [ "bytes", "chrono", "coarsetime", - "convert_case", + "convert_case 0.8.0", "criterion", "csv", "data-encoding", @@ -322,7 +365,7 @@ version = "0.38.0+1.3.281" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" dependencies = [ - "libloading", + "libloading 0.8.8", ] [[package]] @@ -335,6 +378,18 @@ dependencies = [ "serde_json", ] +[[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" @@ -360,6 +415,96 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[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", + "slab", + "windows-sys 0.61.1", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +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", +] + +[[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.106", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.1", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -382,6 +527,12 @@ dependencies = [ "syn 2.0.106", ] +[[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.89" @@ -393,6 +544,29 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -588,6 +762,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake3" version = "1.8.2" @@ -616,6 +802,81 @@ 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 = "block2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +dependencies = [ + "objc2 0.6.2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases 0.2.1", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bstr" version = "1.12.0" @@ -695,7 +956,7 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2afa81c868c1a9b3fad25c31176945d0cc5181ba7b77c0456bc05cf57fca975c" dependencies = [ - "ahash", + "ahash 0.8.12", "bincode", "burn-common", "burn-dataset", @@ -906,6 +1167,39 @@ dependencies = [ "cubecl", ] +[[package]] +name = "byte-unit" +version = "5.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cd29c3c585209b0cbc7309bfe3ed7efd8c84c21b7af29c8bfae908f8777174" +dependencies = [ + "rust_decimal", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.23.2" @@ -937,6 +1231,9 @@ name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] [[package]] name = "bytesize" @@ -944,11 +1241,39 @@ version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.9.3", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "camino" version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5" +dependencies = [ + "serde", +] [[package]] name = "candle-core" @@ -972,6 +1297,39 @@ dependencies = [ "zip 1.1.4", ] +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.16", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.5", +] + [[package]] name = "cast" version = "0.3.0" @@ -989,12 +1347,45 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon 0.12.16", +] + [[package]] name = "cfg-if" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "cfg_aliases" version = "0.2.1" @@ -1010,7 +1401,8 @@ dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-link", + "serde", + "windows-link 0.1.3", ] [[package]] @@ -1078,7 +1470,7 @@ version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.106", @@ -1157,6 +1549,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "compression-codecs" version = "0.4.30" @@ -1198,6 +1600,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "convert_case" version = "0.8.0" @@ -1207,6 +1615,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1233,6 +1651,19 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.9.3", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types 0.5.0", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.1.3" @@ -1244,6 +1675,17 @@ dependencies = [ "libc", ] +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.9.3", + "core-foundation 0.10.1", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1345,6 +1787,23 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + [[package]] name = "cssparser" version = "0.35.0" @@ -1389,6 +1848,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.106", +] + [[package]] name = "cubecl" version = "0.5.0" @@ -1414,7 +1883,7 @@ checksum = "79251bfc7f067ac9038232fe38a317adc2f31cb2fc3800e69fd409ccac7abc1f" dependencies = [ "bytemuck", "derive-new 0.6.0", - "derive_more", + "derive_more 1.0.0", "dirs 5.0.1", "embassy-futures", "futures-lite", @@ -1443,7 +1912,7 @@ dependencies = [ "cubecl-macros", "cubecl-runtime", "derive-new 0.6.0", - "derive_more", + "derive_more 1.0.0", "half", "hashbrown 0.14.5", "log", @@ -1523,7 +1992,7 @@ checksum = "e096d77646590f0180ed4ce1aa7df4ecc7219f3c4616e9fe72d93ab63a352855" dependencies = [ "cubecl-common", "cubecl-macros-internal", - "derive_more", + "derive_more 1.0.0", "float-ord", "fnv", "half", @@ -1557,7 +2026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd74622b5c8cb161e3f7fa0b2b751784ef89ab45acfa355f511eb2219dde337e" dependencies = [ "cubecl-common", - "darling", + "darling 0.20.11", "derive-new 0.6.0", "ident_case", "prettyplease", @@ -1572,7 +2041,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a89898212c1eaba0e2f0dffcadc9790b20b75d2ec8836da084370b043be2623" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.106", @@ -1600,7 +2069,7 @@ dependencies = [ "async-channel", "bytemuck", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.2.1", "cubecl-common", "cubecl-ir", "derive-new 0.6.0", @@ -1635,12 +2104,12 @@ dependencies = [ "async-channel", "bytemuck", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.2.1", "cubecl-common", "cubecl-core", "cubecl-runtime", "derive-new 0.6.0", - "derive_more", + "derive_more 1.0.0", "hashbrown 0.14.5", "log", "web-time", @@ -1653,7 +2122,7 @@ version = "0.13.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "486c221362668c63a1636cfa51463b09574433b39029326cff40864b3ba12b6e" dependencies = [ - "libloading", + "libloading 0.8.8", ] [[package]] @@ -1662,8 +2131,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -1680,13 +2159,38 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", "quote", "syn 2.0.106", ] @@ -1721,6 +2225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75d7cc94194b4dd0fa12845ef8c911101b7f37633cda14997a6e82099aa0b693" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -1771,7 +2276,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.106", @@ -1787,6 +2292,19 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.106", +] + [[package]] name = "derive_more" version = "1.0.0" @@ -1864,7 +2382,23 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.60.2", + "windows-sys 0.61.1", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.3", + "objc2 0.6.2", ] [[package]] @@ -1878,6 +2412,29 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "dlopen2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "document-features" version = "0.2.11" @@ -1887,6 +2444,21 @@ dependencies = [ "litrs", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + [[package]] name = "dtoa" version = "1.0.10" @@ -1908,6 +2480,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "dyn-stack" version = "0.10.0" @@ -1965,6 +2543,12 @@ dependencies = [ "winreg 0.55.0", ] +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1974,13 +2558,40 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "enum-as-inner" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[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.106", @@ -2030,6 +2641,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "errno" version = "0.3.13" @@ -2079,6 +2700,45 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "filetime" version = "0.2.26" @@ -2266,6 +2926,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -2384,6 +3050,114 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + [[package]] name = "gemm" version = "0.17.1" @@ -2631,6 +3405,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" +dependencies = [ + "rustix", + "windows-targets 0.52.6", +] + [[package]] name = "getopts" version = "0.2.24" @@ -2640,6 +3424,17 @@ dependencies = [ "unicode-width 0.2.1", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -2673,6 +3468,38 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -2684,6 +3511,53 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.9.3", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "glob" version = "0.3.3" @@ -2724,6 +3598,17 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "gpu-alloc" version = "0.6.0" @@ -2775,6 +3660,58 @@ dependencies = [ "bitflags 2.9.3", ] +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "h2" version = "0.3.27" @@ -2787,7 +3724,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.11.0", "slab", "tokio", "tokio-util", @@ -2806,7 +3743,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap", + "indexmap 2.11.0", "slab", "tokio", "tokio-util", @@ -2844,13 +3781,22 @@ dependencies = [ "thiserror 2.0.16", ] +[[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.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.12", ] [[package]] @@ -2859,7 +3805,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.12", "allocator-api2", "serde", ] @@ -2909,6 +3855,12 @@ dependencies = [ "http 1.3.1", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -2956,6 +3908,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever 0.14.1", + "match_token 0.1.0", +] + [[package]] name = "html5ever" version = "0.35.0" @@ -2964,7 +3928,7 @@ checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" dependencies = [ "log", "markup5ever 0.35.0", - "match_token", + "match_token 0.35.0", ] [[package]] @@ -3189,6 +4153,16 @@ dependencies = [ "cc", ] +[[package]] +name = "ico" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +dependencies = [ + "byteorder", + "png", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -3327,6 +4301,17 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.11.0" @@ -3335,6 +4320,7 @@ checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown 0.15.5", + "serde", ] [[package]] @@ -3343,6 +4329,15 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + [[package]] name = "inflections" version = "1.1.1" @@ -3454,6 +4449,29 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "jiff" version = "0.2.15" @@ -3478,6 +4496,22 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni-sys" version = "0.3.0" @@ -3504,6 +4538,28 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "junction" version = "1.2.0" @@ -3514,6 +4570,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.9.3", + "serde", + "unicode-segmentation", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -3521,7 +4588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading", + "libloading 0.8.8", "pkg-config", ] @@ -3551,6 +4618,18 @@ dependencies = [ "libc", ] +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser 0.29.6", + "html5ever 0.29.1", + "indexmap 2.11.0", + "selectors", +] + [[package]] name = "launcher" version = "1.0.0" @@ -3570,12 +4649,73 @@ dependencies = [ "windows 0.61.3", ] +[[package]] +name = "launcher-gui" +version = "1.0.0" +dependencies = [ + "anki_i18n", + "anki_io", + "anki_process", + "anki_proto", + "anki_proto_gen", + "anyhow", + "data-encoding", + "dirs 6.0.0", + "futures", + "inflections", + "libc", + "libc-stdhandle", + "phf 0.11.3", + "portable-pty", + "prettyplease", + "prost", + "prost-build", + "prost-reflect", + "serde", + "serde_json", + "snafu", + "strum 0.27.2", + "syn 2.0.106", + "tauri", + "tauri-build", + "tauri-plugin-log", + "tauri-plugin-os", + "tauri-plugin-single-instance", + "tokio", + "widestring", + "windows 0.61.3", +] + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + [[package]] name = "libc" version = "0.2.175" @@ -3592,6 +4732,16 @@ dependencies = [ "libc", ] +[[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.8" @@ -3732,6 +4882,9 @@ name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +dependencies = [ + "value-bag", +] [[package]] name = "lru-slab" @@ -3763,7 +4916,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ac9c19702c37bae1a53d130a326b1c4f58cb17d472538cf547d44b46dbbe3aa" dependencies = [ "bytemuck", - "cfg_aliases", + "cfg_aliases 0.2.1", "half", "macerator-macros", "moddef", @@ -3778,7 +4931,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd48b535b9b37a25a2589ab8d4f997886a2c68f59960ce06588525f38dd4944" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.106", @@ -3813,6 +4966,20 @@ dependencies = [ "tendril", ] +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + [[package]] name = "markup5ever" version = "0.35.0" @@ -3836,6 +5003,17 @@ dependencies = [ "xml5ever", ] +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "match_token" version = "0.35.0" @@ -3958,7 +5136,7 @@ checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ "bitflags 2.9.3", "block", - "core-graphics-types", + "core-graphics-types 0.1.3", "foreign-types 0.5.0", "log", "objc", @@ -4007,6 +5185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -4027,6 +5206,27 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a0b3262dc837d2513fe2ef31ff8461352ef932dcca31ba0c0abe33547cf6b9b" +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2 0.6.2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "once_cell", + "png", + "serde", + "thiserror 2.0.16", + "windows-sys 0.60.2", +] + [[package]] name = "multer" version = "3.1.0" @@ -4059,12 +5259,12 @@ dependencies = [ "arrayvec", "bit-set", "bitflags 2.9.3", - "cfg_aliases", + "cfg_aliases 0.2.1", "codespan-reporting 0.12.0", "half", "hashbrown 0.15.5", "hexf-parse", - "indexmap", + "indexmap 2.11.0", "log", "num-traits", "once_cell", @@ -4108,6 +5308,27 @@ dependencies = [ "rayon", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.3", + "jni-sys", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "ndk-sys" version = "0.5.0+25.2.9519653" @@ -4117,6 +5338,15 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -4143,6 +5373,37 @@ dependencies = [ "which", ] +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.9.3", + "cfg-if", + "cfg_aliases 0.1.1", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.3", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "nom" version = "7.1.3" @@ -4357,12 +5618,21 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.3.0", "proc-macro2", "quote", "syn 2.0.106", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "nvml-wrapper" version = "0.10.0" @@ -4370,7 +5640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9bff0aa1d48904a1385ea2a8b97576fbdcbc9a3cfccd0d31fe978e1c4038c5" dependencies = [ "bitflags 2.9.3", - "libloading", + "libloading 0.8.8", "nvml-wrapper-sys", "static_assertions", "thiserror 1.0.69", @@ -4383,7 +5653,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "698d45156f28781a4e79652b6ebe2eaa0589057d588d3aec1333f6466f13fcb5" dependencies = [ - "libloading", + "libloading 0.8.8", ] [[package]] @@ -4406,6 +5676,243 @@ dependencies = [ "objc_id", ] +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +dependencies = [ + "bitflags 2.9.3", + "block2 0.6.1", + "libc", + "objc2 0.6.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-foundation 0.3.1", + "objc2-quartz-core 0.3.1", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" +dependencies = [ + "bitflags 2.9.3", + "objc2 0.6.2", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" +dependencies = [ + "bitflags 2.9.3", + "objc2 0.6.2", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags 2.9.3", + "dispatch2", + "objc2 0.6.2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +dependencies = [ + "bitflags 2.9.3", + "dispatch2", + "objc2 0.6.2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" +dependencies = [ + "objc2 0.6.2", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.9.3", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +dependencies = [ + "bitflags 2.9.3", + "block2 0.6.1", + "libc", + "objc2 0.6.2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" +dependencies = [ + "bitflags 2.9.3", + "objc2 0.6.2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-javascript-core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a" +dependencies = [ + "objc2 0.6.2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.9.3", + "block2 0.5.1", + "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.9.3", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" +dependencies = [ + "bitflags 2.9.3", + "objc2 0.6.2", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-security" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44" +dependencies = [ + "bitflags 2.9.3", + "objc2 0.6.2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" +dependencies = [ + "bitflags 2.9.3", + "objc2 0.6.2", + "objc2-core-foundation", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" +dependencies = [ + "bitflags 2.9.3", + "block2 0.6.1", + "objc2 0.6.2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "objc2-javascript-core", + "objc2-security", +] + [[package]] name = "objc_id" version = "0.1.1" @@ -4521,6 +6028,53 @@ dependencies = [ "num-traits", ] +[[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 = "os_info" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3" +dependencies = [ + "log", + "plist", + "serde", + "windows-sys 0.52.0", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "parking" version = "2.2.1" @@ -4647,7 +6201,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.11.0", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", ] [[package]] @@ -4656,7 +6219,9 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ + "phf_macros 0.10.0", "phf_shared 0.10.0", + "proc-macro-hack", ] [[package]] @@ -4665,10 +6230,20 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros", + "phf_macros 0.11.3", "phf_shared 0.11.3", ] +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + [[package]] name = "phf_codegen" version = "0.10.0" @@ -4689,6 +6264,16 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + [[package]] name = "phf_generator" version = "0.10.0" @@ -4709,6 +6294,20 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "phf_macros" version = "0.11.3" @@ -4722,6 +6321,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + [[package]] name = "phf_shared" version = "0.10.0" @@ -4772,12 +6380,36 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.11.0", + "quick-xml", + "serde", + "time", +] + [[package]] name = "plotters" version = "0.3.7" @@ -4806,6 +6438,33 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "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", + "windows-sys 0.61.1", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -4824,6 +6483,27 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "portable-pty" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "downcast-rs", + "filedescriptor", + "lazy_static", + "libc", + "log", + "nix 0.28.0", + "serial2", + "shared_library", + "shell-words", + "winapi", + "winreg 0.10.1", +] + [[package]] name = "potential_utf" version = "0.1.3" @@ -4878,7 +6558,26 @@ checksum = "5676d703dda103cbb035b653a9f11448c0a7216c7926bd35fcb5865475d0c970" dependencies = [ "autocfg", "equivalent", - "indexmap", + "indexmap 2.11.0", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.7", ] [[package]] @@ -4887,7 +6586,31 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ - "toml_edit", + "toml_edit 0.22.27", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] @@ -4927,7 +6650,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ - "heck", + "heck 0.5.0", "itertools 0.14.0", "log", "multimap", @@ -4974,6 +6697,26 @@ dependencies = [ "prost", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "pulldown-cmark" version = "0.8.0" @@ -5073,7 +6816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" dependencies = [ "once_cell", - "target-lexicon", + "target-lexicon 0.13.2", ] [[package]] @@ -5104,13 +6847,22 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "pyo3-build-config", "quote", "syn 2.0.106", ] +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.9" @@ -5118,7 +6870,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", - "cfg_aliases", + "cfg_aliases 0.2.1", "pin-project-lite", "quinn-proto", "quinn-udp", @@ -5158,7 +6910,7 @@ version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", "once_cell", "socket2 0.6.0", @@ -5181,6 +6933,26 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + [[package]] name = "rand" version = "0.8.5" @@ -5202,6 +6974,16 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -5222,6 +7004,15 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -5250,6 +7041,24 @@ dependencies = [ "rand 0.9.2", ] +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + [[package]] name = "range-alloc" version = "0.1.4" @@ -5343,6 +7152,26 @@ dependencies = [ "thiserror 2.0.16", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "regex" version = "1.11.2" @@ -5378,6 +7207,15 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -5485,6 +7323,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rmp" version = "0.8.14" @@ -5535,7 +7402,7 @@ checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" dependencies = [ "cfg-if", "glob", - "proc-macro-crate", + "proc-macro-crate 3.3.0", "proc-macro2", "quote", "regex", @@ -5581,6 +7448,22 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust_decimal" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8975fc98059f365204d635119cf9c5a60ae67b841ed49b5422a9a7e56cdfac0" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -5745,12 +7628,69 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.106", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "2.11.1" @@ -5787,6 +7727,24 @@ dependencies = [ "libc", ] +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser 0.29.6", + "derive_more 0.99.20", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + [[package]] name = "self_cell" version = "1.2.0" @@ -5798,6 +7756,9 @@ name = "semver" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +dependencies = [ + "serde", +] [[package]] name = "seq-macro" @@ -5826,6 +7787,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "serde-untagged" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34836a629bcbc6f1afdf0907a744870039b1e14c0561cb26094fa683b158eff3" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + [[package]] name = "serde-value" version = "0.7.0" @@ -5856,6 +7828,17 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "serde_json" version = "1.0.143" @@ -5889,6 +7872,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_spanned" version = "1.0.0" @@ -5931,6 +7923,81 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.11.0", + "schemars 0.9.0", + "schemars 1.0.4", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serial2" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc76fa68e25e771492ca1e3c53d447ef0be3093e05cd3b47f4b712ba10c6f3c" +dependencies = [ + "cfg-if", + "libc", + "winapi", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + [[package]] name = "sha1" version = "0.10.6" @@ -5962,6 +8029,22 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -5983,6 +8066,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "0.3.11" @@ -6031,7 +8120,7 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186f5ba9999528053fb497fdf0dd330efcc69cfe4ad03776c9d704bc54fee10f" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.106", @@ -6063,6 +8152,54 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "bytemuck", + "cfg_aliases 0.2.1", + "core-graphics", + "foreign-types 0.5.0", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", + "raw-window-handle", + "redox_syscall", + "wasm-bindgen", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "spin" version = "0.9.8" @@ -6159,7 +8296,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -6172,7 +8309,7 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.106", @@ -6184,6 +8321,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + [[package]] name = "syn" version = "1.0.109" @@ -6232,6 +8380,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + [[package]] name = "sysctl" version = "0.5.5" @@ -6295,6 +8452,19 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.23", + "version-compare", +] + [[package]] name = "systemstat" version = "0.2.5" @@ -6309,6 +8479,63 @@ dependencies = [ "winapi", ] +[[package]] +name = "tao" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7" +dependencies = [ + "bitflags 2.9.3", + "block2 0.6.1", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "objc2 0.6.2", + "objc2-app-kit", + "objc2-foundation 0.3.1", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.44" @@ -6320,12 +8547,305 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "target-lexicon" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +[[package]] +name = "tauri" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs 6.0.0", + "dunce", + "embed_plist", + "getrandom 0.3.3", + "glob", + "gtk", + "heck 0.5.0", + "http 1.3.1", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2 0.6.2", + "objc2-app-kit", + "objc2-foundation 0.3.1", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.12.23", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.16", + "tokio", + "tray-icon", + "url", + "urlpattern", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows 0.61.3", +] + +[[package]] +name = "tauri-build" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs 6.0.0", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.5", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.106", + "tauri-utils", + "thiserror 2.0.16", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.5", + "walkdir", +] + +[[package]] +name = "tauri-plugin-log" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c1438bc7662acd16d508c919b3c087efd63669a4c75625dff829b1c75975ec" +dependencies = [ + "android_logger", + "byte-unit", + "fern", + "log", + "objc2 0.6.2", + "objc2-foundation 0.3.1", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "thiserror 2.0.16", + "time", +] + +[[package]] +name = "tauri-plugin-os" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a1c77ebf6f20417ab2a74e8c310820ba52151406d0c80fbcea7df232e3f6ba" +dependencies = [ + "gethostname", + "log", + "os_info", + "serde", + "serde_json", + "serialize-to-javascript", + "sys-locale", + "tauri", + "tauri-plugin", + "thiserror 2.0.16", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb9cac815bf11c4a80fb498666bcdad66d65b89e3ae24669e47806febb76389c" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.16", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + +[[package]] +name = "tauri-runtime" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http 1.3.1", + "jni", + "objc2 0.6.2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.16", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807" +dependencies = [ + "gtk", + "http 1.3.1", + "jni", + "log", + "objc2 0.6.2", + "objc2-app-kit", + "objc2-foundation 0.3.1", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever 0.29.1", + "http 1.3.1", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.16", + "toml 0.9.5", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074" +dependencies = [ + "embed-resource", + "toml 0.9.5", +] + [[package]] name = "tempfile" version = "3.21.0" @@ -6445,7 +8965,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca967379f9d8eb8058d86ed467d81d03e81acd45757e4ca341c24affbe8e8e3" dependencies = [ "deranged", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", @@ -6587,19 +9109,31 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + [[package]] name = "toml" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ - "indexmap", + "indexmap 2.11.0", "serde", - "serde_spanned", + "serde_spanned 1.0.0", "toml_datetime 0.7.0", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.13", ] [[package]] @@ -6607,6 +9141,9 @@ name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] [[package]] name = "toml_datetime" @@ -6617,15 +9154,39 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.11.0", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.11.0", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap", + "indexmap 2.11.0", + "serde", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", - "winnow", + "winnow 0.7.13", ] [[package]] @@ -6634,7 +9195,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" dependencies = [ - "winnow", + "winnow 0.7.13", ] [[package]] @@ -6779,6 +9340,28 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tray-icon" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" +dependencies = [ + "crossbeam-channel", + "dirs 6.0.0", + "libappindicator", + "muda", + "objc2 0.6.2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.1", + "once_cell", + "png", + "serde", + "thiserror 2.0.16", + "windows-sys 0.59.0", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -6811,6 +9394,12 @@ dependencies = [ "rustc-hash 2.1.1", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.18.0" @@ -6823,6 +9412,17 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "ug" version = "0.1.0" @@ -6831,7 +9431,7 @@ checksum = "03719c61a91b51541f076dfdba45caacf750b230cefaa4b32d6f5411c3f7f437" dependencies = [ "gemm 0.18.2", "half", - "libloading", + "libloading 0.8.8", "memmap2", "num", "num-traits", @@ -6920,6 +9520,17 @@ dependencies = [ "unic-ucd-version", ] +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + [[package]] name = "unic-ucd-version" version = "0.9.0" @@ -7007,12 +9618,30 @@ dependencies = [ "serde", ] +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -7030,6 +9659,12 @@ name = "uuid" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] [[package]] name = "valuable" @@ -7037,6 +9672,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-bag" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" + [[package]] name = "variadics_please" version = "1.1.0" @@ -7054,6 +9695,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.5" @@ -7099,6 +9746,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -7239,6 +9892,50 @@ dependencies = [ "string_cache_codegen", ] +[[package]] +name = "webkit2gtk" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + [[package]] name = "webpki-roots" version = "1.0.2" @@ -7248,6 +9945,42 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webview2-com" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-implement 0.60.0", + "windows-interface 0.59.1", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" +dependencies = [ + "thiserror 2.0.16", + "windows 0.61.3", + "windows-core 0.61.2", +] + [[package]] name = "wgpu" version = "25.0.2" @@ -7256,7 +9989,7 @@ checksum = "ec8fb398f119472be4d80bc3647339f56eb63b2a331f6a3d16e25d8144197dd9" dependencies = [ "arrayvec", "bitflags 2.9.3", - "cfg_aliases", + "cfg_aliases 0.2.1", "document-features", "hashbrown 0.15.5", "js-sys", @@ -7286,10 +10019,10 @@ dependencies = [ "bit-set", "bit-vec", "bitflags 2.9.3", - "cfg_aliases", + "cfg_aliases 0.2.1", "document-features", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.11.0", "log", "naga", "once_cell", @@ -7348,8 +10081,8 @@ dependencies = [ "block", "bytemuck", "cfg-if", - "cfg_aliases", - "core-graphics-types", + "cfg_aliases 0.2.1", + "core-graphics-types 0.1.3", "glow", "glutin_wgl_sys", "gpu-alloc", @@ -7359,11 +10092,11 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading", + "libloading 0.8.8", "log", "metal", "naga", - "ndk-sys", + "ndk-sys 0.5.0+25.2.9519653", "objc", "ordered-float 4.6.0", "parking_lot", @@ -7443,6 +10176,21 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2 0.6.2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + [[package]] name = "windows" version = "0.57.0" @@ -7472,7 +10220,7 @@ dependencies = [ "windows-collections", "windows-core 0.61.2", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -7518,7 +10266,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", - "windows-link", + "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", ] @@ -7530,7 +10278,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", - "windows-link", + "windows-link 0.1.3", "windows-threading", ] @@ -7606,6 +10354,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -7613,7 +10367,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.2", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -7640,7 +10394,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -7659,7 +10413,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", ] [[package]] @@ -7698,6 +10461,30 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +dependencies = [ + "windows-link 0.2.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -7735,7 +10522,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -7752,9 +10539,24 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] +[[package]] +name = "windows-version" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "700dad7c058606087f6fdc1f88da5841e06da40334413c6cd4367b25ef26d24e" +dependencies = [ + "windows-link 0.2.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -7773,6 +10575,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -7791,6 +10599,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -7821,6 +10635,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -7839,6 +10659,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -7857,6 +10683,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -7875,6 +10707,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -7893,6 +10731,15 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.7.13" @@ -7902,6 +10749,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winreg" version = "0.50.0" @@ -7963,7 +10819,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a76ff259533532054cfbaefb115c613203c73707017459206380f03b3b3f266e" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.106", @@ -7975,6 +10831,81 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "wry" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f0e9642a0d061f6236c54ccae64c2722a7879ad4ec7dff59bd376d446d8e90" +dependencies = [ + "base64 0.22.1", + "block2 0.6.1", + "cookie", + "crossbeam-channel", + "dirs 6.0.0", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever 0.29.1", + "http 1.3.1", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2 0.6.2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.16", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[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 = "xattr" version = "1.5.1" @@ -8059,6 +10990,66 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix 0.30.1", + "ordered-stream", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "windows-sys 0.60.2", + "winnow 0.7.13", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.106", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.13", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.26" @@ -8149,7 +11140,7 @@ dependencies = [ "crc32fast", "crossbeam-utils", "displaydoc", - "indexmap", + "indexmap 2.11.0", "num_enum", "thiserror 1.0.69", ] @@ -8163,7 +11154,7 @@ dependencies = [ "arbitrary", "crc32fast", "flate2", - "indexmap", + "indexmap 2.11.0", "memchr", "time", "zopfli", @@ -8214,3 +11205,43 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zvariant" +version = "5.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.13", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.106", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.106", + "winnow 0.7.13", +] diff --git a/Cargo.toml b/Cargo.toml index fe7f5acd5..0fb229fbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,6 +102,7 @@ once_cell = "1.21.3" pbkdf2 = { version = "0.12", features = ["simple"] } phf = { version = "0.11.3", features = ["macros"] } pin-project = "1.1.10" +portable-pty = "0.9" prettyplease = "0.2.34" prost = "0.13" prost-build = "0.13" @@ -127,6 +128,11 @@ snafu = { version = "0.8.6", features = ["rust_1_61"] } strum = { version = "0.27.1", features = ["derive"] } syn = { version = "2.0.103", features = ["parsing", "printing"] } tar = "0.4.44" +tauri = { version = "2", features = ["devtools"] } +tauri-build = { version = "2", features = [] } +tauri-plugin-log = "2" +tauri-plugin-os = "2" +tauri-plugin-single-instance = "2" tempfile = "3.20.0" termcolor = "1.4.1" tokio = { version = "1.45", features = ["fs", "rt-multi-thread", "macros", "signal"] } diff --git a/cargo/licenses.json b/cargo/licenses.json index 53b832fda..4a22bde89 100644 --- a/cargo/licenses.json +++ b/cargo/licenses.json @@ -2503,6 +2503,14 @@ "name": "num_enum_derive", "repository": "https://github.com/illicitonion/num_enum" }, + { + "authors": "Jacob Pratt ", + "description": "A minimal library that determines the number of running threads for the current process.", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "name": "num_threads", + "repository": "https://github.com/jhpratt/num_threads" + }, { "authors": "Cldfire", "description": "A safe and ergonomic Rust wrapper for the NVIDIA Management Library", @@ -3463,6 +3471,14 @@ "name": "serde_repr", "repository": "https://github.com/dtolnay/serde-repr" }, + { + "authors": null, + "description": "Serde-compatible spanned Value", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "name": "serde_spanned", + "repository": "https://github.com/toml-rs/toml" + }, { "authors": "Jacob Brown ", "description": "De/serialize structs with named fields as array of values", @@ -4247,6 +4263,14 @@ "name": "valuable", "repository": "https://github.com/tokio-rs/valuable" }, + { + "authors": "Ashley Mannix ", + "description": "Anonymous structured values", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "name": "value-bag", + "repository": "https://github.com/sval-rs/value-bag" + }, { "authors": null, "description": "Implement things as if rust had variadics", @@ -4607,6 +4631,14 @@ "name": "windows-link", "repository": "https://github.com/microsoft/windows-rs" }, + { + "authors": null, + "description": "Linking for Windows", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "name": "windows-link", + "repository": "https://github.com/microsoft/windows-rs" + }, { "authors": null, "description": "Windows numeric types", @@ -4687,6 +4719,14 @@ "name": "windows-sys", "repository": "https://github.com/microsoft/windows-rs" }, + { + "authors": null, + "description": "Rust for Windows", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "name": "windows-sys", + "repository": "https://github.com/microsoft/windows-rs" + }, { "authors": "Microsoft", "description": "Import libs for Windows", From 3b3dce1105c3a74d042100ef78e0558747829606 Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 14 Oct 2025 09:13:22 +0800 Subject: [PATCH 02/74] update package.json --- package.json | 3 +++ yarn.lock | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/package.json b/package.json index f85945cd5..ad5020288 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,9 @@ "@fluent/bundle": "^0.18.0", "@mdi/svg": "^7.0.96", "@popperjs/core": "^2.11.8", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-log": "^2.7.0", + "@xterm/xterm": "^5.5.0", "bootstrap": "^5.3.0", "bootstrap-icons": "^1.10.5", "codemirror": "^5.63.1", diff --git a/yarn.lock b/yarn.lock index 761f20972..acef9d12d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1116,6 +1116,22 @@ __metadata: languageName: node linkType: hard +"@tauri-apps/api@npm:^2, @tauri-apps/api@npm:^2.8.0": + version: 2.8.0 + resolution: "@tauri-apps/api@npm:2.8.0" + checksum: 10c0/fb111e4d7572372997b440ebe6879543fa8c4765151878e3fddfbfe809b18da29eed142ce83061d14a9ca6d896b3266dc8a4927c642d71cdc0b4277dc7e3aabf + languageName: node + linkType: hard + +"@tauri-apps/plugin-log@npm:^2.7.0": + version: 2.7.0 + resolution: "@tauri-apps/plugin-log@npm:2.7.0" + dependencies: + "@tauri-apps/api": "npm:^2.8.0" + checksum: 10c0/c699710898a666ebc888ec8b2a3f36d5887e5cc06f8c69e9b404438407f6a0df19840e00732a3651aa83ba5f056cba403a5a14c556d3097368e143bfe995b1a8 + languageName: node + linkType: hard + "@tootallnate/once@npm:2": version: 2.0.0 resolution: "@tootallnate/once@npm:2.0.0" @@ -1787,6 +1803,13 @@ __metadata: languageName: node linkType: hard +"@xterm/xterm@npm:^5.5.0": + version: 5.5.0 + resolution: "@xterm/xterm@npm:5.5.0" + checksum: 10c0/358801feece58617d777b2783bec68dac1f52f736da3b0317f71a34f4e25431fb0b1920244f678b8d673f797145b4858c2a5ccb463a4a6df7c10c9093f1c9267 + languageName: node + linkType: hard + "abab@npm:^2.0.5, abab@npm:^2.0.6": version: 2.0.6 resolution: "abab@npm:2.0.6" @@ -1904,6 +1927,8 @@ __metadata: "@sveltejs/adapter-static": "npm:^3.0.0" "@sveltejs/kit": "npm:^2.22.2" "@sveltejs/vite-plugin-svelte": "npm:5.1" + "@tauri-apps/api": "npm:^2" + "@tauri-apps/plugin-log": "npm:^2.7.0" "@types/bootstrap": "npm:^5.0.12" "@types/codemirror": "npm:^5.60.0" "@types/d3": "npm:^7.0.0" @@ -1916,6 +1941,7 @@ __metadata: "@types/node": "npm:^22" "@typescript-eslint/eslint-plugin": "npm:^5.60.1" "@typescript-eslint/parser": "npm:^5.60.1" + "@xterm/xterm": "npm:^5.5.0" bootstrap: "npm:^5.3.0" bootstrap-icons: "npm:^1.10.5" caniuse-lite: "npm:^1.0.30001431" From 8c6607ff2a983580a9a138626605cea7887968cf Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 14 Oct 2025 09:14:27 +0800 Subject: [PATCH 03/74] codegen i18n ts for launcher --- .cargo/config.toml | 1 + build/configure/src/rust.rs | 1 + rslib/i18n/build.rs | 4 +++- rslib/i18n/typescript.rs | 4 ++-- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 49aaa3a6c..86289d0b9 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,7 @@ [env] STRINGS_PY = { value = "out/pylib/anki/_fluent.py", relative = true } STRINGS_TS = { value = "out/ts/lib/generated/ftl.ts", relative = true } +STRINGS_LAUNCHER_TS = { value = "out/ts/lib/generated/ftl-launcher.ts", relative = true } DESCRIPTORS_BIN = { value = "out/rslib/proto/descriptors.bin", relative = true } # build script will append .exe if necessary PROTOC = { value = "out/extracted/protoc/bin/protoc", relative = true } diff --git a/build/configure/src/rust.rs b/build/configure/src/rust.rs index 906ecd37e..b2bc6d577 100644 --- a/build/configure/src/rust.rs +++ b/build/configure/src/rust.rs @@ -57,6 +57,7 @@ fn prepare_translations(build: &mut Build) -> Result<()> { outputs: &[ RustOutput::Data("py", "pylib/anki/_fluent.py"), RustOutput::Data("ts", "ts/lib/generated/ftl.ts"), + RustOutput::Data("launcher_ts", "ts/lib/generated/ftl_launcher.ts"), ], target: None, extra_args: "-p anki_i18n", diff --git a/rslib/i18n/build.rs b/rslib/i18n/build.rs index f604c9167..bdfc3083c 100644 --- a/rslib/i18n/build.rs +++ b/rslib/i18n/build.rs @@ -28,7 +28,7 @@ fn main() -> Result<()> { let mut modules = get_modules(&map); write_strings(&map, &modules, "strings.rs", "All"); - typescript::write_ts_interface(&modules)?; + typescript::write_ts_interface(&modules, "STRINGS_TS")?; python::write_py_interface(&modules)?; // write strings.json file to requested path @@ -48,5 +48,7 @@ fn main() -> Result<()> { modules.retain(|module| module.name == "launcher"); write_strings(&map, &modules, "strings_launcher.rs", "Launcher"); + typescript::write_ts_interface(&modules, "STRINGS_LAUNCHER_TS")?; + Ok(()) } diff --git a/rslib/i18n/typescript.rs b/rslib/i18n/typescript.rs index ce30048e2..56fc870b6 100644 --- a/rslib/i18n/typescript.rs +++ b/rslib/i18n/typescript.rs @@ -15,14 +15,14 @@ use crate::extract::Module; use crate::extract::Variable; use crate::extract::VariableKind; -pub fn write_ts_interface(modules: &[Module]) -> Result<()> { +pub fn write_ts_interface(modules: &[Module], path_key: &str) -> Result<()> { let mut ts_out = header(); write_imports(&mut ts_out); render_module_map(modules, &mut ts_out); render_methods(modules, &mut ts_out); - if let Ok(path) = env::var("STRINGS_TS") { + if let Ok(path) = env::var(path_key) { let path = PathBuf::from(path); create_dir_all(path.parent().unwrap())?; write_file_if_changed(path, ts_out)?; From 8f6b1497e0cb123440884a0d7aebaa1a853b0423 Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 14 Oct 2025 09:16:38 +0800 Subject: [PATCH 04/74] codegen launcher rpc (partly) --- proto/anki/launcher.proto | 87 +++++++++++++++++++++++++++++++++++++++ rslib/proto/build.rs | 10 ++++- rslib/proto/rust.rs | 1 + rslib/proto/src/lib.rs | 1 + rslib/proto/typescript.rs | 19 +++++++-- rslib/rust_interface.rs | 10 ++++- 6 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 proto/anki/launcher.proto diff --git a/proto/anki/launcher.proto b/proto/anki/launcher.proto new file mode 100644 index 000000000..d16bc760c --- /dev/null +++ b/proto/anki/launcher.proto @@ -0,0 +1,87 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +syntax = "proto3"; + +package anki.launcher; + +import "anki/generic.proto"; + +service LauncherService { + rpc I18nResources(I18nResourcesRequest) returns (generic.Json); + rpc GetLangs(generic.Empty) returns (GetLangsResponse); + rpc SetLang(generic.String) returns (generic.Empty); + rpc GetVersions(generic.Empty) returns (GetVersionsResponse); + rpc ChooseVersion(ChooseVersionRequest) returns (ChooseVersionResponse); + rpc GetOptions(generic.Empty) returns (Options); + rpc GetMirrors(generic.Empty) returns (GetMirrorsResponse); + rpc WindowReady(generic.Empty) returns (generic.Empty); + rpc ZoomWebview(ZoomWebviewRequest) returns (generic.Empty); +} + +// TODO: this should not be necessary +service BackendLauncherService {} + +// TODO: codegen +enum Event { + TERM_INPUT = 0; + WINDOWS_READY = 1; +} + +message I18nResourcesRequest { + repeated string modules = 1; +} + +message I18nResourcesResponse { + repeated string langs = 1; + repeated string resources = 2; +} + +message GetLangsResponse { + message Pair { + string name = 1; + string locale = 2; + } + string user_locale = 1; + repeated Pair langs = 2; +} + +enum Mirror { + DISABLED = 0; + CHINA = 1; +} + +message GetMirrorsResponse { + message Pair { + Mirror mirror = 1; + string name = 2; + } + repeated Pair mirrors = 1; +} + +message Options { + bool allow_betas = 1; + bool download_caching = 2; + Mirror mirror = 3; +} + +message GetVersionsResponse { + repeated string all = 1; + repeated string latest = 2; + optional string current = 3; + optional string previous = 4; +} + +message ChooseVersionRequest { + string version = 1; + bool keep_existing = 2; + Options options = 3; +} + +message ChooseVersionResponse { + string version = 1; +} + +message ZoomWebviewRequest { + float scale_factor = 1; +} diff --git a/rslib/proto/build.rs b/rslib/proto/build.rs index feb84d410..4ede26409 100644 --- a/rslib/proto/build.rs +++ b/rslib/proto/build.rs @@ -14,8 +14,16 @@ fn main() -> Result<()> { let pool = rust::write_rust_protos(descriptors_path)?; let (_, services) = get_services(&pool); + + let (services, launcher_services): (Vec<_>, Vec<_>) = services + .into_iter() + .partition(|s| !s.name.trim_start_matches("Backend").starts_with("Launcher")); + python::write_python_interface(&services)?; - typescript::write_ts_interface(&services)?; + typescript::write_ts_interface(&services, false)?; + + // for launcher-gui + typescript::write_ts_interface(&launcher_services, true)?; Ok(()) } diff --git a/rslib/proto/rust.rs b/rslib/proto/rust.rs index 85834f19f..61e12ba61 100644 --- a/rslib/proto/rust.rs +++ b/rslib/proto/rust.rs @@ -49,6 +49,7 @@ pub fn write_rust_protos(descriptors_path: PathBuf) -> Result { "ImportAnkiPackageUpdateCondition", "#[derive(serde::Deserialize, serde::Serialize)]", ) + .type_attribute(".anki.launcher.Mirror", "#[derive(strum::EnumIter)]") .compile_protos(paths.as_slice(), &[proto_dir]) .context("prost build")?; diff --git a/rslib/proto/src/lib.rs b/rslib/proto/src/lib.rs index 86d7d1580..7396192e3 100644 --- a/rslib/proto/src/lib.rs +++ b/rslib/proto/src/lib.rs @@ -37,3 +37,4 @@ protobuf!(stats, "stats"); protobuf!(sync, "sync"); protobuf!(tags, "tags"); protobuf!(ankihub, "ankihub"); +protobuf!(launcher, "launcher"); diff --git a/rslib/proto/typescript.rs b/rslib/proto/typescript.rs index 4e941a0ca..66cc7e590 100644 --- a/rslib/proto/typescript.rs +++ b/rslib/proto/typescript.rs @@ -13,7 +13,7 @@ use anyhow::Result; use inflections::Inflect; use itertools::Itertools; -pub(crate) fn write_ts_interface(services: &[BackendService]) -> Result<()> { +pub(crate) fn write_ts_interface(services: &[BackendService], is_launcher: bool) -> Result<()> { let root = Path::new("../../out/ts/lib/generated"); create_dir_all(root)?; @@ -29,13 +29,17 @@ pub(crate) fn write_ts_interface(services: &[BackendService]) -> Result<()> { let method = MethodDetails::from_method(method); record_referenced_type(&mut referenced_packages, &method.input_type); record_referenced_type(&mut referenced_packages, &method.output_type); - write_ts_method(&method, &mut ts_out); + write_ts_method(&method, &mut ts_out, is_launcher); } } let imports = imports(referenced_packages); write_file_if_changed( - root.join("backend.ts"), + root.join(if is_launcher { + "backend-launcher.ts" + } else { + "backend.ts" + }), format!("{}{}{}", ts_header(), imports, ts_out), )?; @@ -75,12 +79,19 @@ fn write_ts_method( comments, }: &MethodDetails, out: &mut String, + is_launcher: bool, ) { let comments = format_comments(comments); + let proto_method_name = method_name; + let options = if is_launcher { + "{ ...options, customProtocol: true }" + } else { + "options" + }; writeln!( out, r#"{comments}export async function {method_name}(input: PlainMessage<{input_type}>, options?: PostProtoOptions): Promise<{output_type}> {{ - return await postProto("{method_name}", new {input_type}(input), {output_type}, options); + return await postProto("{proto_method_name}", new {input_type}(input), {output_type}, {options}); }}"# ).unwrap() } diff --git a/rslib/rust_interface.rs b/rslib/rust_interface.rs index 6861df7dc..fb3a0ec96 100644 --- a/rslib/rust_interface.rs +++ b/rslib/rust_interface.rs @@ -20,14 +20,20 @@ pub fn write_rust_interface(pool: &DescriptorPool) -> Result<()> { let mut buf = String::new(); buf.push_str("use crate::error::Result; use prost::Message;"); + // TODO: we're misusing this for the launcher let (col_services, backend_services) = get_services(pool); let col_services = col_services .into_iter() - .filter(|s| s.name != "FrontendService") + .filter(|s| !matches!(&*s.name, "FrontendService" | "LauncherService")) .collect_vec(); let backend_services = backend_services .into_iter() - .filter(|s| s.name != "BackendFrontendService") + .filter(|s| { + !matches!( + &*s.name, + "BackendFrontendService" | "BackendLauncherService" + ) + }) .collect_vec(); render_collection_services(&col_services, &mut buf)?; From 581618610eb35895c8df3cc14e998d84f7543e46 Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 14 Oct 2025 09:20:23 +0800 Subject: [PATCH 05/74] modify helper files to support launcher --- ts/lib/generated/post.ts | 8 ++++++-- ts/lib/tslib/i18n/utils.ts | 6 ++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/ts/lib/generated/post.ts b/ts/lib/generated/post.ts index 90e372520..d3f44cd09 100644 --- a/ts/lib/generated/post.ts +++ b/ts/lib/generated/post.ts @@ -4,21 +4,25 @@ export interface PostProtoOptions { /** True by default. Shows a dialog with the error message, then rethrows. */ alertOnError?: boolean; + customProtocol?: boolean; } +const CUSTOM_PROTOCOL_URI = (navigator.platform.indexOf("Win") == -1) ? "anki://localhost" : "http://anki.localhost"; + export async function postProto( method: string, input: { toBinary(): Uint8Array; getType(): { typeName: string } }, outputType: { fromBinary(arr: Uint8Array): T }, options: PostProtoOptions = {}, ): Promise { + const { alertOnError = true, customProtocol = false } = options; try { const inputBytes = input.toBinary(); - const path = `/_anki/${method}`; + const backendUrl = customProtocol ? CUSTOM_PROTOCOL_URI : "/_anki"; + const path = `${backendUrl}/${method}`; const outputBytes = await postProtoInner(path, inputBytes); return outputType.fromBinary(outputBytes); } catch (err) { - const { alertOnError = true } = options; if (alertOnError && !(err instanceof Error && err.message === "500: Interrupted")) { alert(err); } diff --git a/ts/lib/tslib/i18n/utils.ts b/ts/lib/tslib/i18n/utils.ts index d13e6c2c3..df1650949 100644 --- a/ts/lib/tslib/i18n/utils.ts +++ b/ts/lib/tslib/i18n/utils.ts @@ -4,6 +4,7 @@ import "intl-pluralrules"; import { i18nResources } from "@generated/backend"; +import { i18nResources as launcherI18nResources } from "@generated/backend-launcher"; import type { ModuleName } from "@generated/ftl"; import { FluentBundle, FluentResource } from "@generated/ftl"; import { firstLanguage, setBundles } from "@generated/ftl"; @@ -77,8 +78,9 @@ export function withoutUnicodeIsolation(s: string): string { return s.replace(/[\u2068-\u2069]+/g, ""); } -export async function setupI18n(args: { modules: ModuleName[] }): Promise { - const resources = await i18nResources(args); +export async function setupI18n(args: { modules: ModuleName[] }, launcher = false): Promise { + const fn = launcher ? launcherI18nResources : i18nResources; + const resources = await fn(args); const json = JSON.parse(new TextDecoder().decode(resources.json)); const newBundles: FluentBundle[] = []; From ef149840ce7267c8532349328ef2e2e9af8c8b7e Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 14 Oct 2025 09:20:56 +0800 Subject: [PATCH 06/74] add launcher strings --- ftl/core/launcher.ftl | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ftl/core/launcher.ftl b/ftl/core/launcher.ftl index ee3aa6320..19a59e01b 100644 --- a/ftl/core/launcher.ftl +++ b/ftl/core/launcher.ftl @@ -36,3 +36,13 @@ launcher-beta-releases-enabled = Beta releases enabled. launcher-beta-releases-disabled = Beta releases disabled. launcher-download-caching-enabled = Download caching enabled. launcher-download-caching-disabled = Download caching disabled and cache cleared. + +# TODO: dedup +launcher-language = Language +launcher-latest-anki-version = Install Latest Anki ({ $version }) +launcher-allow-betas-toggle = Allow betas +launcher-use-mirror = Download mirror +launcher-download-caching = Download caching +launcher-advanced = Advanced +launcher-output = Output +launcher-syncing = Syncing... From d4565742d8c34f8970c6aa4d0249fd1028ccee01 Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 14 Oct 2025 09:22:33 +0800 Subject: [PATCH 07/74] qt/launcher-gui --- qt/launcher-gui/.gitignore | 10 + qt/launcher-gui/src-tauri/.gitignore | 3 + qt/launcher-gui/src-tauri/.taurignore | 0 qt/launcher-gui/src-tauri/Cargo.toml | 50 + qt/launcher-gui/src-tauri/build.rs | 19 + .../src-tauri/capabilities/default.json | 11 + qt/launcher-gui/src-tauri/icons/icon.ico | Bin 0 -> 103420 bytes qt/launcher-gui/src-tauri/rust_interface.rs | 149 +++ qt/launcher-gui/src-tauri/src/app.rs | 85 ++ qt/launcher-gui/src-tauri/src/commands.rs | 211 ++++ qt/launcher-gui/src-tauri/src/error.rs | 11 + qt/launcher-gui/src-tauri/src/generated.rs | 4 + qt/launcher-gui/src-tauri/src/lang.rs | 143 +++ qt/launcher-gui/src-tauri/src/main.rs | 38 + qt/launcher-gui/src-tauri/src/platform/mac.rs | 99 ++ qt/launcher-gui/src-tauri/src/platform/mod.rs | 71 ++ .../src-tauri/src/platform/unix.rs | 53 + .../src-tauri/src/platform/windows.rs | 180 ++++ qt/launcher-gui/src-tauri/src/uv.rs | 930 ++++++++++++++++++ qt/launcher-gui/src-tauri/tauri.conf.json | 38 + qt/launcher-gui/src/app.html | 13 + qt/launcher-gui/src/routes/+layout.svelte | 26 + qt/launcher-gui/src/routes/+layout.ts | 18 + qt/launcher-gui/src/routes/+page.svelte | 47 + qt/launcher-gui/src/routes/+page.ts | 27 + qt/launcher-gui/src/routes/Start.svelte | 315 ++++++ qt/launcher-gui/src/routes/Warning.svelte | 21 + qt/launcher-gui/src/routes/base.scss | 19 + qt/launcher-gui/src/routes/stores.ts | 12 + qt/launcher-gui/src/routes/svg.d.ts | 13 + qt/launcher-gui/static/anki.png | Bin 0 -> 34595 bytes qt/launcher-gui/svelte.config.js | 32 + qt/launcher-gui/tsconfig.json | 19 + qt/launcher-gui/vite.config.js | 39 + 34 files changed, 2706 insertions(+) create mode 100644 qt/launcher-gui/.gitignore create mode 100644 qt/launcher-gui/src-tauri/.gitignore create mode 100644 qt/launcher-gui/src-tauri/.taurignore create mode 100644 qt/launcher-gui/src-tauri/Cargo.toml create mode 100644 qt/launcher-gui/src-tauri/build.rs create mode 100644 qt/launcher-gui/src-tauri/capabilities/default.json create mode 100644 qt/launcher-gui/src-tauri/icons/icon.ico create mode 100644 qt/launcher-gui/src-tauri/rust_interface.rs create mode 100644 qt/launcher-gui/src-tauri/src/app.rs create mode 100644 qt/launcher-gui/src-tauri/src/commands.rs create mode 100644 qt/launcher-gui/src-tauri/src/error.rs create mode 100644 qt/launcher-gui/src-tauri/src/generated.rs create mode 100644 qt/launcher-gui/src-tauri/src/lang.rs create mode 100644 qt/launcher-gui/src-tauri/src/main.rs create mode 100644 qt/launcher-gui/src-tauri/src/platform/mac.rs create mode 100644 qt/launcher-gui/src-tauri/src/platform/mod.rs create mode 100644 qt/launcher-gui/src-tauri/src/platform/unix.rs create mode 100644 qt/launcher-gui/src-tauri/src/platform/windows.rs create mode 100644 qt/launcher-gui/src-tauri/src/uv.rs create mode 100644 qt/launcher-gui/src-tauri/tauri.conf.json create mode 100644 qt/launcher-gui/src/app.html create mode 100644 qt/launcher-gui/src/routes/+layout.svelte create mode 100644 qt/launcher-gui/src/routes/+layout.ts create mode 100644 qt/launcher-gui/src/routes/+page.svelte create mode 100644 qt/launcher-gui/src/routes/+page.ts create mode 100644 qt/launcher-gui/src/routes/Start.svelte create mode 100644 qt/launcher-gui/src/routes/Warning.svelte create mode 100644 qt/launcher-gui/src/routes/base.scss create mode 100644 qt/launcher-gui/src/routes/stores.ts create mode 100644 qt/launcher-gui/src/routes/svg.d.ts create mode 100644 qt/launcher-gui/static/anki.png create mode 100644 qt/launcher-gui/svelte.config.js create mode 100644 qt/launcher-gui/tsconfig.json create mode 100644 qt/launcher-gui/vite.config.js diff --git a/qt/launcher-gui/.gitignore b/qt/launcher-gui/.gitignore new file mode 100644 index 000000000..6635cf554 --- /dev/null +++ b/qt/launcher-gui/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/qt/launcher-gui/src-tauri/.gitignore b/qt/launcher-gui/src-tauri/.gitignore new file mode 100644 index 000000000..44828a436 --- /dev/null +++ b/qt/launcher-gui/src-tauri/.gitignore @@ -0,0 +1,3 @@ +# Generated by Tauri +# will have schema files for capabilities auto-completion +/gen/schemas diff --git a/qt/launcher-gui/src-tauri/.taurignore b/qt/launcher-gui/src-tauri/.taurignore new file mode 100644 index 000000000..e69de29bb diff --git a/qt/launcher-gui/src-tauri/Cargo.toml b/qt/launcher-gui/src-tauri/Cargo.toml new file mode 100644 index 000000000..82493085d --- /dev/null +++ b/qt/launcher-gui/src-tauri/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "launcher-gui" +version = "1.0.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +publish = false +rust-version.workspace = true + +[build-dependencies] +anki_io.workspace = true +anki_proto_gen.workspace = true +anyhow.workspace = true +inflections.workspace = true +prettyplease.workspace = true +prost-build.workspace = true +prost-reflect.workspace = true +tauri-build.workspace = true +syn.workspace = true + +[dependencies] +anki_i18n.workspace = true +anki_io.workspace = true +anki_process.workspace = true +anki_proto.workspace = true +anyhow.workspace = true +data-encoding.workspace = true +dirs.workspace = true +futures.workspace = true +phf.workspace = true +portable-pty.workspace = true +prost.workspace = true +serde.workspace = true +serde_json.workspace = true +snafu.workspace = true +strum.workspace = true +tauri.workspace = true +tauri-plugin-log.workspace = true +tauri-plugin-os.workspace = true +tauri-plugin-single-instance.workspace = true +tokio.workspace = true + +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +libc.workspace = true + +[target.'cfg(windows)'.dependencies] +windows.workspace = true +widestring.workspace = true +libc.workspace = true +libc-stdhandle.workspace = true diff --git a/qt/launcher-gui/src-tauri/build.rs b/qt/launcher-gui/src-tauri/build.rs new file mode 100644 index 000000000..84709a6be --- /dev/null +++ b/qt/launcher-gui/src-tauri/build.rs @@ -0,0 +1,19 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +mod rust_interface; + +use anki_proto_gen::descriptors_path; +use anyhow::Result; +use prost_reflect::DescriptorPool; + +fn main() -> Result<()> { + let descriptors_path = descriptors_path(); + println!("cargo:rerun-if-changed={}", descriptors_path.display()); + let pool = DescriptorPool::decode(std::fs::read(descriptors_path)?.as_ref())?; + rust_interface::write_rust_interface(&pool)?; + + tauri_build::build(); + + Ok(()) +} diff --git a/qt/launcher-gui/src-tauri/capabilities/default.json b/qt/launcher-gui/src-tauri/capabilities/default.json new file mode 100644 index 000000000..43af1fe4d --- /dev/null +++ b/qt/launcher-gui/src-tauri/capabilities/default.json @@ -0,0 +1,11 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": [ + "core:default", + "os:default", + "log:default" + ] +} diff --git a/qt/launcher-gui/src-tauri/icons/icon.ico b/qt/launcher-gui/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fd03c333ea8f5f8b0d057e3541dad7e4d7ef66dc GIT binary patch literal 103420 zcmeFa2Ut_xwl2I90)!qqQZyn}ssc(UbP%OTN2-k?Ql+=hn=}Ctq$9mbkq!Y12uKkW zK@d=o4w2q-SKOc9v+IBNIp?1Le*4_L9tSHctSsgnW6V+BF=hk+2tW+r4+ww+?xFy| z9ej;O|LK|s4gn^>_k@N2bX~*=05Em%PULUb2mmN^fB;NPf4Yt!0RX)K2tY^or|SkH z0GNn?00{6t{PS&K0FV?10V1_ERVhfBNx?rwp{}NM9sgTA!NL0hQ5D4>e1Hv?wDh4gPM0Cz%-leHFE%-n z0DgJ8#`%6A6lO6$JNwY2-T(MB31EfFkaUZx&@JrO{qf^R9-To&lPO0IVRjFOgtzOW zi>s@J0cQFRA$)@q@JyLpPkQoXub6$gKAyi8COfn%z|TMNC^^}!6$*U{-tf?SckLLZ zpwOuyn^l=+$}t1EI0JXM(tBMgNOiQ#crz0QICdZCh}n%iDegWDRwl1zzZCBx6crPb z|Ktf%owABb$u$ULUFgk(hE{aTmb3^EW}v4RGHG((%ggIo)%r7jlb*i5kOGTtM}sv9 z-Bgy#0>;(PP*BtYLqJTL*gbznMn;dojJM&hD<;C}kUEUMcg#50uilwCsQ`i#xwWpF zm@wb5w*K5??eBj=GiQE$`TlqkF9dV^+M>{)+yqz1|7pj7i`f0(*)xthLP76BuCiIm zbOL1Q01;EVxN}e}n^b`dg~3{}6J0ycZlnF`cwH9<(4aKgOuW=v*7mNA^?FJrlB_4)vK_mG#$M)*OR zgCezQc!4N^l9?F?3i0tp@2pLho=3cszRb;S-PB1)o(N1Mwik!z<(1b0hWlpK)YP;Y zNB-)kc zGS>mcUDnF+Z=6Wad6~G=GFOPHPK$1?lx=NoZH`u$HS0jp$RHob&F}B(dCGjrGUd_b zZB{SCN^FJpOk3}HackZ|Fg`0DnsDtPM6*n1Xb=(-w)34~Ybm0FYXA#V?KrZ?$jG=& z@{0@s-`>RNKlR6YNE6uH?NnRKURqF3GRxRm8OysTNZ#h5so9^%XHe#84n<#U+Ip|? zwD8%pqk?X%vvlyn)P4+6WD1$Elre{gcH=h3G%TBSw$U6CMIJg{VO(>!5(cn{I!$Bt zK^FRX5z$^SGD)tk**qr}UOwJ&Qv`NyIVS^_XzJMwp%CntxFA2OcYYNO-HeYr@=h)sb3_nL9@ zQTzd7_fWZcdBt9v-%E-I`f|l??w7kz=9r=%Njc4@Vp%7i-y%-Q0)oQm1TBkTz-D#8 zFcAf_{pP;!P+6elT$1hxTsBXBc5?3B0@QQOIJF_TghLQ3PPRJHm^go68cHVqj!%N< zB@s~3v{aJv=+UD$RYb70R_!BWot0EVg2JTmWF6BY(gKzCJjAjWsgZQE@BT(12k<2O ztYVmbTx6uJgD4Yr(QznOnQ+Sg^k$|pt)=j13;K@i^O0mN&j~)ixUZ`W&AXSp`9jd7 zwkQS$Xf6#GYSnvhFO^$T;FX`3lk@ds+kvuM+Sr;hQ=!J-o?CX(zI#>UMx%Fb-+o}; z=rSis0)xw8Fxu7>Ffg>N?_egVvGYVsy8ql+XY*Oye zUflm&!ezN7BfWXa@k?3H*BmH81mMqT)q1a{y85(5_3gJBGyu5`cDh4^hL$#*$?ei7 zoZW|#v7I>&QemY=2{Lx$FT%oN6pjH|0jMQ#cs0lOk5ZRa& zMv$HnG;i6vL;?_rK3Ev6BaNOiUiYy%{#14;~?0$T5FZfqFvn>Jt-@XxUSRAmM_wRc;RCNq>p>WN%0CS6x~j>=q^ z%5Ir|j?C~CTwf2WAdZ%kmCZc7td%CJp8R%ljW~k6n6-g0g8r9dM}r& zCYiOxe%bY(KaO?yoFjiT^RasPb(R1dmq*-g|vzzL$+v+sTm@(%ntnZ%b@{9GeNunKTyZk<_66>)j_@ckGCO5ine0FTVmIPR$Jid5wZxksrKMzq^Y@JNn^x}#= zC1Ig=qEHBdRD0IkeW#~vucw#9j0k>U-2M1gU8g2;dtpdXe`hQQRK|?hGjeO^g*(Y3 z;PS!Fd`~Vrn%BYrf~Mt)=BOB=Jh$bKc1P`#8RPd~Ojek8kQv8NyG5Ec>g1`UjqZuf z1RVVcAIH|G9^R^NKicRm2K~j0`Jq>lwEEgEi5d$20F3yQ8SdTXsXcI+0%*&iW5dd4 zDQpr`6}FnSMOGg+2YFGb7x_;e+l(iFc#UDF0&p3Z943m+&l;s87;tC^!NN+-0K>(& z0?iD&1;%L@a+LXm=~I*=L8zg zsm7MYJ0_$cS#Sr-m3Esclcc_vP;+VbDuzVY?S_7=DD-p-6%>eVcP!Kl$mU&pdP#yb z%47<%^{^kbl~ABoa^s;$KVl7PKdN9kZz41&G}2t4Uupm!td@DvfCIgS&C4)~B~21) z$QNJX%^Yu9s?}>k+0Xf|FD`xj@xXXfd~)-9e@3ZnSM2qH9*+(Han-_aI~fL?YdTzO zrjP)G%XJRM{M$99*It|p5`X&+D+ueJo6q#A7<7KK9yCUt{4^-IX=`EN+y2)361|>o zzT87e?)RbBN)|K8tdS9C-06MX#ewp5PLR8R3?-(AgK;Jij49k*hD_ewv%GVboK;+# zzEwe;g=%hS`QCNj`s~?VIZtyMxBJhiSVgS+^^PHBQF6Hw0M3mDU=qFg3hqFOp**~& z`eUA+`V}dY(GcU->SxBeF@;Oq{^Kbc>tz~wLAe3`o8KlMEtGj50K)gjMM2)S+t+O0 z?LfbV`0mf8&;XtgbP7RL^EYGtgCA+klQi;uZB*;)i(gu$%pMFWhYj@g2o)?1eJvc# zH5(J%AAQiN6}&cz3n^?G%{8dozP1F$I0ci<9%Lz-#^2t31vmg5;b04p!ZK@U=tfxJv9w~PgYbvr8d#PNay083N)X$YjU@YUBKkE-6UwT+laOGCn zwYSa(7od)9`vprmJl;b!K;UbL8elmnvpaDsnxHDLp@x*h-Q%9QFe0@(X}$%sUfHyC z{u-8o4sv>LhG~u`$a~uC#>c1vGJ52~`y%F)h$^|fc&_{6>do&-djt+HB6#MYUsm+H zu>L}=TaVI8dKU@L9LY!{V=9i1HU~HXEEH{^dQPFQM^IpVc;M}%iJrl&1ZQe2u6-tq zg}lolt@<9W>Rjo>Z9apO@Syt_5@hWRFRSQJ)K#)vb1O-iYK^;1|K8(swe5#C*7HHu zmc=$X3_iLBtpPs{*AJ(@xY1ou#MQ%y2^u>f)6FPgut^gN1h2L3nk!NQfTeYP-M6{< zuyn`oUhZ`M2L9jJJSME3MD7<=N2m(Pn!i9_nCzkRXxm5>QicF%RL;g`4PC{IrAlDa zv<5>{<;uiGN+gOTV&Jkj=kn62=`d;c6(ijqsd{mAqwD+?V44sxr^LjjJ9e!`t>4L(wp?uZD_hbu=bHzBD zm0*U7fTz1U*xfc4Qw3~Y;3#^USi3Z>jmGI{??(weW_84ppVG2FUE-(ezlInkL@Ntj z|ur2NL^m;QeDt}(Cn@)T7$5xj<2pk3mE z+WCTGqr3@xF}cD;*Q9sZmA7W%#l{ zG#_i8AD)v#%ZVg1lbQnf%}>_>X@2Z6kTmhl2Hu@SJgW|T-%s_Zl%K(}F9 z@4zNt(Z;GQX}{>V6#*KRoiS?~zzDDuMYOP@Z#0)1()nvTQ+jv#Y%k>-EDW6sSz&*0 z*=S~WKw&4@;?o>MYo#8swyDF=VGvy?_F~K0mglfTmlMLI=>ac(o%EHo{WMnil^IMW z%acUlR5~7)Lgz0%`k7Fe1KY4z@RYo3W3|pUDNR@JOJZd@<@6b3k>y!zvRr$55yH~I zK8;D)u(qs1k~uXW^fE|AtY61Jc9Q z>4NWd2k+f^nck!MY?sB71oeaRT-pnX49YtU`lee;?a{ak+d9e~GZ!r&lggDjYI>9- zB@FWOVo~{TNWSMGbS4?;Lyt%mxc{UH3pwEp4*+*}MZV~9hqD7t{p@r{Y&|tpY?6BA zpGs9DD18H7z9mf*R8TyF==++;fYe4W#`heO*znRUk49V<4`R}n7VafMW<2@um5-ki z)mRS$7=R};z)IDj0r40GS)iHVn-$7|Ls<}3C}eSLNuZ&jAz$BL&XG2GA=UhY3mZbK ztaaM;3F~)DAUri=7T{MRZ@II;MF9aSDbd!u zbQB2oYcURy!k{rn(h;&rdsgMVr+RR)KBDb}FL8scIGtB>U_V^+0WinVq;-y2c)DqN zS-zWEn_KYF(#;OOEQ5>md5_Q11>RNWmIIi!pW}Iau#i{a1{k<2*Vs4EQcC8|`qt!z zq#nNM>Kcwet2m-{PJw-3|8un-qG$oP&ev67D7?5|do^&pOj^h31@hUsxNI(x1iea? zq_p^vH4fa)XU7kcVVX_=n8~F87%0HUie@8x%7dgPBrZxRc}<%eXK@w zy<=GGTuc3Jc7_y@McaIeXzaeIhZk;2{R4Z^oz+K2LAhL)d?B*n7)i|obAReFvy6AG z03|Bv=724dnnX$SwbX;PiPR?d)gIQGYvG$Vy$LNY)r|}{&XgtPSw7C{DFn{FmlcwH zuUa@7SwJL~8`3YMcY$a*;AqeJd!vB_+IF}=9po^YhY~{)nlUT^D>oOCWCX4R<}txd z{bOv_-$G4VD_{9tVz&&_*WR+N+PD+2+qswF?1k$Uu!%wcr(XI3FfBaH@4r)+akgL_j zn4P{T3`bx#E*JCOohL53*->HiTp@;h_Rek$Xl}1&Na{n;)G?<--UxFt2@xY7ks)l) zA}Ba=m7Z8Sw%>4UtxJC)`-C(qD*1JmV(Pv!J)IZ)T}*Ur_L7cll)lLWC6aq_5oQhW zGXsLT6P@f7DD2|ie#!*vu9WK7@v0u%%Usnxp|3kSuT>x1L@*E|As857<2Gw-oiP_t zFctZgU6Tuk;GDk~NO&c1cVXydmC*BuU7i)WJ_j4;D=(oj(L2swp96jNmOW+jJM5tIfx5wqu7avjkdy7w(k?lEAg6eqL8beVWMEa=*Z z+ugudwn$+~D&v3~b#`-mM&E`4GP(;DPlev8(G~I0UX&XjjnQe{H%iLLxE6pru5Vg+ zKCP85ZgtP<>3*#KBU31v1Q=Xev82Y3KjktRUVmIXBrpZTJ^#|`BQh6|P%9#qMahG- zoTX%CD@F&j8oeZW$aZ??Fhk12Zl`kc6Gi4a6oOjz0i9;k!BS~-seY*nn4a1xc1B`m z6A_T3>U8}}*J=x<1a$Lmxi8**?M-%q+A-Yr!Ng_6UI7p}#niJM$#-eb3UGLrRAGd7kd`2&{RWLbl(* zmb2Snj^5F+&FMKt+h~9X`pDF5iX+m#U2W^_=Pu1aNHjq_-XK8VTI3kqLjq8M03g6X zCxXw2YY%n`jE(cXl`9K7>6|}v8jU4%O3X$~lt&OeOCW6Ux?WpiIg+=(Ilp{&cJ9=< z<_UTsV(d)#nJWMS0^B90nVu#l$=GPBMll;1~i1jfDXXYV+O)>WQl$o8$iI}`8raY zi^}Z^><8_uvdsaxEYOSEF3=);sX~Se3JZtdCUSq!N?Lxi-Js>-a#5G=Q?dz48N%Cn zi>|-jIM<*0QA>-o_wJ;JKwxgx%14*!IiTQbDmw&22_V^$hc~1qEFeTS>gwut!@d5? zx5cX$Hp|ReXle8ALawN*>%1-n!SU))VpV2Q{s;WNqOBcMD{XIcYPNoU z{%_Eu3&-BI0XToL;5asesWPK{4@wPi4WN)f18@7Gd=7}W4V=qorisbpb#OuEz47uCzTRYjR{;aQ~gr>5gO38#CSoD=A1^X1$a z1x?j%ys&px7QGomh8H{%ET^nEI8;q)^FYaw^B;?KRp*$hhQ}f~woJ?kYA6j&M)R^P z`zc^o1r#O#)39aUm3HcvSkQ|M4&|wu5i6J9;%CD=Q%~doK0$zyJlD?wUzS`gy|;oP z%ybX^KaQ66tK>@1v@Yl9HhhiZdy)&FFg)dqbPs8{u1>F=Yd*$EhTkFlIgXX{M-xO) zoHNfb0M{<~P=3Quzes{JE9`tFgbV?#Y~TADU3~xZix*sEz9%3Sq0#$|lbOz#6+?W+ z7vwF7ZWkZXyz@eZ1Hvc*;ROjk2`f8tw;(!SVpIqZrsuMa63n9i*#3jZ)C@;-^B{^dG;^DuRZQklVcYfWoV`M=u;Ii#|rE9V`laaVXHnh@iLb4A8 zc!;|F^;IMdpah^lH`jfRCT?a&19wFLMawG=m8s0Df~O;(V9n%`j7}960yw>#GOlT)4w3L-liC}>oAhNN{P%Qo=LdGH8M7>9Zndu=fw`k7<1Iuqs>5f~m{FKvR ziU6W@#DGKN9D^aFOVKv0BS7&UWh_wKG&F5Ce>X;fI$CL2dQXR(NjRH)!U*R0>XgV` zZ55R;+jzI?I3s+#^Atdvy5(r^c~QWRLd=G#hz40AcnBCuT8M-x*iS`ToU&6YH7L)G zVYxica7~F%&RAWZ`|1^0*EEE$=HqF#VL545&%gv=nERXUPSCq{G5nTT~lUz#KnyS zv!=L4_3+T6bdPl1r&=8S<6D;oP(=XLPr5&ypY^@W5cgp@M+D6Xa#dM2c=HVQ%jo@k zkyBd06|D%EgoMNc?6kbwX#M#hae>Zh@!vGKr3CBht@Fu`=xSsY7wRgyA5*lX#iLvF zS${mVdJl}zwkrJATX*p)iUvWo$(rVJ#w}ROD3ly;?Ybvb^4lk`<_KQ2w&01X{F&wi z0hs0H@=^M*KwOv+fV3H}Ew+ojwVLMeggIl4OzNBCgNLsnEg-}LY627`zVc!QfSt5k zsoQ)kOC=@pK7fkYSDWojl(!J&UhS>YX7Uq)V7Pz@=Pov@*6vP1_z^o41F$PtniEiG z+gwr)|LjFkwsi3b$lg7cT5mZALBW8KLkB*WAL5gY!o3jq;Isw8ZqAyA<{1`B^wVI| z0&6Xs{Z$^HW?E0bIJ$IyyhvR%!QOTKORL>cv)p3AXA~d^7{KT#As$Fc*Xrb5XQ@ia zaw?d_PBb`!<^zhlGjX3F?pEO0^+1CVR#FQPF;^1^3AAa@jzkVpp|pSslu(@^&-EfH z{g}vI$)8RfoB{~No*jNHbxgieq?RaVGl*1Vk%1RCbj6*ieEiC#Nyum(t!w+GrE$sr z*=u^eyRLTFsj?*{-0^wf$MpvrXC|4tAvR{*c)J<0NlANj*>_4cmescSB?G?^x_UTsk<18zp~!wE zenhIo`$_5iPCAt493?QX4pflmPGPaXgI&2$hO8XB|Cx?eh61;+G&gcn<2Fa!zmbhf-+ zRr3>N({)(<5ty@wjsE;oAmCfiJ?Bw6fa>Vu&h%7J>*_d<{d6m?SXrX{M6)muxT4VR znXT_X5&5vypt3%<6i%s6Sy2DQTLX%Q1K3AL4)dqtq&MIh&Tme2N;+;YeQ>(uTrRXC zP4E>gWLx!gd?cXGf1lCxWr6iEJ`A)3uPAarrXc5nLRAJ2B08WTt)9S;zNf%Hzi> zx86L|(QpqR{u<~2pa79vImFxI7MOeGs8s)Z5Z^+8C|dl~s_gt+iiLqIu4cgk=aGIF zm#L2PxJ6)RDiT#nn;_!&{+YT}WAx1*imGUa1uaWr@`;OL;2OXD#ZwWAW&_aWEf=3H z1+lDxx#-K4hYwrW?e=y~Uon&&C}C7Ab+CQ)nq*p{A;7ZLJ7J@l1qC>&IS~k$)DDU! zm zfRNrXMJsS6LS7p6)~zP`J&A7K>C2s80rP-kHxjV%U^>%ht(Z;_BRmQgs-wnE%?H$G z8Lap6df4w^yhR%XvW5p|-zMriBTXlAjOWj)^RROmb19XoE0zHY(^Fv13Pl3|mbuh! zN4$D6mgRD+F>niv@9iWVv_dIulMu=Q6O&KuUMUGn(MF-3FjGs7#56i1Nz9)EjiEYdT!2A{}Wf=qw$S*K_`a`5Oh^;xZZFm@;9f>iE$A@O4K zs&U$svg)4Auvtv;H5buM*`hq+gl}LiBXd5sEPZ}g5Qe}MV)_IfI0mFTAT40Hig$6S zhVC+xt}OopTQRaq>Ny4m-PKVK9fLN157Up*u?*z4m=DErxTBi$?w;jBN(7KHJ^=dc zf=DUdZC)ZT42Tu0;a4IUY*`rfl%0GNwr<`2c;o7Nd%lm$!dKhF)Qs)d)83-lVp$5< z#`#R$UPGUJp>!DZ2^ARU+{p2+9t*wyab$rQX!U8^=;8a*h9C-KTlI-(-q6M1r`*%*}WoI22gt!F4CW^W7dMPFH3-#8D{xUj+g-2 zfXjtlif;0)jr+LLJGWGI*@tsDc%I88L&~ZXCT7{Kk-bn0hlaE7Apm$SzYu$HkP$1U zSE!vIA1F?M$%2(Y&B?L_?)sSSImy%;&w6m7uYK7bvofj&#Mmp;l{|kJ#I2dGO&`Sb zVo_|Z+m;$;iUM)W583BLqGUwopX22Pg&-$x8|>A!AMbn)bK64=9_?ni#YaW$OReT? z(kyOkBsTUdtIQ0@9`@DTd^9`|xDF^?cqYy$BLt2`L#UMlKNe{$hQNxi=Ec2HOWo`_SiR-a@3)k4*x|5p?67!HwUmys(PnOUh^mHTOKfr#TXB zjg*FY^#y^g6(zdwPmT}S);G+nE8IBtF zl~2!2jYVX}nPZ7F zD;Zb8Bwd7xRXkhP71R$qSw8_wIOjaXfufB7%R|op;qC*0XaXiF_c9nj4NNN=e26f6 zC0J^xG_2KqGJLnwyoZnft2x%YtJ20eqpj_QY$#dI0xwN!f2Pb+um|LOv2*C2`$uEz zV)_K*U9yT^gfH0H0(u3?i+tBjWVQws`xtS~)|V!?wr9|m>bL&HSRUp=?BhNxZr? zKfb=s?6ZIib!viuMzUz1Hx?}mZVXM_u2xR>5cxmvOHAe^=+;?v#yw+xjea8Eg z&geHIFb8#8n=odxmZ8RIflC>bqZELKrI;sf_Da@|l+*3p(mN#dfx9yI-B4i%*4+2| z2x_Iu(PzKiAcpR}h*{Nny0)CIHp4+t1_C$~Rt)7S)G|UC3gml4zrISpIezt`4QGAH zC8ghFBZA0y-}~On&kgE&l67Z<{n@d~T7bVumIM+my<7eL!(wooTao5AR-1fnvN`2+ zz>%}+o6nJ6fg|?_h{y-aEWk7h0Hi@|?D3q36al3kaVfEd|Et7v1!wM_HLzR9_$;oH z*Y`^)w6k{CS+3)(of)ZdjU>;h9~>`=*`P{2mqw6be60=CY`ubdCe-i=#S)X;3|15dIF8>^Z{K;rf_Uwo0Jg}T!bXOt5KjI__Yybj2ow}Oos@=#) z!u5Rm`QT9PO1r!5j|zM52SXPs2rK89O=rYYznvC78*fl%Hfdv*#&j)`RGfpCjLsl9 zDEHAUN3jHs-wrpWs=;{tyi8&36=&c`wY%uCL@vpuv?uA3{$-l8?a@R~0G%~0;sjPQ zQ(VIz14t%7DO-*R9zEG!^G%npwbrInfq7%@>>X+(-`T4aT!}v9 z$>0C<_G&_#Jy;p4SavH*dpz@&No|^02im``%!7~fAD)&m=|;bdjfS$te?I$i`R%;s zdquLi;*EfPWg}LOQ7JOm-8F%zgBL}bn-!%4VyCF9TU_RPsv?g@dTK)&2{i$K0#L9J z^oNYo-_u+NWa{}RMCg)IEv|wZm%mvOK;9c#KJA}e$+22mOiOuT;*Pa@55DePxn}ygKL(yaaMue$*NAh+z;x4J6PKX)XTuc4w-n{l$#=(M@ zUJpb|wSBA>fxsR15tlV*BDhir@*c&PTl3sQ(K2?fyPOlY39|2ah_h~>EE$AB_y7)g z6NJ2C>1MIiL%tVe(_z>Zykj`!#h&#dgGW2qE53V}eAzzsn1KqG_M9Lna5`*9%w}Al zcY;<(L?kiOdx=*}*|^B8x!e@DSeSJMaH0XG8G4u_aumZ$m<@GcKF6^mzT+So<&$LQ zU%26Dv!VRFUlSU*)>wvQhoA=Yj9LAB{QROKXS3Y9Ays$ZF>8UnE2d!LNC*fslRr7g z^xaT*V4So(fBw##$K$m%so{-9Pj~8i^840ZbP!m9yrM8-Z+)myp8s8YTi}v$x5nVX zIhOcQI{&*ByEuOmfD!`OEuW$wx>@50f(p7+eFpBEQ?cKN&RqFiz<*G7bkqN1Du4Iq zOM#dJnO6X0+4LEgsvlGYqRMmQ1ndCN_4jYHruUZy%Zk%9zyie^iTU)rCi9I!&Cq4z>(B!W69-BjRF+ZmIrQrVoWs)d9Lr))cw z1qZbkN$&PNISaw411K+nyg-yZaV9i9TP7+UxA^$sql>1w#@2$N>CUopL#S+er`y~x z2?RW;u2S;gfgPe(W>75?;yTrzA;oqh+kv2iJIfaY|0sY#7I?$)SJp3slP^<4w^E_d z;zs4pJ2t%=FOW#f^Ynxeh(b8`wpPxUcl+-u)E))#$kFj`U-`j6J*ojXF#(yLUrD}O zZmmyc;p+y+D4yfC1Cd1c^lpz>H+MLEZ*7JZG0k*6CiU&*2oKysUfhVbMLlEj@IX8; z4F#%*2o|3piLlSUQgSq`3W6O6`=PHXd1lK90xuk{N+XyizpcfJ+4k+L7aW{|hy*&c z%^;R~eFTD1H~?`tQ1xSzoam7YC9`Pv)=RBfx}l@?i~8Ybtm#}XiD3l+$a>@C(>Ft} z{N3J$WKq4#lFMp4Ik>0we3d=x9A3Gm*}CslG^)gqR4K4fet(Z!?I}oC5aOAAMG!cN zm?a|7*_xa7wmW;>94xNdN{ASXCm}EUfaOX?2wE(j=Ns5|B`l_2Pp|O2UsQH!7WdOry(nRNo*G=CUt`IC&%uT@T-$I5D3w04|7uqmUQgt zru4?!1KO9qM#IjSr%WkJ-&z_erU52kfJiXR4?}NgYBes2Q0t->1R9U7B!%=71TL(| z+zmt;rC_})Z#PwuWW_1l*?}=-58sltIPb;^09OCPA;9ym?nEu_tK%gu2(OdXBa8*r z_xJB*B=XHecF;&V+m_n*0X6JYpGP8dc#oG|wy9f=%>B0YlL_F4F>T44vS2G93E%*r zGa%t=gJ5}nX{@GjuF47#b(?=8wVaFK!tIQ5yJ^C6oP08j5%MwFKw zu2(o>R>I55`0+h!BLhx}APWf!G=GYcD}|PpkC!(R?ESrBHKN%8TP`-IvA z(}%0ZNfvtc%y5N-Xw>Ms9A>E4Kui|c1KT=A82Y26rA8HZ!H>!Vo?r(7+Zm@PBtng| zG<6jKU6=vY^$8qLMw5s1FLf!O*Ww#|FK(~}g$Xf=oF7$`xu&8q=U;HCp+ zCq4QgBg?uHL{e}SO+)e<4Jx=&C^8OoKN*i~?8VeIG;{^TB6g`RgTqCDpP>dOK3QQP zD0O%&Ch8IP^L^v3(H}>H-#2MtFsk&YYBE|@+{ftosZ1H4PECJC2u!!&Y69N>N$*a$ zo*hDrmgsBD5ot=Tis}j_f>2D>(c{xewx47X4aw0+tmpdo54N7F?p{K#4v52#4BQzl zan7LGgrIo17e}awfP1o2>C7f_PvOAMz7Na+ zGaT*F;#yGjO8^TtQ~2D(=y)KIgSEE0-^p^mV^mqRN(hxg3CWCM(B`weeS#SOG*R0}Fn}9k2_|8gLx^*m!X=0f6HIPnz-?GElPfV?DIA$)AQhFl z+F!AC!^o(}`~KbAxAjOEEx{>5(J7*@3bXtr%{(UE`ce+S1Z7pd*Mr0N)2b<~a_j2% zD3ZPJPaW~mzty?+XisqYF+wn8%wNG~*kKUndLpEMJ*dxgBzqrg<$<2w7%mw`#1>(| zE{-D3cB5k!N=mtwQGXC*p#joiXhw6-4&4nTU63?TwT2Qrj-3cfkN#v{6%VgaklPSG zdnxEkgJ|j^Jv>d=DkzPHKb_9Yy&?r~wmoezy4SDc`_{lgZfl>uFY%~repl<#=8iKo zFat50kYVSN4EfyR(6aQciQWnOMgO>WYglUkY5#ga~eEO7lX2W~)ddGBz&9SlD}KP%uR^gIyUet~Zp zzF=WDmbpSB2Xv51_~-=hG(;^8QEw^4jnloRzCcl!jfZ+GdiNdr$ymXbez09i`Si+f zZGn9$3RcwbF5Ys7`msWxBD*Mk%S=*v5dXMg{32aj@6h?4Ic5o5&B_3L8+~hc{nPVv zo1qN$z1NNR*mV}0?4f~0h}pZ5L!wL4FD@c71GS03ieI?i%=}gh=tqTt7O+D#^F87v zSrv#ggb5`Tt{m}*nw%ZiL?X~jZ&a_pWY*DR)AvcjU3ZOIaJrkr{4mK(d)7!WXvi$F zNS@-wMW-(BIy=6GcCz=S60`ye>62W}-9%4!7JOJJF|fZgJ@YdB!_BkipFb!= z13_&CFdEEX!%urhlo(!)jg?B%S9?k||6F7pp(TTPk{% zFl;KR0G)UWIVf4*1Xa*-FI7%p?ZJS+YA_|M__X@Ymsp#xxEoM(G+2Wn2`*6pBXz5` z!IP~ZBsbC@|0zvVj*QPdk7;hQuejB^h&aL23^wZ9Ga=TGr>9+YF6=#|bML7QC1Wh{ zhnPR!Ts6gkP@+0MhHZJJkXt>7oq3rm6teLZY?yl>vd+Y?OfEh;&u8F%WMro=W#U<= zXjo+q37Vqs&(8kj3wK$bwP|W3kV-xPw`?q|)=x21fA*%vdl(SBJ%!cPl1UCDKh^1* zBbPjR<|gWSWS>+=mzAqrp*%mQeza5VrU=~+z@az7tXn|jMs*dP6Zt%h*1-*r?R5*i*a6OIKGxOIsbt$ImYI@YGCSxJDyWt4*iwJ>{Wy)>W?o4ie)k%fNHr=E6D$ z7XB34@^D|BYV%v{6B)SO*2ks%LXdy6;amN3y=Lr8b!D@%HeHEM4EKhd0 zWQe@x)tP`JogB1E0~ZN}>`A}WI_P3(0PP2ze6)ALR{N3aI~p%jc(n~w)lZ`>M{mf8 zSX@x%E%TSUGg^6yW#%m;XwfMcYIh^{d#Qc`0X{n+d0_X*c{>|a<9i3LdPGR3%MR%< z;4;_~NJ~T_pFq_;nv?7hce$w17%o~XXAFpd||ygTu*k^r?f!8hmZ~IzUf?l)&5k}Hunub zxD7yl+rsJQVC)@wmpNiU=49_%vfvG2z$gvEMg+i;_GH!-WUN%;PQNQVP0p}ysR9w{ zKb>~M6{+)>$mU&|SV=Gi7joH8djR+lqrLx0ljK43)_MT0xy(iT&Rd4`#Kd#7LE;nu zZhs(vhnJUkmX;_~K{bZ?_2U%KB-ps;to6qY9Eh~m;6bCjw!-soCr zf*Qt6I2G-@JWL1e>=4M881sOX_nOIokR4eF$^L~SOygN@<0ya}3Q^Jo#ll_{ZJ z??_xn%{+8&va8BU&s{HdmL3}$6ZKd#9j~#D0tWUT=(9b{7M+C<9J%xRkC1LP zu@5YHdDbru7uGaE&F$mV0AVip$1q#{inas0Ca_<@Y>F^Aj*3}Sje^8IDqevs8XsR} zrXPVjIPGj}!RZvE$p;hxSOa_2zLVy7o1YX28AmrJhDjmHZq&y<7P9DPzRnT%1Vojd z2Z8sXa>0SW?E*%%uYCP0j7^~r?U=l_?lOp$B(qU#DccF%Pfy}OYms8=SYaED) zY(tf7ZZD74gfT`Ue8Ro0LdgK6o_*QbrwEqPg2(TBx|hGd3W5VdlFhX~yAB^7CM6_L zyEnV(1JKrG$oLswEM#tez9wwXIUg)+pyS5*?u#sQaLf+6lDgcx8AyiqxLIye$KT3h z|E>42fbQh2913d%Xn?&%#{7)?51{TiFuA|JO#z>K z6|{2*mKDVAp9wc)&D}#ZTa3-^I#ulCc}RAWPangbn>1i zi)7x*b5ztpSiKs_C%Igl;sEUY*=tM}u`q9voAsZb-wJP9@LKtrZ01i@XeVbfzdG4$ zNMZLi+df}Rcbpo03dy3rN|k2k&0r7YBeMpH3o#X;4!{HudzM%hK=rIAD_X%HpK-?d z55>rL(|BhYGUzoxFuYd|$(AGC*xkQ;c4&S#OsfmtNC0mGdt}Uh2t*)oaU5g;V3&oa zZjw9SK%^6O+w3EaXMN0X(BIsUdmvaf5^+`Y0A=gpWqTp?9Dtczr^>&O=a5a+0G5@D z!3^#C-SV61;VkV#SdIHLE=~ekcLA4eU2G z$$K1(Wvl-`3dWZnzDHoDIou9FS!dA1`!s5xHd4MIL+td@Q6 zec_S!5_J;nxcJp72-P$wV#)S;Na5)CW=iLyllMj!tyJa`ro09e`;~9=o`9wGkt+LW zSY>u;(qjh7=z@z7bW^Mby`bTEBgy&sTuZL%NhrB5GriO^k_#<|omuy0z{%TbGhFrI znY)wNrai8mKNr#)RBR7SkCLGFFDkrx7k>j-tVnP~P&_~BmI8(##u4!(qkI=jlcxAi zC{)>2zFW?9jGx zN50gM6ndFYK&9VGodI4D7?->g@}AN#jB!K1Ltd%)$IzAL45r1`9O~yeKW%dJu8QTa z7GD+mUfLO)rob81$r342qJ2I2oZc=f@(jnJ)b&#qgWO$4oE&86b(B_qPKm197^nUt zOIoG!Hf6o9dONCj0<1^?dXhT0l^AM<=(dxs+4)P^pHSuR2W-Z!S*i1k24C0Uz9QGr z?VjnjfCuB_9JDF#&aD zO{EG2%OD=`H5&g01pg0-fA#O<-yHZi2mZ~0e{_0u3J37tgme|8Sdf}Q zngIz567GLE;paFF5`NAxAmMoceuvcmGY3C7(@c;yLHeIA>wk8BKYrs0A9((sda!^K zD+1{Rq<@w#|F7M54DzM`q*EaMPdaXZli2|2|2293XXlIO6`p7R>IW2@Ks-qQkPpPf z#5fTV5uCBHG0x4+4F`V9AP&T)aY;!@KPfRW5r-%6tGa$C{O|D;A0Pia#l^+peo<^} zEDldGF)_c>!-o%XzbHC78i%K-sHoow{6aMxp74MF7lnt1<9<g|j(~vRZ}{~a4-@{yq;LB7WKT&+iF@$i!T&Y6|99K?|ES!3e0*@f z$lKc+XKrqeBO@dGM>=5gFQNmi;5h##w_|2z{^>XWJM8=~zm7lu-|+4KRk`D3k0)?< z8P49`9!Ez<_xE%FpCjUPs6YBq3=Z%w#*g>)cw73<`1gOMnC}0i-0`x<`vE-J+1dRg zJCOee7rp>EzQ4%t@%|of^Z!r8bod+qZ$JK(*84B}jz9le=fBqapUWICcYHjECwF&u zoT;hl--`$E_`n|-Bg*>gwXdzM{a=pdi;9YH-@kvy9UmX#@W<-vD(>~`*FSaP|IZu_ zzux~*EdSRr-G5f@c-i9#p9{;%%Kn8N{f)O7;Q5bukb#5x3%^fKPyaJN|F7ik#l^+A zgM)*ga{uce`}_O2`uh5xdHa7gcmI{M-n z^e0C5U%~1AD00Wk9#8l0-^YRf{lo8k{f)=?y5S$~XaxuK`#ABK?mxn4|5<#tp`qa) ziTm%qkM{?7fAI^q`H$eUe=T?XGs?=!aD#(`xW&aq+|JI<&lse+x%nr)`(N?zzt%ba z{O4=^m&{#UTyS{8+kZT9adG|5*WY;D^hXmuI39fc{`YYj8XEre{l74I{P~}i`>&Yp z&6_uWBj=-?HQdoI=o3!<6bt_K4JRijxR#cde;SwnbGhSR=jG+$K7Ra&J3Ku6>qq~7 zJwHGHGe-Co&*S6x|F2m7e`n+Wk=*gJcXD#VDJm-d&ez{~JoN{Z|6kPi@MHfa`~Rri z@$$#Vb6s6szw`Xpi=*vj+yQnPclhHg?&M(S*V}({9vB$-=?nguxDLP8>+0%oD=RC1 z^PGS51_p!qDf9nMx#MMzCj$e6zm`893;LrSe=$b9?fuWn{Z}mi_vC-LwSYTV{rtb$ zI}b3siYw2H1&19L1KzgxMmzYbzw&!_u+rMmB_->q}& ze^Q+~Rpq+s*14|QwXVB8J7%S&rQQe899$^tjN^zE&YL&S>-KCoXZyaPp&=8m>90@M zm%I!2E3UZ0QGWjOpZDln1D?-?cJ-i#SL`dUgM{md%Fy8rN05(sfoXbDfoM zyKRjdv)ThYa?3sb_~Sk1;qJTdK8Jqj?(XjO{G=~-Y)>A4?rPkB^PAr|%H@||-lJ~~ z_=3k@*!MS9r|}#03opFT3y$uV3d4Q5$Gx*+k?Sa5;5s(VcU{H{JGzr`;vs9-u66hm zRaI5Jo|*K8?eS(jw!*cpn&=w-I?8o#>8WRkNhnq~So@B=OV))sHNL+}8uO433R?b! z`(Wl{CQh8#3z}}z{~q_v3q9@~Wv0JN=eRA!Gu;-u-`>^I>o@)Rg5UGJriFL9hBt0- z4fC#Zjeot-^h-}&fo;hL_t=-0PZ#U>Y`8NYL>X@Q4+eLQ-yVACpu#;=^}D{lfUnx$ecm z%|<`>l^Br1OKV>q0Q!m`>XZ>GSKl3WL)qKtPVje1=vHM_g2YcoNhnxHl z2KO+hE{^^0*xK&41h}`ae_It%hC3XxzNK`oYkvD7 zllPl<;y(9UFYhVb|J~KS{s-4E_fPiw9X;T0Z*TXyUw%CGQ0aNeyKo14J8g5=;Xe?0 z4{_JHz1Nt0dv{9D=fJ&X`6I4n*;v=SbhK+;@}O(68eV8K{%Bf!pKE;c);@8sd-eCO z?v;PFwqERkJs%jC3j1A!JD96ncG+da4*zU=UbdY&pZM&?jW)K6F6?~Ux+$(TBkvQv zyvMjV8`e#W?r}|T{lzsdxWjP2Ex`SHlkK#;&%WC9d`jMhd+m(hxZ0Qh#ckhGAGPj$ z9`aytmrs{X)aE4{o6 z_uA>dG`_j3SKGvX!5+IlRJL6@J_+|<{NfjmGTiL{U~yNhZt>#9y)eMX?+f>)g@4Y- zJL9*8c`12snDa+hAIf_I?lmu7>S|v2XCGsT8=o>|%6p3YrI%jnD8mi^{_6Q`G1|A@ zdMk#&wXJ{7?0Z_?&4=45?|0d_Js;fvx2wzGeudZb$a~F;zwo%%Jb#I+opFWveLctg z_@d1D^mpvuA3ZO57w*)m-QLmz9qz0g_+D?HcglMf+-LnC8^@(_fAx1>uJP+?U;0%Kc^B?gQ~uFaP5yhEV_F|K?dP+J z)Akjs>#w}WvM$WQo$|Av{cPCb-xucC_axlOryDnJoX_#>1%Q3gRl9m8?h9`B@j7qc ziPhRzJtOaV;r>%6+^eVjz~f%^?Dzb4@t7aKFJIj8=koFC^1&VKha>)y_x|KkGG<_2 zjvS7pi`cx6*<{82KU{qV_kVMB8F~M8B=5<%S3UDxSNZgLevBBmPcD+?VyJh8jt}Ks zxc~I0KXsJhrvLkcJNkaoq)CprTDHq$Z*d&r^Ql;^Pnb+4u5z3!FYnm-@Ndp@{7b&kiq;)!p%ipS4P#Gf1jWWK+)9lx$WdS3D_+^H|V z_~K!Se>VFL?#Mj3`*D1~2P4?{tE2o)FY}s<^>cD*dC!Xb^vle*le}N-sx$Kb53VYd z_XzjRkDcZ=PdM3ph--R~!H1TX7C-g_|6O6 zvG{Jo8^r9c^Rk$-@7Zv_+|>y87k*~Gds^P7{?PpSP~NlP{^%)AxR;OniYp&;oZB?| zSRX@3qLFoHA@12QPa4Aw1b46>j`$0E?QuZ9JoEX@&CN;O>c87s_l_UiWpKCL3(dhZ z7n_=!F?nZh=A}GvfA&K2=To>>KKUJ2k(T$0$4+;fv*5nz;bYvUhmLY(4}8{gie5{k zm9fBkh5L_x{9{KMZt@@E4)*wgH8nMf{keT>o6Y4-+9~gO;ZBTR^8Qa2vkT-sFWgUX zJLUb0ZYS=aab@=(>B{aq-1y+=#M}>Fg4{Iub;$Zq%e!!A9WZ6M;UBM$VJ%J4wXLz( z=4Nl-ITt$%J)bZao00cyxPRYOWpF>&W#s*dZ@8ZFo(1;@Kbyh5^xnf<>D`CAlDj_c zHjVj`<&#zPBApn;=+UEhIqqQp!yo=|SmLksG1N)(IK=WA=KZNx&P|Uwc*8w4HnXSqAq{ImvtJUH{94xEJ64KV9){`?-y`?BmA-jD|; z#N#)O+mn2GVV@7~V9(xo!%hFk>!O}{=9#$bdVJpA)#|!xR{FVJa<7?xWe&cFyyu1c zsfLy0{j09LhrIWVd&!?a;xf43`XRUR=Dpp`*bhhiSsN3skHH^c4kiD~@-)rvc9gwg`90$-zV`m-=dEh#BU?7w`z+}_dP^ZT(CUe7;%hK=J=xMQQs$ED=G+ zzQoRrzU;L1Kg`u-$Nh6QhL^m5(v|j*_k3{QBO~)6_Ws`V!IqMl5k=0AZ$p{yk9`OC z7;|uE?QgvPcb|RsfgcL(>cOKS?%CE!5F>;amMmG4GbY%pqtjoo{&(|ullva>o(=bp znLnS#{f@sizb-BBxBL&Y@1eZsz&&jP-~pZ$uL*=K@}6Sc<;TmH?@QK0+`;~$i!RFa z?Z}ZMeg0{ofp7F=Xykq%>!OGmju|t?vBw7U1Nhc`UCoQ{vDnSg=Fg{bzyApH=RzKjU$|4F zCkq|_!K{x_o=sA&#bb{>)(g7Um5-+7{b2Lw)AD}%hn?gd+&A9%0Y8rKwEY5h)rIay z#}7u&BkvUE3ehFZGf*_2M$Ozxq2sUhG2t4SzVYtEhL#W^GF|B;CM&41%@ zFB-L%jp6^BYkcGST>?G(gR!SbU%1PcmrobUI+$O0;f3DUXRZvH2lpqQc)}OWr%|H^ z3mt#0k=Paa^7sPrwdtYH?UeTjcl`BD7Vl+GkN9Hj0Z)#jp1_P4)*XpZH2hgF1!%Wrws@EheF=NTnpvc zB*l`66>rSo{ys17;kccglK85vt@U`vF`Ib&Hq^enbUc_-h!HW5Bv}{c*gBPPE)Acc z(DC0D>m-y*$=ZVWVm%l!dh~pVJNJy+;tOAmJhY*X({_n&wps9e=Hr$mYw-#`iauk~|B>LUFG6*2bbGJgDcuiL>q8}9g&6wRk-PNmTC?=SB8Hq5ug7xOu>?AN_w>o}te{}CJi zUgQ4V?|#>>A!1*n5O3sNa!w82?0p;-&8KM3u|mf`A74JnwoAwNm0Lys#TRqMA^s(n zyTN`z@rC~e_Skimp^n+ewu3o^{$PwC-5z5O?jhdjhOmV5Dcl!2{?hN%{nhbc-&al@ zRN{@!1%6 z@JG*w@*d(HVvepC&Z>pCYVu1};`x+9$6wg@*RKn6DMMLL%B^C|9AB&(YJa!)zOkGg zgv}VL_-tt3;RF21`|rO$bju+n4!dh~UtP6AUNmRl;=4h+X@sDwrKbMbf z?+bUuYsj-}YHEsEAn`Nni~EYt_Gj#lea9ET9|t4Z^%!^A^n5V~dtuHvK?Tgin0le( z|6Z++0oT4RatZMF6sOIVXVX`V7Tk#$k@JhpYP=@hF1%I8xFg?Tk*re-Yt{HFD&af| zG4(>ne^cNyJqdT|cIp^+;jJ3nDYPBO(+eH{Fjg1(bja&KaSt(%<2AHFzrX$V+iuRB zIsR-&{(hji^E`5ocxFDh3v=oi_iUJhzp$2^3v+7r98n>j9>!D)9e?_-FWh&u6P zdM-as`d#*1GOjwr9jwD5Ij0Ws7VbJfGmN9duZ51k;x$?mh0JT*PTiMJ7u)xt7`vmB z$m;=@d@#?Z+e2BGyi?iE1|ouL`WQ-=fo?}eW4Yd!|d z$(^F#!CW$~S~?#27T&5u9iN1`Wn`Z+Vl|b7<1vR zn*Ea4H-~ttVrtZdj{ke9Mj=3Oc4|~X32y7;Jt4Oa?OU8wHtm7r)NtlDX zw}*HObJ=t7@3o)7j_r>9$-|3tf+gd^Js0N0mnp)Xnz5;HS6rpg z@y`{j>&utNcR-)B4g%Q$Z}KRFHFb!$WL>z2x?OfXn~cX}wj|8KTDZ%POVazCO-%_e zatUQoq51M%c{CV~z#)l)O%=go$Pj^o}^_1VYmVAQnVt;DZ6|$dMcrgx$aS!oM!kn|y z;`7slyW%Q^j=!)+*0BZXFXphw!QkDHBjh$)9&8fs!kk*N9_n}DF3hPz|1H+<;4QsQ zJLSXZ=_GpTD<`Ln6xbw<2vF3g2@jCpv5ns8TKrO@y{>ZqgKvBw_kPCDr%_nq&2 z$LnqS8yvN+1+2p&Ij0Wi(S^Nqyl_v#Jl601!CbL-bbHd8qEL==-EVE!U}q99PMt|@ z=Ztdbt=MLF_l8YU+WW;r=AbyecAL7?@-ROVV+CY zW!HsyY}04V=)FEH7v}!|Yz-0Evqx3OroUQw)1OBk+klRa4!3C0BEPN=dmqcWa8|9e z(^Rvkh;Ua7wb1Yv?#ffoCFh~6OUC8HWz+4#JlmK}_B__{((l3@S(gu|nlU8&72EV} zO{E@N_W9rp;B8GMZkz2%#`&@28Fp@d%kLNB&mJ$C>-SA3+BREb$E7~?uEPJDusr>aHl3;BG&DF<;NxAE_epJUG$&K~A`Sk5DCFM2suHooG|C*Ic5GalUY z!KqKIL1cUYPYUOxnmh!>Q>gK$3Jrg)OHiJ=@D61iyDnRu9 zA@*Rd!g^Q5Q>YnR7aIQB<4bw!^5J64L%g{U^?DNKp-tCVO?7P3g?X&w!?_mCu?h1y zHiNGg?@QO+R_R(+JZiUQu}3FTdWJ9w)|$H#al;U4OD$$G5c zvA4oqvA8fcqj7rDUe%pd%lz46tSRGM63!QAU(%N4|%CUY=!W{gCwQ6B5n=Y(lKQ5b1 z4`p3(_ZW9@4oet|(>*>~kIQT|=S*{+IQvyMFS*ak16CePoka>?EF0aEFVGd}4LhT{ z)$_$f^CxB|u1TdmVI9}xseri(^Ie68f4DY*IFIyvHvKNVsq@8L`aP8O*q0OLl5^@1 zYu$(QYe}&ge#e;c?3|7JU5+|DC>^EQ8mMTRdH z!WVWHwedxh@k3|DTjo=A_jr~*us(rz4e=J{;7(z!iSk;hnGY*8{J~s>T;(KRE+5P# z&3QC767@rRb&9SrvAMe8b#pQs>RiyUy<` z*7(K^wwLHAe@3C_3p=M09_VgKUYmly& z`g2W)3&s~?i)G7}dA^iGjZ$OTu0DJl5}_4<}up z59Ud-9^?%6Q!k6n*tJ;Xi68yzpK zspGgSe~*3+uSWCxYF@n5?*(0J=M8cW1ZU9IzD)VGtFtrdIEymH7r!%}_O#XmT${kXa;6v=? zg%^76AF!>V$j+CuGov|o%FdSrXU6`-_&V$y0DOPX8!w43e&u;1~rC*eO{R_ zr)a+c)rE#X`FLRo@#bEBoMb)p?^a>v%5``5!;A?A{8)xuhJxaX(VAgeAk z{I%bQYT+#(F0|*uTXl$ch`Dg@OSfmkJ;a>*FupEX7w#H+LvMU_mcMCO_CEV7{=03> zo6Hvci$7bwdg_nte2DrUe*&GtI+kxR5AlxgFSy_W7nVZ9Kiuzwd&xPqa1QHietRy= zW1AkzdTi5$J+@uA$MJPx9@c}&0|9K#3B*2dK3zNq2>p`nzVHVBaL?EC&p+RVrO@yX z*CueU@wqUU&z^*PY}4tdu!K53wCS?z!kSvXoN&(vbB)0nhbPH@4@aCzcn_8^;U)t)YyE@*U!>Tsqavse}DF=bV!%g@(W7CwC}Eof=)7 zBQ%6h{ z?9V>?Y+o$Dw!P5sxBSF<<)~BBN7?i{dR|ylXVdLT_B_NK+@;G^=PToV;T>Zh`fa&< zH~ihCJ?YR7xnx{CAk3+iQ-?kUd-AF&-~RTus|pQ&o10vw9Cd2ugJsXDWzz?OJ2;0W zl=ED8_a)<@e8+falX1y4=E6e5-{O)Jm7`9L4nVH+#XYp?+{bb*yB_-Lu?_DFZ)85iJeN+-CF5WpVxF%Z z&n4rM^)Qc;dva=!eXuvV_vMsRP8nBd_?vzDXXU7?re8vN&t=!aTP2qDTr!@d$HVVJ zyki;9SEt8#57drJ)`fK_<1y}h#_vOy3ioe)>syXuYixd0X!x6c+TU`MJHtG6>{f`m z#_5uE;hiMwx#T>=H|ajaJeQ1zIy~wAy^`?|`!J6d8;a};_iujlo9-Lm_(uDezx?G7 z6&n7~V>V!Vn5WJ??~@JpSk9Atxc=(&;V9!F=3yQ!x!zzNmXl9Dd2%6Q4^2krNk;Py z=vILTkWpkl#GCtUHXVJRB-i10Lm}g#d=I5g&quEd^AK<4PGj?#M?vmQ-ovuvt6%-< z@r8;%blLpW>^N5)e-KQCx9ZTYBj>qfJ;ZycWPDfpZP|1>xQCc4XF9COp}^k*d-L@( z#bp1bLdG82jOHUPPq{<+>dINhk3oJzyDrSL;T~c>P&>Y>WIV(->0a_3;~mO)h`H{; zo>-4?=ge=)>8GE*<@n=||3snV4}InfUMbv_vrLUGK<0#dsL#0%@&2zauG1f8OgNeU{dR=4gY_-N@s{1oG%O>L? zwn_KGTQzyp=u2ci#2nmDJMA=g;)y3du6`XZwZ$m*G9Gw6&RbRnd)z?+oZqQ zzxTx#U-WBvmCGOZwQ@O?+m(%O&D%1^%iQ}BM;xJdxL||?l?zt!LFlB)B{}TF{BTnM&S6+F=)4rp7 zYi4aJ`%;DcEk0r0I_p#`Dk|K91q*ym1#~OtA`p{dd?NART3YwY^&#{CAw~N8#GBz1#2U+iLr~YR@_7=_c|B*lC&4i@cY)cu6@>Zaz=^mO~XC~9pwvb|DwBH!<=hU zdluOF2FMD06Y`1rTXd7lz&=l3`N~%YO~2L$#5xdN>gl)shi>-KZMMCan{7|criFj@ zdo4E1z1GuR_v-I#&%GP`-!ELa(65_%2C&WoWv_+64h)!ol5`;aw0`}1PY?T- zH!Zx=?|16^JEZ%?pZk5Ws;2y-+uqgcs;jFreJvhIqMNlrtP{66&q4RU`dc}LlO|1a z?d_@kdXQcAWo?-IC%-3OroTn^~X^lLtJv%ZnMC7Zh&H2t#g^!eJg zYdsz4{4K?=8&6-KqTBWuWS=?q8Dk%^sx;jd6Tj^?KX#gxGfbYFU6<`!hdma5W24{` zt<8sSthJ)p^>{H|%}O9*FFXn4#PD(5jmJ53cf=3!LaK zAAf=?AM+)*>7k=+U(G-G-#0ckc-=3$p;>gZwh+3>p<<0mTK^A9_aUn<|WF@H+epSN9gaEZt@?=`8?*B zV+P&-=xQ*RJz{^dZB51g91`{`ub%c}SCOWB^CKr(e;;T4{dpJCU3}aAZsW~=H8~SSej9|amkKs2L-+khSbraC{e6%V z-5YQGpewq5gfAO!e7~K+v(Uc-x&b~>e~WJArGEbNpLd5HcG#f!C&^CYr{EKziy9jn z?H#rn&wOP zjhA29N&G3<$xsJ!mJb*E-WD6(8*kp*m#uXvUB%fIV21C>-Un@MZI1meIOB-ESMImy zCjSq*@y{7^pg#sfe>S?M0~y(*uDQo02GR&FbNf=z^OorS`^&O_kc*&7?*7x^0u{UM*IzeRUwD_5*o z;c0JZsP|(h=@QXRUMBd_rybbP-_oj{vM2wy}{5Q`cwJp5_lclk-zY)6wwTS(C@Fl`l^=`_R|gb zQAKZJ8_@mu*Mp&7eVt?{Lz%>f!B#U~hR#q9L;ip_=yP-y7klJ|`#?gs;vMLIcy2KC zXQLY|(5?6YV2=KPKj>HJr$`Rr4e>O8%Xy7lJIppS9wwfLu2&on+=xHIcj%14&`{C`%RprkXie&=jJf8LXw z7ue33aCR14SM6H=PP}Bd#UrtWiW#5-;NOAJZ+h=u`d>6lcfb$QdEq|X=(~{SFkZ_0 zF@D%;_RXJb^tPQ_vwWgnrZM_h38J&!Sm! zs6C{iTly~49mt7dckuI^IdeR(G}_r4{*0D4Z}nq;&g1E-d&g+s;VLUDGv}=^-atQK z2M0pGz4tx*Ui?8{hx`G}qFa3p|4MgA4i*2ZuBvoh7ALJ`A9>q9o-+mNXZ^d610c`r zOOAf{qpg*tp!@N?SXVO;`t7~%!gh#e@kUIube{HdlJ1Zk!pra;7iS%DK3ZqRB0KYF zlI@9onZ-osxfb(PMRT~`55JN1J_Dg2{iD5xwa={jSu}_I0o@_Z_%7Igiu_Y!Apah% zEzQ2>J+_q2cAXtt{5U0Cn?~FKz0JCXfzWTh;$N^G*uv1Zh-Uahab3+>;KRr9I`|l# z;+%T+t)wsL<7v~T+3TdnmaTRUJ@iuWp|D#Nd>30Y5lm+1|0cF(<68VS`Z1(g{2`jd zeujTFKLkIBClmwI9GChTn^#&|;-*iZo}n3=rMUrQ*Vb;-=TG&{zqEA$r{U9}L)F(I z&GfVAO`@4ThTf3oq&}vfMXzEV*d25y>rrfd(5#(Q_3LgA^Uq4~bLdCutVodUQMQ03BgJi{}3Jv1p|hy^7I* zH8vN&mi1aTcgS9P1D!whZ({s+wejJ0coBU-o4m_V^fB~O;1$Lx@GI+ESSM<0N!DBZ z^WdO62YhRNaiOifDPatVJ`UGog=?=^gA=Z)(|W3K{gl>BX&of%-?RpfHE%}uR^yqm zyI|eR9=5LN6nmGet=wvD-a@f9dlYN`W)<5@?Pe8QlXH`b(RUNY=I3ryvAM46Y`*BH zM)%(ixTA;J54{mI>qa^#Gu>{sy#Vz!R{zo8ju>(Bh!Ng^?5^|f2kS!LZ<&6aZJ+H+ zSCZ~$-|-_xkf(L1l~b$?(>$&B8R=JAS!-ph6<)o+h2O2V@;fX2*$d(Q_V+XP-+zBU z|AmjDyc~Qkd?vmN{c!BR?5J!%b`ZVzp$~lsx?k=G4S)FPM?dPHulNCe9^+-&Pihxm zR6aiTPrd~{8#Z8{efEI{^2qYJ)(~5hPt5x=M&fEVe-PhF<50CrfADv-E5AT~0Co-C zZw-S*KG#w#?Tkb)djqmGScXzj&GG&T?Ut$1d=I806Z^Cx5-`H)&3)V3F zkk6HA7dr6$$qk_GEv0k)d7j&CUZ%Tyo9o`%X}J&!({o=tyx-0H%3shNLpoLn4f$OD zIr1s-%@}8Nw$}PH<^5UWjIUQuOr0%gXUB1-cy(oY#^%$m<_9#7ZVl(%e$31B=FM}R z%@ux(-^jf9yzBg#b$*Wag`c_h+6|5~P}DBtOWIY8JRR%GyM4(Je74Z?lD1o46C?5S z+IB_;XX@2hyW|frH_SypCAAALr(;!lw~q`FM`&(pbY0f(Ez8FFe39B2SNNO@+O2%* zTvze<8GcT;y=J|8?bTQO`zo%k*tF(w`_uo(P;E_>AJ^B<`A>UaYnR-EDL=5hgY(ku ze$8!q_-I#p?{5Bpi|YOa(RjpppYG~f0$FBkLjl10V`7UOHHTH_v_ z@JMD3E^L?IVmrbZG_g7`&A0uyU1SKJLMIZNcz4|@-!|xX>V~2^|*V+6%c^5@R8@#U)`by~GeA*{2 zMl2EB!WcV#hvr?vSZ`Hjg~i9a+}5rRvr(oq?Y!;I&JJ&nkXgm<;GullhfibPrCsrn zFjc(sb;||lY^m|~xOL6b7QdWgG4G{rOLMK!)aB>fuub{uANVxnBgKcsNAd~fi}H?} zHT%cJQ06Kt%Dv1GA3+D_^Zw`_`Ztd6ijP9SjJ_pq*U{GO&oZu` zX}O7(yVY8;%F!>1%V1B@N%^!7ziHktY*RkBd@_6k`W39U8~&}eC9bQZ-8HX$-Zhsm z_q-xM7O&nPB1aXXyHs$y7e$av5hdw^9UXhybfIk#7#RixUye*&iH`}$D zc$?y(^0nm?#j!cPqx^2#1S9#u#Iw++7GEjOr+tgn+=1?+J>E6MFpdMNP3*Mde~P0a zN9bhiD6uDtmyF7%eGA2W(6CyByx_ZoUHpz=dtq!;egpnD?c(;<@MxWEZ)4|NGznZnlTp_a_pMMc<*Lu>b8X4Q^}wde?4t zVBXxhuCAuiwbqrot}QM8op`Rs;^Yb*c;JD#=8jG@`SW%g-G?vP-cs-FYTKG;ZC-Ph zABWb@zS^}kRQu;*GkLCjb$fm;TXUktaWlHj)8AO-bB}!vug#y5*WI+_K{xBQ89knh z4}~rzK9j3|EFSFj=U?Z|bz53&++sd+o#hVJPXDEy3H?J?`Q+I)ei-kjOrGTZD||Y9 zN%@_|M>(HwF=n@5{u{2ndW~zC{U3I2^siFSed-(=ADrxSL^q8-)-_kHw|*Rzc`oB1 ze7f{nwrsC(BI`58jveFbs%;HLeYtB~_p+T$dXax_>Ai=!;yVuTW1un{dp1@Vd!H12 zjbDtP@v)D6EGPZ8C$-n@@EW`^)$C{8if3$W^$}M-?gZCTQ|xLtt#Nf_t6XhGu^(6S zT*g%3j6KLkJp9S}0PzBtF_z*zHd-EMeaYLtP0_2d3pNj53ZE0X%k_Nn-^B~~wfy#- z6*e|nJ;_ad_6a}s)Yt?}8MkO$0>AY|KQb;}5N6og>CaE``GY*0e!$La+@U-O{I)*E zdMA>c<=Md9km#`sb*lj`FcnsA{BblO;e^;hjxt6Y6; zwVQ5v;^>rIxnn#Zym%(>FaKLUsr>GlufOVAieC46qj{-~iOb)1WhEOt@8rsjvY7Ds z=yY&W?gW0W{7rPKk2%htZ93?F%SE}}+PKTDT)x=P%i@FO%Kb50buPS;^i0_lFkZZ1 zuG_GBiCeO8p6QWw?xh!}Wo(SeT2}pRHt&<*6WS_l3eS{p8v2RQLLbtn@FhN)&HHW6 zhA-b+A1y&I=$T)|D78av(M+K9DexWXQ#`V_BS6+qPu&<$PrB=KHy7< z)k9JAJf4jo#^fY*Km9O#>NBmd7CTqD*#7@0E4i+Z?Yr;38_@Bg?#AAR_<#+%D!qp7 z?raaVHMWnzEBFuOt}k1*%*Q0)L-DKliob)G((4jWO?u`bha6IjzJ>1>*?KSX>Kp27 z{JhLt3+B6)#v1qL8?$|`E4;|q1>Uh|U7hs2Lk~T)lCdoM==IlLwVcIeHin<(T1ss` zVdWF9dD(dP(hE~Fb`qH*CS=dLG3j}>Cbts1MOnRaxz8bNcw>}blf*i~+M+l8+zfro z7#kg!jtL|_@8ci;cqQ*!y}86Szw?sMDav@^NhZl+t@X;qWD@ix&IXD;OhtjJfvSd+$V9KKS5+EAc6zaoUt; zT>YliuGC_~W%nNLDi=NK%HMj-H54y!W5+y{vB$`_VKO19{|-9npi24}Ujf@bbH+5c zdHjj4V)2uvhpzGcg^z|F00(q5w#V#r;y(N4gI4k!`E~GIbNMp&%JWZodK8yN$6*&K z*aUm_qe=9c&sm9%V%&iKhqw4GHNJp+acII8@+@qX$x7n;nvYqj=kQ#3mUrT}vVFw1 zJ8f+s`WrcdUk&5&N%R?B4UA`a4``s~S^So`WNTBcYpyAAotD?SX7x%x7J*l7Y&|aN zc{Z+Z;u*5R*me0IU|wFl&iv{JZEY{McadAWdWFwlv+?3OD?YC@YG>J{w!D=$4~zS%fevVNJHH|u3T zcEcYs+w>tgghwQ zA0tT*{u&<%|FzQAy%iU&_hXI;7CT?^)_l*u#Iw+ApZUyZS`!GkN;acx<2$xK-}G|n za#z24s;l0x(8u85F)&WsviK2KA#>=3>asO{Z8mG0tDn2XtzI(Q+gj0CL4T*$*!KE8_HNn2S*~QoEE`jP-*o?BuF`78 z#cLM7?)5%%cjVc{Keo?T!Ncfic)D!(ldRqo*jQdlr9K_+-dWHaga;&*Jpss%5* zSEfGV?I6EH?$CV{FirYBzvFN5L3mH(3LV0@hIvYK7QBIP5&z!SjQ)vO3%r6pm7bk4 zWwLu`&2raLUt#mz^F2=_jp0nk-ivOPPNW@l?!ym1m0`t+mT zmondCOZ Result<()> { + let mut buf = String::new(); + buf.push_str("use prost::Message; use anyhow::Context; use anyhow::anyhow;"); + + let (services, _) = get_services(pool); + if let Some(s) = services + .into_iter() + .find(|s| s.name.starts_with("Launcher")) + { + render_service(&s, &mut buf); + } + + let buf = format_code(buf)?; + let out_dir = env::var("OUT_DIR").unwrap(); + let path = PathBuf::from(out_dir).join("rpc.rs"); + write_file_if_changed(path, buf).context("write file")?; + + Ok(()) +} + +fn render_service(service: &CollectionService, buf: &mut impl Write) { + buf.write_str( + r#" +pub(crate) async fn handle_rpc( + app: ::tauri::AppHandle, + window: ::tauri::WebviewWindow, + req: ::tauri::http::Request>, +) -> ::anyhow::Result> { + let method = &req.uri().path()[1..]; + println!("{}: {method}", window.url().unwrap()); + match method { +"#, + ) + .unwrap(); + + for method in &service.trait_methods { + let method_name = method.name.to_snake_case(); + let handler_method_name = format!("crate::commands::{method_name}"); + let method_name_ts = method_name.to_camel_case(); + + let output_map = if method.output().is_some() { + Cow::from(format!( + ".map(|o: {}| o.encode_to_vec())", + method.output_type().unwrap() + )) + } else { + Cow::from(".map(|()| Vec::new())") + }; + + let handler_call = if method.input().is_some() { + let input_type = method.input_type().unwrap(); + format!( + r##" + let input = ::{input_type}::decode(req.body().as_slice()) + .with_context(|| "failed to decode protobuf for {method_name_ts}")?; + {handler_method_name}(app, window, input) +"## + ) + } else { + format!( + r#" + {handler_method_name}(app, window) +"# + ) + }; + + if let Some(comments) = method.comments.as_deref() { + writeln!( + buf, + r#" + /* + {comments} + */ + "# + ) + .unwrap(); + } + + writeln!( + buf, + r#" + "{method_name_ts}" => {{ + {handler_call} + .await + {output_map} + }} +"# + ) + .unwrap(); + } + buf.write_str( + r#" + _ => Err(anyhow!("{method} not implemented"))?, + } + .with_context(|| format!("{method} rpc call failed")) +} + "#, + ) + .unwrap(); +} + +trait MethodHelpers { + fn input_type(&self) -> Option; + fn output_type(&self) -> Option; +} + +impl MethodHelpers for anki_proto_gen::Method { + fn input_type(&self) -> Option { + self.input().map(|t| rust_type(t.full_name())) + } + + fn output_type(&self) -> Option { + self.output().map(|t| rust_type(t.full_name())) + } +} + +fn rust_type(name: &str) -> String { + let Some((head, tail)) = name.rsplit_once('.') else { + panic!() + }; + format!( + "{}::{}", + head.to_snake_case() + .replace('.', "::") + .replace("anki::", "anki_proto::"), + tail + ) +} + +fn format_code(code: String) -> Result { + let syntax_tree = syn::parse_file(&code)?; + Ok(prettyplease::unparse(&syntax_tree)) +} diff --git a/qt/launcher-gui/src-tauri/src/app.rs b/qt/launcher-gui/src-tauri/src/app.rs new file mode 100644 index 000000000..afe517be7 --- /dev/null +++ b/qt/launcher-gui/src-tauri/src/app.rs @@ -0,0 +1,85 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +// use std::sync::Mutex; + +use tauri::http; +use tauri::App; +use tauri::AppHandle; +use tauri::Manager; +use tauri::Runtime; +use tauri::UriSchemeContext; +use tauri::UriSchemeResponder; +use tauri_plugin_os::locale; + +use crate::generated; +use crate::lang::setup_i18n; +use crate::uv; + +pub const PROTOCOL: &str = "anki"; + +pub fn setup(app: &mut App, state: uv::State) -> anyhow::Result<()> { + setup_i18n(app.app_handle(), &[&locale().unwrap_or_default()]); + + app.manage(state); + + #[cfg(debug_assertions)] + let _ = app + .get_webview_window("main") + .unwrap() + .set_always_on_top(true); + + Ok(()) +} + +pub fn serve( + ctx: UriSchemeContext<'_, R>, + req: http::Request>, + responder: UriSchemeResponder, +) { + let app = ctx.app_handle().to_owned(); + let window = app + .get_webview_window(ctx.webview_label()) + .expect("could not get webview"); + + let builder = http::Response::builder() + .header(http::header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .header(http::header::CONTENT_TYPE, "application/binary"); + + tauri::async_runtime::spawn(async move { + match *req.method() { + http::Method::POST => { + let response = match generated::handle_rpc(app, window, req).await { + Ok(res) if !res.is_empty() => builder.body(res), + Ok(res) => builder.status(http::StatusCode::NO_CONTENT).body(res), + Err(e) => { + eprintln!("ERROR: {e:?}"); + builder + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .header(http::header::CONTENT_TYPE, "text/plain") + .body(format!("{e:?}").as_bytes().to_vec()) + } + }; + responder.respond(response.expect("could not build response")); + } + // handle preflight requests (on windows at least) + http::Method::OPTIONS => { + responder.respond( + builder + .header(http::header::ACCESS_CONTROL_ALLOW_METHODS, "POST") + .header(http::header::ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type") + .body(vec![]) + .unwrap(), + ); + } + _ => unimplemented!("rpc calls must use POST"), + } + }); +} + +pub fn on_second_instance(app: &AppHandle, _args: Vec, _cwd: String) { + let _ = app + .get_webview_window("main") + .expect("no main window") + .set_focus(); +} diff --git a/qt/launcher-gui/src-tauri/src/commands.rs b/qt/launcher-gui/src-tauri/src/commands.rs new file mode 100644 index 000000000..29368cb96 --- /dev/null +++ b/qt/launcher-gui/src-tauri/src/commands.rs @@ -0,0 +1,211 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use anki_proto::generic; +use anki_proto::launcher::get_langs_response; +use anki_proto::launcher::get_mirrors_response; +use anki_proto::launcher::ChooseVersionRequest; +use anki_proto::launcher::ChooseVersionResponse; +use anki_proto::launcher::GetLangsResponse; +use anki_proto::launcher::GetMirrorsResponse; +use anki_proto::launcher::GetVersionsResponse; +use anki_proto::launcher::I18nResourcesRequest; +use anki_proto::launcher::Mirror; +use anki_proto::launcher::Options; +use anki_proto::launcher::ZoomWebviewRequest; +use anyhow::Context; +use anyhow::Result; +use strum::IntoEnumIterator; +use tauri::AppHandle; +use tauri::Manager; +use tauri::Runtime; +use tauri::WebviewWindow; +use tauri_plugin_os::locale; + +use crate::lang::get_tr; +use crate::lang::setup_i18n; +use crate::lang::LANGS; +use crate::lang::LANGS_DEFAULT_REGION; +use crate::lang::LANGS_WITH_REGIONS; +use crate::uv; + +pub async fn i18n_resources( + app: AppHandle, + _window: WebviewWindow, + input: I18nResourcesRequest, +) -> Result { + let tr = get_tr(&app)?; + serde_json::to_vec(&tr.resources_for_js(&input.modules)) + .with_context(|| "failed to serialise i18n resources") + .map(Into::into) +} + +pub async fn get_langs( + _app: AppHandle, + _window: WebviewWindow, +) -> Result { + let langs = LANGS + .into_iter() + .map(|(locale, name)| get_langs_response::Pair { + name: name.to_string(), + locale: locale.to_string(), + }) + .collect(); + + let user_locale = locale() + .and_then(|l| { + if LANGS.contains_key(&l) { + Some(l) + } else { + LANGS_DEFAULT_REGION + .get(l.split('-').next().unwrap()) + .or_else(|| LANGS_DEFAULT_REGION.get("en")) + .map(ToString::to_string) + } + }) + .unwrap(); + + Ok(GetLangsResponse { user_locale, langs }) +} + +pub async fn set_lang( + app: AppHandle, + _window: WebviewWindow, + input: generic::String, +) -> Result<()> { + // python's lang_to_disk_lang + let input = input.val; + let input = if LANGS_WITH_REGIONS.contains(input.as_str()) { + input + } else { + input.split('-').next().unwrap().to_owned() + }; + setup_i18n(&app, &[&*input]); + Ok(()) +} + +pub async fn get_mirrors( + app: AppHandle, + _window: WebviewWindow, +) -> Result { + let tr = get_tr(&app)?; + Ok(GetMirrorsResponse { + mirrors: Mirror::iter() + .map(|mirror| get_mirrors_response::Pair { + mirror: mirror.into(), + name: match mirror { + Mirror::Disabled => tr.launcher_mirror_no_mirror(), + Mirror::China => tr.launcher_mirror_china(), + } + .into(), + }) + .collect(), + }) +} + +pub async fn get_options( + app: AppHandle, + _window: WebviewWindow, +) -> Result { + let state = app.state::(); + let allow_betas = state.prerelease_marker.exists(); + let download_caching = !state.no_cache_marker.exists(); + let mirror = if state.mirror_path.exists() { + Mirror::China + } else { + Mirror::Disabled + } + .into(); + + Ok(Options { + allow_betas, + download_caching, + mirror, + }) +} + +pub async fn get_versions( + app: AppHandle, + _window: WebviewWindow, +) -> Result { + let state = (*app.state::()).clone(); + // TODO: why... + let mut state1 = state.clone(); + + let releases_fut = tauri::async_runtime::spawn_blocking(move || uv::get_releases(&state)); + let check_fut = tauri::async_runtime::spawn_blocking(move || uv::check_versions(&mut state1)); + + let (releases, check) = futures::future::join(releases_fut, check_fut).await; + // TODO: handle errors properly + let uv::Releases { latest, all } = releases.unwrap().unwrap(); + let (current, previous) = check.unwrap().unwrap(); + + Ok(GetVersionsResponse { + latest, + all, + current, + previous, + }) +} + +pub async fn choose_version( + app: AppHandle, + _window: WebviewWindow, + input: ChooseVersionRequest, +) -> Result { + let state = (*app.state::()).clone(); + let version = input.version.clone(); + + tauri::async_runtime::spawn_blocking(move || -> Result<()> { + if let Some(options) = input.options { + uv::set_allow_betas(&state, options.allow_betas)?; + uv::set_cache_enabled(&state, options.download_caching)?; + uv::set_mirror(&state, options.mirror != Mirror::Disabled as i32)?; + } + + if !input.keep_existing || state.pyproject_modified_by_user { + // install or resync + let res = uv::handle_version_install_or_update( + app.clone(), + &state, + &input.version, + input.keep_existing, + ); + println!("handle_version_install_or_update: {res:?}"); + res?; + } + + uv::post_install(&state)?; + + // TODO: show some sort of notification before closing + // if let Some(window) = app.get_webview_window("main") { + // let _ = window.destroy(); + // } + // // app.exit can't be called from the main thread + // app.exit(0); + + Ok(()) + }) + .await??; + + Ok(ChooseVersionResponse { version }) +} + +/// NOTE: [zoomHotkeysEnabled](https://v2.tauri.app/reference/config/#zoomhotkeysenabled) exists +/// but the polyfill it uses on lin doesn't allow regular scrolling +pub async fn zoom_webview( + _app: AppHandle, + window: WebviewWindow, + input: ZoomWebviewRequest, +) -> Result<()> { + let factor = input.scale_factor.into(); + // NOTE: not supported on windows + let _ = window.set_zoom(factor); + Ok(()) +} + +pub async fn window_ready(_app: AppHandle, window: WebviewWindow) -> Result<()> { + window + .show() + .with_context(|| format!("could not show window: {}", window.label())) +} diff --git a/qt/launcher-gui/src-tauri/src/error.rs b/qt/launcher-gui/src-tauri/src/error.rs new file mode 100644 index 000000000..11c5c8690 --- /dev/null +++ b/qt/launcher-gui/src-tauri/src/error.rs @@ -0,0 +1,11 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use snafu::Snafu; + +// TODO: these aren't used yet +#[derive(Debug, PartialEq, Snafu)] +pub enum Error { + OsUnsupported, + InvalidInput, +} diff --git a/qt/launcher-gui/src-tauri/src/generated.rs b/qt/launcher-gui/src-tauri/src/generated.rs new file mode 100644 index 000000000..ffb771b08 --- /dev/null +++ b/qt/launcher-gui/src-tauri/src/generated.rs @@ -0,0 +1,4 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +include!(concat!(env!("OUT_DIR"), "/rpc.rs")); diff --git a/qt/launcher-gui/src-tauri/src/lang.rs b/qt/launcher-gui/src-tauri/src/lang.rs new file mode 100644 index 000000000..9c6d918c0 --- /dev/null +++ b/qt/launcher-gui/src-tauri/src/lang.rs @@ -0,0 +1,143 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::sync::RwLock; + +use anyhow::anyhow; +use anyhow::Result; +use phf::phf_map; +use phf::phf_ordered_map; +use phf::phf_set; +use tauri::AppHandle; +use tauri::Manager; +use tauri::Runtime; + +pub type I18n = anki_i18n::I18n; +pub type Tr = RwLock>; + +pub fn setup_i18n(app: &AppHandle, locales: &[&str]) { + app.manage(Tr::default()); // no-op if it already exists + *app.state::().write().expect("tr lock was poisoned!") = Some(I18n::new(locales)); +} + +pub fn get_tr(app: &AppHandle) -> Result { + let tr_state = app.state::(); + let guard = tr_state.read().expect("tr lock was poisoned!"); + guard + .clone() + .ok_or_else(|| anyhow!("tr was not initialised!")) +} + +pub const LANGS: phf::OrderedMap<&'static str, &'static str> = phf_ordered_map! { + // "af-ZA" => "Afrikaans", + // "ms-MY" => "Bahasa Melayu", + // "ca-ES" => "Català", + // "da-DK" => "Dansk", + // "de-DE" => "Deutsch", + // "et-EE" => "Eesti", + "en-US" => "English (United States)", + // "en-GB" => "English (United Kingdom)", + // "es-ES" => "Español", + // "eo-UY" => "Esperanto", + // "eu-ES" => "Euskara", + "fr-FR" => "Français", + // "gl-ES" => "Galego", + // "hr-HR" => "Hrvatski", + // "it-IT" => "Italiano", + // "jbo-EN" => "lo jbobau", + // "oc-FR" => "Lenga d'òc", + // "kk-KZ" => "Қазақша", + // "hu-HU" => "Magyar", + // "nl-NL" => "Nederlands", + // "nb-NO" => "Norsk", + // "pl-PL" => "Polski", + // "pt-BR" => "Português Brasileiro", + // "pt-PT" => "Português", + // "ro-RO" => "Română", + // "sk-SK" => "Slovenčina", + // "sl-SI" => "Slovenščina", + // "fi-FI" => "Suomi", + // "sv-SE" => "Svenska", + // "vi-VN" => "Tiếng Việt", + // "tr-TR" => "Türkçe", + // "zh-CN" => "简体中文", + "ja-JP" => "日本語", + // "zh-TW" => "繁體中文", + // "ko-KR" => "한국어", + // "cs-CZ" => "Čeština", + // "el-GR" => "Ελληνικά", + // "bg-BG" => "Български", + // "mn-MN" => "Монгол хэл", + // "ru-RU" => "Pусский язык", + // "sr-SP" => "Српски", + // "uk-UA" => "Українська мова", + // "hy-AM" => "Հայերեն", + // "he-IL" => "עִבְרִית", + // "yi" => "ייִדיש", + "ar-SA" => "العربية", + // "fa-IR" => "فارسی", + // "th-TH" => "ภาษาไทย", + // "la-LA" => "Latin", + // "ga-IE" => "Gaeilge", + // "be-BY" => "Беларуская мова", + // "or-OR" => "ଓଡ଼ିଆ", + // "tl" => "Filipino", + // "ug" => "ئۇيغۇر", + // "uz-UZ" => "Oʻzbekcha", +}; + +pub const LANGS_DEFAULT_REGION: phf::Map<&str, &str> = phf_map! { + "af" => "af-ZA", + "ar" => "ar-SA", + "be" => "be-BY", + "bg" => "bg-BG", + "ca" => "ca-ES", + "cs" => "cs-CZ", + "da" => "da-DK", + "de" => "de-DE", + "el" => "el-GR", + "en" => "en-US", + "eo" => "eo-UY", + "es" => "es-ES", + "et" => "et-EE", + "eu" => "eu-ES", + "fa" => "fa-IR", + "fi" => "fi-FI", + "fr" => "fr-FR", + "gl" => "gl-ES", + "he" => "he-IL", + "hr" => "hr-HR", + "hu" => "hu-HU", + "hy" => "hy-AM", + "it" => "it-IT", + "ja" => "ja-JP", + "jbo" => "jbo-EN", + "kk" => "kk-KZ", + "ko" => "ko-KR", + "la" => "la-LA", + "mn" => "mn-MN", + "ms" => "ms-MY", + "nl" => "nl-NL", + "nb" => "nb-NL", + "no" => "nb-NL", + "oc" => "oc-FR", + "or" => "or-OR", + "pl" => "pl-PL", + "pt" => "pt-PT", + "ro" => "ro-RO", + "ru" => "ru-RU", + "sk" => "sk-SK", + "sl" => "sl-SI", + "sr" => "sr-SP", + "sv" => "sv-SE", + "th" => "th-TH", + "tr" => "tr-TR", + "uk" => "uk-UA", + "uz" => "uz-UZ", + "vi" => "vi-VN", + "yi" => "yi", +}; + +pub const LANGS_WITH_REGIONS: phf::Set<&str> = phf_set![ + "en-GB", "ga-IE", "hy-AM", "nb-NO", "nn-NO", "pt-BR", "pt-PT", "sv-SE", "zh-CN", "zh-TW" +]; diff --git a/qt/launcher-gui/src-tauri/src/main.rs b/qt/launcher-gui/src-tauri/src/main.rs new file mode 100644 index 000000000..ad0e86800 --- /dev/null +++ b/qt/launcher-gui/src-tauri/src/main.rs @@ -0,0 +1,38 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +mod app; +mod commands; +mod error; +mod generated; +mod lang; +mod platform; +mod uv; + +fn main() { + let Some(state) = uv::init_state().unwrap() else { + // either anki was spawned or os not supported (TODO) + return; + }; + + tauri::Builder::default() + .plugin(tauri_plugin_os::init()) + .plugin( + tauri_plugin_log::Builder::new() + .clear_targets() + .target(tauri_plugin_log::Target::new( + tauri_plugin_log::TargetKind::Stdout, + )) + .level(tauri_plugin_log::log::LevelFilter::Trace) + .build(), + ) + .plugin(tauri_plugin_single_instance::init(app::on_second_instance)) + .setup(|app| Ok(app::setup(app, state)?)) + .register_asynchronous_uri_scheme_protocol(app::PROTOCOL, app::serve) + // .invoke_handler(tauri::generate_handler![]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/qt/launcher-gui/src-tauri/src/platform/mac.rs b/qt/launcher-gui/src-tauri/src/platform/mac.rs new file mode 100644 index 000000000..3f5b0ce2e --- /dev/null +++ b/qt/launcher-gui/src-tauri/src/platform/mac.rs @@ -0,0 +1,99 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::io; +use std::io::Write; +use std::path::Path; +use std::process::Command; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +use anki_process::CommandExt as AnkiCommandExt; +use anyhow::Context; +use anyhow::Result; + +pub fn prepare_for_launch_after_update(mut cmd: Command, root: &Path) -> Result<()> { + // Pre-validate by running --version to trigger any Gatekeeper checks + print!("\n\x1B[1mThis may take a few minutes. Please wait\x1B[0m"); + io::stdout().flush().unwrap(); + + // Start progress indicator + let running = Arc::new(AtomicBool::new(true)); + let running_clone = running.clone(); + let progress_thread = thread::spawn(move || { + while running_clone.load(Ordering::Relaxed) { + print!("."); + io::stdout().flush().unwrap(); + thread::sleep(Duration::from_secs(1)); + } + }); + + let _ = cmd + .env("ANKI_FIRST_RUN", "1") + .arg("--version") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .ensure_success(); + + if cfg!(target_os = "macos") { + // older Anki versions had a short mpv timeout and didn't support + // ANKI_FIRST_RUN, so we need to ensure mpv passes Gatekeeper + // validation prior to launch + let mpv_path = root.join(".venv/lib/python3.9/site-packages/anki_audio/mpv"); + if mpv_path.exists() { + let _ = Command::new(&mpv_path) + .arg("--version") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .ensure_success(); + } + } + + // Stop progress indicator + running.store(false, Ordering::Relaxed); + progress_thread.join().unwrap(); + println!(); // New line after dots + Ok(()) +} + +pub fn relaunch_in_terminal() -> Result<()> { + let current_exe = std::env::current_exe().context("Failed to get current executable path")?; + Command::new("open") + .args(["-na", "Terminal"]) + .arg(current_exe) + .env_remove("ANKI_LAUNCHER_WANT_TERMINAL") + .ensure_spawn()?; + std::process::exit(0); +} + +pub fn finalize_uninstall() { + if let Ok(exe_path) = std::env::current_exe() { + // Find the .app bundle by walking up the directory tree + let mut app_bundle_path = exe_path.as_path(); + while let Some(parent) = app_bundle_path.parent() { + if let Some(name) = parent.file_name() { + if name.to_string_lossy().ends_with(".app") { + let result = Command::new("trash").arg(parent).output(); + + match result { + Ok(output) if output.status.success() => { + println!("Anki has been uninstalled."); + return; + } + _ => { + // Fall back to manual instructions + println!( + "Please manually drag Anki.app to the trash to complete uninstall." + ); + } + } + return; + } + } + app_bundle_path = parent; + } + } +} diff --git a/qt/launcher-gui/src-tauri/src/platform/mod.rs b/qt/launcher-gui/src-tauri/src/platform/mod.rs new file mode 100644 index 000000000..20fbbc5e5 --- /dev/null +++ b/qt/launcher-gui/src-tauri/src/platform/mod.rs @@ -0,0 +1,71 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +#[cfg(all(unix, not(target_os = "macos")))] +pub mod unix; + +#[cfg(target_os = "macos")] +pub mod mac; + +#[cfg(target_os = "windows")] +pub mod windows; + +use std::path::PathBuf; + +use anki_process::CommandExt; +use anyhow::Context; +use anyhow::Result; + +pub fn get_exe_and_resources_dirs() -> Result<(PathBuf, PathBuf)> { + let exe_dir = std::env::current_exe() + .context("Failed to get current executable path")? + .parent() + .context("Failed to get executable directory")? + .to_owned(); + + let resources_dir = if cfg!(target_os = "macos") { + // On macOS, resources are in ../Resources relative to the executable + exe_dir + .parent() + .context("Failed to get parent directory")? + .join("Resources") + } else { + // On other platforms, resources are in the same directory as executable + exe_dir.clone() + }; + + Ok((exe_dir, resources_dir)) +} + +pub fn get_uv_binary_name() -> &'static str { + if cfg!(target_os = "windows") { + "uv.exe" + } else if cfg!(target_os = "macos") { + "uv" + } else if cfg!(target_arch = "x86_64") { + "uv.amd64" + } else { + "uv.arm64" + } +} + +pub fn launch_anki_normally(mut cmd: std::process::Command) -> Result<()> { + #[cfg(windows)] + { + crate::platform::windows::prepare_to_launch_normally(); + cmd.ensure_spawn()?; + } + #[cfg(unix)] + cmd.ensure_spawn()?; + Ok(()) +} + +pub fn ensure_os_supported() -> Result<()> { + #[cfg(all(unix, not(target_os = "macos")))] + unix::ensure_glibc_supported()?; + + #[cfg(target_os = "windows")] + windows::ensure_windows_version_supported()?; + + Ok(()) +} diff --git a/qt/launcher-gui/src-tauri/src/platform/unix.rs b/qt/launcher-gui/src-tauri/src/platform/unix.rs new file mode 100644 index 000000000..29e860033 --- /dev/null +++ b/qt/launcher-gui/src-tauri/src/platform/unix.rs @@ -0,0 +1,53 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use anyhow::Result; + +pub fn finalize_uninstall() { + use std::io::stdin; + use std::io::stdout; + use std::io::Write; + + let uninstall_script = std::path::Path::new("/usr/local/share/anki/uninstall.sh"); + + if uninstall_script.exists() { + println!("To finish uninstalling, run 'sudo /usr/local/share/anki/uninstall.sh'"); + } else { + println!("Anki has been uninstalled."); + } + println!("Press enter to quit."); + let _ = stdout().flush(); + let mut input = String::new(); + let _ = stdin().read_line(&mut input); +} + +pub fn ensure_glibc_supported() -> Result<()> { + use std::ffi::CStr; + let get_glibc_version = || -> Option<(u32, u32)> { + let version_ptr = unsafe { libc::gnu_get_libc_version() }; + if version_ptr.is_null() { + return None; + } + + let version_cstr = unsafe { CStr::from_ptr(version_ptr) }; + let version_str = version_cstr.to_str().ok()?; + + // Parse version string (format: "2.36" or "2.36.1") + let version_parts: Vec<&str> = version_str.split('.').collect(); + if version_parts.len() < 2 { + return None; + } + + let major: u32 = version_parts[0].parse().ok()?; + let minor: u32 = version_parts[1].parse().ok()?; + + Some((major, minor)) + }; + + let (major, minor) = get_glibc_version().unwrap_or_default(); + if major < 2 || (major == 2 && minor < 36) { + anyhow::bail!("Anki requires a modern Linux distro with glibc 2.36 or later."); + } + + Ok(()) +} diff --git a/qt/launcher-gui/src-tauri/src/platform/windows.rs b/qt/launcher-gui/src-tauri/src/platform/windows.rs new file mode 100644 index 000000000..72725058f --- /dev/null +++ b/qt/launcher-gui/src-tauri/src/platform/windows.rs @@ -0,0 +1,180 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::io::stdin; +use std::process::Command; + +use anyhow::Context; +use anyhow::Result; +use widestring::u16cstr; +use windows::core::PCWSTR; +use windows::Wdk::System::SystemServices::RtlGetVersion; +use windows::Win32::System::Console::AttachConsole; +use windows::Win32::System::Console::GetConsoleWindow; +use windows::Win32::System::Console::ATTACH_PARENT_PROCESS; +use windows::Win32::System::Registry::RegCloseKey; +use windows::Win32::System::Registry::RegOpenKeyExW; +use windows::Win32::System::Registry::RegQueryValueExW; +use windows::Win32::System::Registry::HKEY; +use windows::Win32::System::Registry::HKEY_CURRENT_USER; +use windows::Win32::System::Registry::KEY_READ; +use windows::Win32::System::Registry::REG_SZ; +use windows::Win32::System::SystemInformation::OSVERSIONINFOW; +use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID; + +/// Returns true if running on Windows 10 (not Windows 11) +fn is_windows_10() -> bool { + unsafe { + let mut info = OSVERSIONINFOW { + dwOSVersionInfoSize: std::mem::size_of::() as u32, + ..Default::default() + }; + if RtlGetVersion(&mut info).is_ok() { + // Windows 10 has build numbers < 22000, Windows 11 >= 22000 + info.dwBuildNumber < 22000 && info.dwMajorVersion == 10 + } else { + false + } + } +} + +/// Ensures Windows 10 version 1809 or later +pub fn ensure_windows_version_supported() -> Result<()> { + unsafe { + let mut info = OSVERSIONINFOW { + dwOSVersionInfoSize: std::mem::size_of::() as u32, + ..Default::default() + }; + + if RtlGetVersion(&mut info).is_err() { + anyhow::bail!("Failed to get Windows version information"); + } + + if info.dwBuildNumber >= 17763 { + return Ok(()); + } + + anyhow::bail!("Windows 10 version 1809 or later is required.") + } +} + +pub fn finalize_uninstall() { + let uninstaller_path = get_uninstaller_path(); + + match uninstaller_path { + Some(path) => { + println!("Launching Windows uninstaller..."); + let result = Command::new(&path).env("ANKI_LAUNCHER", "1").spawn(); + + match result { + Ok(_) => { + println!("Uninstaller launched successfully."); + return; + } + Err(e) => { + println!("Failed to launch uninstaller: {e}"); + println!("You can manually run: {}", path.display()); + } + } + } + None => { + println!("Windows uninstaller not found."); + println!("You may need to uninstall via Windows Settings > Apps."); + } + } + println!("Press enter to close..."); + let mut input = String::new(); + let _ = stdin().read_line(&mut input); +} + +fn get_uninstaller_path() -> Option { + // Try to read install directory from registry + if let Some(install_dir) = read_registry_install_dir() { + let uninstaller = install_dir.join("uninstall.exe"); + if uninstaller.exists() { + return Some(uninstaller); + } + } + + // Fall back to default location + let default_dir = dirs::data_local_dir()?.join("Programs").join("Anki"); + let uninstaller = default_dir.join("uninstall.exe"); + if uninstaller.exists() { + return Some(uninstaller); + } + + None +} + +fn read_registry_install_dir() -> Option { + unsafe { + let mut hkey = HKEY::default(); + + // Convert the registry path to wide string + let subkey = u16cstr!("SOFTWARE\\Anki"); + + // Open the registry key + let result = RegOpenKeyExW( + HKEY_CURRENT_USER, + PCWSTR(subkey.as_ptr()), + Some(0), + KEY_READ, + &mut hkey, + ); + + if result.is_err() { + return None; + } + + // Query the Install_Dir64 value + let value_name = u16cstr!("Install_Dir64"); + + let mut value_type = REG_SZ; + let mut data_size = 0u32; + + // First call to get the size + let result = RegQueryValueExW( + hkey, + PCWSTR(value_name.as_ptr()), + None, + Some(&mut value_type), + None, + Some(&mut data_size), + ); + + if result.is_err() || data_size == 0 { + let _ = RegCloseKey(hkey); + return None; + } + + // Allocate buffer and read the value + let mut buffer: Vec = vec![0; (data_size / 2) as usize]; + let result = RegQueryValueExW( + hkey, + PCWSTR(value_name.as_ptr()), + None, + Some(&mut value_type), + Some(buffer.as_mut_ptr() as *mut u8), + Some(&mut data_size), + ); + + let _ = RegCloseKey(hkey); + + if result.is_ok() { + // Convert wide string back to PathBuf + let len = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len()); + let path_str = String::from_utf16_lossy(&buffer[..len]); + Some(std::path::PathBuf::from(path_str)) + } else { + None + } + } +} + +pub fn prepare_to_launch_normally() { + // Set the App User Model ID for Windows taskbar grouping + unsafe { + let _ = + SetCurrentProcessExplicitAppUserModelID(PCWSTR(u16cstr!("Ankitects.Anki").as_ptr())); + } +} diff --git a/qt/launcher-gui/src-tauri/src/uv.rs b/qt/launcher-gui/src-tauri/src/uv.rs new file mode 100644 index 000000000..05212dcfd --- /dev/null +++ b/qt/launcher-gui/src-tauri/src/uv.rs @@ -0,0 +1,930 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::io::stdin; +use std::io::stdout; +use std::io::Write; +use std::process::Command; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; + +use anki_io::copy_file; +use anki_io::create_dir_all; +use anki_io::modified_time; +use anki_io::read_file; +use anki_io::remove_file; +use anki_io::write_file; +use anki_io::ToUtf8Path; +use anki_process::CommandExt as AnkiCommandExt; +use anyhow::anyhow; +use anyhow::Context; +use anyhow::Result; +use tauri::AppHandle; +use tauri::Emitter; +use tauri::Runtime; + +use crate::platform; +use crate::platform::ensure_os_supported; +use crate::platform::get_exe_and_resources_dirs; +use crate::platform::get_uv_binary_name; +pub use crate::platform::launch_anki_normally; + +#[derive(Debug, Clone)] +pub struct State { + pub current_version: Option, + pub prerelease_marker: std::path::PathBuf, + uv_install_root: std::path::PathBuf, + uv_cache_dir: std::path::PathBuf, + pub no_cache_marker: std::path::PathBuf, + anki_base_folder: std::path::PathBuf, + uv_path: std::path::PathBuf, + uv_python_install_dir: std::path::PathBuf, + user_pyproject_path: std::path::PathBuf, + user_python_version_path: std::path::PathBuf, + dist_pyproject_path: std::path::PathBuf, + dist_python_version_path: std::path::PathBuf, + uv_lock_path: std::path::PathBuf, + sync_complete_marker: std::path::PathBuf, + launcher_trigger_file: std::path::PathBuf, + pub mirror_path: std::path::PathBuf, + pub pyproject_modified_by_user: bool, + previous_version: Option, + resources_dir: std::path::PathBuf, + venv_folder: std::path::PathBuf, + /// system Python + PyQt6 library mode + system_qt: bool, +} + +#[derive(Debug, Clone)] +pub enum VersionKind { + PyOxidizer(String), + Uv(String), +} + +#[derive(Debug)] +pub struct Releases { + pub latest: Vec, + pub all: Vec, +} + +pub fn init_state() -> Result> { + let uv_install_root = if let Ok(custom_root) = std::env::var("ANKI_LAUNCHER_VENV_ROOT") { + std::path::PathBuf::from(custom_root) + } else { + dirs::data_local_dir() + .context("Unable to determine data_dir")? + .join("AnkiProgramFiles") + }; + + let (exe_dir, resources_dir) = get_exe_and_resources_dirs()?; + + let mut state = State { + // TODO: return error instead of relying on member field here if os unsupported + current_version: None, + prerelease_marker: uv_install_root.join("prerelease"), + uv_install_root: uv_install_root.clone(), + uv_cache_dir: uv_install_root.join("cache"), + no_cache_marker: uv_install_root.join("nocache"), + anki_base_folder: get_anki_base_path()?, + uv_path: exe_dir.join(get_uv_binary_name()), + uv_python_install_dir: uv_install_root.join("python"), + user_pyproject_path: uv_install_root.join("pyproject.toml"), + user_python_version_path: uv_install_root.join(".python-version"), + dist_pyproject_path: resources_dir.join("pyproject.toml"), + dist_python_version_path: resources_dir.join(".python-version"), + uv_lock_path: uv_install_root.join("uv.lock"), + sync_complete_marker: uv_install_root.join(".sync_complete"), + launcher_trigger_file: uv_install_root.join(".want-launcher"), + mirror_path: uv_install_root.join("mirror"), + pyproject_modified_by_user: false, // calculated later + previous_version: None, + system_qt: (cfg!(unix) && !cfg!(target_os = "macos")) + && resources_dir.join("system_qt").exists(), + resources_dir, + venv_folder: uv_install_root.join(".venv"), + }; + + // Check for uninstall request from Windows uninstaller + if std::env::var("ANKI_LAUNCHER_UNINSTALL").is_ok() { + // handle_uninstall(&state)?; + println!("TODO: UNINSTALL"); + return Ok(None); + } + + // Create install directory + create_dir_all(&state.uv_install_root)?; + + let launcher_requested = + state.launcher_trigger_file.exists() || !state.user_pyproject_path.exists(); + + // Calculate whether user has custom edits that need syncing + let pyproject_time = file_timestamp_secs(&state.user_pyproject_path); + let sync_time = file_timestamp_secs(&state.sync_complete_marker); + state.pyproject_modified_by_user = pyproject_time > sync_time; + let pyproject_has_changed = state.pyproject_modified_by_user; + + let debug = cfg!(debug_assertions); + + if !launcher_requested && !pyproject_has_changed && !debug { + let args: Vec = std::env::args().skip(1).collect(); + let cmd = build_python_command(&state, &args)?; + launch_anki_normally(cmd)?; + return Ok(None); + } + + if launcher_requested { + // Remove the trigger file to make request ephemeral + let _ = remove_file(&state.launcher_trigger_file); + } + + // TODO: + let _ = ensure_os_supported(); + + // TODO: we should call this here instead of via getVersions + // check_versions(&mut state); + + Ok(Some(state)) +} + +pub fn post_install(state: &State) -> Result<()> { + // Write marker file to indicate we've completed the sync process + write_sync_marker(state)?; + + #[cfg(target_os = "macos")] + { + let cmd = build_python_command(&state, &[])?; + platform::mac::prepare_for_launch_after_update(cmd, &uv_install_root)?; + } + + // respawn the launcher as a disconnected subprocess for normal startup + // respawn_launcher()?; + let args: Vec = std::env::args().skip(1).collect(); + let cmd = build_python_command(state, &args)?; + launch_anki_normally(cmd)?; + + Ok(()) +} + +fn extract_aqt_version(state: &State) -> Option { + // Check if .venv exists first + if !state.venv_folder.exists() { + return None; + } + + let output = uv_command(state) + .ok()? + .env("VIRTUAL_ENV", &state.venv_folder) + .args(["pip", "show", "aqt"]) + .output(); + + let output = output.ok()?; + + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8(output.stdout).ok()?; + for line in stdout.lines() { + if let Some(version) = line.strip_prefix("Version: ") { + return Some(version.trim().to_string()); + } + } + None +} + +pub fn check_versions(state: &mut State) -> Result<(Option, Option)> { + // If sync_complete_marker is missing, do nothing + if !state.sync_complete_marker.exists() { + return Ok((None, None)); + } + + // Determine current version by invoking uv pip show aqt + match extract_aqt_version(state) { + Some(version) => { + state.current_version = Some(normalize_version(&version)); + } + None => { + Err(anyhow::anyhow!( + "Warning: Could not determine current Anki version" + ))?; + } + } + + // Read previous version from "previous-version" file + let previous_version_path = state.uv_install_root.join("previous-version"); + if let Ok(content) = read_file(&previous_version_path) { + if let Ok(version_str) = String::from_utf8(content) { + let version = version_str.trim().to_string(); + if !version.is_empty() { + state.previous_version = Some(normalize_version(&version)); + } + } + } + + Ok(( + state.current_version.clone(), + state.previous_version.clone(), + )) +} + +pub fn handle_version_install_or_update( + app: AppHandle, + state: &State, + version: &str, + keep_existing: bool, +) -> Result<()> { + let version_kind = + parse_version_kind(version).ok_or_else(|| anyhow!("{version} is not a valid version!"))?; + if !keep_existing { + apply_version_kind(&version_kind, state)?; + } + + // Extract current version before syncing (but don't write to file yet) + let previous_version_to_save = state.current_version.clone(); + + // Remove sync marker before attempting sync + let _ = remove_file(&state.sync_complete_marker); + + let python_version_trimmed = if state.user_python_version_path.exists() { + let python_version = read_file(&state.user_python_version_path)?; + let python_version_str = + String::from_utf8(python_version).context("Invalid UTF-8 in .python-version")?; + Some(python_version_str.trim().to_string()) + } else { + None + }; + + // Prepare to sync the venv + let mut command = uv_pty_command(state)?; + + if cfg!(target_os = "macos") { + // remove CONDA_PREFIX/bin from PATH to avoid conda interference + if let Ok(conda_prefix) = std::env::var("CONDA_PREFIX") { + if let Ok(current_path) = std::env::var("PATH") { + let conda_bin = format!("{conda_prefix}/bin"); + let filtered_paths: Vec<&str> = current_path + .split(':') + .filter(|&path| path != conda_bin) + .collect(); + let new_path = filtered_paths.join(":"); + command.env("PATH", new_path); + } + } + // put our fake install_name_tool at the top of the path to override + // potential conflicts + if let Ok(current_path) = std::env::var("PATH") { + let exe_dir = std::env::current_exe() + .ok() + .and_then(|exe| exe.parent().map(|p| p.to_path_buf())); + if let Some(exe_dir) = exe_dir { + let new_path = format!("{}:{}", exe_dir.display(), current_path); + command.env("PATH", new_path); + } + } + } + + // Create venv with system site packages if system Qt is enabled + if state.system_qt { + let mut venv_command = uv_command(state)?; + venv_command.args([ + "venv", + "--no-managed-python", + "--system-site-packages", + "--no-config", + ]); + venv_command.ensure_success()?; + } + + command.env("UV_CACHE_DIR", &state.uv_cache_dir); + command.env("UV_PYTHON_INSTALL_DIR", &state.uv_python_install_dir); + command.env( + "UV_HTTP_TIMEOUT", + std::env::var("UV_HTTP_TIMEOUT").unwrap_or_else(|_| "180".to_string()), + ); + + command.args(["sync", "--upgrade", "--no-config"]); + if !state.system_qt { + command.arg("--managed-python"); + } + + // Add python version if .python-version file exists (but not for system Qt) + if let Some(version) = &python_version_trimmed { + if !state.system_qt { + command.args(["--python", version]); + } + } + + if state.no_cache_marker.exists() { + command.env("UV_NO_CACHE", "1"); + } + + // NOTE: pty and child must live in the same thread + let pty_system = portable_pty::NativePtySystem::default(); + + use portable_pty::PtySystem; + let pair = pty_system + .openpty(portable_pty::PtySize { + // NOTE: must be the same as xterm.js', otherwise text won't wrap + // TODO: maybe don't hardcode? + rows: 12, + cols: 60, + pixel_width: 0, + pixel_height: 0, + }) + .unwrap(); + + let mut reader = pair.master.try_clone_reader().unwrap(); + let mut writer = pair.master.take_writer().unwrap(); + + tauri::async_runtime::spawn_blocking(move || { + let mut buf = [0u8; 1024]; + loop { + let res = reader.read(&mut buf); + match res { + // EOF + Ok(0) => break, + Ok(n) => { + let output = String::from_utf8_lossy(&buf[..n]).to_string(); + // NOTE: windows requests curspr position before actually running child + if output == "\x1b[6n" { + writeln!(&mut writer, "\x1b[0;0R").unwrap(); + } + // cheaper to base64ise a string than jsonify an [u8] + let data = data_encoding::BASE64.encode(&buf[..n]); + let _ = app.emit("pty-data", data); + } + Err(e) => { + eprintln!("Error reading from PTY: {}", e); + break; + } + } + } + }); + + let mut child = pair.slave.spawn_command(command).unwrap(); + drop(pair.slave); + println!("waiting on uv..."); + let status = child.wait(); + println!("uv exited with status: {:?}", status); + + match status { + Ok(_) => { + // Sync succeeded + if !keep_existing && matches!(version_kind, VersionKind::PyOxidizer(_)) { + inject_helper_addon()?; + } + + // Now that sync succeeded, save the previous version + if let Some(current_version) = previous_version_to_save { + let previous_version_path = state.uv_install_root.join("previous-version"); + if let Err(e) = write_file(&previous_version_path, ¤t_version) { + // TODO: + println!("Warning: Could not save previous version: {e}"); + } + } + + Ok(()) + } + Err(e) => { + // TODO: + // If sync fails due to things like a missing wheel on pypi, + // we need to remove the lockfile or uv will cache the bad result. + let _ = remove_file(&state.uv_lock_path); + println!("Install failed: {e:#}"); + println!(); + Err(e.into()) + } + } +} + +pub fn set_allow_betas(state: &State, allow_betas: bool) -> Result<()> { + if allow_betas { + write_file(&state.prerelease_marker, "")?; + } else { + let _ = remove_file(&state.prerelease_marker); + } + Ok(()) +} + +pub fn set_cache_enabled(state: &State, cache_enabled: bool) -> Result<()> { + if cache_enabled { + let _ = remove_file(&state.no_cache_marker); + } else { + write_file(&state.no_cache_marker, "")?; + // Delete the cache directory and everything in it + if state.uv_cache_dir.exists() { + let _ = anki_io::remove_dir_all(&state.uv_cache_dir); + } + } + Ok(()) +} + +fn write_sync_marker(state: &State) -> Result<()> { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .context("Failed to get system time")? + .as_secs(); + write_file(&state.sync_complete_marker, timestamp.to_string())?; + Ok(()) +} + +/// Get mtime of provided file, or 0 if unavailable +fn file_timestamp_secs(path: &std::path::Path) -> i64 { + modified_time(path) + .map(|t| t.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs() as i64) + .unwrap_or_default() +} + +fn with_only_latest_patch(versions: &[String]) -> Vec { + // Only show the latest patch release for a given (major, minor) + let mut seen_major_minor = std::collections::HashSet::new(); + versions + .iter() + .filter(|v| { + let (major, minor, _, _) = parse_version_for_filtering(v); + if major == 2 { + return true; + } + let major_minor = (major, minor); + if seen_major_minor.contains(&major_minor) { + false + } else { + seen_major_minor.insert(major_minor); + true + } + }) + .cloned() + .collect() +} + +fn parse_version_for_filtering(version_str: &str) -> (u32, u32, u32, bool) { + // Remove any build metadata after + + let version_str = version_str.split('+').next().unwrap_or(version_str); + + // Check for prerelease markers + let is_prerelease = ["a", "b", "rc", "alpha", "beta"] + .iter() + .any(|marker| version_str.to_lowercase().contains(marker)); + + // Extract numeric parts (stop at first non-digit/non-dot character) + let numeric_end = version_str + .find(|c: char| !c.is_ascii_digit() && c != '.') + .unwrap_or(version_str.len()); + let numeric_part = &version_str[..numeric_end]; + + let parts: Vec<&str> = numeric_part.split('.').collect(); + + let major = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0); + let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); + let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0); + + (major, minor, patch, is_prerelease) +} + +fn normalize_version(version: &str) -> String { + let (major, minor, patch, _is_prerelease) = parse_version_for_filtering(version); + + if major <= 2 { + // Don't transform versions <= 2.x + return version.to_string(); + } + + // For versions > 2, pad the minor version with leading zero if < 10 + let normalized_minor = if minor < 10 { + format!("0{minor}") + } else { + minor.to_string() + }; + + // Find any prerelease suffix + let mut prerelease_suffix = ""; + + // Look for prerelease markers after the numeric part + let numeric_end = version + .find(|c: char| !c.is_ascii_digit() && c != '.') + .unwrap_or(version.len()); + if numeric_end < version.len() { + let suffix_part = &version[numeric_end..]; + let suffix_lower = suffix_part.to_lowercase(); + + for marker in ["alpha", "beta", "rc", "a", "b"] { + if suffix_lower.starts_with(marker) { + prerelease_suffix = &version[numeric_end..]; + break; + } + } + } + + // Reconstruct the version + if version.matches('.').count() >= 2 { + format!("{major}.{normalized_minor}.{patch}{prerelease_suffix}") + } else { + format!("{major}.{normalized_minor}{prerelease_suffix}") + } +} + +fn filter_and_normalize_versions( + all_versions: Vec, + include_prereleases: bool, +) -> Vec { + let mut valid_versions: Vec = all_versions + .into_iter() + .map(|v| normalize_version(&v)) + .collect(); + + // Reverse to get chronological order (newest first) + valid_versions.reverse(); + + if !include_prereleases { + valid_versions.retain(|v| { + let (_, _, _, is_prerelease) = parse_version_for_filtering(v); + !is_prerelease + }); + } + + valid_versions +} + +fn fetch_versions(state: &State) -> Result> { + let versions_script = state.resources_dir.join("versions.py"); + + let mut cmd = uv_command(state)?; + cmd.args(["run", "--no-project", "--no-config", "--managed-python"]) + .args(["--with", "pip-system-certs,requests[socks]"]); + + let python_version = read_file(&state.dist_python_version_path)?; + let python_version_str = + String::from_utf8(python_version).context("Invalid UTF-8 in .python-version")?; + let version_trimmed = python_version_str.trim(); + if !version_trimmed.is_empty() { + cmd.args(["--python", version_trimmed]); + } + + cmd.arg(&versions_script); + + let output = match cmd.utf8_output() { + Ok(output) => output, + Err(e) => { + return Err(e.into()); + } + }; + let versions = serde_json::from_str(&output.stdout).context("Failed to parse versions JSON")?; + Ok(versions) +} + +pub fn get_releases(state: &State) -> Result { + let include_prereleases = state.prerelease_marker.exists(); + let all_versions = fetch_versions(state)?; + let all_versions = filter_and_normalize_versions(all_versions, include_prereleases); + + let latest_patches = with_only_latest_patch(&all_versions); + let latest_releases: Vec = latest_patches.into_iter().take(5).collect(); + Ok(Releases { + latest: latest_releases, + all: all_versions, + }) +} + +pub fn apply_version_kind(version_kind: &VersionKind, state: &State) -> Result<()> { + let content = read_file(&state.dist_pyproject_path)?; + let content_str = String::from_utf8(content).context("Invalid UTF-8 in pyproject.toml")?; + let updated_content = match version_kind { + VersionKind::PyOxidizer(version) => { + // Replace package name and add PyQt6 dependencies + content_str.replace( + "anki-release", + &format!( + concat!( + "aqt[qt6]=={}\",\n", + " \"anki-audio==0.1.0; sys.platform == 'win32' or sys.platform == 'darwin'\",\n", + " \"pyqt6==6.6.1\",\n", + " \"pyqt6-qt6==6.6.2\",\n", + " \"pyqt6-webengine==6.6.0\",\n", + " \"pyqt6-webengine-qt6==6.6.2\",\n", + " \"pyqt6_sip==13.6.0" + ), + version + ), + ) + } + VersionKind::Uv(version) => content_str.replace( + "anki-release", + &format!("anki-release=={version}\",\n \"anki=={version}\",\n \"aqt=={version}"), + ), + }; + + let final_content = if state.system_qt { + format!( + concat!( + "{}\n\n[tool.uv]\n", + "override-dependencies = [\n", + " \"pyqt6; sys_platform=='never'\",\n", + " \"pyqt6-qt6; sys_platform=='never'\",\n", + " \"pyqt6-webengine; sys_platform=='never'\",\n", + " \"pyqt6-webengine-qt6; sys_platform=='never'\",\n", + " \"pyqt6_sip; sys_platform=='never'\"\n", + "]\n" + ), + updated_content + ) + } else { + updated_content + }; + + write_file(&state.user_pyproject_path, &final_content)?; + + // Update .python-version based on version kind + match version_kind { + VersionKind::PyOxidizer(_) => { + write_file(&state.user_python_version_path, "3.9")?; + } + VersionKind::Uv(_) => { + copy_file( + &state.dist_python_version_path, + &state.user_python_version_path, + )?; + } + } + Ok(()) +} + +pub fn parse_version_kind(version: &str) -> Option { + let numeric_chars: String = version + .chars() + .filter(|c| c.is_ascii_digit() || *c == '.') + .collect(); + + let parts: Vec<&str> = numeric_chars.split('.').collect(); + + if parts.len() < 2 { + return None; + } + + let major: u32 = match parts[0].parse() { + Ok(val) => val, + Err(_) => return None, + }; + + let minor: u32 = match parts[1].parse() { + Ok(val) => val, + Err(_) => return None, + }; + + let patch: u32 = if parts.len() >= 3 { + match parts[2].parse() { + Ok(val) => val, + Err(_) => return None, + } + } else { + 0 // Default patch to 0 if not provided + }; + + // Reject versions < 2.1.50 + if major == 2 && (minor != 1 || patch < 50) { + return None; + } + + if major < 25 || (major == 25 && minor < 6) { + Some(VersionKind::PyOxidizer(version.to_string())) + } else { + Some(VersionKind::Uv(version.to_string())) + } +} + +fn inject_helper_addon() -> Result<()> { + let addons21_path = get_anki_addons21_path()?; + + if !addons21_path.exists() { + return Ok(()); + } + + let addon_folder = addons21_path.join("anki-launcher"); + + // Remove existing anki-launcher folder if it exists + if addon_folder.exists() { + anki_io::remove_dir_all(&addon_folder)?; + } + + // Create the anki-launcher folder + create_dir_all(&addon_folder)?; + + // Write the embedded files + let init_py_content = include_str!("../../../launcher/addon/__init__.py"); + let manifest_json_content = include_str!("../../../launcher/addon/manifest.json"); + + write_file(addon_folder.join("__init__.py"), init_py_content)?; + write_file(addon_folder.join("manifest.json"), manifest_json_content)?; + + Ok(()) +} + +fn get_anki_base_path() -> Result { + let anki_base_path = if cfg!(target_os = "windows") { + // Windows: %APPDATA%\Anki2 + dirs::config_dir() + .context("Unable to determine config directory")? + .join("Anki2") + } else if cfg!(target_os = "macos") { + // macOS: ~/Library/Application Support/Anki2 + dirs::data_dir() + .context("Unable to determine data directory")? + .join("Anki2") + } else { + // Linux: ~/.local/share/Anki2 + dirs::data_dir() + .context("Unable to determine data directory")? + .join("Anki2") + }; + + Ok(anki_base_path) +} + +fn get_anki_addons21_path() -> Result { + Ok(get_anki_base_path()?.join("addons21")) +} + +// TODO: revert +#[allow(unused)] +fn handle_uninstall(state: &State) -> Result { + // println!("{}", state.tr.launcher_uninstall_confirm()); + print!("> "); + let _ = stdout().flush(); + + let mut input = String::new(); + let _ = stdin().read_line(&mut input); + let input = input.trim().to_lowercase(); + + if input != "y" { + // println!("{}", state.tr.launcher_uninstall_cancelled()); + println!(); + return Ok(false); + } + + // Remove program files + if state.uv_install_root.exists() { + anki_io::remove_dir_all(&state.uv_install_root)?; + // println!("{}", state.tr.launcher_program_files_removed()); + } + + println!(); + // println!("{}", state.tr.launcher_remove_all_profiles_confirm()); + print!("> "); + let _ = stdout().flush(); + + let mut input = String::new(); + let _ = stdin().read_line(&mut input); + let input = input.trim().to_lowercase(); + + if input == "y" && state.anki_base_folder.exists() { + anki_io::remove_dir_all(&state.anki_base_folder)?; + // println!("{}", state.tr.launcher_user_data_removed()); + } + + println!(); + + // Platform-specific messages + #[cfg(target_os = "macos")] + platform::mac::finalize_uninstall(); + + #[cfg(target_os = "windows")] + platform::windows::finalize_uninstall(); + + #[cfg(all(unix, not(target_os = "macos")))] + platform::unix::finalize_uninstall(); + + Ok(true) +} + +fn uv_command(state: &State) -> Result { + let mut command = Command::new(&state.uv_path); + command.current_dir(&state.uv_install_root); + + // remove UV_* environment variables to avoid interference + for (key, _) in std::env::vars() { + if key.starts_with("UV_") { + command.env_remove(key); + } + } + command + .env_remove("VIRTUAL_ENV") + .env_remove("SSLKEYLOGFILE"); + + // Add mirror environment variable if enabled + if let Some((python_mirror, pypi_mirror)) = get_mirror_urls(state)? { + command + .env("UV_PYTHON_INSTALL_MIRROR", &python_mirror) + .env("UV_DEFAULT_INDEX", &pypi_mirror); + } + + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + + command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); + } + Ok(command) +} + +fn uv_pty_command(state: &State) -> Result { + let mut command = portable_pty::CommandBuilder::new(&state.uv_path); + command.cwd(&state.uv_install_root); + + // remove UV_* environment variables to avoid interference + for (key, _) in std::env::vars() { + if key.starts_with("UV_") { + command.env_remove(key); + } + } + command.env_remove("VIRTUAL_ENV"); + command.env_remove("SSLKEYLOGFILE"); + + // Add mirror environment variable if enabled + if let Some((python_mirror, pypi_mirror)) = get_mirror_urls(state)? { + command.env("UV_PYTHON_INSTALL_MIRROR", &python_mirror); + command.env("UV_DEFAULT_INDEX", &pypi_mirror); + } + + Ok(command) +} + +pub fn build_python_command(state: &State, args: &[String]) -> Result { + let python_exe = if cfg!(target_os = "windows") { + let show_console = std::env::var("ANKI_CONSOLE").is_ok(); + if show_console { + state.venv_folder.join("Scripts/python.exe") + } else { + state.venv_folder.join("Scripts/pythonw.exe") + } + } else { + state.venv_folder.join("bin/python") + }; + + let mut cmd = Command::new(&python_exe); + cmd.args(["-c", "import aqt, sys; sys.argv[0] = 'Anki'; aqt.run()"]); + cmd.args(args); + // tell the Python code it was invoked by the launcher, and updating is + // available + cmd.env("ANKI_LAUNCHER", std::env::current_exe()?.utf8()?.as_str()); + + // Set UV and Python paths for the Python code + cmd.env("ANKI_LAUNCHER_UV", state.uv_path.utf8()?.as_str()); + cmd.env("UV_PROJECT", state.uv_install_root.utf8()?.as_str()); + cmd.env_remove("SSLKEYLOGFILE"); + + Ok(cmd) +} + +fn get_mirror_urls(state: &State) -> Result> { + if !state.mirror_path.exists() { + return Ok(None); + } + + let content = read_file(&state.mirror_path)?; + let content_str = String::from_utf8(content).context("Invalid UTF-8 in mirror file")?; + + let lines: Vec<&str> = content_str.lines().collect(); + if lines.len() >= 2 { + Ok(Some(( + lines[0].trim().to_string(), + lines[1].trim().to_string(), + ))) + } else { + Ok(None) + } +} + +pub fn set_mirror(state: &State, enabled: bool) -> Result<()> { + if enabled { + let china_mirrors = "https://registry.npmmirror.com/-/binary/python-build-standalone/\nhttps://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/"; + write_file(&state.mirror_path, china_mirrors)?; + } else if state.mirror_path.exists() { + let _ = remove_file(&state.mirror_path); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_normalize_version() { + // Test versions <= 2.x (should not be transformed) + assert_eq!(normalize_version("2.1.50"), "2.1.50"); + + // Test basic versions > 2 with zero-padding + assert_eq!(normalize_version("25.7"), "25.07"); + assert_eq!(normalize_version("25.07"), "25.07"); + assert_eq!(normalize_version("25.10"), "25.10"); + assert_eq!(normalize_version("24.6.1"), "24.06.1"); + assert_eq!(normalize_version("24.06.1"), "24.06.1"); + + // Test prerelease versions + assert_eq!(normalize_version("25.7a1"), "25.07a1"); + assert_eq!(normalize_version("25.7.1a1"), "25.07.1a1"); + + // Test versions with patch = 0 + assert_eq!(normalize_version("25.7.0"), "25.07.0"); + assert_eq!(normalize_version("25.7.0a1"), "25.07.0a1"); + } +} diff --git a/qt/launcher-gui/src-tauri/tauri.conf.json b/qt/launcher-gui/src-tauri/tauri.conf.json new file mode 100644 index 000000000..bfc46a44f --- /dev/null +++ b/qt/launcher-gui/src-tauri/tauri.conf.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "anki-launcher", + "version": "0.1.0", + "identifier": "com.ichi2.anki-launcher", + "build": { + "beforeDevCommand": "./launcher", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "./yarn lb", + "frontendDist": "../build" + }, + "app": { + "windows": [ + { + "title": "Anki Launcher", + "width": 600, + "height": 600, + "visible": true + } + ], + "security": { + "csp": { + "default-src": "'self'", + "connect-src": "anki: http://anki.localhost ipc: http://ipc.localhost tauri: http://tauri.localhost", + "img-src": "data: 'self'", + "style-src": "'self' 'unsafe-inline'" + } + } + }, + "bundle": { + "active": false, + "targets": [], + "icon": [ + "../../launcher/lin/anki.png", + "icons/icon.ico" + ] + } +} diff --git a/qt/launcher-gui/src/app.html b/qt/launcher-gui/src/app.html new file mode 100644 index 000000000..b338b1f81 --- /dev/null +++ b/qt/launcher-gui/src/app.html @@ -0,0 +1,13 @@ + + + + + + + Anki Launcher + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/qt/launcher-gui/src/routes/+layout.svelte b/qt/launcher-gui/src/routes/+layout.svelte new file mode 100644 index 000000000..cd943cbe7 --- /dev/null +++ b/qt/launcher-gui/src/routes/+layout.svelte @@ -0,0 +1,26 @@ + + + + diff --git a/qt/launcher-gui/src/routes/+layout.ts b/qt/launcher-gui/src/routes/+layout.ts new file mode 100644 index 000000000..d9484bbfe --- /dev/null +++ b/qt/launcher-gui/src/routes/+layout.ts @@ -0,0 +1,18 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +// import { checkNightMode } from "@tslib/nightmode"; +import type { LayoutLoad } from "./$types"; + +// Tauri doesn't have a Node.js server to do proper SSR +// so we use adapter-static with a fallback to index.html to put the site in SPA mode +// See: https://svelte.dev/docs/kit/single-page-apps +// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info +export const ssr = false; + +export const load: LayoutLoad = async () => { + // checkNightMode(); + // TODO: don't force nightmode + document.documentElement.className = "night-mode"; + document.documentElement.dataset.bsTheme = "dark"; +}; diff --git a/qt/launcher-gui/src/routes/+page.svelte b/qt/launcher-gui/src/routes/+page.svelte new file mode 100644 index 000000000..600662bcd --- /dev/null +++ b/qt/launcher-gui/src/routes/+page.svelte @@ -0,0 +1,47 @@ + + + + + + diff --git a/qt/launcher-gui/src/routes/+page.ts b/qt/launcher-gui/src/routes/+page.ts new file mode 100644 index 000000000..7d2ac7a88 --- /dev/null +++ b/qt/launcher-gui/src/routes/+page.ts @@ -0,0 +1,27 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import { getLangs, getMirrors, getOptions, getVersions } from "@generated/backend-launcher"; +import { ModuleName, setupI18n } from "@tslib/i18n"; +import type { PageLoad } from "./$types"; +import { versionsStore } from "./stores"; + +export const load = (async () => { + const i18nPromise = setupI18n({ modules: [ModuleName.LAUNCHER] }, true); + const langsPromise = getLangs({}); + const optionsPromise = getOptions({}); + const mirrorsPromise = getMirrors({}); + + getVersions({}).then((res) => { + versionsStore.set(res); + }); + + const [_, { userLocale, langs }, options, { mirrors }] = await Promise.all([ + i18nPromise, + langsPromise, + optionsPromise, + mirrorsPromise, + ]); + + return { langs, userLocale, options, mirrors }; +}) satisfies PageLoad; diff --git a/qt/launcher-gui/src/routes/Start.svelte b/qt/launcher-gui/src/routes/Start.svelte new file mode 100644 index 000000000..cd7dfb95e --- /dev/null +++ b/qt/launcher-gui/src/routes/Start.svelte @@ -0,0 +1,315 @@ + + + + + + + + + + + + + + + + {#key $currentLang} + + + + +

{tr.launcherTitle()}

+
+ + + {tr.launcherLanguage()} + + +
+ {#if latestVersion != null && latestVersion != currentVersion} + + + + {/if} + {#if currentVersion != null} + + + + {/if} + + +
+ {"->"} +
+
+ {#if availableVersions.length !== 0} + + {:else} + {"loading"} + {/if} +
+
+
+
+
+ {#await choosePromise} + + {:then res} + {#if res != null} + + {/if} + {/await} + {/key} + +
+ {#key $currentLang} + {tr.launcherOutput()} + {/key} +
+
+
+ {#key $currentLang} + + +
+ + + {tr.launcherAllowBetasToggle()} + + +
+
+ + + {tr.launcherDownloadCaching()} + + +
+
+ + + {tr.launcherUseMirror()} + + +
+
+
+ {/key} +
+ + diff --git a/qt/launcher-gui/src/routes/Warning.svelte b/qt/launcher-gui/src/routes/Warning.svelte new file mode 100644 index 000000000..f7549e6ca --- /dev/null +++ b/qt/launcher-gui/src/routes/Warning.svelte @@ -0,0 +1,21 @@ + + + +{#if warning} + +
+ {withoutUnicodeIsolation(warning)} +
+
+{/if} diff --git a/qt/launcher-gui/src/routes/base.scss b/qt/launcher-gui/src/routes/base.scss new file mode 100644 index 000000000..c65f458a8 --- /dev/null +++ b/qt/launcher-gui/src/routes/base.scss @@ -0,0 +1,19 @@ +@import "$lib/sass/base"; + +// override Bootstrap transition duration +$carousel-transition: var(--transition); + +@import "bootstrap/scss/buttons"; +@import "bootstrap/scss/button-group"; +@import "bootstrap/scss/transitions"; +@import "bootstrap/scss/modal"; +@import "bootstrap/scss/carousel"; +@import "bootstrap/scss/close"; +@import "bootstrap/scss/alert"; +@import "bootstrap/scss/badge"; +@import "$lib/sass/bootstrap-forms"; +@import "$lib/sass/bootstrap-tooltip"; + +input { + color: var(--fg); +} diff --git a/qt/launcher-gui/src/routes/stores.ts b/qt/launcher-gui/src/routes/stores.ts new file mode 100644 index 000000000..864dfb31c --- /dev/null +++ b/qt/launcher-gui/src/routes/stores.ts @@ -0,0 +1,12 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import type { GetLangsResponse_Pair, GetMirrorsResponse_Pair, GetVersionsResponse } from "@generated/anki/launcher_pb"; +import { writable } from "svelte/store"; + +export const zoomFactor = writable(1.2); +export const langsStore = writable([]); +export const mirrorsStore = writable([]); +export const currentLang = writable(""); +export const initialLang = writable(""); +export const versionsStore = writable(undefined); diff --git a/qt/launcher-gui/src/routes/svg.d.ts b/qt/launcher-gui/src/routes/svg.d.ts new file mode 100644 index 000000000..e96a7886e --- /dev/null +++ b/qt/launcher-gui/src/routes/svg.d.ts @@ -0,0 +1,13 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +// TODO: this is purely to make svelte-check happy, as it complains about the icon components not being found otherwise + +declare module "*.svg?component" { + import type { Component } from "svelte"; + import type { SVGAttributes } from "svelte/elements"; + + const content: Component>; + + export default content; +} diff --git a/qt/launcher-gui/static/anki.png b/qt/launcher-gui/static/anki.png new file mode 100644 index 0000000000000000000000000000000000000000..5700121d60358ed2b6278a806835b9dcd92cdb48 GIT binary patch literal 34595 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 QC`R<2yRQcV}? zbUKG-433^6=sASv9^TZwy}eq2=g}XQNGYT&>f5A*NJtR2UmVbAs+vz0)S%Xy$mR5;;m#91&a5be%faPEi-1Z?c{uaAbmH#d3esiznW26VgKsu$T&lMTmB(;apZGzC7#?STtB z5LJ>vmNgXxHujw*?Zj{^c;L!H${=0bn7rBc)9mJ6%o?{9WHF`TuXQ3WZ1lKe8Io!$ zVT6jJ0}v z(eL*;b?Q_FGmJ4hyRe3m6c;s-dVQB zlJ8yFXK16+gKD%f5T!VqP5BvT#f&30`;^tE3bkDWBNWORq%~L>krNY>bACy3&*=_* znPaUgnzBfN!h$V&9-}7JB9|^*VrOTksvms*`RDoWcfZT#<|a!^OKfaxaNBLS@lXEA zKM_Ba1{^;A-M{;H?$V`8Y;JC{x3|Y|IIJ|ao{oDpKKK9sczqte{XiVYRfCvrx69Jf r5+_ca;I`Xt<5z$6S6>y4?f-886j&)73?t#}00000NkvXXu0mjf^0KgP literal 0 HcmV?d00001 diff --git a/qt/launcher-gui/svelte.config.js b/qt/launcher-gui/svelte.config.js new file mode 100644 index 000000000..2398e9474 --- /dev/null +++ b/qt/launcher-gui/svelte.config.js @@ -0,0 +1,32 @@ +// Tauri doesn't have a Node.js server to do proper SSR +// so we use adapter-static with a fallback to index.html to put the site in SPA mode +// See: https://svelte.dev/docs/kit/single-page-apps +// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info +import adapter from "@sveltejs/adapter-static"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import { dirname, join } from "path"; +import { sveltePreprocess } from "svelte-preprocess"; +import { fileURLToPath } from "url"; + +// This prevents errors being shown when opening VSCode on the root of the +// project, instead of the ts folder. +const tsFolder = dirname(fileURLToPath(import.meta.url)); + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: [vitePreprocess(), sveltePreprocess()], + kit: { + adapter: adapter({ + fallback: "index.html", + }), + alias: { + "@tslib": join(tsFolder, "../../ts/lib/tslib"), + "@generated": join(tsFolder, "../../out/ts/lib/generated"), + }, + files: { + lib: join(tsFolder, "../../ts/lib"), + }, + }, +}; + +export default config; diff --git a/qt/launcher-gui/tsconfig.json b/qt/launcher-gui/tsconfig.json new file mode 100644 index 000000000..fdc216c0d --- /dev/null +++ b/qt/launcher-gui/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": ["./.svelte-kit/tsconfig.json"], + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "noImplicitAny": false + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/qt/launcher-gui/vite.config.js b/qt/launcher-gui/vite.config.js new file mode 100644 index 000000000..efc824a41 --- /dev/null +++ b/qt/launcher-gui/vite.config.js @@ -0,0 +1,39 @@ +import svg from "@poppanator/sveltekit-svg"; +import { sveltekit } from "@sveltejs/kit/vite"; +import { realpathSync } from "fs"; +import { defineConfig } from "vite"; + +const host = process.env.TAURI_DEV_HOST; + +// https://vite.dev/config/ +export default defineConfig(async () => ({ + plugins: [sveltekit(), svg({})], + + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // + // 1. prevent Vite from obscuring rust errors + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: "ws", + host, + port: 1421, + } + : undefined, + watch: { + // 3. tell Vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], + }, + fs: { + allow: [ + realpathSync("../../out"), + realpathSync("../../ts"), + ], + }, + }, +})); From 2c731f5610de0aa8816389c280e51fc850d43eca Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 14 Oct 2025 09:23:51 +0800 Subject: [PATCH 08/74] add to workspace, add helper scripts --- Cargo.toml | 1 + launcher | 19 +++++++++++++++++++ launcher.bat | 16 ++++++++++++++++ package.json | 5 ++++- 4 files changed, 40 insertions(+), 1 deletion(-) create mode 100755 launcher create mode 100755 launcher.bat diff --git a/Cargo.toml b/Cargo.toml index 0fb229fbe..de94f17f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "ftl", "pylib/rsbridge", "qt/launcher", + "qt/launcher-gui/src-tauri", "rslib", "rslib/i18n", "rslib/io", diff --git a/launcher b/launcher new file mode 100755 index 000000000..bf9dc0afb --- /dev/null +++ b/launcher @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +./ninja node_modules extract:uv ts:generated + +mkdir -p target/debug target/release + +cp .python-version target/debug/ +cp qt/launcher/versions.py target/debug/ +cp qt/launcher/pyproject.toml target/debug/ +cp out/extracted/uv/uv target/debug/uv.amd64 +cp out/extracted/uv/uv target/debug/uv.arm64 + +cp .python-version target/release/ +cp qt/launcher/versions.py target/release/ +cp qt/launcher/pyproject.toml target/release/ +cp out/extracted/uv/uv target/release/uv.amd64 +cp out/extracted/uv/uv target/debug/uv.arm64 diff --git a/launcher.bat b/launcher.bat new file mode 100755 index 000000000..ee727be66 --- /dev/null +++ b/launcher.bat @@ -0,0 +1,16 @@ +@echo off + +.\tools\ninja.bat node_modules extract:uv ts:generated + +mkdir ,\target\debug +mkdir .\target\release + +copy .\.python-version .\target\debug\ +copy .\qt\launcher\versions.py .\target\debug\ +copy .\qt\launcher\pyproject.toml .\target\debug\ +copy .\out\extracted\uv\uv.exe .\target\debug\ + +copy .\.python-version .\target\release\ +copy .\qt\launcher\versions.py .\target\release\ +copy .\qt\launcher\pyproject.toml .\target\debug\ +copy .\out\extracted\uv\uv.exe .\target\release\ diff --git a/package.json b/package.json index ad5020288..81d65d977 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,10 @@ "svelte-check:once": "cd ts && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --fail-on-warnings --threshold warning", "svelte-check": "cd ts && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "vitest:once": "cd ts && vitest run", - "vitest": "cd ts && vitest" + "vitest": "cd ts && vitest", + "ld": "cd qt/launcher-gui && vite dev", + "lb": "cd qt/launcher-gui && vite build", + "lsc": "cd qt/launcher-gui && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --fail-on-warnings --threshold warning" }, "devDependencies": { "@bufbuild/protoc-gen-es": "^1.8.0", From c31985982ff87a18627ad2c8a67590fa03a92c76 Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 14 Oct 2025 09:24:59 +0800 Subject: [PATCH 09/74] modify i18n resources struct for launcher use --- rslib/i18n/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rslib/i18n/src/lib.rs b/rslib/i18n/src/lib.rs index 95b960fad..fa790b144 100644 --- a/rslib/i18n/src/lib.rs +++ b/rslib/i18n/src/lib.rs @@ -467,10 +467,10 @@ fn format_number_values(val: &FluentValue, alt_separator: Option<&'static str>) } } -#[derive(Serialize)] +#[derive(Debug, Serialize)] pub struct ResourcesForJavascript { - langs: Vec, - resources: Vec, + pub langs: Vec, + pub resources: Vec, } pub fn without_unicode_isolation(s: &str) -> String { From a76ef8c4226616ed6f3547f9e3515613cbb439ba Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 14 Oct 2025 09:26:41 +0800 Subject: [PATCH 10/74] update minilints --- build/configure/src/rust.rs | 2 +- tools/minilints/src/main.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/build/configure/src/rust.rs b/build/configure/src/rust.rs index b2bc6d577..20b765f44 100644 --- a/build/configure/src/rust.rs +++ b/build/configure/src/rust.rs @@ -248,7 +248,7 @@ pub fn check_minilints(build: &mut Build) -> Result<()> { let files = inputs![ glob![ "**/*.{py,rs,ts,svelte,mjs,md}", - "{node_modules,ts/.svelte-kit}/**" + "{node_modules,ts/.svelte-kit,qt/launcher-gui/.svelte-kit}/**" ], "Cargo.lock" ]; diff --git a/tools/minilints/src/main.rs b/tools/minilints/src/main.rs index 6d38278b5..b90910993 100644 --- a/tools/minilints/src/main.rs +++ b/tools/minilints/src/main.rs @@ -36,6 +36,7 @@ const IGNORED_FOLDERS: &[&str] = &[ ".mypy_cache", "./extra", "./ts/.svelte-kit", + "./qt/launcher-gui/.svelte-kit", ]; fn main() -> Result<()> { From c74a373cdc326e576b8a80fbe533c7b1f8e948c7 Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 14 Oct 2025 09:27:43 +0800 Subject: [PATCH 11/74] update check_web (svelte-check still missing) --- build/configure/src/web.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/build/configure/src/web.rs b/build/configure/src/web.rs index ef2d268bb..f3bf3a8d1 100644 --- a/build/configure/src/web.rs +++ b/build/configure/src/web.rs @@ -308,7 +308,7 @@ fn build_and_check_reviewer(build: &mut Build) -> Result<()> { } fn check_web(build: &mut Build) -> Result<()> { - let fmt_excluded = "{target,ts/.svelte-kit,node_modules}/**"; + let fmt_excluded = "{target,ts/.svelte-kit,node_modules,qt/launcher-gui/.svelte-kit}/**"; let dprint_files = inputs![glob!["**/*.{ts,mjs,js,md,json,toml,scss}", fmt_excluded]]; let prettier_files = inputs![glob!["**/*.svelte", fmt_excluded]]; @@ -363,8 +363,11 @@ fn check_web(build: &mut Build) -> Result<()> { }, )?; let eslint_rc = inputs![".eslintrc.cjs"]; - for folder in ["ts", "qt/aqt/data/web/js"] { - let inputs = inputs![glob![format!("{folder}/**"), "ts/.svelte-kit/**"]]; + for folder in ["ts", "qt/aqt/data/web/js", "qt/launcher-gui/src"] { + let inputs = inputs![glob![ + format!("{folder}/**"), + "{ts,qt/launcher-gui}/.svelte-kit/**" + ]]; build.add_action( "check:eslint", Eslint { From 2860e628ee135cf62d7a2bae4fffe0f5efe8e427 Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 14 Oct 2025 09:28:12 +0800 Subject: [PATCH 12/74] add option to hide revert buttons --- ts/lib/components/EnumSelectorRow.svelte | 5 ++++- ts/lib/components/SwitchRow.svelte | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ts/lib/components/EnumSelectorRow.svelte b/ts/lib/components/EnumSelectorRow.svelte index 2570b8e65..f05bd8860 100644 --- a/ts/lib/components/EnumSelectorRow.svelte +++ b/ts/lib/components/EnumSelectorRow.svelte @@ -18,6 +18,7 @@ export let choices: Choice[]; export let disabled: boolean = false; export let disabledChoices: T[] = []; + export let hideRevert: boolean = false; @@ -27,7 +28,9 @@ - + {#if !hideRevert} + + {/if} diff --git a/ts/lib/components/SwitchRow.svelte b/ts/lib/components/SwitchRow.svelte index 461dc27e7..82e351580 100644 --- a/ts/lib/components/SwitchRow.svelte +++ b/ts/lib/components/SwitchRow.svelte @@ -13,6 +13,7 @@ export let value: boolean; export let defaultValue: boolean; export let disabled: boolean = false; + export let hideRevert: boolean = false; const id = Math.random().toString(36).substring(2); @@ -22,7 +23,9 @@ - + {#if !hideRevert} + + {/if} From ada0722aeb9cbd6ccd2114153e79c054a6367060 Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 14 Oct 2025 09:28:29 +0800 Subject: [PATCH 13/74] add title slot --- ts/lib/components/TitledContainer.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/ts/lib/components/TitledContainer.svelte b/ts/lib/components/TitledContainer.svelte index 3ea932ba3..501638023 100644 --- a/ts/lib/components/TitledContainer.svelte +++ b/ts/lib/components/TitledContainer.svelte @@ -24,6 +24,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html style:--container-margin="0" >
+

{title}

From 6aeb1daecdc64c5e89fa10914904828e553e3e24 Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 14 Oct 2025 09:48:26 +0800 Subject: [PATCH 14/74] fix launcher scripts --- launcher | 2 +- launcher.bat | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher b/launcher index bf9dc0afb..8957b2ddf 100755 --- a/launcher +++ b/launcher @@ -16,4 +16,4 @@ cp .python-version target/release/ cp qt/launcher/versions.py target/release/ cp qt/launcher/pyproject.toml target/release/ cp out/extracted/uv/uv target/release/uv.amd64 -cp out/extracted/uv/uv target/debug/uv.arm64 +cp out/extracted/uv/uv target/release/uv.arm64 diff --git a/launcher.bat b/launcher.bat index ee727be66..7a533dcc8 100755 --- a/launcher.bat +++ b/launcher.bat @@ -12,5 +12,5 @@ copy .\out\extracted\uv\uv.exe .\target\debug\ copy .\.python-version .\target\release\ copy .\qt\launcher\versions.py .\target\release\ -copy .\qt\launcher\pyproject.toml .\target\debug\ +copy .\qt\launcher\pyproject.toml .\target\release\ copy .\out\extracted\uv\uv.exe .\target\release\ From 362d09e31f17c234c9635a6836501820dc2b1215 Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 14 Oct 2025 13:22:10 +0800 Subject: [PATCH 15/74] update advisory allowlist... --- .deny.toml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.deny.toml b/.deny.toml index 7cdf0cf99..2c7d91e80 100644 --- a/.deny.toml +++ b/.deny.toml @@ -7,6 +7,23 @@ db-urls = ["https://github.com/rustsec/advisory-db"] ignore = [ # burn depends on an unmaintained package 'paste' "RUSTSEC-2024-0436", + # proc-macro-error unmaintained (tauri) + "RUSTSEC-2024-0370", + # gtk-rs' gtk3 bindings unmaintained (tauri) + "RUSTSEC-2024-0411", + "RUSTSEC-2024-0412", + "RUSTSEC-2024-0413", + "RUSTSEC-2024-0414", + "RUSTSEC-2024-0415", + "RUSTSEC-2024-0416", + "RUSTSEC-2024-0417", + "RUSTSEC-2024-0418", + "RUSTSEC-2024-0419", + "RUSTSEC-2024-0420", + # UB in glib crate (tauri) + "RUSTSEC-2024-0429", + # fxhash unmaintained (tauri) + "RUSTSEC-2025-0057", ] [licenses] From 778955beb01a4731023c38b15246c45483d4fe94 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 09:42:26 +0800 Subject: [PATCH 16/74] add tauri-plugin-dialog crate to make alert() work --- Cargo.lock | 178 ++++++++++++++++++++++++++- Cargo.toml | 1 + qt/launcher-gui/src-tauri/Cargo.toml | 1 + 3 files changed, 179 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 418e49730..a0d3d1d72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -368,6 +368,27 @@ dependencies = [ "libloading 0.8.8", ] +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -2398,6 +2419,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.9.3", + "block2 0.6.1", + "libc", "objc2 0.6.2", ] @@ -2412,6 +2435,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.8", +] + [[package]] name = "dlopen2" version = "0.8.0" @@ -4678,6 +4710,7 @@ dependencies = [ "syn 2.0.106", "tauri", "tauri-build", + "tauri-plugin-dialog", "tauri-plugin-log", "tauri-plugin-os", "tauri-plugin-single-instance", @@ -6405,7 +6438,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", "indexmap 2.11.0", - "quick-xml", + "quick-xml 0.38.3", "serde", "time", ] @@ -6854,6 +6887,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.38.3" @@ -7309,6 +7351,31 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.1", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2 0.6.2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "ring" version = "0.17.14" @@ -7679,6 +7746,12 @@ dependencies = [ "syn 2.0.106", ] +[[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" @@ -8691,6 +8764,46 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beee42a4002bc695550599b011728d9dfabf82f767f134754ed6655e434824e" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.16", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.16", + "toml 0.9.5", + "url", +] + [[package]] name = "tauri-plugin-log" version = "2.7.0" @@ -9041,6 +9154,7 @@ dependencies = [ "slab", "socket2 0.6.0", "tokio-macros", + "tracing", "windows-sys 0.59.0", ] @@ -9860,6 +9974,66 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.9.3", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.9.3", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -11014,6 +11188,7 @@ dependencies = [ "ordered-stream", "serde", "serde_repr", + "tokio", "tracing", "uds_windows", "windows-sys 0.60.2", @@ -11215,6 +11390,7 @@ dependencies = [ "endi", "enumflags2", "serde", + "url", "winnow 0.7.13", "zvariant_derive", "zvariant_utils", diff --git a/Cargo.toml b/Cargo.toml index de94f17f1..f4e71f27a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,6 +131,7 @@ syn = { version = "2.0.103", features = ["parsing", "printing"] } tar = "0.4.44" tauri = { version = "2", features = ["devtools"] } tauri-build = { version = "2", features = [] } +tauri-plugin-dialog = "2" tauri-plugin-log = "2" tauri-plugin-os = "2" tauri-plugin-single-instance = "2" diff --git a/qt/launcher-gui/src-tauri/Cargo.toml b/qt/launcher-gui/src-tauri/Cargo.toml index 82493085d..9c49b8f91 100644 --- a/qt/launcher-gui/src-tauri/Cargo.toml +++ b/qt/launcher-gui/src-tauri/Cargo.toml @@ -35,6 +35,7 @@ serde_json.workspace = true snafu.workspace = true strum.workspace = true tauri.workspace = true +tauri-plugin-dialog.workspace = true tauri-plugin-log.workspace = true tauri-plugin-os.workspace = true tauri-plugin-single-instance.workspace = true From 70487305034656fdc754e3cc61a0ee77bb8dc4f3 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 09:43:41 +0800 Subject: [PATCH 17/74] add @xterm/addon-webgl package xterm.js' new DOM renderer seems to have some visual glitches, try using the webgl renderer first before falling back --- package.json | 1 + yarn.lock | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/package.json b/package.json index 81d65d977..6a8a98297 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@popperjs/core": "^2.11.8", "@tauri-apps/api": "^2", "@tauri-apps/plugin-log": "^2.7.0", + "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", "bootstrap": "^5.3.0", "bootstrap-icons": "^1.10.5", diff --git a/yarn.lock b/yarn.lock index acef9d12d..59192c32e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1803,6 +1803,15 @@ __metadata: languageName: node linkType: hard +"@xterm/addon-webgl@npm:^0.18.0": + version: 0.18.0 + resolution: "@xterm/addon-webgl@npm:0.18.0" + peerDependencies: + "@xterm/xterm": ^5.0.0 + checksum: 10c0/682a3f5f128ee09a0cf1b41cbb7b2f925a5e43056e12ba0c523b93a1f5f188045caef9e31f32db933b8a7a1b12d8f9babaddfa11e6f11df0c7b265009103476c + languageName: node + linkType: hard + "@xterm/xterm@npm:^5.5.0": version: 5.5.0 resolution: "@xterm/xterm@npm:5.5.0" @@ -1941,6 +1950,7 @@ __metadata: "@types/node": "npm:^22" "@typescript-eslint/eslint-plugin": "npm:^5.60.1" "@typescript-eslint/parser": "npm:^5.60.1" + "@xterm/addon-webgl": "npm:^0.18.0" "@xterm/xterm": "npm:^5.5.0" bootstrap: "npm:^5.3.0" bootstrap-icons: "npm:^1.10.5" From c973f0f78692829a9def3c6c44b949459f6c0dc2 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 09:45:24 +0800 Subject: [PATCH 18/74] add and use isWindows helper fn --- ts/lib/generated/post.ts | 5 ++++- ts/lib/tslib/platform.ts | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ts/lib/generated/post.ts b/ts/lib/generated/post.ts index d3f44cd09..be9e962be 100644 --- a/ts/lib/generated/post.ts +++ b/ts/lib/generated/post.ts @@ -1,13 +1,16 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import { isWindows } from "@tslib/platform"; + export interface PostProtoOptions { /** True by default. Shows a dialog with the error message, then rethrows. */ alertOnError?: boolean; + // whether to use the "anki:" custom protocol or not customProtocol?: boolean; } -const CUSTOM_PROTOCOL_URI = (navigator.platform.indexOf("Win") == -1) ? "anki://localhost" : "http://anki.localhost"; +const CUSTOM_PROTOCOL_URI = isWindows() ? "http://anki.localhost" : "anki://localhost" ; export async function postProto( method: string, diff --git a/ts/lib/tslib/platform.ts b/ts/lib/tslib/platform.ts index d055bcb53..d9727c3a2 100644 --- a/ts/lib/tslib/platform.ts +++ b/ts/lib/tslib/platform.ts @@ -10,6 +10,11 @@ export function isApplePlatform(): boolean { ); } +export function isWindows(): boolean { + const platform = window.navigator["platform" + ""]; + return platform.startsWith("Win"); +} + export function isDesktop(): boolean { return !(/iphone|ipad|ipod|android/i.test(window.navigator.userAgent)); } From 5ca2b84dfb7e53b311e9b695807cf968c8d707fe Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 09:47:09 +0800 Subject: [PATCH 19/74] add new rpc calls --- proto/anki/launcher.proto | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/proto/anki/launcher.proto b/proto/anki/launcher.proto index d16bc760c..e3f2e2eb9 100644 --- a/proto/anki/launcher.proto +++ b/proto/anki/launcher.proto @@ -6,17 +6,23 @@ syntax = "proto3"; package anki.launcher; import "anki/generic.proto"; +import "google/protobuf/empty.proto"; service LauncherService { rpc I18nResources(I18nResourcesRequest) returns (generic.Json); rpc GetLangs(generic.Empty) returns (GetLangsResponse); rpc SetLang(generic.String) returns (generic.Empty); - rpc GetVersions(generic.Empty) returns (GetVersionsResponse); rpc ChooseVersion(ChooseVersionRequest) returns (ChooseVersionResponse); rpc GetOptions(generic.Empty) returns (Options); rpc GetMirrors(generic.Empty) returns (GetMirrorsResponse); rpc WindowReady(generic.Empty) returns (generic.Empty); rpc ZoomWebview(ZoomWebviewRequest) returns (generic.Empty); + + rpc GetAvailableVersions(generic.Empty) returns (Versions); + rpc GetExistingVersions(generic.Empty) returns (ExistingVersions); + + rpc LaunchAnki(generic.Empty) returns (generic.Empty); + rpc Exit(generic.Empty) returns (generic.Empty); } // TODO: this should not be necessary @@ -74,14 +80,32 @@ message GetVersionsResponse { message ChooseVersionRequest { string version = 1; - bool keep_existing = 2; - Options options = 3; + optional string current = 2; + bool keep_existing = 3; + Options options = 4; } message ChooseVersionResponse { string version = 1; + bool warming_up = 2; } message ZoomWebviewRequest { float scale_factor = 1; } + +message Version { + string version = 1; + bool is_prerelease = 2; +} + +message Versions { + repeated Version all = 1; + repeated Version latest = 2; +} + +message ExistingVersions { + optional Version current = 1; + optional Version previous = 2; + bool pyproject_modified_by_user = 3; +} From 920fbdba7af4a46c2ecc8a59682e8eeb705c65b4 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 09:52:19 +0800 Subject: [PATCH 20/74] update capabilities --- qt/launcher-gui/src-tauri/capabilities/default.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qt/launcher-gui/src-tauri/capabilities/default.json b/qt/launcher-gui/src-tauri/capabilities/default.json index 43af1fe4d..05147c580 100644 --- a/qt/launcher-gui/src-tauri/capabilities/default.json +++ b/qt/launcher-gui/src-tauri/capabilities/default.json @@ -6,6 +6,7 @@ "permissions": [ "core:default", "os:default", - "log:default" + "log:default", + "dialog:default" ] } From 6d6df1c91d9a3f0c0b213196578c2e7cb6c10ef8 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 09:55:07 +0800 Subject: [PATCH 21/74] refactor state and uv code --- qt/launcher-gui/src-tauri/src/platform/mac.rs | 35 - qt/launcher-gui/src-tauri/src/platform/mod.rs | 56 +- qt/launcher-gui/src-tauri/src/state.rs | 122 ++++ qt/launcher-gui/src-tauri/src/uv.rs | 645 ++++++++++-------- 4 files changed, 524 insertions(+), 334 deletions(-) create mode 100644 qt/launcher-gui/src-tauri/src/state.rs diff --git a/qt/launcher-gui/src-tauri/src/platform/mac.rs b/qt/launcher-gui/src-tauri/src/platform/mac.rs index 3f5b0ce2e..bc9759fce 100644 --- a/qt/launcher-gui/src-tauri/src/platform/mac.rs +++ b/qt/launcher-gui/src-tauri/src/platform/mac.rs @@ -1,35 +1,14 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use std::io; -use std::io::Write; use std::path::Path; use std::process::Command; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::thread; -use std::time::Duration; use anki_process::CommandExt as AnkiCommandExt; -use anyhow::Context; use anyhow::Result; pub fn prepare_for_launch_after_update(mut cmd: Command, root: &Path) -> Result<()> { // Pre-validate by running --version to trigger any Gatekeeper checks - print!("\n\x1B[1mThis may take a few minutes. Please wait\x1B[0m"); - io::stdout().flush().unwrap(); - - // Start progress indicator - let running = Arc::new(AtomicBool::new(true)); - let running_clone = running.clone(); - let progress_thread = thread::spawn(move || { - while running_clone.load(Ordering::Relaxed) { - print!("."); - io::stdout().flush().unwrap(); - thread::sleep(Duration::from_secs(1)); - } - }); let _ = cmd .env("ANKI_FIRST_RUN", "1") @@ -52,23 +31,9 @@ pub fn prepare_for_launch_after_update(mut cmd: Command, root: &Path) -> Result< } } - // Stop progress indicator - running.store(false, Ordering::Relaxed); - progress_thread.join().unwrap(); - println!(); // New line after dots Ok(()) } -pub fn relaunch_in_terminal() -> Result<()> { - let current_exe = std::env::current_exe().context("Failed to get current executable path")?; - Command::new("open") - .args(["-na", "Terminal"]) - .arg(current_exe) - .env_remove("ANKI_LAUNCHER_WANT_TERMINAL") - .ensure_spawn()?; - std::process::exit(0); -} - pub fn finalize_uninstall() { if let Ok(exe_path) = std::env::current_exe() { // Find the .app bundle by walking up the directory tree diff --git a/qt/launcher-gui/src-tauri/src/platform/mod.rs b/qt/launcher-gui/src-tauri/src/platform/mod.rs index 20fbbc5e5..07af93720 100644 --- a/qt/launcher-gui/src-tauri/src/platform/mod.rs +++ b/qt/launcher-gui/src-tauri/src/platform/mod.rs @@ -4,7 +4,7 @@ #[cfg(all(unix, not(target_os = "macos")))] pub mod unix; -#[cfg(target_os = "macos")] +// #[cfg(target_os = "macos")] pub mod mac; #[cfg(target_os = "windows")] @@ -49,6 +49,58 @@ pub fn get_uv_binary_name() -> &'static str { } } +pub fn respawn_launcher() -> Result<()> { + use std::process::Stdio; + + let mut launcher_cmd = if cfg!(target_os = "macos") { + // On macOS, we need to launch the .app bundle, not the executable directly + let current_exe = + std::env::current_exe().context("Failed to get current executable path")?; + + // Navigate from Contents/MacOS/launcher to the .app bundle + let app_bundle = current_exe + .parent() // MacOS + .and_then(|p| p.parent()) // Contents + .and_then(|p| p.parent()) // .app + .context("Failed to find .app bundle")?; + + let mut cmd = std::process::Command::new("open"); + cmd.arg(app_bundle); + cmd + } else { + let current_exe = + std::env::current_exe().context("Failed to get current executable path")?; + std::process::Command::new(current_exe) + }; + + launcher_cmd + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + // TODO: remove + launcher_cmd.env("ANKI_LAUNCHER_SKIP", "1"); + + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; + const DETACHED_PROCESS: u32 = 0x00000008; + launcher_cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS); + } + + #[cfg(all(unix, not(target_os = "macos")))] + { + use std::os::unix::process::CommandExt; + launcher_cmd.process_group(0); + } + + let child = launcher_cmd.ensure_spawn()?; + std::mem::forget(child); + + Ok(()) +} + pub fn launch_anki_normally(mut cmd: std::process::Command) -> Result<()> { #[cfg(windows)] { @@ -56,7 +108,7 @@ pub fn launch_anki_normally(mut cmd: std::process::Command) -> Result<()> { cmd.ensure_spawn()?; } #[cfg(unix)] - cmd.ensure_spawn()?; + cmd.ensure_exec()?; Ok(()) } diff --git a/qt/launcher-gui/src-tauri/src/state.rs b/qt/launcher-gui/src-tauri/src/state.rs new file mode 100644 index 000000000..09cde5ef4 --- /dev/null +++ b/qt/launcher-gui/src-tauri/src/state.rs @@ -0,0 +1,122 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::sync::Arc; + +pub use anki_proto::launcher::ExistingVersions; +use anki_proto::launcher::Mirror; +use anki_proto::launcher::Options as OptionsProto; +pub use anki_proto::launcher::Version; +pub use anki_proto::launcher::Versions; +use anyhow::anyhow; +use anyhow::Result; +use tokio::sync::watch; + +use crate::uv; + +pub struct Options { + allow_betas: bool, + download_caching: bool, + mirror: Mirror, +} + +impl From<&Options> for OptionsProto { + fn from(o: &Options) -> Self { + Self { + allow_betas: o.allow_betas, + download_caching: o.download_caching, + mirror: o.mirror.into(), + } + } +} + +impl From<&uv::Paths> for Options { + fn from(state: &uv::Paths) -> Self { + let allow_betas = state.prerelease_marker.exists(); + let download_caching = !state.no_cache_marker.exists(); + let mirror = if state.mirror_path.exists() { + Mirror::China + } else { + Mirror::Disabled + }; + Self { + allow_betas, + download_caching, + mirror, + } + } +} + +pub struct NormalState { + pub paths: Arc, + pub initial_options: Options, + pub current_versions: Option>>>, + pub available_versions: Option>>>, +} + +impl From for NormalState { + fn from(paths: uv::Paths) -> Self { + Self { + initial_options: Options::from(&paths), + current_versions: None, + available_versions: None, + paths: paths.into(), + } + } +} + +impl From for State { + fn from(state: NormalState) -> Self { + Self::Normal(state) + } +} + +pub enum State { + LaunchAnki(Arc), + Error(anyhow::Error), + Uninstall(Arc), + Normal(NormalState), +} + +impl State { + pub fn normal(&self) -> Result<&NormalState> { + match self { + State::Normal(state) => Ok(state), + _ => Err(anyhow!("unexpected state")), + } + } + + pub fn paths(&self) -> Result<&uv::Paths> { + match self { + State::LaunchAnki(paths) => Ok(paths), + State::Uninstall(paths) => Ok(paths), + State::Normal(state) => Ok(&state.paths), + _ => Err(anyhow!("unexpected state")), + } + } + + pub fn should_launch_anki(&self) -> bool { + matches!(self, State::LaunchAnki(_)) + } +} + +impl NormalState { + pub fn check_versions(&mut self) { + let (av_tx, av_rx) = tokio::sync::watch::channel(None); + let paths = self.paths.clone(); + tauri::async_runtime::spawn_blocking(move || { + let res = paths.get_releases(); + let _ = av_tx.send(Some(res)); + }); + + let (cv_tx, cv_rx) = tokio::sync::watch::channel(None); + let paths = self.paths.clone(); + tauri::async_runtime::spawn_blocking(move || { + let res = paths.check_versions(); + let _ = cv_tx.send(Some(res)); + }); + + self.current_versions = Some(cv_rx); + self.available_versions = Some(av_rx); + } +} diff --git a/qt/launcher-gui/src-tauri/src/uv.rs b/qt/launcher-gui/src-tauri/src/uv.rs index 05212dcfd..227bf3f3f 100644 --- a/qt/launcher-gui/src-tauri/src/uv.rs +++ b/qt/launcher-gui/src-tauri/src/uv.rs @@ -19,19 +19,19 @@ use anki_process::CommandExt as AnkiCommandExt; use anyhow::anyhow; use anyhow::Context; use anyhow::Result; -use tauri::AppHandle; -use tauri::Emitter; -use tauri::Runtime; use crate::platform; use crate::platform::ensure_os_supported; use crate::platform::get_exe_and_resources_dirs; use crate::platform::get_uv_binary_name; pub use crate::platform::launch_anki_normally; +use crate::platform::respawn_launcher; +use crate::state::ExistingVersions; +use crate::state::Version; +use crate::state::Versions; #[derive(Debug, Clone)] -pub struct State { - pub current_version: Option, +pub struct Paths { pub prerelease_marker: std::path::PathBuf, uv_install_root: std::path::PathBuf, uv_cache_dir: std::path::PathBuf, @@ -48,7 +48,6 @@ pub struct State { launcher_trigger_file: std::path::PathBuf, pub mirror_path: std::path::PathBuf, pub pyproject_modified_by_user: bool, - previous_version: Option, resources_dir: std::path::PathBuf, venv_folder: std::path::PathBuf, /// system Python + PyQt6 library mode @@ -61,186 +60,25 @@ pub enum VersionKind { Uv(String), } -#[derive(Debug)] -pub struct Releases { - pub latest: Vec, - pub all: Vec, -} - -pub fn init_state() -> Result> { - let uv_install_root = if let Ok(custom_root) = std::env::var("ANKI_LAUNCHER_VENV_ROOT") { - std::path::PathBuf::from(custom_root) - } else { - dirs::data_local_dir() - .context("Unable to determine data_dir")? - .join("AnkiProgramFiles") - }; - - let (exe_dir, resources_dir) = get_exe_and_resources_dirs()?; - - let mut state = State { - // TODO: return error instead of relying on member field here if os unsupported - current_version: None, - prerelease_marker: uv_install_root.join("prerelease"), - uv_install_root: uv_install_root.clone(), - uv_cache_dir: uv_install_root.join("cache"), - no_cache_marker: uv_install_root.join("nocache"), - anki_base_folder: get_anki_base_path()?, - uv_path: exe_dir.join(get_uv_binary_name()), - uv_python_install_dir: uv_install_root.join("python"), - user_pyproject_path: uv_install_root.join("pyproject.toml"), - user_python_version_path: uv_install_root.join(".python-version"), - dist_pyproject_path: resources_dir.join("pyproject.toml"), - dist_python_version_path: resources_dir.join(".python-version"), - uv_lock_path: uv_install_root.join("uv.lock"), - sync_complete_marker: uv_install_root.join(".sync_complete"), - launcher_trigger_file: uv_install_root.join(".want-launcher"), - mirror_path: uv_install_root.join("mirror"), - pyproject_modified_by_user: false, // calculated later - previous_version: None, - system_qt: (cfg!(unix) && !cfg!(target_os = "macos")) - && resources_dir.join("system_qt").exists(), - resources_dir, - venv_folder: uv_install_root.join(".venv"), - }; - - // Check for uninstall request from Windows uninstaller - if std::env::var("ANKI_LAUNCHER_UNINSTALL").is_ok() { - // handle_uninstall(&state)?; - println!("TODO: UNINSTALL"); - return Ok(None); - } - - // Create install directory - create_dir_all(&state.uv_install_root)?; - - let launcher_requested = - state.launcher_trigger_file.exists() || !state.user_pyproject_path.exists(); - - // Calculate whether user has custom edits that need syncing - let pyproject_time = file_timestamp_secs(&state.user_pyproject_path); - let sync_time = file_timestamp_secs(&state.sync_complete_marker); - state.pyproject_modified_by_user = pyproject_time > sync_time; - let pyproject_has_changed = state.pyproject_modified_by_user; - - let debug = cfg!(debug_assertions); - - if !launcher_requested && !pyproject_has_changed && !debug { - let args: Vec = std::env::args().skip(1).collect(); - let cmd = build_python_command(&state, &args)?; - launch_anki_normally(cmd)?; - return Ok(None); - } - - if launcher_requested { - // Remove the trigger file to make request ephemeral - let _ = remove_file(&state.launcher_trigger_file); - } - - // TODO: - let _ = ensure_os_supported(); - - // TODO: we should call this here instead of via getVersions - // check_versions(&mut state); - - Ok(Some(state)) -} - -pub fn post_install(state: &State) -> Result<()> { - // Write marker file to indicate we've completed the sync process - write_sync_marker(state)?; - - #[cfg(target_os = "macos")] - { - let cmd = build_python_command(&state, &[])?; - platform::mac::prepare_for_launch_after_update(cmd, &uv_install_root)?; - } - - // respawn the launcher as a disconnected subprocess for normal startup - // respawn_launcher()?; - let args: Vec = std::env::args().skip(1).collect(); - let cmd = build_python_command(state, &args)?; - launch_anki_normally(cmd)?; - - Ok(()) -} - -fn extract_aqt_version(state: &State) -> Option { - // Check if .venv exists first - if !state.venv_folder.exists() { - return None; - } - - let output = uv_command(state) - .ok()? - .env("VIRTUAL_ENV", &state.venv_folder) - .args(["pip", "show", "aqt"]) - .output(); - - let output = output.ok()?; - - if !output.status.success() { - return None; - } - - let stdout = String::from_utf8(output.stdout).ok()?; - for line in stdout.lines() { - if let Some(version) = line.strip_prefix("Version: ") { - return Some(version.trim().to_string()); - } - } - None -} - -pub fn check_versions(state: &mut State) -> Result<(Option, Option)> { - // If sync_complete_marker is missing, do nothing - if !state.sync_complete_marker.exists() { - return Ok((None, None)); - } - - // Determine current version by invoking uv pip show aqt - match extract_aqt_version(state) { - Some(version) => { - state.current_version = Some(normalize_version(&version)); - } - None => { - Err(anyhow::anyhow!( - "Warning: Could not determine current Anki version" - ))?; - } - } - - // Read previous version from "previous-version" file - let previous_version_path = state.uv_install_root.join("previous-version"); - if let Ok(content) = read_file(&previous_version_path) { - if let Ok(version_str) = String::from_utf8(content) { - let version = version_str.trim().to_string(); - if !version.is_empty() { - state.previous_version = Some(normalize_version(&version)); - } - } - } - - Ok(( - state.current_version.clone(), - state.previous_version.clone(), - )) -} - -pub fn handle_version_install_or_update( - app: AppHandle, - state: &State, +pub fn handle_version_install_or_update( + state: &Paths, version: &str, keep_existing: bool, -) -> Result<()> { - let version_kind = - parse_version_kind(version).ok_or_else(|| anyhow!("{version} is not a valid version!"))?; + previous_version_to_save: Option<&str>, + on_pty_data: F, +) -> Result<()> +where + F: Fn(String) + Send + 'static, +{ + let version_kind = parse_version_kind(version) + .ok_or_else(|| anyhow!(r#""{version}" is not a valid version!"#))?; if !keep_existing { apply_version_kind(&version_kind, state)?; } + // TODO: support this // Extract current version before syncing (but don't write to file yet) - let previous_version_to_save = state.current_version.clone(); + // let previous_version_to_save = state.current_version.clone(); // Remove sync marker before attempting sync let _ = remove_file(&state.sync_complete_marker); @@ -326,15 +164,15 @@ pub fn handle_version_install_or_update( .openpty(portable_pty::PtySize { // NOTE: must be the same as xterm.js', otherwise text won't wrap // TODO: maybe don't hardcode? - rows: 12, - cols: 60, + rows: 10, + cols: 50, pixel_width: 0, pixel_height: 0, }) - .unwrap(); + .with_context(|| "failed to open pty")?; - let mut reader = pair.master.try_clone_reader().unwrap(); - let mut writer = pair.master.take_writer().unwrap(); + let mut reader = pair.master.try_clone_reader()?; + let mut writer = pair.master.take_writer()?; tauri::async_runtime::spawn_blocking(move || { let mut buf = [0u8; 1024]; @@ -345,13 +183,13 @@ pub fn handle_version_install_or_update( Ok(0) => break, Ok(n) => { let output = String::from_utf8_lossy(&buf[..n]).to_string(); - // NOTE: windows requests curspr position before actually running child + // NOTE: windows requests cursor position before actually running child if output == "\x1b[6n" { writeln!(&mut writer, "\x1b[0;0R").unwrap(); } // cheaper to base64ise a string than jsonify an [u8] let data = data_encoding::BASE64.encode(&buf[..n]); - let _ = app.emit("pty-data", data); + on_pty_data(data); } Err(e) => { eprintln!("Error reading from PTY: {}", e); @@ -361,6 +199,8 @@ pub fn handle_version_install_or_update( } }); + let cmdline = command.as_unix_command_line()?; + let mut child = pair.slave.spawn_command(command).unwrap(); drop(pair.slave); println!("waiting on uv..."); @@ -368,8 +208,8 @@ pub fn handle_version_install_or_update( println!("uv exited with status: {:?}", status); match status { - Ok(_) => { - // Sync succeeded + // Sync succeeded + Ok(exit_status) if exit_status.success() => { if !keep_existing && matches!(version_kind, VersionKind::PyOxidizer(_)) { inject_helper_addon()?; } @@ -377,7 +217,7 @@ pub fn handle_version_install_or_update( // Now that sync succeeded, save the previous version if let Some(current_version) = previous_version_to_save { let previous_version_path = state.uv_install_root.join("previous-version"); - if let Err(e) = write_file(&previous_version_path, ¤t_version) { + if let Err(e) = write_file(&previous_version_path, current_version) { // TODO: println!("Warning: Could not save previous version: {e}"); } @@ -385,19 +225,21 @@ pub fn handle_version_install_or_update( Ok(()) } - Err(e) => { - // TODO: - // If sync fails due to things like a missing wheel on pypi, - // we need to remove the lockfile or uv will cache the bad result. + // If sync fails due to things like a missing wheel on pypi, + // we need to remove the lockfile or uv will cache the bad result. + Ok(exit_status) => { + let _ = remove_file(&state.uv_lock_path); + let code = exit_status.exit_code(); + Err(anyhow!("Failed to run ({code}): {cmdline}")) + } + Err(e) => { let _ = remove_file(&state.uv_lock_path); - println!("Install failed: {e:#}"); - println!(); Err(e.into()) } } } -pub fn set_allow_betas(state: &State, allow_betas: bool) -> Result<()> { +pub fn set_allow_betas(state: &Paths, allow_betas: bool) -> Result<()> { if allow_betas { write_file(&state.prerelease_marker, "")?; } else { @@ -406,7 +248,7 @@ pub fn set_allow_betas(state: &State, allow_betas: bool) -> Result<()> { Ok(()) } -pub fn set_cache_enabled(state: &State, cache_enabled: bool) -> Result<()> { +pub fn set_cache_enabled(state: &Paths, cache_enabled: bool) -> Result<()> { if cache_enabled { let _ = remove_file(&state.no_cache_marker); } else { @@ -419,15 +261,6 @@ pub fn set_cache_enabled(state: &State, cache_enabled: bool) -> Result<()> { Ok(()) } -fn write_sync_marker(state: &State) -> Result<()> { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .context("Failed to get system time")? - .as_secs(); - write_file(&state.sync_complete_marker, timestamp.to_string())?; - Ok(()) -} - /// Get mtime of provided file, or 0 if unavailable fn file_timestamp_secs(path: &std::path::Path) -> i64 { modified_time(path) @@ -435,13 +268,13 @@ fn file_timestamp_secs(path: &std::path::Path) -> i64 { .unwrap_or_default() } -fn with_only_latest_patch(versions: &[String]) -> Vec { +fn with_only_latest_patch(versions: &[Version]) -> Vec { // Only show the latest patch release for a given (major, minor) let mut seen_major_minor = std::collections::HashSet::new(); versions .iter() .filter(|v| { - let (major, minor, _, _) = parse_version_for_filtering(v); + let (major, minor, _, _) = parse_version_for_filtering(&v.version); if major == 2 { return true; } @@ -481,12 +314,15 @@ fn parse_version_for_filtering(version_str: &str) -> (u32, u32, u32, bool) { (major, minor, patch, is_prerelease) } -fn normalize_version(version: &str) -> String { - let (major, minor, patch, _is_prerelease) = parse_version_for_filtering(version); +fn normalize_version(version: &str) -> Version { + let (major, minor, patch, is_prerelease) = parse_version_for_filtering(version); if major <= 2 { // Don't transform versions <= 2.x - return version.to_string(); + return Version { + version: version.to_string(), + is_prerelease, + }; } // For versions > 2, pad the minor version with leading zero if < 10 @@ -516,18 +352,20 @@ fn normalize_version(version: &str) -> String { } // Reconstruct the version - if version.matches('.').count() >= 2 { + let version = if version.matches('.').count() >= 2 { format!("{major}.{normalized_minor}.{patch}{prerelease_suffix}") } else { format!("{major}.{normalized_minor}{prerelease_suffix}") + }; + + Version { + version, + is_prerelease, } } -fn filter_and_normalize_versions( - all_versions: Vec, - include_prereleases: bool, -) -> Vec { - let mut valid_versions: Vec = all_versions +fn filter_and_normalize_versions1(all_versions: Vec) -> Vec { + let mut valid_versions: Vec = all_versions .into_iter() .map(|v| normalize_version(&v)) .collect(); @@ -535,57 +373,10 @@ fn filter_and_normalize_versions( // Reverse to get chronological order (newest first) valid_versions.reverse(); - if !include_prereleases { - valid_versions.retain(|v| { - let (_, _, _, is_prerelease) = parse_version_for_filtering(v); - !is_prerelease - }); - } - valid_versions } -fn fetch_versions(state: &State) -> Result> { - let versions_script = state.resources_dir.join("versions.py"); - - let mut cmd = uv_command(state)?; - cmd.args(["run", "--no-project", "--no-config", "--managed-python"]) - .args(["--with", "pip-system-certs,requests[socks]"]); - - let python_version = read_file(&state.dist_python_version_path)?; - let python_version_str = - String::from_utf8(python_version).context("Invalid UTF-8 in .python-version")?; - let version_trimmed = python_version_str.trim(); - if !version_trimmed.is_empty() { - cmd.args(["--python", version_trimmed]); - } - - cmd.arg(&versions_script); - - let output = match cmd.utf8_output() { - Ok(output) => output, - Err(e) => { - return Err(e.into()); - } - }; - let versions = serde_json::from_str(&output.stdout).context("Failed to parse versions JSON")?; - Ok(versions) -} - -pub fn get_releases(state: &State) -> Result { - let include_prereleases = state.prerelease_marker.exists(); - let all_versions = fetch_versions(state)?; - let all_versions = filter_and_normalize_versions(all_versions, include_prereleases); - - let latest_patches = with_only_latest_patch(&all_versions); - let latest_releases: Vec = latest_patches.into_iter().take(5).collect(); - Ok(Releases { - latest: latest_releases, - all: all_versions, - }) -} - -pub fn apply_version_kind(version_kind: &VersionKind, state: &State) -> Result<()> { +pub fn apply_version_kind(version_kind: &VersionKind, state: &Paths) -> Result<()> { let content = read_file(&state.dist_pyproject_path)?; let content_str = String::from_utf8(content).context("Invalid UTF-8 in pyproject.toml")?; let updated_content = match version_kind { @@ -745,7 +536,7 @@ fn get_anki_addons21_path() -> Result { // TODO: revert #[allow(unused)] -fn handle_uninstall(state: &State) -> Result { +fn handle_uninstall(state: &Paths) -> Result { // println!("{}", state.tr.launcher_uninstall_confirm()); print!("> "); let _ = stdout().flush(); @@ -795,7 +586,7 @@ fn handle_uninstall(state: &State) -> Result { Ok(true) } -fn uv_command(state: &State) -> Result { +fn uv_command(state: &Paths) -> Result { let mut command = Command::new(&state.uv_path); command.current_dir(&state.uv_install_root); @@ -825,7 +616,7 @@ fn uv_command(state: &State) -> Result { Ok(command) } -fn uv_pty_command(state: &State) -> Result { +fn uv_pty_command(state: &Paths) -> Result { let mut command = portable_pty::CommandBuilder::new(&state.uv_path); command.cwd(&state.uv_install_root); @@ -847,34 +638,7 @@ fn uv_pty_command(state: &State) -> Result { Ok(command) } -pub fn build_python_command(state: &State, args: &[String]) -> Result { - let python_exe = if cfg!(target_os = "windows") { - let show_console = std::env::var("ANKI_CONSOLE").is_ok(); - if show_console { - state.venv_folder.join("Scripts/python.exe") - } else { - state.venv_folder.join("Scripts/pythonw.exe") - } - } else { - state.venv_folder.join("bin/python") - }; - - let mut cmd = Command::new(&python_exe); - cmd.args(["-c", "import aqt, sys; sys.argv[0] = 'Anki'; aqt.run()"]); - cmd.args(args); - // tell the Python code it was invoked by the launcher, and updating is - // available - cmd.env("ANKI_LAUNCHER", std::env::current_exe()?.utf8()?.as_str()); - - // Set UV and Python paths for the Python code - cmd.env("ANKI_LAUNCHER_UV", state.uv_path.utf8()?.as_str()); - cmd.env("UV_PROJECT", state.uv_install_root.utf8()?.as_str()); - cmd.env_remove("SSLKEYLOGFILE"); - - Ok(cmd) -} - -fn get_mirror_urls(state: &State) -> Result> { +fn get_mirror_urls(state: &Paths) -> Result> { if !state.mirror_path.exists() { return Ok(None); } @@ -893,7 +657,7 @@ fn get_mirror_urls(state: &State) -> Result> { } } -pub fn set_mirror(state: &State, enabled: bool) -> Result<()> { +pub fn set_mirror(state: &Paths, enabled: bool) -> Result<()> { if enabled { let china_mirrors = "https://registry.npmmirror.com/-/binary/python-build-standalone/\nhttps://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/"; write_file(&state.mirror_path, china_mirrors)?; @@ -903,12 +667,299 @@ pub fn set_mirror(state: &State, enabled: bool) -> Result<()> { Ok(()) } +impl crate::state::State { + pub fn init() -> Result { + let uv_install_root = if let Ok(custom_root) = std::env::var("ANKI_LAUNCHER_VENV_ROOT") { + std::path::PathBuf::from(custom_root) + } else { + dirs::data_local_dir() + .context("Unable to determine data_dir")? + .join("AnkiProgramFiles") + }; + + let (exe_dir, resources_dir) = get_exe_and_resources_dirs()?; + + let mut paths = Paths { + prerelease_marker: uv_install_root.join("prerelease"), + uv_install_root: uv_install_root.clone(), + uv_cache_dir: uv_install_root.join("cache"), + no_cache_marker: uv_install_root.join("nocache"), + anki_base_folder: get_anki_base_path()?, + uv_path: exe_dir.join(get_uv_binary_name()), + uv_python_install_dir: uv_install_root.join("python"), + user_pyproject_path: uv_install_root.join("pyproject.toml"), + user_python_version_path: uv_install_root.join(".python-version"), + dist_pyproject_path: resources_dir.join("pyproject.toml"), + dist_python_version_path: resources_dir.join(".python-version"), + uv_lock_path: uv_install_root.join("uv.lock"), + sync_complete_marker: uv_install_root.join(".sync_complete"), + launcher_trigger_file: uv_install_root.join(".want-launcher"), + mirror_path: uv_install_root.join("mirror"), + pyproject_modified_by_user: false, // calculated later + system_qt: (cfg!(unix) && !cfg!(target_os = "macos")) + && resources_dir.join("system_qt").exists(), + resources_dir, + venv_folder: uv_install_root.join(".venv"), + }; + + // Check for uninstall request from Windows uninstaller + if std::env::var("ANKI_LAUNCHER_UNINSTALL").is_ok() { + return Ok(Self::Uninstall(paths.into())); + } + + // Create install directory + create_dir_all(&paths.uv_install_root)?; + + let launcher_requested = + paths.launcher_trigger_file.exists() || !paths.user_pyproject_path.exists(); + + // TODO: remove + let skip = std::env::var("ANKI_LAUNCHER_SKIP").is_ok(); + + // Calculate whether user has custom edits that need syncing + let pyproject_time = file_timestamp_secs(&paths.user_pyproject_path); + let sync_time = file_timestamp_secs(&paths.sync_complete_marker); + paths.pyproject_modified_by_user = pyproject_time > sync_time; + let pyproject_has_changed = paths.pyproject_modified_by_user; + + #[allow(clippy::nonminimal_bool)] + let debug = true && cfg!(debug_assertions); + + if !launcher_requested && !pyproject_has_changed && (!debug || skip) { + return Ok(Self::LaunchAnki(paths.into())); + } + + if launcher_requested { + // Remove the trigger file to make request ephemeral + let _ = remove_file(&paths.launcher_trigger_file); + } + + ensure_os_supported()?; + + Ok(Self::Normal(paths.into())) + } +} + +impl Paths { + fn get_mirror_urls(&self) -> Result> { + if !self.mirror_path.exists() { + return Ok(None); + } + + let content = read_file(&self.mirror_path)?; + let content_str = String::from_utf8(content).context("Invalid UTF-8 in mirror file")?; + + let lines: Vec<&str> = content_str.lines().collect(); + if lines.len() >= 2 { + Ok(Some(( + lines[0].trim().to_string(), + lines[1].trim().to_string(), + ))) + } else { + Ok(None) + } + } + + fn uv_command(&self) -> Result { + let mut command = Command::new(&self.uv_path); + command.current_dir(&self.uv_install_root); + + // remove UV_* environment variables to avoid interference + for (key, _) in std::env::vars() { + if key.starts_with("UV_") { + command.env_remove(key); + } + } + command + .env_remove("VIRTUAL_ENV") + .env_remove("SSLKEYLOGFILE"); + + // Add mirror environment variable if enabled + if let Some((python_mirror, pypi_mirror)) = self.get_mirror_urls()? { + command + .env("UV_PYTHON_INSTALL_MIRROR", &python_mirror) + .env("UV_DEFAULT_INDEX", &pypi_mirror); + } + + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + + command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); + } + Ok(command) + } + + fn fetch_versions(&self) -> Result> { + let versions_script = self.resources_dir.join("versions.py"); + + let mut cmd = self.uv_command()?; + cmd.args(["run", "--no-project", "--no-config", "--managed-python"]) + .args(["--with", "pip-system-certs,requests[socks]"]); + + let python_version = read_file(&self.dist_python_version_path)?; + let python_version_str = + String::from_utf8(python_version).context("Invalid UTF-8 in .python-version")?; + let version_trimmed = python_version_str.trim(); + if !version_trimmed.is_empty() { + cmd.args(["--python", version_trimmed]); + } + + cmd.arg(&versions_script); + + let output = match cmd.utf8_output() { + Ok(output) => output, + Err(e) => { + return Err(e.into()); + } + }; + let versions = + serde_json::from_str(&output.stdout).context("Failed to parse versions JSON")?; + Ok(versions) + } + + pub fn get_releases(&self) -> Result { + let all_versions = self.fetch_versions()?; + let all_versions = filter_and_normalize_versions1(all_versions); + + let latest_patches = with_only_latest_patch(&all_versions); + let latest_releases: Vec = latest_patches.into_iter().take(5).collect(); + + Ok(Versions { + latest: latest_releases, + all: all_versions, + }) + } + + fn extract_aqt_version(&self) -> Option { + // Check if .venv exists first + if !self.venv_folder.exists() { + return None; + } + + let output = self + .uv_command() + .ok()? + .env("VIRTUAL_ENV", &self.venv_folder) + .args(["pip", "show", "aqt"]) + .output(); + + let output = output.ok()?; + + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8(output.stdout).ok()?; + for line in stdout.lines() { + if let Some(version) = line.strip_prefix("Version: ") { + return Some(version.trim().to_string()); + } + } + None + } + + pub fn check_versions(&self) -> Result { + let mut res = ExistingVersions { + pyproject_modified_by_user: self.pyproject_modified_by_user, + ..Default::default() + }; + + // If sync_complete_marker is missing, do nothing + if !self.sync_complete_marker.exists() { + return Ok(res); + } + + // Determine current version by invoking uv pip show aqt + match self.extract_aqt_version() { + Some(version) => { + res.current = Some(normalize_version(&version)); + } + None => { + Err(anyhow::anyhow!( + "Warning: Could not determine current Anki version" + ))?; + } + } + + // Read previous version from "previous-version" file + let previous_version_path = self.uv_install_root.join("previous-version"); + if let Ok(content) = read_file(&previous_version_path) { + if let Ok(version_str) = String::from_utf8(content) { + let version = version_str.trim().to_string(); + if !version.is_empty() { + res.previous = Some(normalize_version(&version)); + } + } + } + + Ok(res) + } + + fn write_sync_marker(&self) -> Result<()> { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .context("Failed to get system time")? + .as_secs(); + write_file(&self.sync_complete_marker, timestamp.to_string())?; + Ok(()) + } + + pub fn post_install(&self) -> Result { + // Write marker file to indicate we've completed the sync process + self.write_sync_marker()?; + + // whether or not anki needs to warm up + Ok(cfg!(target_os = "macos")) + } + + pub fn launch_anki(&self) -> Result<()> { + #[cfg(target_os = "macos")] + { + let cmd = self.build_python_command(&[])?; + platform::mac::prepare_for_launch_after_update(cmd, &self.uv_install_root)?; + } + + // respawn the launcher as a disconnected subprocess for normal startup + respawn_launcher() + } + + pub fn build_python_command(&self, args: &[String]) -> Result { + let python_exe = if cfg!(target_os = "windows") { + let show_console = std::env::var("ANKI_CONSOLE").is_ok(); + if show_console { + self.venv_folder.join("Scripts/python.exe") + } else { + self.venv_folder.join("Scripts/pythonw.exe") + } + } else { + self.venv_folder.join("bin/python") + }; + + let mut cmd = Command::new(&python_exe); + cmd.args(["-c", "import aqt, sys; sys.argv[0] = 'Anki'; aqt.run()"]); + cmd.args(args); + // tell the Python code it was invoked by the launcher, and updating is + // available + cmd.env("ANKI_LAUNCHER", std::env::current_exe()?.utf8()?.as_str()); + + // Set UV and Python paths for the Python code + cmd.env("ANKI_LAUNCHER_UV", self.uv_path.utf8()?.as_str()); + cmd.env("UV_PROJECT", self.uv_install_root.utf8()?.as_str()); + cmd.env_remove("SSLKEYLOGFILE"); + + Ok(cmd) + } +} + #[cfg(test)] mod tests { use super::*; #[test] fn test_normalize_version() { + let normalize_version = |v| normalize_version(v).version; + // Test versions <= 2.x (should not be transformed) assert_eq!(normalize_version("2.1.50"), "2.1.50"); From eaa514fb7a01bd3e54c0e4a3c6fa62ff3c9bfec7 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 09:56:47 +0800 Subject: [PATCH 22/74] refactor app --- qt/launcher-gui/src-tauri/src/app.rs | 25 ++++++++++++++++++++++--- qt/launcher-gui/src-tauri/src/lang.rs | 1 - qt/launcher-gui/src-tauri/src/main.rs | 9 ++++----- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/qt/launcher-gui/src-tauri/src/app.rs b/qt/launcher-gui/src-tauri/src/app.rs index afe517be7..578ae8c32 100644 --- a/qt/launcher-gui/src-tauri/src/app.rs +++ b/qt/launcher-gui/src-tauri/src/app.rs @@ -13,13 +13,32 @@ use tauri::UriSchemeResponder; use tauri_plugin_os::locale; use crate::generated; -use crate::lang::setup_i18n; +use crate::lang::I18n; +use crate::state::State; use crate::uv; pub const PROTOCOL: &str = "anki"; -pub fn setup(app: &mut App, state: uv::State) -> anyhow::Result<()> { - setup_i18n(app.app_handle(), &[&locale().unwrap_or_default()]); +pub fn init() -> Option { + let mut state = State::init().unwrap_or_else(State::Error); + + match state { + State::Normal(ref mut state) => state.check_versions(), + State::LaunchAnki(ref paths) => { + let args: Vec = std::env::args().skip(1).collect(); + let cmd = paths.build_python_command(&args).unwrap(); + uv::launch_anki_normally(cmd).unwrap(); + return None; + } + _ => {} + } + + Some(state) +} + +pub fn setup(app: &mut App, state: State) -> anyhow::Result<()> { + let tr = I18n::new(&[&locale().unwrap_or_default()]); + app.manage(crate::lang::Tr::new(Some(tr))); app.manage(state); diff --git a/qt/launcher-gui/src-tauri/src/lang.rs b/qt/launcher-gui/src-tauri/src/lang.rs index 9c6d918c0..dc61b0817 100644 --- a/qt/launcher-gui/src-tauri/src/lang.rs +++ b/qt/launcher-gui/src-tauri/src/lang.rs @@ -16,7 +16,6 @@ pub type I18n = anki_i18n::I18n; pub type Tr = RwLock>; pub fn setup_i18n(app: &AppHandle, locales: &[&str]) { - app.manage(Tr::default()); // no-op if it already exists *app.state::().write().expect("tr lock was poisoned!") = Some(I18n::new(locales)); } diff --git a/qt/launcher-gui/src-tauri/src/main.rs b/qt/launcher-gui/src-tauri/src/main.rs index ad0e86800..3e477b9da 100644 --- a/qt/launcher-gui/src-tauri/src/main.rs +++ b/qt/launcher-gui/src-tauri/src/main.rs @@ -10,16 +10,13 @@ mod error; mod generated; mod lang; mod platform; +mod state; mod uv; fn main() { - let Some(state) = uv::init_state().unwrap() else { - // either anki was spawned or os not supported (TODO) - return; - }; + let Some(state) = app::init() else { return }; tauri::Builder::default() - .plugin(tauri_plugin_os::init()) .plugin( tauri_plugin_log::Builder::new() .clear_targets() @@ -29,6 +26,8 @@ fn main() { .level(tauri_plugin_log::log::LevelFilter::Trace) .build(), ) + .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_single_instance::init(app::on_second_instance)) .setup(|app| Ok(app::setup(app, state)?)) .register_asynchronous_uri_scheme_protocol(app::PROTOCOL, app::serve) From 572cbf1043c7bbe83e333b9287c2add86823a9a3 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 09:58:07 +0800 Subject: [PATCH 23/74] remove unneeded rpc error message --- qt/launcher-gui/src-tauri/rust_interface.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/launcher-gui/src-tauri/rust_interface.rs b/qt/launcher-gui/src-tauri/rust_interface.rs index 1326b6b9f..981d4b080 100644 --- a/qt/launcher-gui/src-tauri/rust_interface.rs +++ b/qt/launcher-gui/src-tauri/rust_interface.rs @@ -18,6 +18,7 @@ pub fn write_rust_interface(pool: &DescriptorPool) -> Result<()> { let mut buf = String::new(); buf.push_str("use prost::Message; use anyhow::Context; use anyhow::anyhow;"); + // TODO: render as trait for better compiler errors let (services, _) = get_services(pool); if let Some(s) = services .into_iter() @@ -108,7 +109,6 @@ pub(crate) async fn handle_rpc( r#" _ => Err(anyhow!("{method} not implemented"))?, } - .with_context(|| format!("{method} rpc call failed")) } "#, ) From b04520a352f413622e42bcdf03c9e93f33f956fc Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 09:58:58 +0800 Subject: [PATCH 24/74] refactor and implement rpc commands --- qt/launcher-gui/src-tauri/src/commands.rs | 134 ++++++++++++---------- 1 file changed, 73 insertions(+), 61 deletions(-) diff --git a/qt/launcher-gui/src-tauri/src/commands.rs b/qt/launcher-gui/src-tauri/src/commands.rs index 29368cb96..faa97330f 100644 --- a/qt/launcher-gui/src-tauri/src/commands.rs +++ b/qt/launcher-gui/src-tauri/src/commands.rs @@ -8,15 +8,16 @@ use anki_proto::launcher::ChooseVersionRequest; use anki_proto::launcher::ChooseVersionResponse; use anki_proto::launcher::GetLangsResponse; use anki_proto::launcher::GetMirrorsResponse; -use anki_proto::launcher::GetVersionsResponse; use anki_proto::launcher::I18nResourcesRequest; use anki_proto::launcher::Mirror; use anki_proto::launcher::Options; use anki_proto::launcher::ZoomWebviewRequest; +use anyhow::anyhow; use anyhow::Context; use anyhow::Result; use strum::IntoEnumIterator; use tauri::AppHandle; +use tauri::Emitter; use tauri::Manager; use tauri::Runtime; use tauri::WebviewWindow; @@ -27,6 +28,9 @@ use crate::lang::setup_i18n; use crate::lang::LANGS; use crate::lang::LANGS_DEFAULT_REGION; use crate::lang::LANGS_WITH_REGIONS; +use crate::state::ExistingVersions; +use crate::state::State; +use crate::state::Versions; use crate::uv; pub async fn i18n_resources( @@ -107,45 +111,40 @@ pub async fn get_options( app: AppHandle, _window: WebviewWindow, ) -> Result { - let state = app.state::(); - let allow_betas = state.prerelease_marker.exists(); - let download_caching = !state.no_cache_marker.exists(); - let mirror = if state.mirror_path.exists() { - Mirror::China - } else { - Mirror::Disabled - } - .into(); - - Ok(Options { - allow_betas, - download_caching, - mirror, - }) + let state = app.state::(); + let options = (&state.normal()?.initial_options).into(); + Ok(options) } -pub async fn get_versions( +pub async fn get_available_versions( app: AppHandle, _window: WebviewWindow, -) -> Result { - let state = (*app.state::()).clone(); - // TODO: why... - let mut state1 = state.clone(); +) -> Result { + let state = app.state::(); + let state = state.normal()?; + let mut rx = state.available_versions.clone().unwrap(); + rx.changed().await.unwrap(); + let x = rx.borrow(); + match x.as_ref().unwrap() { + Ok(versions) => Ok(versions.clone()), + // TODO: errors are passed as strings to the web + Err(e) => Err(anyhow!("{e:?}")), + } +} - let releases_fut = tauri::async_runtime::spawn_blocking(move || uv::get_releases(&state)); - let check_fut = tauri::async_runtime::spawn_blocking(move || uv::check_versions(&mut state1)); - - let (releases, check) = futures::future::join(releases_fut, check_fut).await; - // TODO: handle errors properly - let uv::Releases { latest, all } = releases.unwrap().unwrap(); - let (current, previous) = check.unwrap().unwrap(); - - Ok(GetVersionsResponse { - latest, - all, - current, - previous, - }) +pub async fn get_existing_versions( + app: AppHandle, + _window: WebviewWindow, +) -> Result { + let state = app.state::(); + let state = state.normal()?; + let mut rx = state.current_versions.clone().unwrap(); + rx.changed().await.unwrap(); + let x = rx.borrow(); + match x.as_ref().unwrap() { + Ok(versions) => Ok(versions.clone()), + Err(e) => Err(anyhow!("{e:?}")), + } } pub async fn choose_version( @@ -153,46 +152,59 @@ pub async fn choose_version( _window: WebviewWindow, input: ChooseVersionRequest, ) -> Result { - let state = (*app.state::()).clone(); - let version = input.version.clone(); + let state = app.state::(); + let state = state.normal()?; + let paths = state.paths.clone(); - tauri::async_runtime::spawn_blocking(move || -> Result<()> { + tauri::async_runtime::spawn_blocking(move || { if let Some(options) = input.options { - uv::set_allow_betas(&state, options.allow_betas)?; - uv::set_cache_enabled(&state, options.download_caching)?; - uv::set_mirror(&state, options.mirror != Mirror::Disabled as i32)?; + uv::set_allow_betas(&paths, options.allow_betas)?; + uv::set_cache_enabled(&paths, options.download_caching)?; + uv::set_mirror(&paths, options.mirror != Mirror::Disabled as i32)?; } - if !input.keep_existing || state.pyproject_modified_by_user { + let version = input.version; + let on_pty_data = move |data| { + let _ = app.emit("pty-data", data); + }; + + if !input.keep_existing || paths.pyproject_modified_by_user { // install or resync - let res = uv::handle_version_install_or_update( - app.clone(), - &state, - &input.version, + uv::handle_version_install_or_update( + &paths, + &version, input.keep_existing, - ); - println!("handle_version_install_or_update: {res:?}"); - res?; + input.current.as_deref(), + on_pty_data, + )?; } - uv::post_install(&state)?; + let warming_up = paths.post_install()?; - // TODO: show some sort of notification before closing - // if let Some(window) = app.get_webview_window("main") { - // let _ = window.destroy(); - // } - // // app.exit can't be called from the main thread - // app.exit(0); - - Ok(()) + Ok(ChooseVersionResponse { + version, + warming_up, + }) }) - .await??; + .await? +} - Ok(ChooseVersionResponse { version }) +pub async fn launch_anki(app: AppHandle, _window: WebviewWindow) -> Result<()> { + app.state::().paths()?.launch_anki() +} + +pub async fn exit(app: AppHandle, window: WebviewWindow) -> Result<()> { + tauri::async_runtime::spawn_blocking(move || { + let _ = window.destroy(); + // can't be called from the main thread + app.exit(0); + }); + + Ok(()) } /// NOTE: [zoomHotkeysEnabled](https://v2.tauri.app/reference/config/#zoomhotkeysenabled) exists -/// but the polyfill it uses on lin doesn't allow regular scrolling +/// but the polyfill it uses on linux doesn't allow regular scrolling pub async fn zoom_webview( _app: AppHandle, window: WebviewWindow, From 8ec9d4d1e5596400a73705d4fd9735afa176d571 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 09:59:39 +0800 Subject: [PATCH 25/74] change codegen to not alert by default on error --- rslib/proto/typescript.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib/proto/typescript.rs b/rslib/proto/typescript.rs index 66cc7e590..ddecb97fc 100644 --- a/rslib/proto/typescript.rs +++ b/rslib/proto/typescript.rs @@ -84,7 +84,7 @@ fn write_ts_method( let comments = format_comments(comments); let proto_method_name = method_name; let options = if is_launcher { - "{ ...options, customProtocol: true }" + "{ ...options, customProtocol: true, alertOnError: false }" } else { "options" }; From a379e20ddd2aedb00c74b5fd8a0fe6a048e395e3 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 10:00:11 +0800 Subject: [PATCH 26/74] update tauri.conf.json --- qt/launcher-gui/src-tauri/tauri.conf.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qt/launcher-gui/src-tauri/tauri.conf.json b/qt/launcher-gui/src-tauri/tauri.conf.json index bfc46a44f..b9ab39e1f 100644 --- a/qt/launcher-gui/src-tauri/tauri.conf.json +++ b/qt/launcher-gui/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "anki-launcher", - "version": "0.1.0", + "productName": "Anki Launcher", + "version": "1.0.0", "identifier": "com.ichi2.anki-launcher", "build": { "beforeDevCommand": "./launcher", @@ -13,8 +13,8 @@ "windows": [ { "title": "Anki Launcher", - "width": 600, - "height": 600, + "width": 800, + "height": 780, "visible": true } ], From 2ba7809de6e79b4ffb1f54871e08fdb82acf9fd6 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 10:00:33 +0800 Subject: [PATCH 27/74] add strings --- ftl/core/launcher.ftl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ftl/core/launcher.ftl b/ftl/core/launcher.ftl index 19a59e01b..d4b64b9cb 100644 --- a/ftl/core/launcher.ftl +++ b/ftl/core/launcher.ftl @@ -45,4 +45,12 @@ launcher-use-mirror = Download mirror launcher-download-caching = Download caching launcher-advanced = Advanced launcher-output = Output -launcher-syncing = Syncing... +launcher-syncing = Syncing +launcher-failed-to-sync = Failed to sync! +launcher-loading = Loading +launcher-loading-versions = Loading versions +lauuncher-failed-to-load-versions = Failed to load versions! +launcher-error-details = Error details +launcher-will-close-in = Launcher will close in { $count } +launcher-anki-is-warming-up = Anki is preparing to run +launcher-this-may-take = This may take a few minutes. Please wait\ From 9434499e7a3987c41bf92cd693db7828562a4510 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 10:00:53 +0800 Subject: [PATCH 28/74] add font fallback --- qt/launcher-gui/src/routes/+layout.svelte | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/qt/launcher-gui/src/routes/+layout.svelte b/qt/launcher-gui/src/routes/+layout.svelte index cd943cbe7..eaf43e1e0 100644 --- a/qt/launcher-gui/src/routes/+layout.svelte +++ b/qt/launcher-gui/src/routes/+layout.svelte @@ -24,3 +24,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + + From b97b8462d9dc1e3e7d80276a480de02bb116d423 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 10:01:38 +0800 Subject: [PATCH 29/74] remove getVersions call --- qt/launcher-gui/src/routes/+page.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/qt/launcher-gui/src/routes/+page.ts b/qt/launcher-gui/src/routes/+page.ts index 7d2ac7a88..f167f5baa 100644 --- a/qt/launcher-gui/src/routes/+page.ts +++ b/qt/launcher-gui/src/routes/+page.ts @@ -1,10 +1,9 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import { getLangs, getMirrors, getOptions, getVersions } from "@generated/backend-launcher"; +import { getLangs, getMirrors, getOptions } from "@generated/backend-launcher"; import { ModuleName, setupI18n } from "@tslib/i18n"; import type { PageLoad } from "./$types"; -import { versionsStore } from "./stores"; export const load = (async () => { const i18nPromise = setupI18n({ modules: [ModuleName.LAUNCHER] }, true); @@ -12,10 +11,6 @@ export const load = (async () => { const optionsPromise = getOptions({}); const mirrorsPromise = getMirrors({}); - getVersions({}).then((res) => { - versionsStore.set(res); - }); - const [_, { userLocale, langs }, options, { mirrors }] = await Promise.all([ i18nPromise, langsPromise, From 167551839d1606216a06c8684cc72bff6067e679 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 10:02:28 +0800 Subject: [PATCH 30/74] provide tr as store to avoid using {#key ...} --- qt/launcher-gui/src/routes/stores.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qt/launcher-gui/src/routes/stores.ts b/qt/launcher-gui/src/routes/stores.ts index 864dfb31c..bb96b02bf 100644 --- a/qt/launcher-gui/src/routes/stores.ts +++ b/qt/launcher-gui/src/routes/stores.ts @@ -1,6 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import * as _tr from "@generated/ftl-launcher"; import type { GetLangsResponse_Pair, GetMirrorsResponse_Pair, GetVersionsResponse } from "@generated/anki/launcher_pb"; import { writable } from "svelte/store"; @@ -10,3 +11,6 @@ export const mirrorsStore = writable([]); export const currentLang = writable(""); export const initialLang = writable(""); export const versionsStore = writable(undefined); + +export const tr = writable(_tr); +currentLang.subscribe(() => tr.set(_tr)); From 1f7fe021df082c2f6124cdbf3b34643ba4ea2e15 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 10:03:41 +0800 Subject: [PATCH 31/74] modify Warning to take icon --- qt/launcher-gui/src/routes/Warning.svelte | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/qt/launcher-gui/src/routes/Warning.svelte b/qt/launcher-gui/src/routes/Warning.svelte index f7549e6ca..befae39b0 100644 --- a/qt/launcher-gui/src/routes/Warning.svelte +++ b/qt/launcher-gui/src/routes/Warning.svelte @@ -13,9 +13,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {#if warning} - +
+ {withoutUnicodeIsolation(warning)}
{/if} + + From e398671e574777a69abb25190869d2cada63785f Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 10:06:31 +0800 Subject: [PATCH 32/74] add CheckDecagramOutline icon --- ts/lib/components/icons.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ts/lib/components/icons.ts b/ts/lib/components/icons.ts index ab07cbf17..57cb08464 100644 --- a/ts/lib/components/icons.ts +++ b/ts/lib/components/icons.ts @@ -17,6 +17,8 @@ import AlignVerticalTop_ from "@mdi/svg/svg/align-vertical-top.svg?component"; import alignVerticalTop_ from "@mdi/svg/svg/align-vertical-top.svg?url"; import CheckCircle_ from "@mdi/svg/svg/check-circle.svg?component"; import checkCircle_ from "@mdi/svg/svg/check-circle.svg?url"; +import CheckDecagramOutline_ from "@mdi/svg/svg/check-decagram-outline.svg?component"; +import checkDecagramOutline_ from "@mdi/svg/svg/check-decagram-outline.svg?url"; import ChevronDown_ from "@mdi/svg/svg/chevron-down.svg?component"; import chevronDown_ from "@mdi/svg/svg/chevron-down.svg?url"; import ChevronUp_ from "@mdi/svg/svg/chevron-up.svg?component"; @@ -192,6 +194,7 @@ import StickySolid_ from "../../icons/sticky-pin-solid.svg?component"; import stickySolid_ from "../../icons/sticky-pin-solid.svg?url"; export const checkCircle = { url: checkCircle_, component: CheckCircle_ }; +export const checkDecagramOutline = { url: checkDecagramOutline_, component: CheckDecagramOutline_ }; export const chevronDown = { url: chevronDown_, component: ChevronDown_ }; export const chevronUp = { url: chevronUp_, component: ChevronUp_ }; export const closeBox = { url: closeBox_, component: CloseBox_ }; From 3bb77f964dee8b7a88309df3c8ca8a80bd10b9c2 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 10:07:18 +0800 Subject: [PATCH 33/74] add components --- qt/launcher-gui/src/routes/Action.svelte | 90 +++++++++++++++++++ .../src/routes/AnkiWillStart.svelte | 65 ++++++++++++++ qt/launcher-gui/src/routes/Options.svelte | 64 +++++++++++++ qt/launcher-gui/src/routes/Spinner.svelte | 70 +++++++++++++++ qt/launcher-gui/src/routes/Term.svelte | 85 ++++++++++++++++++ 5 files changed, 374 insertions(+) create mode 100644 qt/launcher-gui/src/routes/Action.svelte create mode 100644 qt/launcher-gui/src/routes/AnkiWillStart.svelte create mode 100644 qt/launcher-gui/src/routes/Options.svelte create mode 100644 qt/launcher-gui/src/routes/Spinner.svelte create mode 100644 qt/launcher-gui/src/routes/Term.svelte diff --git a/qt/launcher-gui/src/routes/Action.svelte b/qt/launcher-gui/src/routes/Action.svelte new file mode 100644 index 000000000..1aa6d5f6c --- /dev/null +++ b/qt/launcher-gui/src/routes/Action.svelte @@ -0,0 +1,90 @@ + + + +
+ {#if latest != null && latest != current} + + + + {/if} + {#if current != null} + + + + {/if} + + +
+ {"->"} +
+
+ +
+
+
+ + diff --git a/qt/launcher-gui/src/routes/AnkiWillStart.svelte b/qt/launcher-gui/src/routes/AnkiWillStart.svelte new file mode 100644 index 000000000..1626dc0ad --- /dev/null +++ b/qt/launcher-gui/src/routes/AnkiWillStart.svelte @@ -0,0 +1,65 @@ + + + + + +{#await launch} + +
{$tr.launcherAnkiIsWarmingUp()}
+ {#if warmingUp} +
{$tr.launcherThisMayTake()}
+ {/if} +
+{:then} + + + + + + +{/await} diff --git a/qt/launcher-gui/src/routes/Options.svelte b/qt/launcher-gui/src/routes/Options.svelte new file mode 100644 index 000000000..be656f926 --- /dev/null +++ b/qt/launcher-gui/src/routes/Options.svelte @@ -0,0 +1,64 @@ + + + + +
+ + + {$tr.launcherAllowBetasToggle()} + + +
+
+ + + {$tr.launcherDownloadCaching()} + + +
+
+ + + {$tr.launcherUseMirror()} + + +
+
diff --git a/qt/launcher-gui/src/routes/Spinner.svelte b/qt/launcher-gui/src/routes/Spinner.svelte new file mode 100644 index 000000000..259232897 --- /dev/null +++ b/qt/launcher-gui/src/routes/Spinner.svelte @@ -0,0 +1,70 @@ + + + + +
+
+
+
+
+
+
+
+ {label} + {@render children?.()} +
+
+ + diff --git a/qt/launcher-gui/src/routes/Term.svelte b/qt/launcher-gui/src/routes/Term.svelte new file mode 100644 index 000000000..7982fef05 --- /dev/null +++ b/qt/launcher-gui/src/routes/Term.svelte @@ -0,0 +1,85 @@ + + + +
+
+
+ + From 49e850c7daed350a927406b88e979fc29dbc1913 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 10:08:29 +0800 Subject: [PATCH 34/74] refactor Start.svelte --- qt/launcher-gui/src/routes/Start.svelte | 324 ++++++++---------------- 1 file changed, 101 insertions(+), 223 deletions(-) diff --git a/qt/launcher-gui/src/routes/Start.svelte b/qt/launcher-gui/src/routes/Start.svelte index cd7dfb95e..e68f5388f 100644 --- a/qt/launcher-gui/src/routes/Start.svelte +++ b/qt/launcher-gui/src/routes/Start.svelte @@ -3,32 +3,32 @@ Copyright: Ankitects Pty Ltd and contributors License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> - - - - - - - - - - - - {#key $currentLang} - - - - -

{tr.launcherTitle()}

-
- - - {tr.launcherLanguage()} - - -
- {#if latestVersion != null && latestVersion != currentVersion} - - - - {/if} - {#if currentVersion != null} - - - - {/if} - - -
- {"->"} -
-
- {#if availableVersions.length !== 0} - - {:else} - {"loading"} - {/if} -
-
-
-
-
- {#await choosePromise} - - {:then res} - {#if res != null} - - {/if} - {/await} - {/key} -
- {#key $currentLang} - {tr.launcherOutput()} - {/key} -
-
+ + + +

{$tr.launcherTitle()}

+
+ + + {$tr.launcherLanguage()} + + + {#await choosePromise} + + + + {:then res} + {#if res === null} + {#await loadPromise} + + + + {:then [releases, existing]} + + {:catch e} + {setError(e)} + + {/await} + {:else} + + + + {/if} + {:catch e} + {setError(e)} + + {/await} + {#if error != null} + +
{error.message}
+
+ {/if} +
- {#key $currentLang} + + {#if !chosen} - -
- - - {tr.launcherAllowBetasToggle()} - - -
-
- - - {tr.launcherDownloadCaching()} - - -
-
- - - {tr.launcherUseMirror()} - - -
-
+
- {/key} + {/if}
From cd78dd9c95eff063881a91fa75b1a2316d54cb86 Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 10:08:44 +0800 Subject: [PATCH 35/74] add favicon --- qt/launcher-gui/src/app.html | 3 +-- qt/launcher-gui/static/favicon.ico | Bin 0 -> 4286 bytes 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 qt/launcher-gui/static/favicon.ico diff --git a/qt/launcher-gui/src/app.html b/qt/launcher-gui/src/app.html index b338b1f81..564d2dacd 100644 --- a/qt/launcher-gui/src/app.html +++ b/qt/launcher-gui/src/app.html @@ -2,9 +2,8 @@ - + - Anki Launcher %sveltekit.head% diff --git a/qt/launcher-gui/static/favicon.ico b/qt/launcher-gui/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..257849bdc2f34b4a14b3d947646b7d706f38bc5e GIT binary patch literal 4286 zcmcJSc}x^{6vqc+nwSPbo7OZosef48X8vgVr)d+s5byiOTf7nQM!Xg83opd`DhM9b zrb-Kc6ice2C}Qgft+tAS3oOU3%d(t)`@TOjWP>p~m~_coHoG(PnfKoJ9iwS}^4Fz{ z#&<0_P}4rrH0=v9`AX{{#_!cje*574*Y^4gOYd@))>TY@2kR#1_>1|8wf5CcP6!jr z5X%)a-of(ZoHVfrvDVx}yEbjwTvM(o?S4}16S#hyKTuzUAz?A*B% zJ9g~Awr$(6b?a7a-nVJZrHE^YuB#D;>C+Gc<^Azbs+pCu@A+pyu!#{x^!tX z_t2q3ID7W2;Oh{`t|FvZrwVpS+fSKR;|Lyl`FAg#R|-xJsa)Yw|5IT+yb2W z&X_iBTC=~qcI`rDW+q;}YJ%NvM`~)S@|Rrtx@F5296fqe8QCT`IyxH5mMsJQ7%*S} z+O}7$>E)G|(UPXR>J{%4QoKB~@ z7n8eW$r6==g$oyA?AWpJ_xE>M;8W)YV@xh{OaE@)zKzD3a?})N!Bd=rCXWNJ8tc&T zvIw4{o2dTlM>N(tl^)1lv}h5?jf{*$L_`E8OqhU<9XnD7zFvxNa;YiC`TF(iXsouO zx*!#8I`}sDu3Pp{?SxpA-}npuXY}sV}$J=l^$;HoNBb!l=#LqnDJ`}gmUPMtbg=2OS) z8|(omPMlC0a8y;|W!`Z-OZXB`_kNPr8>^JCxO$<7XNiYAJRijb9+g=^QYDZ6;_qB@VfF=NJnaag{5 zx$2GVmA;B!epmJH&7C`UuBsu{md8~NTgo8O-VoHgO4Q6@e%W8fj~}o6B$uDrgZA&= zkJG15H}__%eDB(&mN^Hhsav;hq59!@)Yu>4(xpppaH)0XjkR;;%o&_IbqXg>o92PUkw-1_9ym`p zlT2UfFMZ_`VHvVZ#j0cAn1|l0^-h@ilYG-^gd5biI_l zz*&w)sfoI>2WlTkOG{I1#)^9pHRX#h+SNrzhPQUTb>y8>#-Fj}4#HYgv)M?IS$qk# zw!3I_m%(PUsrz5OdbNtDFTQ9?7fl&ve3?`B9qNcR#GHEXqKq$h(h^%S+<#w@eIQQt zCD+3XYB$Nxf2fXU?J=jm_>yZ~`kmp6Y3`tmE%zjMRk7L)IIAi$V^j`q+_=%akFxft zU0;0Zb-I|rnla<+y>pXAEqm|Vyw_I^HE5`Mf;#65xc|+7r}P056BC>FVD<*~3SWHb zbzNm&$R)?z^UXcbo5@~o?kLR1xpU`KZg}j?n>SHfTB5Ye{hPJJ+8aK6xX=AlYA#Uv zN(T3L`e{B}uztLm+>g1VP*>)z&m5D-@2FGWpSgdthszFbW&hTs*M&*{YvK+-CTr(y zJd;TuIp<6s<4xl;5a;K>fdk!r`}Vc+{Go|X+DX5=BK@z4{gu~l>7yCfmQ3(#4QQlCD3tUiBfqeqW!H*)02FxeY3ByYLwNwUKkaxG=zk0J3iWUn#$_3LMZ zgoGI4pAj4!YzW5?9xbm&j~;13K|v96z1BQ`Hp^7Ffx_)NWXKSmy;1f!UD&#?bz$qm y)rG4IR~N3{yLWG0xO&f?J!##9?ccq7_qVd8Ac7h-?bBLK3!pjvXEQlulKmfS%k(_} literal 0 HcmV?d00001 From 9816c8121ada03c9aba4dd14e5ffc888f421db6b Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 10:09:28 +0800 Subject: [PATCH 36/74] limit select menu height to 200px --- ts/lib/components/Popover.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/lib/components/Popover.svelte b/ts/lib/components/Popover.svelte index 03b195c73..39f98ecdc 100644 --- a/ts/lib/components/Popover.svelte +++ b/ts/lib/components/Popover.svelte @@ -84,7 +84,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html padding: var(--popover-padding-block, 6px) var(--popover-padding-inline, 6px); &.scrollable { - max-height: 400px; + max-height: 200px; overflow: hidden auto; } From 4f52a2c0d665374c763a6770c94b0ecf600fa3ff Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 18 Oct 2025 10:09:49 +0800 Subject: [PATCH 37/74] make TitledContainer's title optional --- ts/lib/components/TitledContainer.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/lib/components/TitledContainer.svelte b/ts/lib/components/TitledContainer.svelte index 501638023..d1d588815 100644 --- a/ts/lib/components/TitledContainer.svelte +++ b/ts/lib/components/TitledContainer.svelte @@ -11,7 +11,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let className: string = ""; export { className as class }; - export let title: string; + export let title: string = "";
Date: Sat, 18 Oct 2025 10:52:00 +0800 Subject: [PATCH 38/74] ./check --- qt/launcher-gui/src-tauri/src/platform/mod.rs | 2 +- qt/launcher-gui/src-tauri/src/state.rs | 5 +---- qt/launcher-gui/src/routes/Action.svelte | 6 +++--- qt/launcher-gui/src/routes/Start.svelte | 2 +- qt/launcher-gui/src/routes/stores.ts | 2 +- ts/lib/generated/post.ts | 5 ++--- ts/lib/tslib/platform.ts | 5 ----- 7 files changed, 9 insertions(+), 18 deletions(-) diff --git a/qt/launcher-gui/src-tauri/src/platform/mod.rs b/qt/launcher-gui/src-tauri/src/platform/mod.rs index 07af93720..f1b49ac10 100644 --- a/qt/launcher-gui/src-tauri/src/platform/mod.rs +++ b/qt/launcher-gui/src-tauri/src/platform/mod.rs @@ -4,7 +4,7 @@ #[cfg(all(unix, not(target_os = "macos")))] pub mod unix; -// #[cfg(target_os = "macos")] +#[cfg(target_os = "macos")] pub mod mac; #[cfg(target_os = "windows")] diff --git a/qt/launcher-gui/src-tauri/src/state.rs b/qt/launcher-gui/src-tauri/src/state.rs index 09cde5ef4..f56a23447 100644 --- a/qt/launcher-gui/src-tauri/src/state.rs +++ b/qt/launcher-gui/src-tauri/src/state.rs @@ -73,6 +73,7 @@ impl From for State { pub enum State { LaunchAnki(Arc), + #[allow(dead_code)] // TODO: use Error(anyhow::Error), Uninstall(Arc), Normal(NormalState), @@ -94,10 +95,6 @@ impl State { _ => Err(anyhow!("unexpected state")), } } - - pub fn should_launch_anki(&self) -> bool { - matches!(self, State::LaunchAnki(_)) - } } impl NormalState { diff --git a/qt/launcher-gui/src/routes/Action.svelte b/qt/launcher-gui/src/routes/Action.svelte index 1aa6d5f6c..d01c9185d 100644 --- a/qt/launcher-gui/src/routes/Action.svelte +++ b/qt/launcher-gui/src/routes/Action.svelte @@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> + +
+
+ + + +
+
{title}
+
{detail}
+
+ + diff --git a/qt/launcher-gui/src/routes/Uninstall.svelte b/qt/launcher-gui/src/routes/Uninstall.svelte new file mode 100644 index 000000000..8ec9714aa --- /dev/null +++ b/qt/launcher-gui/src/routes/Uninstall.svelte @@ -0,0 +1,28 @@ + + + + +
+
TODO: Uninstall
+
+ + From 7b5afb8dd819e570f1a6ae2e255b13dcb721e217 Mon Sep 17 00:00:00 2001 From: llama Date: Mon, 20 Oct 2025 10:04:38 +0800 Subject: [PATCH 46/74] add uninstall button --- qt/launcher-gui/src/routes/Action.svelte | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/qt/launcher-gui/src/routes/Action.svelte b/qt/launcher-gui/src/routes/Action.svelte index 9ad44f311..e939e5985 100644 --- a/qt/launcher-gui/src/routes/Action.svelte +++ b/qt/launcher-gui/src/routes/Action.svelte @@ -13,11 +13,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html existing, allowBetas, choose, + uninstall, }: { releases: Versions; existing: ExistingVersions; allowBetas: boolean; choose: (version: string, existing: boolean, current?: string) => void; + uninstall: (() => void) | null; } = $props(); let availableVersions = $derived( @@ -77,6 +79,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+ {#if uninstall != null} + + + + {/if}
diff --git a/qt/launcher-gui/src/routes/Start.svelte b/qt/launcher-gui/src/routes/Start.svelte index f33d016fa..98bc97c31 100644 --- a/qt/launcher-gui/src/routes/Start.svelte +++ b/qt/launcher-gui/src/routes/Start.svelte @@ -3,79 +3,29 @@ Copyright: Ankitects Pty Ltd and contributors License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> - {#await choosePromise} - - - - {:then res} - {#if res === null} - {#await loadPromise} - - - - {:then [releases, existing]} - - {:catch e} - {setError(e)} - - {/await} - {:else} - - - - {/if} - {:catch e} - {setError(e)} - - {/await} - {#if error != null} - -
{error.message}
-
- {/if} + + {@render children?.()} - - {#if !chosen} - - - - {/if} + + {@render footer?.()}
From 36a0e0dcc7d052ae27a54c69ac105bb591a3060a Mon Sep 17 00:00:00 2001 From: llama Date: Mon, 20 Oct 2025 10:05:35 +0800 Subject: [PATCH 48/74] use getState --- qt/launcher-gui/src/routes/+page.svelte | 28 +++++++++++++++++++++---- qt/launcher-gui/src/routes/+page.ts | 12 +++++------ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/qt/launcher-gui/src/routes/+page.svelte b/qt/launcher-gui/src/routes/+page.svelte index 2f26bc2a7..a08929594 100644 --- a/qt/launcher-gui/src/routes/+page.svelte +++ b/qt/launcher-gui/src/routes/+page.svelte @@ -14,13 +14,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { onMount } from "svelte"; import { tr, zoomFactor } from "./stores"; import Start from "./Start.svelte"; + import ErrorState from "./ErrorState.svelte"; + import Normal from "./Normal.svelte"; + import Uninstall from "./Uninstall.svelte"; + import { launcherOsUnsupported } from "@generated/ftl"; const { data }: PageProps = $props(); - const langs = data.langs; - const options = $state(data.options); - let mirrors = $state(data.mirrors); + let langs = $state(data.langs); let selectedLang = $state(data.userLocale); + let flow = $state(data.state); + let mirrors = $state(data.mirrors); async function onLangChange(lang: string) { await setLang({ val: lang }); @@ -39,6 +43,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html }); onMount(() => windowReady({})); + + let footer: any = $state(null); + + const uninstall = () => { + flow.case = "uninstall"; + }; - + + {#if flow.case === "normal"} + + {:else if flow.case === "uninstall"} + + {:else if flow.case === "osUnsupported" } + + {:else if flow.case === "unknownError" } + + {/if} + diff --git a/qt/launcher-gui/src/routes/+page.ts b/qt/launcher-gui/src/routes/+page.ts index f167f5baa..deb80d96c 100644 --- a/qt/launcher-gui/src/routes/+page.ts +++ b/qt/launcher-gui/src/routes/+page.ts @@ -1,22 +1,22 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import { getLangs, getMirrors, getOptions } from "@generated/backend-launcher"; +import { getLangs, getMirrors, getState } from "@generated/backend-launcher"; import { ModuleName, setupI18n } from "@tslib/i18n"; import type { PageLoad } from "./$types"; export const load = (async () => { const i18nPromise = setupI18n({ modules: [ModuleName.LAUNCHER] }, true); const langsPromise = getLangs({}); - const optionsPromise = getOptions({}); + const statePromise = getState({}); const mirrorsPromise = getMirrors({}); - const [_, { userLocale, langs }, options, { mirrors }] = await Promise.all([ + const [_, { userLocale, langs }, { kind: state }, { mirrors }] = await Promise.all([ i18nPromise, langsPromise, - optionsPromise, - mirrorsPromise, + statePromise, + mirrorsPromise ]); - return { langs, userLocale, options, mirrors }; + return { langs, userLocale, state, mirrors }; }) satisfies PageLoad; From 73ac0a1cfcb59f02eaad61f0ff698342057835d6 Mon Sep 17 00:00:00 2001 From: llama Date: Mon, 20 Oct 2025 10:11:43 +0800 Subject: [PATCH 49/74] convert uv methods back to free fns --- qt/launcher-gui/src-tauri/src/app.rs | 2 +- qt/launcher-gui/src-tauri/src/commands.rs | 4 +- qt/launcher-gui/src-tauri/src/state.rs | 4 +- qt/launcher-gui/src-tauri/src/uv.rs | 411 ++++++++++------------ 4 files changed, 184 insertions(+), 237 deletions(-) diff --git a/qt/launcher-gui/src-tauri/src/app.rs b/qt/launcher-gui/src-tauri/src/app.rs index 862f2932e..bb31b31d0 100644 --- a/qt/launcher-gui/src-tauri/src/app.rs +++ b/qt/launcher-gui/src-tauri/src/app.rs @@ -26,7 +26,7 @@ pub fn init() -> Option { State::Normal(ref mut state) => state.check_versions(), State::LaunchAnki(ref paths) => { let args: Vec = std::env::args().skip(1).collect(); - let cmd = paths.build_python_command(&args).unwrap(); + let cmd = uv::build_python_command(paths, &args).unwrap(); uv::launch_anki_normally(cmd).unwrap(); return None; } diff --git a/qt/launcher-gui/src-tauri/src/commands.rs b/qt/launcher-gui/src-tauri/src/commands.rs index 21f3ddad8..a8afdb2c1 100644 --- a/qt/launcher-gui/src-tauri/src/commands.rs +++ b/qt/launcher-gui/src-tauri/src/commands.rs @@ -199,7 +199,7 @@ pub async fn choose_version( )?; } - let warming_up = paths.post_install()?; + let warming_up = uv::post_install(&paths)?; Ok(ChooseVersionResponse { version, @@ -210,7 +210,7 @@ pub async fn choose_version( } pub async fn launch_anki(app: AppHandle, _window: WebviewWindow) -> Result<()> { - app.state::().paths()?.launch_anki() + app.state::().paths().and_then(uv::launch_anki) } pub async fn exit(app: AppHandle, window: WebviewWindow) -> Result<()> { diff --git a/qt/launcher-gui/src-tauri/src/state.rs b/qt/launcher-gui/src-tauri/src/state.rs index 794bb2fab..f941eac73 100644 --- a/qt/launcher-gui/src-tauri/src/state.rs +++ b/qt/launcher-gui/src-tauri/src/state.rs @@ -102,14 +102,14 @@ impl NormalState { let (av_tx, av_rx) = tokio::sync::watch::channel(None); let paths = self.paths.clone(); tauri::async_runtime::spawn_blocking(move || { - let res = paths.get_releases(); + let res = uv::get_releases(&paths); let _ = av_tx.send(Some(res)); }); let (cv_tx, cv_rx) = tokio::sync::watch::channel(None); let paths = self.paths.clone(); tauri::async_runtime::spawn_blocking(move || { - let res = paths.check_versions(); + let res = uv::check_versions(&paths); let _ = cv_tx.send(Some(res)); }); diff --git a/qt/launcher-gui/src-tauri/src/uv.rs b/qt/launcher-gui/src-tauri/src/uv.rs index daf33c464..6383ba0b1 100644 --- a/qt/launcher-gui/src-tauri/src/uv.rs +++ b/qt/launcher-gui/src-tauri/src/uv.rs @@ -27,6 +27,7 @@ use crate::platform::get_uv_binary_name; pub use crate::platform::launch_anki_normally; use crate::platform::respawn_launcher; use crate::state::ExistingVersions; +use crate::state::State; use crate::state::Version; use crate::state::Versions; @@ -60,6 +61,92 @@ pub enum VersionKind { Uv(String), } +fn extract_aqt_version(state: &Paths) -> Option { + // Check if .venv exists first + if !state.venv_folder.exists() { + return None; + } + + let output = uv_command(state) + .ok()? + .env("VIRTUAL_ENV", &state.venv_folder) + .args(["pip", "show", "aqt"]) + .output(); + + let output = output.ok()?; + + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8(output.stdout).ok()?; + for line in stdout.lines() { + if let Some(version) = line.strip_prefix("Version: ") { + return Some(version.trim().to_string()); + } + } + None +} + +pub fn check_versions(state: &Paths) -> Result { + let mut res = ExistingVersions { + pyproject_modified_by_user: state.pyproject_modified_by_user, + ..Default::default() + }; + + // If sync_complete_marker is missing, do nothing + if !state.sync_complete_marker.exists() { + return Ok(res); + } + + // Determine current version by invoking uv pip show aqt + match extract_aqt_version(state) { + Some(version) => { + res.current = Some(normalize_version(&version)); + } + None => { + Err(anyhow::anyhow!( + "Warning: Could not determine current Anki version" + ))?; + } + } + + // Read previous version from "previous-version" file + let previous_version_path = state.uv_install_root.join("previous-version"); + if let Ok(content) = read_file(&previous_version_path) { + if let Ok(version_str) = String::from_utf8(content) { + let version = version_str.trim().to_string(); + if !version.is_empty() { + res.previous = Some(normalize_version(&version)); + } + } + } + + Ok(res) +} + +pub fn set_allow_betas(state: &Paths, allow_betas: bool) -> Result<()> { + if allow_betas { + write_file(&state.prerelease_marker, "")?; + } else { + let _ = remove_file(&state.prerelease_marker); + } + Ok(()) +} + +pub fn set_cache_enabled(state: &Paths, cache_enabled: bool) -> Result<()> { + if cache_enabled { + let _ = remove_file(&state.no_cache_marker); + } else { + write_file(&state.no_cache_marker, "")?; + // Delete the cache directory and everything in it + if state.uv_cache_dir.exists() { + let _ = anki_io::remove_dir_all(&state.uv_cache_dir); + } + } + Ok(()) +} + pub fn handle_version_install_or_update( state: &Paths, version: &str, @@ -239,25 +326,12 @@ where } } -pub fn set_allow_betas(state: &Paths, allow_betas: bool) -> Result<()> { - if allow_betas { - write_file(&state.prerelease_marker, "")?; - } else { - let _ = remove_file(&state.prerelease_marker); - } - Ok(()) -} - -pub fn set_cache_enabled(state: &Paths, cache_enabled: bool) -> Result<()> { - if cache_enabled { - let _ = remove_file(&state.no_cache_marker); - } else { - write_file(&state.no_cache_marker, "")?; - // Delete the cache directory and everything in it - if state.uv_cache_dir.exists() { - let _ = anki_io::remove_dir_all(&state.uv_cache_dir); - } - } +fn write_sync_marker(state: &Paths) -> Result<()> { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .context("Failed to get system time")? + .as_secs(); + write_file(&state.sync_complete_marker, timestamp.to_string())?; Ok(()) } @@ -376,6 +450,46 @@ fn filter_and_normalize_versions1(all_versions: Vec) -> Vec { valid_versions } +fn fetch_versions(state: &Paths) -> Result> { + let versions_script = state.resources_dir.join("versions.py"); + + let mut cmd = uv_command(state)?; + cmd.args(["run", "--no-project", "--no-config", "--managed-python"]) + .args(["--with", "pip-system-certs,requests[socks]"]); + + let python_version = read_file(&state.dist_python_version_path)?; + let python_version_str = + String::from_utf8(python_version).context("Invalid UTF-8 in .python-version")?; + let version_trimmed = python_version_str.trim(); + if !version_trimmed.is_empty() { + cmd.args(["--python", version_trimmed]); + } + + cmd.arg(&versions_script); + + let output = match cmd.utf8_output() { + Ok(output) => output, + Err(e) => { + return Err(e.into()); + } + }; + let versions = serde_json::from_str(&output.stdout).context("Failed to parse versions JSON")?; + Ok(versions) +} + +pub fn get_releases(state: &Paths) -> Result { + let all_versions = fetch_versions(state)?; + let all_versions = filter_and_normalize_versions1(all_versions); + + let latest_patches = with_only_latest_patch(&all_versions); + let latest_releases: Vec = latest_patches.into_iter().take(5).collect(); + + Ok(Versions { + latest: latest_releases, + all: all_versions, + }) +} + pub fn apply_version_kind(version_kind: &VersionKind, state: &Paths) -> Result<()> { let content = read_file(&state.dist_pyproject_path)?; let content_str = String::from_utf8(content).context("Invalid UTF-8 in pyproject.toml")?; @@ -666,8 +780,53 @@ pub fn set_mirror(state: &Paths, enabled: bool) -> Result<()> { } Ok(()) } +pub fn post_install(state: &Paths) -> Result { + // Write marker file to indicate we've completed the sync process + write_sync_marker(state)?; -impl crate::state::State { + // whether or not anki needs to warm up + Ok(cfg!(target_os = "macos")) +} + +pub fn launch_anki(_state: &Paths) -> Result<()> { + #[cfg(target_os = "macos")] + { + let cmd = self.build_python_command(&[])?; + platform::mac::prepare_for_launch_after_update(cmd, &_state.uv_install_root)?; + } + + // respawn the launcher as a disconnected subprocess for normal startup + respawn_launcher() +} + +pub fn build_python_command(state: &Paths, args: &[String]) -> Result { + let python_exe = if cfg!(target_os = "windows") { + let show_console = std::env::var("ANKI_CONSOLE").is_ok(); + if show_console { + state.venv_folder.join("Scripts/python.exe") + } else { + state.venv_folder.join("Scripts/pythonw.exe") + } + } else { + state.venv_folder.join("bin/python") + }; + + let mut cmd = Command::new(&python_exe); + cmd.args(["-c", "import aqt, sys; sys.argv[0] = 'Anki'; aqt.run()"]); + cmd.args(args); + // tell the Python code it was invoked by the launcher, and updating is + // available + cmd.env("ANKI_LAUNCHER", std::env::current_exe()?.utf8()?.as_str()); + + // Set UV and Python paths for the Python code + cmd.env("ANKI_LAUNCHER_UV", state.uv_path.utf8()?.as_str()); + cmd.env("UV_PROJECT", state.uv_install_root.utf8()?.as_str()); + cmd.env_remove("SSLKEYLOGFILE"); + + Ok(cmd) +} + +impl State { pub fn init() -> Result { let uv_install_root = if let Ok(custom_root) = std::env::var("ANKI_LAUNCHER_VENV_ROOT") { std::path::PathBuf::from(custom_root) @@ -742,218 +901,6 @@ impl crate::state::State { } } -impl Paths { - fn get_mirror_urls(&self) -> Result> { - if !self.mirror_path.exists() { - return Ok(None); - } - - let content = read_file(&self.mirror_path)?; - let content_str = String::from_utf8(content).context("Invalid UTF-8 in mirror file")?; - - let lines: Vec<&str> = content_str.lines().collect(); - if lines.len() >= 2 { - Ok(Some(( - lines[0].trim().to_string(), - lines[1].trim().to_string(), - ))) - } else { - Ok(None) - } - } - - fn uv_command(&self) -> Result { - let mut command = Command::new(&self.uv_path); - command.current_dir(&self.uv_install_root); - - // remove UV_* environment variables to avoid interference - for (key, _) in std::env::vars() { - if key.starts_with("UV_") { - command.env_remove(key); - } - } - command - .env_remove("VIRTUAL_ENV") - .env_remove("SSLKEYLOGFILE"); - - // Add mirror environment variable if enabled - if let Some((python_mirror, pypi_mirror)) = self.get_mirror_urls()? { - command - .env("UV_PYTHON_INSTALL_MIRROR", &python_mirror) - .env("UV_DEFAULT_INDEX", &pypi_mirror); - } - - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - - command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - } - Ok(command) - } - - fn fetch_versions(&self) -> Result> { - let versions_script = self.resources_dir.join("versions.py"); - - let mut cmd = self.uv_command()?; - cmd.args(["run", "--no-project", "--no-config", "--managed-python"]) - .args(["--with", "pip-system-certs,requests[socks]"]); - - let python_version = read_file(&self.dist_python_version_path)?; - let python_version_str = - String::from_utf8(python_version).context("Invalid UTF-8 in .python-version")?; - let version_trimmed = python_version_str.trim(); - if !version_trimmed.is_empty() { - cmd.args(["--python", version_trimmed]); - } - - cmd.arg(&versions_script); - - let output = match cmd.utf8_output() { - Ok(output) => output, - Err(e) => { - return Err(e.into()); - } - }; - let versions = - serde_json::from_str(&output.stdout).context("Failed to parse versions JSON")?; - Ok(versions) - } - - pub fn get_releases(&self) -> Result { - let all_versions = self.fetch_versions()?; - let all_versions = filter_and_normalize_versions1(all_versions); - - let latest_patches = with_only_latest_patch(&all_versions); - let latest_releases: Vec = latest_patches.into_iter().take(5).collect(); - - Ok(Versions { - latest: latest_releases, - all: all_versions, - }) - } - - fn extract_aqt_version(&self) -> Option { - // Check if .venv exists first - if !self.venv_folder.exists() { - return None; - } - - let output = self - .uv_command() - .ok()? - .env("VIRTUAL_ENV", &self.venv_folder) - .args(["pip", "show", "aqt"]) - .output(); - - let output = output.ok()?; - - if !output.status.success() { - return None; - } - - let stdout = String::from_utf8(output.stdout).ok()?; - for line in stdout.lines() { - if let Some(version) = line.strip_prefix("Version: ") { - return Some(version.trim().to_string()); - } - } - None - } - - pub fn check_versions(&self) -> Result { - let mut res = ExistingVersions { - pyproject_modified_by_user: self.pyproject_modified_by_user, - ..Default::default() - }; - - // If sync_complete_marker is missing, do nothing - if !self.sync_complete_marker.exists() { - return Ok(res); - } - - // Determine current version by invoking uv pip show aqt - match self.extract_aqt_version() { - Some(version) => { - res.current = Some(normalize_version(&version)); - } - None => { - Err(anyhow::anyhow!( - "Warning: Could not determine current Anki version" - ))?; - } - } - - // Read previous version from "previous-version" file - let previous_version_path = self.uv_install_root.join("previous-version"); - if let Ok(content) = read_file(&previous_version_path) { - if let Ok(version_str) = String::from_utf8(content) { - let version = version_str.trim().to_string(); - if !version.is_empty() { - res.previous = Some(normalize_version(&version)); - } - } - } - - Ok(res) - } - - fn write_sync_marker(&self) -> Result<()> { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .context("Failed to get system time")? - .as_secs(); - write_file(&self.sync_complete_marker, timestamp.to_string())?; - Ok(()) - } - - pub fn post_install(&self) -> Result { - // Write marker file to indicate we've completed the sync process - self.write_sync_marker()?; - - // whether or not anki needs to warm up - Ok(cfg!(target_os = "macos")) - } - - pub fn launch_anki(&self) -> Result<()> { - #[cfg(target_os = "macos")] - { - let cmd = self.build_python_command(&[])?; - platform::mac::prepare_for_launch_after_update(cmd, &self.uv_install_root)?; - } - - // respawn the launcher as a disconnected subprocess for normal startup - respawn_launcher() - } - - pub fn build_python_command(&self, args: &[String]) -> Result { - let python_exe = if cfg!(target_os = "windows") { - let show_console = std::env::var("ANKI_CONSOLE").is_ok(); - if show_console { - self.venv_folder.join("Scripts/python.exe") - } else { - self.venv_folder.join("Scripts/pythonw.exe") - } - } else { - self.venv_folder.join("bin/python") - }; - - let mut cmd = Command::new(&python_exe); - cmd.args(["-c", "import aqt, sys; sys.argv[0] = 'Anki'; aqt.run()"]); - cmd.args(args); - // tell the Python code it was invoked by the launcher, and updating is - // available - cmd.env("ANKI_LAUNCHER", std::env::current_exe()?.utf8()?.as_str()); - - // Set UV and Python paths for the Python code - cmd.env("ANKI_LAUNCHER_UV", self.uv_path.utf8()?.as_str()); - cmd.env("UV_PROJECT", self.uv_install_root.utf8()?.as_str()); - cmd.env_remove("SSLKEYLOGFILE"); - - Ok(cmd) - } -} - #[cfg(test)] mod tests { use super::*; From 68f9e052491ec7feb10f3090f1e35c301259aa9d Mon Sep 17 00:00:00 2001 From: llama Date: Mon, 20 Oct 2025 10:12:04 +0800 Subject: [PATCH 50/74] ./ninja format --- qt/launcher-gui/src/routes/+page.svelte | 16 ++++++++++++---- qt/launcher-gui/src/routes/+page.ts | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/qt/launcher-gui/src/routes/+page.svelte b/qt/launcher-gui/src/routes/+page.svelte index a08929594..fcbd3200a 100644 --- a/qt/launcher-gui/src/routes/+page.svelte +++ b/qt/launcher-gui/src/routes/+page.svelte @@ -56,9 +56,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {:else if flow.case === "uninstall"} - {:else if flow.case === "osUnsupported" } - - {:else if flow.case === "unknownError" } - + {:else if flow.case === "osUnsupported"} + + {:else if flow.case === "unknownError"} + {/if} diff --git a/qt/launcher-gui/src/routes/+page.ts b/qt/launcher-gui/src/routes/+page.ts index deb80d96c..3de322604 100644 --- a/qt/launcher-gui/src/routes/+page.ts +++ b/qt/launcher-gui/src/routes/+page.ts @@ -15,7 +15,7 @@ export const load = (async () => { i18nPromise, langsPromise, statePromise, - mirrorsPromise + mirrorsPromise, ]); return { langs, userLocale, state, mirrors }; From c0eedbc2a4eab7d75e9c4e3ee1210864771537a4 Mon Sep 17 00:00:00 2001 From: llama Date: Mon, 20 Oct 2025 10:13:41 +0800 Subject: [PATCH 51/74] add todo to uninstall btn --- qt/launcher-gui/src/routes/Action.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/launcher-gui/src/routes/Action.svelte b/qt/launcher-gui/src/routes/Action.svelte index e939e5985..307e317aa 100644 --- a/qt/launcher-gui/src/routes/Action.svelte +++ b/qt/launcher-gui/src/routes/Action.svelte @@ -82,7 +82,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {#if uninstall != null} {/if} From a789a2cb95b00885c9d3da84a0b276a1890ff513 Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 19:24:10 +0800 Subject: [PATCH 52/74] add uninstall rpcs and state --- proto/anki/launcher.proto | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/proto/anki/launcher.proto b/proto/anki/launcher.proto index 3e4f3cadb..4fa4815aa 100644 --- a/proto/anki/launcher.proto +++ b/proto/anki/launcher.proto @@ -23,6 +23,9 @@ service LauncherService { rpc GetAvailableVersions(generic.Empty) returns (Versions); rpc GetExistingVersions(generic.Empty) returns (ExistingVersions); + rpc GetUninstallInfo(generic.Empty) returns (Uninstall); + rpc UninstallAnki(UninstallRequest) returns (UninstallResponse); + rpc LaunchAnki(generic.Empty) returns (generic.Empty); rpc Exit(generic.Empty) returns (generic.Empty); } @@ -92,6 +95,23 @@ message ChooseVersionResponse { bool warming_up = 2; } +message UninstallRequest { + bool delete_base_folder = 1; +} + +message UninstallResponse { + message WindowsInstallerError { + string error = 1; + string path = 2; + } + oneof action_needed { + string unix_script = 1; + google.protobuf.Empty mac_manual = 2; + WindowsInstallerError windows_installer_failed = 3; + google.protobuf.Empty windows_installer_not_found = 4; + } +} + message ZoomWebviewRequest { float scale_factor = 1; } @@ -112,6 +132,11 @@ message ExistingVersions { bool pyproject_modified_by_user = 3; } +message Uninstall { + bool anki_program_files_exists = 1; + bool anki_base_folder_exists = 2; +} + message NormalState { Options options = 1; } From fcc0ee3cb7bbb31b9e14da460d9b975917d15720 Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 19:25:58 +0800 Subject: [PATCH 53/74] add StateExt trait --- qt/launcher-gui/src-tauri/src/app.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/qt/launcher-gui/src-tauri/src/app.rs b/qt/launcher-gui/src-tauri/src/app.rs index bb31b31d0..75418ef24 100644 --- a/qt/launcher-gui/src-tauri/src/app.rs +++ b/qt/launcher-gui/src-tauri/src/app.rs @@ -19,6 +19,16 @@ use crate::uv; pub const PROTOCOL: &str = "anki"; +pub trait StateExt { + fn flow(&self) -> &State; +} + +impl> StateExt for T { + fn flow(&self) -> &State { + self.state::().inner() + } +} + pub fn init() -> Option { let mut state = State::init().unwrap_or_else(State::UnknownError); From 316e176d64e9afe2e6b0217f7167fd32a60b63de Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 19:27:35 +0800 Subject: [PATCH 54/74] add I18nExt trait --- qt/launcher-gui/src-tauri/src/lang.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/qt/launcher-gui/src-tauri/src/lang.rs b/qt/launcher-gui/src-tauri/src/lang.rs index dc61b0817..bf8b5c94a 100644 --- a/qt/launcher-gui/src-tauri/src/lang.rs +++ b/qt/launcher-gui/src-tauri/src/lang.rs @@ -8,23 +8,29 @@ use anyhow::Result; use phf::phf_map; use phf::phf_ordered_map; use phf::phf_set; -use tauri::AppHandle; use tauri::Manager; use tauri::Runtime; pub type I18n = anki_i18n::I18n; pub type Tr = RwLock>; -pub fn setup_i18n(app: &AppHandle, locales: &[&str]) { - *app.state::().write().expect("tr lock was poisoned!") = Some(I18n::new(locales)); +pub trait I18nExt { + fn setup_tr(&self, locales: &[&str]); + fn tr(&self) -> Result; } -pub fn get_tr(app: &AppHandle) -> Result { - let tr_state = app.state::(); - let guard = tr_state.read().expect("tr lock was poisoned!"); - guard - .clone() - .ok_or_else(|| anyhow!("tr was not initialised!")) +impl> I18nExt for T { + fn setup_tr(&self, locales: &[&str]) { + *self.state::().write().expect("tr lock was poisoned!") = Some(I18n::new(locales)); + } + + fn tr(&self) -> Result { + let tr_state = self.state::(); + let guard = tr_state.read().expect("tr lock was poisoned!"); + guard + .clone() + .ok_or_else(|| anyhow!("tr was not initialised!")) + } } pub const LANGS: phf::OrderedMap<&'static str, &'static str> = phf_ordered_map! { From dfe77c8a8aca52a313572c5170d9079bb02a35ac Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 19:31:56 +0800 Subject: [PATCH 55/74] refactor to use traits --- qt/launcher-gui/src-tauri/src/commands.rs | 27 +++++++++-------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/qt/launcher-gui/src-tauri/src/commands.rs b/qt/launcher-gui/src-tauri/src/commands.rs index a8afdb2c1..f9177e936 100644 --- a/qt/launcher-gui/src-tauri/src/commands.rs +++ b/qt/launcher-gui/src-tauri/src/commands.rs @@ -21,13 +21,12 @@ use anyhow::Result; use strum::IntoEnumIterator; use tauri::AppHandle; use tauri::Emitter; -use tauri::Manager; use tauri::Runtime; use tauri::WebviewWindow; use tauri_plugin_os::locale; -use crate::lang::get_tr; -use crate::lang::setup_i18n; +use crate::app::StateExt; +use crate::lang::I18nExt; use crate::lang::LANGS; use crate::lang::LANGS_DEFAULT_REGION; use crate::lang::LANGS_WITH_REGIONS; @@ -41,7 +40,7 @@ pub async fn i18n_resources( _window: WebviewWindow, input: I18nResourcesRequest, ) -> Result { - let tr = get_tr(&app)?; + let tr = app.tr()?; serde_json::to_vec(&tr.resources_for_js(&input.modules)) .with_context(|| "failed to serialise i18n resources") .map(Into::into) @@ -87,7 +86,7 @@ pub async fn set_lang( } else { input.split('-').next().unwrap().to_owned() }; - setup_i18n(&app, &[&*input]); + app.setup_tr(&[&*input]); Ok(()) } @@ -95,8 +94,7 @@ pub async fn get_state( app: AppHandle, _window: WebviewWindow, ) -> Result { - let state = app.state::(); - let kind = match &*state { + let kind = match app.flow() { State::LaunchAnki(_) => unreachable!(), State::OsUnsupported(e) => StateProtoKind::OsUnsupported(format!("{e:?}")), State::UnknownError(e) => StateProtoKind::UnknownError(format!("{e:?}")), @@ -112,7 +110,7 @@ pub async fn get_mirrors( app: AppHandle, _window: WebviewWindow, ) -> Result { - let tr = get_tr(&app)?; + let tr = app.tr()?; Ok(GetMirrorsResponse { mirrors: Mirror::iter() .map(|mirror| get_mirrors_response::Pair { @@ -131,7 +129,7 @@ pub async fn get_options( app: AppHandle, _window: WebviewWindow, ) -> Result { - let state = app.state::(); + let state = app.flow(); let options = (&state.normal()?.initial_options).into(); Ok(options) } @@ -140,8 +138,7 @@ pub async fn get_available_versions( app: AppHandle, _window: WebviewWindow, ) -> Result { - let state = app.state::(); - let state = state.normal()?; + let state = app.flow().normal()?; let mut rx = state.available_versions.clone().unwrap(); rx.changed().await.unwrap(); let x = rx.borrow(); @@ -156,8 +153,7 @@ pub async fn get_existing_versions( app: AppHandle, _window: WebviewWindow, ) -> Result { - let state = app.state::(); - let state = state.normal()?; + let state = app.flow().normal()?; let mut rx = state.current_versions.clone().unwrap(); rx.changed().await.unwrap(); let x = rx.borrow(); @@ -172,8 +168,7 @@ pub async fn choose_version( _window: WebviewWindow, input: ChooseVersionRequest, ) -> Result { - let state = app.state::(); - let state = state.normal()?; + let state = app.flow().normal()?; let paths = state.paths.clone(); tauri::async_runtime::spawn_blocking(move || { @@ -210,7 +205,7 @@ pub async fn choose_version( } pub async fn launch_anki(app: AppHandle, _window: WebviewWindow) -> Result<()> { - app.state::().paths().and_then(uv::launch_anki) + app.flow().paths().and_then(uv::launch_anki) } pub async fn exit(app: AppHandle, window: WebviewWindow) -> Result<()> { From 8f1a3dca94d9b526d3f20f915187204d2cab2032 Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 19:33:39 +0800 Subject: [PATCH 56/74] implement uninstall rpcs --- qt/launcher-gui/src-tauri/src/commands.rs | 21 +++++++++++++++++++ qt/launcher-gui/src-tauri/src/state.rs | 25 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/qt/launcher-gui/src-tauri/src/commands.rs b/qt/launcher-gui/src-tauri/src/commands.rs index f9177e936..57cba37ec 100644 --- a/qt/launcher-gui/src-tauri/src/commands.rs +++ b/qt/launcher-gui/src-tauri/src/commands.rs @@ -14,6 +14,9 @@ use anki_proto::launcher::Mirror; use anki_proto::launcher::NormalState as NormalStateProto; use anki_proto::launcher::Options; use anki_proto::launcher::State as StateProto; +use anki_proto::launcher::Uninstall as UninstallProto; +use anki_proto::launcher::UninstallRequest; +use anki_proto::launcher::UninstallResponse; use anki_proto::launcher::ZoomWebviewRequest; use anyhow::anyhow; use anyhow::Context; @@ -32,6 +35,7 @@ use crate::lang::LANGS_DEFAULT_REGION; use crate::lang::LANGS_WITH_REGIONS; use crate::state::ExistingVersions; use crate::state::State; +use crate::state::Uninstall; use crate::state::Versions; use crate::uv; @@ -218,6 +222,23 @@ pub async fn exit(app: AppHandle, window: WebviewWindow) -> Re Ok(()) } +pub async fn get_uninstall_info( + app: AppHandle, + _window: WebviewWindow, +) -> Result { + app.flow().paths().map(Uninstall::from).map(Into::into) +} + +pub async fn uninstall_anki( + app: AppHandle, + _window: WebviewWindow, + input: UninstallRequest, +) -> Result { + let paths = app.flow().paths()?; + let action_needed = uv::handle_uninstall(paths, input.delete_base_folder)?; + Ok(UninstallResponse { action_needed }) +} + /// NOTE: [zoomHotkeysEnabled](https://v2.tauri.app/reference/config/#zoomhotkeysenabled) exists /// but the polyfill it uses on linux doesn't allow regular scrolling pub async fn zoom_webview( diff --git a/qt/launcher-gui/src-tauri/src/state.rs b/qt/launcher-gui/src-tauri/src/state.rs index f941eac73..7f230a9c1 100644 --- a/qt/launcher-gui/src-tauri/src/state.rs +++ b/qt/launcher-gui/src-tauri/src/state.rs @@ -6,6 +6,7 @@ use std::sync::Arc; pub use anki_proto::launcher::ExistingVersions; use anki_proto::launcher::Mirror; use anki_proto::launcher::Options as OptionsProto; +use anki_proto::launcher::Uninstall as UninstallProto; pub use anki_proto::launcher::Version; pub use anki_proto::launcher::Versions; use anyhow::anyhow; @@ -71,6 +72,30 @@ impl From for State { } } +pub struct Uninstall { + anki_program_files_exists: bool, + anki_base_folder_exists: bool, +} + +impl> From for Uninstall { + fn from(paths: T) -> Self { + let paths = paths.as_ref(); + Self { + anki_program_files_exists: paths.uv_install_root.exists(), + anki_base_folder_exists: paths.anki_base_folder.exists(), + } + } +} + +impl From for UninstallProto { + fn from(u: Uninstall) -> Self { + Self { + anki_program_files_exists: u.anki_program_files_exists, + anki_base_folder_exists: u.anki_base_folder_exists, + } + } +} + pub enum State { LaunchAnki(Arc), OsUnsupported(anyhow::Error), From dd0a676fcdc7cbac2cb0289f2728ddba714d5d36 Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 19:35:32 +0800 Subject: [PATCH 57/74] update platform-specific uninstall fns --- qt/launcher-gui/src-tauri/src/platform/mac.rs | 18 ++++++-------- .../src-tauri/src/platform/unix.rs | 21 ++++++---------- .../src-tauri/src/platform/windows.rs | 24 ++++++++++--------- 3 files changed, 27 insertions(+), 36 deletions(-) diff --git a/qt/launcher-gui/src-tauri/src/platform/mac.rs b/qt/launcher-gui/src-tauri/src/platform/mac.rs index bc9759fce..863a05c89 100644 --- a/qt/launcher-gui/src-tauri/src/platform/mac.rs +++ b/qt/launcher-gui/src-tauri/src/platform/mac.rs @@ -5,6 +5,7 @@ use std::path::Path; use std::process::Command; use anki_process::CommandExt as AnkiCommandExt; +use anki_proto::launcher::uninstall_response::ActionNeeded; use anyhow::Result; pub fn prepare_for_launch_after_update(mut cmd: Command, root: &Path) -> Result<()> { @@ -34,7 +35,7 @@ pub fn prepare_for_launch_after_update(mut cmd: Command, root: &Path) -> Result< Ok(()) } -pub fn finalize_uninstall() { +pub fn finalize_uninstall() -> Result> { if let Ok(exe_path) = std::env::current_exe() { // Find the .app bundle by walking up the directory tree let mut app_bundle_path = exe_path.as_path(); @@ -43,22 +44,17 @@ pub fn finalize_uninstall() { if name.to_string_lossy().ends_with(".app") { let result = Command::new("trash").arg(parent).output(); - match result { - Ok(output) if output.status.success() => { - println!("Anki has been uninstalled."); - return; - } + return Ok(match result { + Ok(output) if output.status.success() => None, _ => { // Fall back to manual instructions - println!( - "Please manually drag Anki.app to the trash to complete uninstall." - ); + Some(ActionNeeded::MacManual(())) } - } - return; + }); } } app_bundle_path = parent; } } + Ok(None) } diff --git a/qt/launcher-gui/src-tauri/src/platform/unix.rs b/qt/launcher-gui/src-tauri/src/platform/unix.rs index 29e860033..771b6c5ba 100644 --- a/qt/launcher-gui/src-tauri/src/platform/unix.rs +++ b/qt/launcher-gui/src-tauri/src/platform/unix.rs @@ -1,24 +1,17 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use anki_proto::launcher::uninstall_response::ActionNeeded; use anyhow::Result; -pub fn finalize_uninstall() { - use std::io::stdin; - use std::io::stdout; - use std::io::Write; - +pub fn finalize_uninstall() -> Result> { let uninstall_script = std::path::Path::new("/usr/local/share/anki/uninstall.sh"); - if uninstall_script.exists() { - println!("To finish uninstalling, run 'sudo /usr/local/share/anki/uninstall.sh'"); - } else { - println!("Anki has been uninstalled."); - } - println!("Press enter to quit."); - let _ = stdout().flush(); - let mut input = String::new(); - let _ = stdin().read_line(&mut input); + Ok(uninstall_script + .exists() + .then_some(ActionNeeded::UnixScript( + uninstall_script.display().to_string(), + ))) } pub fn ensure_glibc_supported() -> Result<()> { diff --git a/qt/launcher-gui/src-tauri/src/platform/windows.rs b/qt/launcher-gui/src-tauri/src/platform/windows.rs index 72725058f..b255c9ae8 100644 --- a/qt/launcher-gui/src-tauri/src/platform/windows.rs +++ b/qt/launcher-gui/src-tauri/src/platform/windows.rs @@ -4,7 +4,8 @@ use std::io::stdin; use std::process::Command; -use anyhow::Context; +use anki_proto::launcher::uninstall_response::ActionNeeded; +use anki_proto::launcher::uninstall_response::WindowsInstallerError; use anyhow::Result; use widestring::u16cstr; use windows::core::PCWSTR; @@ -58,33 +59,34 @@ pub fn ensure_windows_version_supported() -> Result<()> { } } -pub fn finalize_uninstall() { +pub fn finalize_uninstall() -> Result> { let uninstaller_path = get_uninstaller_path(); - match uninstaller_path { + Ok(match uninstaller_path { Some(path) => { println!("Launching Windows uninstaller..."); let result = Command::new(&path).env("ANKI_LAUNCHER", "1").spawn(); match result { - Ok(_) => { - println!("Uninstaller launched successfully."); - return; - } + Ok(_) => None, Err(e) => { println!("Failed to launch uninstaller: {e}"); println!("You can manually run: {}", path.display()); + Some(ActionNeeded::WindowsInstallerFailed( + WindowsInstallerError { + error: format!("{e:?}"), + path: path.display().to_string(), + }, + )) } } } None => { println!("Windows uninstaller not found."); println!("You may need to uninstall via Windows Settings > Apps."); + Some(ActionNeeded::WindowsInstallerNotFound(())) } - } - println!("Press enter to close..."); - let mut input = String::new(); - let _ = stdin().read_line(&mut input); + }) } fn get_uninstaller_path() -> Option { From 6f0865efda7b99ea56e404ee80f8cc52e511d1aa Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 19:37:26 +0800 Subject: [PATCH 58/74] update handle_uninstall --- qt/launcher-gui/src-tauri/src/uv.rs | 64 +++++++++-------------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/qt/launcher-gui/src-tauri/src/uv.rs b/qt/launcher-gui/src-tauri/src/uv.rs index 6383ba0b1..7fab513da 100644 --- a/qt/launcher-gui/src-tauri/src/uv.rs +++ b/qt/launcher-gui/src-tauri/src/uv.rs @@ -1,8 +1,6 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use std::io::stdin; -use std::io::stdout; use std::io::Write; use std::process::Command; use std::time::SystemTime; @@ -16,6 +14,7 @@ use anki_io::remove_file; use anki_io::write_file; use anki_io::ToUtf8Path; use anki_process::CommandExt as AnkiCommandExt; +use anki_proto::launcher::uninstall_response::ActionNeeded; use anyhow::anyhow; use anyhow::Context; use anyhow::Result; @@ -34,10 +33,10 @@ use crate::state::Versions; #[derive(Debug, Clone)] pub struct Paths { pub prerelease_marker: std::path::PathBuf, - uv_install_root: std::path::PathBuf, + pub uv_install_root: std::path::PathBuf, uv_cache_dir: std::path::PathBuf, pub no_cache_marker: std::path::PathBuf, - anki_base_folder: std::path::PathBuf, + pub anki_base_folder: std::path::PathBuf, uv_path: std::path::PathBuf, uv_python_install_dir: std::path::PathBuf, user_pyproject_path: std::path::PathBuf, @@ -55,6 +54,12 @@ pub struct Paths { system_qt: bool, } +impl AsRef for Paths { + fn as_ref(&self) -> &Paths { + self + } +} + #[derive(Debug, Clone)] pub enum VersionKind { PyOxidizer(String), @@ -648,56 +653,25 @@ fn get_anki_addons21_path() -> Result { Ok(get_anki_base_path()?.join("addons21")) } -// TODO: revert -#[allow(unused)] -fn handle_uninstall(state: &Paths) -> Result { - // println!("{}", state.tr.launcher_uninstall_confirm()); - print!("> "); - let _ = stdout().flush(); - - let mut input = String::new(); - let _ = stdin().read_line(&mut input); - let input = input.trim().to_lowercase(); - - if input != "y" { - // println!("{}", state.tr.launcher_uninstall_cancelled()); - println!(); - return Ok(false); - } - +pub fn handle_uninstall(state: &Paths, delete_base_folder: bool) -> Result> { // Remove program files - if state.uv_install_root.exists() { - anki_io::remove_dir_all(&state.uv_install_root)?; - // println!("{}", state.tr.launcher_program_files_removed()); + anki_io::remove_dir_all(&state.uv_install_root) + .with_context(|| anyhow!("Failed to delete AnkiProgramFiles"))?; + + if delete_base_folder { + anki_io::remove_dir_all(&state.anki_base_folder) + .with_context(|| anyhow!("Failed to delete anki base folder"))?; } - println!(); - // println!("{}", state.tr.launcher_remove_all_profiles_confirm()); - print!("> "); - let _ = stdout().flush(); - - let mut input = String::new(); - let _ = stdin().read_line(&mut input); - let input = input.trim().to_lowercase(); - - if input == "y" && state.anki_base_folder.exists() { - anki_io::remove_dir_all(&state.anki_base_folder)?; - // println!("{}", state.tr.launcher_user_data_removed()); - } - - println!(); - // Platform-specific messages #[cfg(target_os = "macos")] - platform::mac::finalize_uninstall(); + return platform::mac::finalize_uninstall(); #[cfg(target_os = "windows")] - platform::windows::finalize_uninstall(); + return platform::windows::finalize_uninstall(); #[cfg(all(unix, not(target_os = "macos")))] - platform::unix::finalize_uninstall(); - - Ok(true) + return platform::unix::finalize_uninstall(); } fn uv_command(state: &Paths) -> Result { From 326f57820de0f4ca196ac6c0090ba546574e0aae Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 19:38:23 +0800 Subject: [PATCH 59/74] unhardcode pty-data event name --- proto/anki/launcher.proto | 6 +----- qt/launcher-gui/src-tauri/src/commands.rs | 3 ++- qt/launcher-gui/src/routes/Term.svelte | 3 ++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/proto/anki/launcher.proto b/proto/anki/launcher.proto index 4fa4815aa..0f531960a 100644 --- a/proto/anki/launcher.proto +++ b/proto/anki/launcher.proto @@ -33,11 +33,7 @@ service LauncherService { // TODO: this should not be necessary service BackendLauncherService {} -// TODO: codegen -enum Event { - TERM_INPUT = 0; - WINDOWS_READY = 1; -} +enum Event { PTY_DATA = 0; } message I18nResourcesRequest { repeated string modules = 1; diff --git a/qt/launcher-gui/src-tauri/src/commands.rs b/qt/launcher-gui/src-tauri/src/commands.rs index 57cba37ec..9f18ffe7c 100644 --- a/qt/launcher-gui/src-tauri/src/commands.rs +++ b/qt/launcher-gui/src-tauri/src/commands.rs @@ -7,6 +7,7 @@ use anki_proto::launcher::get_mirrors_response; use anki_proto::launcher::state::Kind as StateProtoKind; use anki_proto::launcher::ChooseVersionRequest; use anki_proto::launcher::ChooseVersionResponse; +use anki_proto::launcher::Event; use anki_proto::launcher::GetLangsResponse; use anki_proto::launcher::GetMirrorsResponse; use anki_proto::launcher::I18nResourcesRequest; @@ -184,7 +185,7 @@ pub async fn choose_version( let version = input.version; let on_pty_data = move |data| { - let _ = app.emit("pty-data", data); + let _ = app.emit(Event::PtyData.as_str_name(), data); }; if !input.keep_existing || paths.pyproject_modified_by_user { diff --git a/qt/launcher-gui/src/routes/Term.svelte b/qt/launcher-gui/src/routes/Term.svelte index e6525819e..d9b20ec88 100644 --- a/qt/launcher-gui/src/routes/Term.svelte +++ b/qt/launcher-gui/src/routes/Term.svelte @@ -10,6 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { Terminal } from "@xterm/xterm"; import { WebglAddon } from "@xterm/addon-webgl"; import "@xterm/xterm/css/xterm.css"; + import { Event as BackendEvent } from "@generated/anki/launcher_pb"; let { term = $bindable(), @@ -41,7 +42,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html console.log("WebGL addon threw an exception during load", e); } - const unlisten = listen("pty-data", (e) => { + const unlisten = listen(BackendEvent[BackendEvent.PTY_DATA], (e) => { const data = protoBase64.dec(e.payload); open = true; term!.write(data); From 172f924acb4dfecaf15ce77cde0ec5c2b1efe566 Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 19:38:57 +0800 Subject: [PATCH 60/74] add/modify strings --- ftl/core/launcher.ftl | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ftl/core/launcher.ftl b/ftl/core/launcher.ftl index b04135e54..6d73ce51b 100644 --- a/ftl/core/launcher.ftl +++ b/ftl/core/launcher.ftl @@ -22,10 +22,10 @@ launcher-versions-before-cant-be-installed = Versions before 2.1.50 can't be ins launcher-invalid-version = Invalid version. launcher-unable-to-check-for-versions = Unable to check for Anki versions. Please check your internet connection. launcher-checking-for-updates = Checking for updates... -launcher-uninstall-confirm = Uninstall Anki's program files? (y/n) +launcher-uninstall-confirm = Uninstall Anki's program files launcher-uninstall-cancelled = Uninstall cancelled. launcher-program-files-removed = Program files removed. -launcher-remove-all-profiles-confirm = Remove all profiles/cards? (y/n) +launcher-remove-all-profiles-confirm = Remove all profiles/cards? launcher-user-data-removed = User data removed. launcher-download-mirror-options = Download mirror options: launcher-mirror-no-mirror = No mirror @@ -56,3 +56,15 @@ launcher-anki-is-warming-up = Anki is preparing to run launcher-this-may-take = This may take a few minutes. Please wait launcher-os-unsupported = Unsupported OS launcher-unknown-error = Unknown error +launcher-remove-profiles-warning = This will irreversibly delete all of your Anki flashcards and media! +launcher-uninstall-confirm-and-remove-profiles = Uninstall Anki's program files and remove profiles +launcher-uninstalling = Uninstalling +launcher-failed-to-uninstall = Failed to uninstall! +launcher-uninstall-complete = Anki has been uninstalled +launcher-uninstall-action-needed = Action needed to finish uninstalling! +launcher-uninstall-unix = To finish uninstalling, run '{ $path }' +launcher-uninstall-mac = Please manually drag Anki.app to the trash to complete uninstall. +launcher-uninstall-win-not-found = Windows uninstaller not found +launcher-uninstall-win-not-found-extra = You may need to uninstall via Windows Settings > Apps. +launcher-uninstall-win-failed = Failed to launch uninstaller +launcher-uninstall-win-failed-extra = You can manually run: { $path } From 42ded9ab2e6c976939ce4cc54df81827a7c1e333 Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 19:40:45 +0800 Subject: [PATCH 61/74] move font fallback to base.scss --- qt/launcher-gui/src/routes/+layout.svelte | 7 ------- qt/launcher-gui/src/routes/base.scss | 7 ++++++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/qt/launcher-gui/src/routes/+layout.svelte b/qt/launcher-gui/src/routes/+layout.svelte index eaf43e1e0..cd943cbe7 100644 --- a/qt/launcher-gui/src/routes/+layout.svelte +++ b/qt/launcher-gui/src/routes/+layout.svelte @@ -24,10 +24,3 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - - diff --git a/qt/launcher-gui/src/routes/base.scss b/qt/launcher-gui/src/routes/base.scss index c65f458a8..123be70d5 100644 --- a/qt/launcher-gui/src/routes/base.scss +++ b/qt/launcher-gui/src/routes/base.scss @@ -15,5 +15,10 @@ $carousel-transition: var(--transition); @import "$lib/sass/bootstrap-tooltip"; input { - color: var(--fg); + color: var(--fg); +} + +// follows AnkiWebView.standard_css +html { + font-family: "Segoe UI", "Helvetica", sans-serif; } From 3c3d476bf047be17f3b9a67c4eb28f00a76a0197 Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 19:41:12 +0800 Subject: [PATCH 62/74] fix spinner not being centred --- qt/launcher-gui/src/routes/Spinner.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qt/launcher-gui/src/routes/Spinner.svelte b/qt/launcher-gui/src/routes/Spinner.svelte index 259232897..94fc7528b 100644 --- a/qt/launcher-gui/src/routes/Spinner.svelte +++ b/qt/launcher-gui/src/routes/Spinner.svelte @@ -36,7 +36,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html position: absolute; width: 32px; height: 32px; - margin: 32px; + inset: 0; + margin: auto; border: 2px solid #000; border-radius: 50%; animation: spin 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; From 6b82e84b7b5a605b055ed282ae5f1df81623ec06 Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 19:43:56 +0800 Subject: [PATCH 63/74] implement uninstall screen --- qt/launcher-gui/src/routes/Uninstall.svelte | 118 ++++++++++++++++++-- 1 file changed, 107 insertions(+), 11 deletions(-) diff --git a/qt/launcher-gui/src/routes/Uninstall.svelte b/qt/launcher-gui/src/routes/Uninstall.svelte index 8ec9714aa..7e9089f09 100644 --- a/qt/launcher-gui/src/routes/Uninstall.svelte +++ b/qt/launcher-gui/src/routes/Uninstall.svelte @@ -4,25 +4,121 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> -
-
TODO: Uninstall
-
+{#await uninstallPromise} + + + +{:then res} + {#if !res} +
+ + + {$tr.launcherRemoveAllProfilesConfirm()} + + + {#if deleteBaseFolder} + + {/if} + + + +
+ {:else} + {@const kind = res.actionNeeded?.case} + {#if !kind} + + + + {:else} + + + + {#if kind === "unixScript"} + + {$tr.launcherUninstallUnix({ path: res.actionNeeded.value })} + + {:else if kind === "macManual"} + + {$tr.launcherUninstallMac()} + + {:else if kind === "windowsInstallerNotFound"} + + {$tr.launcherUninstallWinNotFound()} + + + {$tr.launcherUninstallWinNotFoundExtra()} + + {:else} + {@const { error, path } = res.actionNeeded.value} + + {$tr.launcherUninstallWinFailed()} + + + {$tr.launcherUninstallWinFailedExtra({ path })} + + +
{error}
+
+ {/if} + {/if} + {/if} +{:catch e} + + +
{e.message}
+
+{/await} From 0c66df1dc38310b92adbe99d414902352734b141 Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 19:44:41 +0800 Subject: [PATCH 64/74] fetch and pass uninstall info --- qt/launcher-gui/src/routes/+page.svelte | 12 +++++++----- qt/launcher-gui/src/routes/+page.ts | 8 +++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/qt/launcher-gui/src/routes/+page.svelte b/qt/launcher-gui/src/routes/+page.svelte index fcbd3200a..69932a936 100644 --- a/qt/launcher-gui/src/routes/+page.svelte +++ b/qt/launcher-gui/src/routes/+page.svelte @@ -17,7 +17,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import ErrorState from "./ErrorState.svelte"; import Normal from "./Normal.svelte"; import Uninstall from "./Uninstall.svelte"; - import { launcherOsUnsupported } from "@generated/ftl"; const { data }: PageProps = $props(); @@ -25,6 +24,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let selectedLang = $state(data.userLocale); let flow = $state(data.state); let mirrors = $state(data.mirrors); + let uninstallInfo = data.uninstallInfo; async function onLangChange(lang: string) { await setLang({ val: lang }); @@ -46,16 +46,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let footer: any = $state(null); - const uninstall = () => { - flow.case = "uninstall"; - }; + const uninstall = uninstallInfo.ankiProgramFilesExists + ? () => { + flow.case = "uninstall"; + } + : null; {#if flow.case === "normal"} {:else if flow.case === "uninstall"} - + {:else if flow.case === "osUnsupported"} { const langsPromise = getLangs({}); const statePromise = getState({}); const mirrorsPromise = getMirrors({}); + const uninstallInfoPromise = getUninstallInfo({}); - const [_, { userLocale, langs }, { kind: state }, { mirrors }] = await Promise.all([ + const [_, { userLocale, langs }, { kind: state }, { mirrors }, uninstallInfo] = await Promise.all([ i18nPromise, langsPromise, statePromise, mirrorsPromise, + uninstallInfoPromise, ]); - return { langs, userLocale, state, mirrors }; + return { langs, userLocale, state, mirrors, uninstallInfo }; }) satisfies PageLoad; From 8d2fab7dea64488e451f66bc8a8fdf3044ac79f7 Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 19:45:02 +0800 Subject: [PATCH 65/74] fix reactivity issue this broke somewhere down the line and needs a tick now --- qt/launcher-gui/src/routes/Normal.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qt/launcher-gui/src/routes/Normal.svelte b/qt/launcher-gui/src/routes/Normal.svelte index 1e048f164..425c86929 100644 --- a/qt/launcher-gui/src/routes/Normal.svelte +++ b/qt/launcher-gui/src/routes/Normal.svelte @@ -23,6 +23,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import Term from "./Term.svelte"; import AnkiWillStart from "./AnkiWillStart.svelte"; import type { Terminal } from "@xterm/xterm"; + import { tick } from "svelte"; let { options, @@ -52,7 +53,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let error: Error | null = $state(null); const setError = (e: Error) => { - error = e; + tick().then(() => (error = e)); }; let term: Terminal | undefined = $state(undefined); From 5aeb49641bf0c6fccd70c6c967ae5bbb3c273235 Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 19:46:04 +0800 Subject: [PATCH 66/74] cleanup --- qt/launcher-gui/src/routes/Action.svelte | 2 +- qt/launcher-gui/src/routes/Normal.svelte | 22 ++-------------------- qt/launcher-gui/src/routes/stores.ts | 1 - 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/qt/launcher-gui/src/routes/Action.svelte b/qt/launcher-gui/src/routes/Action.svelte index 307e317aa..e939e5985 100644 --- a/qt/launcher-gui/src/routes/Action.svelte +++ b/qt/launcher-gui/src/routes/Action.svelte @@ -82,7 +82,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {#if uninstall != null} {/if} diff --git a/qt/launcher-gui/src/routes/Normal.svelte b/qt/launcher-gui/src/routes/Normal.svelte index 425c86929..96b34c746 100644 --- a/qt/launcher-gui/src/routes/Normal.svelte +++ b/qt/launcher-gui/src/routes/Normal.svelte @@ -100,6 +100,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {setError(e)} {/await} + {#if error != null}
{error.message}
@@ -132,30 +133,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -webkit-text-size-adjust: 100%; } - .logo { - max-width: 50px; - margin-inline-end: 1em; - - -webkit-user-drag: none; - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - } - - .title { - align-items: center; - } - :global(.centre) { justify-content: center; } pre { - white-space: pre-wrap; /* Since CSS 2.1 */ - white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ - white-space: -pre-wrap; /* Opera 4-6 */ - white-space: -o-pre-wrap; /* Opera 7 */ - word-wrap: break-word; /* Internet Explorer 5.5+ */ + white-space: pre-wrap; } diff --git a/qt/launcher-gui/src/routes/stores.ts b/qt/launcher-gui/src/routes/stores.ts index 4a5b4233c..d83cb1d0e 100644 --- a/qt/launcher-gui/src/routes/stores.ts +++ b/qt/launcher-gui/src/routes/stores.ts @@ -1,7 +1,6 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import type { GetLangsResponse_Pair, GetMirrorsResponse_Pair, GetVersionsResponse } from "@generated/anki/launcher_pb"; import * as _tr from "@generated/ftl-launcher"; import { writable } from "svelte/store"; From 37e5be4d0e933a5492c12f0176e56c5b608e2ffc Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 24 Oct 2025 20:16:47 +0800 Subject: [PATCH 67/74] debug flag --- qt/launcher-gui/src-tauri/src/uv.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/qt/launcher-gui/src-tauri/src/uv.rs b/qt/launcher-gui/src-tauri/src/uv.rs index 7fab513da..13ef85223 100644 --- a/qt/launcher-gui/src-tauri/src/uv.rs +++ b/qt/launcher-gui/src-tauri/src/uv.rs @@ -846,19 +846,16 @@ impl State { let launcher_requested = paths.launcher_trigger_file.exists() || !paths.user_pyproject_path.exists(); - // TODO: remove - let skip = std::env::var("ANKI_LAUNCHER_SKIP").is_ok(); - // Calculate whether user has custom edits that need syncing let pyproject_time = file_timestamp_secs(&paths.user_pyproject_path); let sync_time = file_timestamp_secs(&paths.sync_complete_marker); paths.pyproject_modified_by_user = pyproject_time > sync_time; let pyproject_has_changed = paths.pyproject_modified_by_user; - #[allow(clippy::nonminimal_bool)] - let debug = true && cfg!(debug_assertions); + // TODO: remove + let debug = cfg!(debug_assertions) && std::env::var("ANKI_LAUNCHER_SKIP").is_err(); - if !launcher_requested && !pyproject_has_changed && (!debug || skip) { + if !launcher_requested && !pyproject_has_changed && !debug { return Ok(Self::LaunchAnki(paths.into())); } From a4c54b212caa88f090bbc1a5958d426f9af19b2c Mon Sep 17 00:00:00 2001 From: llama Date: Sat, 25 Oct 2025 21:06:33 +0800 Subject: [PATCH 68/74] remove use of setError --- qt/launcher-gui/src/routes/Normal.svelte | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/qt/launcher-gui/src/routes/Normal.svelte b/qt/launcher-gui/src/routes/Normal.svelte index 96b34c746..0aa548ebc 100644 --- a/qt/launcher-gui/src/routes/Normal.svelte +++ b/qt/launcher-gui/src/routes/Normal.svelte @@ -51,11 +51,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html Promise.resolve(null), ); - let error: Error | null = $state(null); - const setError = (e: Error) => { - tick().then(() => (error = e)); - }; - let term: Terminal | undefined = $state(undefined); let termOpen = $state(false); let chosen = $state(false); @@ -85,11 +80,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {:then [releases, existing]} {:catch e} - {setError(e)} + +
{e.message}
+
{/await} {:else} @@ -97,15 +94,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {/if} {:catch e} - {setError(e)} -{/await} - -{#if error != null} -
{error.message}
+
{e.message}
-{/if} +{/await} {#snippet _footer()} From 222a868e691e12169528d8103614895e74e7bdb7 Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 31 Oct 2025 21:26:40 +0800 Subject: [PATCH 69/74] port 739e41ce1 --- qt/launcher-gui/src-tauri/src/uv.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/qt/launcher-gui/src-tauri/src/uv.rs b/qt/launcher-gui/src-tauri/src/uv.rs index 13ef85223..4f5d578aa 100644 --- a/qt/launcher-gui/src-tauri/src/uv.rs +++ b/qt/launcher-gui/src-tauri/src/uv.rs @@ -225,7 +225,6 @@ where venv_command.ensure_success()?; } - command.env("UV_CACHE_DIR", &state.uv_cache_dir); command.env("UV_PYTHON_INSTALL_DIR", &state.uv_python_install_dir); command.env( "UV_HTTP_TIMEOUT", @@ -244,10 +243,6 @@ where } } - if state.no_cache_marker.exists() { - command.env("UV_NO_CACHE", "1"); - } - // NOTE: pty and child must live in the same thread let pty_system = portable_pty::NativePtySystem::default(); @@ -695,6 +690,12 @@ fn uv_command(state: &Paths) -> Result { .env("UV_DEFAULT_INDEX", &pypi_mirror); } + if state.no_cache_marker.exists() { + command.env("UV_NO_CACHE", "1"); + } else { + command.env("UV_CACHE_DIR", &state.uv_cache_dir); + } + #[cfg(windows)] { use std::os::windows::process::CommandExt; From 3edfc0b45934db23c6752a550d393d385de18166 Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 31 Oct 2025 21:38:18 +0800 Subject: [PATCH 70/74] port a05c90cbc --- qt/launcher-gui/src-tauri/src/uv.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qt/launcher-gui/src-tauri/src/uv.rs b/qt/launcher-gui/src-tauri/src/uv.rs index 4f5d578aa..9bbb474b0 100644 --- a/qt/launcher-gui/src-tauri/src/uv.rs +++ b/qt/launcher-gui/src-tauri/src/uv.rs @@ -696,6 +696,9 @@ fn uv_command(state: &Paths) -> Result { command.env("UV_CACHE_DIR", &state.uv_cache_dir); } + // have uv use the system certstore instead of webpki-roots' + command.env("UV_NATIVE_TLS", "1"); + #[cfg(windows)] { use std::os::windows::process::CommandExt; From 81938457638ef86ccd4620ecd4f8e774e094d367 Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 31 Oct 2025 21:38:54 +0800 Subject: [PATCH 71/74] port 23263caea --- qt/launcher-gui/src-tauri/build.rs | 4 ++++ qt/launcher-gui/src-tauri/src/uv.rs | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/qt/launcher-gui/src-tauri/build.rs b/qt/launcher-gui/src-tauri/build.rs index 84709a6be..bdd27838b 100644 --- a/qt/launcher-gui/src-tauri/build.rs +++ b/qt/launcher-gui/src-tauri/build.rs @@ -13,6 +13,10 @@ fn main() -> Result<()> { let pool = DescriptorPool::decode(std::fs::read(descriptors_path)?.as_ref())?; rust_interface::write_rust_interface(&pool)?; + println!("cargo:rerun-if-changed=../../../out/buildhash"); + let buildhash = std::fs::read_to_string("../../../out/buildhash").unwrap_or_default(); + println!("cargo:rustc-env=BUILDHASH={buildhash}"); + tauri_build::build(); Ok(()) diff --git a/qt/launcher-gui/src-tauri/src/uv.rs b/qt/launcher-gui/src-tauri/src/uv.rs index 9bbb474b0..fbebdfeab 100644 --- a/qt/launcher-gui/src-tauri/src/uv.rs +++ b/qt/launcher-gui/src-tauri/src/uv.rs @@ -804,6 +804,20 @@ pub fn build_python_command(state: &Paths, args: &[String]) -> Result { Ok(cmd) } +fn diff_launcher_was_installed(state: &Paths) -> Result { + let launcher_version = option_env!("BUILDHASH").unwrap_or("dev").trim(); + let launcher_version_path = state.uv_install_root.join("launcher-version"); + if let Ok(content) = read_file(&launcher_version_path) { + if let Ok(version_str) = String::from_utf8(content) { + if version_str.trim() == launcher_version { + return Ok(false); + } + } + } + write_file(launcher_version_path, launcher_version)?; + Ok(true) +} + impl State { pub fn init() -> Result { let uv_install_root = if let Ok(custom_root) = std::env::var("ANKI_LAUNCHER_VENV_ROOT") { @@ -855,11 +869,12 @@ impl State { let sync_time = file_timestamp_secs(&paths.sync_complete_marker); paths.pyproject_modified_by_user = pyproject_time > sync_time; let pyproject_has_changed = paths.pyproject_modified_by_user; + let different_launcher = diff_launcher_was_installed(&paths)?; // TODO: remove let debug = cfg!(debug_assertions) && std::env::var("ANKI_LAUNCHER_SKIP").is_err(); - if !launcher_requested && !pyproject_has_changed && !debug { + if !launcher_requested && !pyproject_has_changed && !different_launcher && !debug { return Ok(Self::LaunchAnki(paths.into())); } From 1622f9bfde81e6b56906d51ecc9d353be980df1f Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 31 Oct 2025 21:44:01 +0800 Subject: [PATCH 72/74] replace unwraps --- qt/launcher-gui/src-tauri/src/commands.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qt/launcher-gui/src-tauri/src/commands.rs b/qt/launcher-gui/src-tauri/src/commands.rs index 9f18ffe7c..3507c152d 100644 --- a/qt/launcher-gui/src-tauri/src/commands.rs +++ b/qt/launcher-gui/src-tauri/src/commands.rs @@ -145,7 +145,7 @@ pub async fn get_available_versions( ) -> Result { let state = app.flow().normal()?; let mut rx = state.available_versions.clone().unwrap(); - rx.changed().await.unwrap(); + rx.changed().await?; let x = rx.borrow(); match x.as_ref().unwrap() { Ok(versions) => Ok(versions.clone()), @@ -160,7 +160,7 @@ pub async fn get_existing_versions( ) -> Result { let state = app.flow().normal()?; let mut rx = state.current_versions.clone().unwrap(); - rx.changed().await.unwrap(); + rx.changed().await?; let x = rx.borrow(); match x.as_ref().unwrap() { Ok(versions) => Ok(versions.clone()), From 04332f36b5ac8e57674b6ea3e08c696b5b4e1d8d Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 31 Oct 2025 23:01:06 +0800 Subject: [PATCH 73/74] modify strings --- ftl/core/launcher.ftl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ftl/core/launcher.ftl b/ftl/core/launcher.ftl index 6d73ce51b..c4850eb4f 100644 --- a/ftl/core/launcher.ftl +++ b/ftl/core/launcher.ftl @@ -7,7 +7,7 @@ launcher-updating-anki = Updating Anki... launcher-latest-anki = Install Latest Anki (default) launcher-choose-a-version = Choose a version launcher-sync-project-changes = Sync project changes -launcher-keep-existing-version = Keep existing version ({ $current }) +launcher-keep-existing-version = Launch existing version ({ $current }) launcher-revert-to-previous = Revert to previous version ({ $prev }) launcher-allow-betas = Allow betas: { $state } launcher-on = on @@ -49,7 +49,9 @@ launcher-syncing = Syncing launcher-failed-to-sync = Failed to sync! launcher-loading = Loading launcher-loading-versions = Loading versions -lauuncher-failed-to-load-versions = Failed to load versions! +launcher-checking-existing = Checking existing version +launcher-failed-to-get-existing-and-available = Failed to get existing and available versions! +launcher-failed-to-get-releases = Failed to get available versions! launcher-error-details = Error details launcher-will-close-in = Launcher will close in { $count } launcher-anki-is-warming-up = Anki is preparing to run From 934d11aebbf3cdd18b462018d854c8652a02abfc Mon Sep 17 00:00:00 2001 From: llama Date: Fri, 31 Oct 2025 23:03:52 +0800 Subject: [PATCH 74/74] allow launching existing or installing new if either fails --- qt/launcher-gui/src/routes/Action.svelte | 109 +++++++++++++---------- qt/launcher-gui/src/routes/Normal.svelte | 34 ++++--- 2 files changed, 87 insertions(+), 56 deletions(-) diff --git a/qt/launcher-gui/src/routes/Action.svelte b/qt/launcher-gui/src/routes/Action.svelte index e939e5985..11b66f87a 100644 --- a/qt/launcher-gui/src/routes/Action.svelte +++ b/qt/launcher-gui/src/routes/Action.svelte @@ -7,32 +7,39 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import type { ExistingVersions, Versions } from "@generated/anki/launcher_pb"; import Row from "$lib/components/Row.svelte"; import EnumSelector from "$lib/components/EnumSelector.svelte"; + import Warning from "./Warning.svelte"; + import Spinner from "./Spinner.svelte"; let { - releases, - existing, + releasesPromise, + existingPromise, allowBetas, choose, uninstall, }: { - releases: Versions; - existing: ExistingVersions; + releasesPromise: Promise; + existingPromise: Promise; allowBetas: boolean; choose: (version: string, existing: boolean, current?: string) => void; uninstall: (() => void) | null; } = $props(); + // TODO: replace once svelte's experimental async mode is on + let releases = $state(undefined as Versions | undefined); + let existing = $state(undefined as ExistingVersions | undefined); + releasesPromise.then((r) => (releases = r)); + existingPromise.then((r) => (existing = r)); + let availableVersions = $derived( - releases.all + releases?.all .filter((v) => allowBetas || !v.isPrerelease) .map((v) => ({ label: v.version, value: v.version })), ); - let latest = $derived(availableVersions[0]?.value ?? null); - let selected = $derived(availableVersions[0]?.value ?? null); - let current = $derived(existing.current?.version); - - let pyprojectModified = $derived(existing.pyprojectModifiedByUser); + let latest = $derived(availableVersions?.[0]?.value); + let selected = $derived(availableVersions?.[0]?.value); + let current = $derived(existing!?.current?.version); + let pyprojectModified = $derived(existing?.pyprojectModifiedByUser); function _choose(version: string, keepExisting: boolean = false) { choose(version, keepExisting, current); @@ -40,45 +47,57 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
- {#if latest != null && latest != current} + {#await releasesPromise} + + {:then} + {#if latest != null && latest != current} + + + + {/if} + {:catch} + + {/await} + {#await existingPromise} + + {:then} + {#if current != null} + + + + {/if} + {/await} + {#if availableVersions} - +
+ {"->"} +
+
+ +
{/if} - {#if current != null} - - - - {/if} - - -
- {"->"} -
-
- -
-
{#if uninstall != null}